openvibers/openvibe.wiki Commits
openvibe-shared v1.5.0 (Track R release manifest), openvibe-contracts v0.32.0, openvibe-sdk v0.5.0
Changes
@@@ -189,9 +189,9 @@ applies). Errors are RFC 9457 problem+json. The full route list is at the top ofThe ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposal/) (the plan's`wiki.search` becomes the three-segment `wiki.search.query`) and are released in openvibe-contracts--v0.17.0 with the service manifest (this repo pins v0.19.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).--The proposal for `wiki.revision.publish` now also lists the revision review route, which the--released manifest does not name yet.++v0.17.0 with the service manifest (this repo pins v0.32.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).++The proposal for `wiki.revision.publish` also lists the revision review route, which the released++capability names since v0.32.0 (as `wiki.page.create` does the space import route).## Events@@@ -221,8 +221,8 @@ Production: `/opt/openvibe.wiki`, env `/etc/openvibe/wiki.env`, unit## Depends on--- `openvibe-publishing` v0.2.1, `openvibe-contracts` v0.19.0, `openvibe-shared` v1.3.0 (chrome,-- release, metrics, readiness, SEO helpers, legal pages), `openvibe-sdk` v0.2.2 (auth, events++- `openvibe-publishing` v0.2.1, `openvibe-contracts` v0.32.0, `openvibe-shared` v1.5.0 (chrome,++ release, metrics, readiness, SEO helpers, legal pages), `openvibe-sdk` v0.5.0 (auth, eventsoutbox) — pinned release tarballs.- OpenVibe.Network (SSO, JWKS, service principal `wiki`), OpenVibe.Events, OpenVibe.Community,OpenVibe.Sources, OpenVibe.Media, OpenVibe.Search (consumer of the index events). All but the@@@ -7,11 +7,11 @@"runtime": "node 22, express, better-sqlite3","port": 4800,"domain": "openvibe.wiki",-- "contracts": "openvibe-contracts v0.19.0",++ "contracts": "openvibe-contracts v0.32.0","packages": ["openvibe-publishing v0.2.1",-- "openvibe-shared v1.3.0",-- "openvibe-sdk v0.2.2"++ "openvibe-shared v1.5.0",++ "openvibe-sdk v0.5.0"],"capabilities": ["wiki.space.create",@@@ -14,10 +14,10 @@"dotenv": "^16.4.7","express": "^4.21.2","express-rate-limit": "^7.5.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.19.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0"},"engines": {"node": ">=22.0.0"@@@ -999,9 +999,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.19.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.19.0",-- "integrity": "sha512-5DjPvXIfc68wZhFXgKlCzDarbbFD5dUV4yYQIKEK1qD+VIuR+4twbON1brdyM9rYZyRyYClPigAqReocmRGytg==",++ "version": "0.32.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",++ "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1044,18 +1044,18 @@}},"node_modules/openvibe-sdk": {-- "version": "0.2.2",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",-- "integrity": "sha512-kDifUp3EcmnTWzElAmaCZnX659WErcnm4+qVEoAiYQWX/w/1p4TWvoHbUPsrX65TSROVR3tiyVkl801n/0yyCw==",++ "version": "0.5.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==","license": "MIT","engines": {"node": ">=20"}},"node_modules/openvibe-shared": {-- "version": "1.3.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",-- "integrity": "sha512-w8yl/5+9eend8I2O/DzAJnEAL2N3GTUY7c7pzK+MLJuLAGD8FmJJQ9gwbgyrq70Snjt3ebrg3TlpORhRAp2kgA==",++ "version": "1.5.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0",++ "integrity": "sha512-cehnscuYMkJwuCYGcI6wPSswbUn0bhTyGAaD5feNCoO//2e9gyC7cgjkEcIOtM8DQF3yTuUNyXnOwckIeM4fUg==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -20,10 +20,10 @@"dotenv": "^16.4.7","express": "^4.21.2","express-rate-limit": "^7.5.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.19.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0"},"engines": {"node": ">=22.0.0"@@@ -59,7 +59,8 @@ function createApp({ config, svc, viewers, platform, keys, db, log = console, ra{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'wiki', service: 'wiki', host: 'openvibe.wiki', name: 'OpenVibe.Wiki', profile: 'ugc' })); }app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-wiki', version: VERSION }));-- app.get('/release.json', release.handler);++ // GET /release.json (ADR-016) and POST /release-metrics: open tabs' update reports into /metrics.++ release.mount(app, { registry: metrics.registry });const ready = createReadiness({service: 'wiki', release: release.release,checks: [@@@ -68,6 +68,11 @@ const { sanitizeNext } = require('../server/auth/session');const ready = await H.req(h, 'GET', '/api/ready');assert.strictEqual(ready.status, 200);assert.strictEqual(ready.json.checks.db.status, 'ok');++ // The release manifest (registry.release-manifest@1) names where open tabs report updates.++ const rel = await H.req(h, 'GET', '/release.json');++ assert.strictEqual(rel.json.service, 'wiki');++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', rel.json).errors, []);++ assert.strictEqual(rel.json.metrics_url, '/release-metrics');console.log('auth ok');} finally {await h.stop();@@@ -22,10 +22,14 @@ function userToken({ subject: sub = subject(), username = 'someone', role = 'usereturn serviceAuth.signServiceToken({ iss: issuer, aud, sub: 42, id: 42, subject_id: sub, username, display_name: username, role, iat: now, exp: now + exp }, privateKey);}--/** A principal token: svc:<client> by default; `sub` + `actorType` for an app:/mod: principal, `extra` for on_behalf_of, env… */++/**++ * A principal token: svc:<client> by default; `sub` + `actorType` for an app:/mod: principal, `extra` for on_behalf_of, env…++ * An app token carries its developer project and env, as Network's do (identity.service-token-claims 1.2.0).++ */function serviceToken({ client = 'ai', cap = [], aud = 'openvibe.wiki', exp = 300, sub, actorType = 'service', extra = {} } = {}) {const now = Math.floor(Date.now() / 1000);-- return serviceAuth.signServiceToken({ iss: ISSUER, sub: sub || `svc:${client}`, actor_type: actorType, aud: [aud], cap, ns: [], iat: now, exp: now + exp, jti: `tok_${crypto.randomBytes(8).toString('hex')}`, ...extra }, privateKey);++ const app = actorType === 'app' ? { project_id: 'prj_01J8ZQ4Y7N3M2K1H0G9F8E7D6C', env: 'production' } : {};++ return serviceAuth.signServiceToken({ iss: ISSUER, sub: sub || `svc:${client}`, actor_type: actorType, aud: [aud], cap, ns: [], iat: now, exp: now + exp, jti: `tok_${crypto.randomBytes(8).toString('hex')}`, ...app, ...extra }, privateKey);}const quiet = { log() {}, warn() {}, error() {} };
@@ -189,9 +189,9 @@ applies). Errors are RFC 9457 problem+json. The full route list is at the top of
@@ -189,9 +189,9 @@ applies). Errors are RFC 9457 problem+json. The full route list is at the top of
The ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposal/) (the plan's
The ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposal/) (the plan's
`wiki.search` becomes the three-segment `wiki.search.query`) and are released in openvibe-contracts
`wiki.search` becomes the three-segment `wiki.search.query`) and are released in openvibe-contracts
-v0.17.0 with the service manifest (this repo pins v0.19.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).
-The proposal for `wiki.revision.publish` now also lists the revision review route, which the
-released manifest does not name yet.
+v0.17.0 with the service manifest (this repo pins v0.32.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).
+The proposal for `wiki.revision.publish` also lists the revision review route, which the released
+capability names since v0.32.0 (as `wiki.page.create` does the space import route).
## Events
## Events
@@ -221,8 +221,8 @@ Production: `/opt/openvibe.wiki`, env `/etc/openvibe/wiki.env`, unit
@@ -221,8 +221,8 @@ Production: `/opt/openvibe.wiki`, env `/etc/openvibe/wiki.env`, unit
## Depends on
## Depends on
-- `openvibe-publishing` v0.2.1, `openvibe-contracts` v0.19.0, `openvibe-shared` v1.3.0 (chrome,
- release, metrics, readiness, SEO helpers, legal pages), `openvibe-sdk` v0.2.2 (auth, events
+- `openvibe-publishing` v0.2.1, `openvibe-contracts` v0.32.0, `openvibe-shared` v1.5.0 (chrome,
+ release, metrics, readiness, SEO helpers, legal pages), `openvibe-sdk` v0.5.0 (auth, events
outbox) — pinned release tarballs.
outbox) — pinned release tarballs.
- OpenVibe.Network (SSO, JWKS, service principal `wiki`), OpenVibe.Events, OpenVibe.Community,
- OpenVibe.Network (SSO, JWKS, service principal `wiki`), OpenVibe.Events, OpenVibe.Community,
OpenVibe.Sources, OpenVibe.Media, OpenVibe.Search (consumer of the index events). All but the
OpenVibe.Sources, OpenVibe.Media, OpenVibe.Search (consumer of the index events). All but the
@@ -7,11 +7,11 @@
@@ -7,11 +7,11 @@
"runtime": "node 22, express, better-sqlite3",
"runtime": "node 22, express, better-sqlite3",
"port": 4800,
"port": 4800,
"domain": "openvibe.wiki",
"domain": "openvibe.wiki",
- "contracts": "openvibe-contracts v0.19.0",
+ "contracts": "openvibe-contracts v0.32.0",
"packages": [
"packages": [
"openvibe-publishing v0.2.1",
"openvibe-publishing v0.2.1",
- "openvibe-shared v1.3.0",
- "openvibe-sdk v0.2.2"
+ "openvibe-shared v1.5.0",
+ "openvibe-sdk v0.5.0"
],
],
"capabilities": [
"capabilities": [
"wiki.space.create",
"wiki.space.create",
@@ -14,10 +14,10 @@
@@ -14,10 +14,10 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.19.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0"
},
},
"engines": {
"engines": {
"node": ">=22.0.0"
"node": ">=22.0.0"
@@ -999,9 +999,9 @@
@@ -999,9 +999,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.19.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.19.0",
- "integrity": "sha512-5DjPvXIfc68wZhFXgKlCzDarbbFD5dUV4yYQIKEK1qD+VIuR+4twbON1brdyM9rYZyRyYClPigAqReocmRGytg==",
+ "version": "0.32.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
+ "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1044,18 +1044,18 @@
@@ -1044,18 +1044,18 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.2.2",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
- "integrity": "sha512-kDifUp3EcmnTWzElAmaCZnX659WErcnm4+qVEoAiYQWX/w/1p4TWvoHbUPsrX65TSROVR3tiyVkl801n/0yyCw==",
+ "version": "0.5.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.3.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
- "integrity": "sha512-w8yl/5+9eend8I2O/DzAJnEAL2N3GTUY7c7pzK+MLJuLAGD8FmJJQ9gwbgyrq70Snjt3ebrg3TlpORhRAp2kgA==",
+ "version": "1.5.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0",
+ "integrity": "sha512-cehnscuYMkJwuCYGcI6wPSswbUn0bhTyGAaD5feNCoO//2e9gyC7cgjkEcIOtM8DQF3yTuUNyXnOwckIeM4fUg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -20,10 +20,10 @@
@@ -20,10 +20,10 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.19.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v0.2.1",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0"
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.0"
},
},
"engines": {
"engines": {
"node": ">=22.0.0"
"node": ">=22.0.0"
@@ -59,7 +59,8 @@ function createApp({ config, svc, viewers, platform, keys, db, log = console, ra
@@ -59,7 +59,8 @@ function createApp({ config, svc, viewers, platform, keys, db, log = console, ra
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'wiki', service: 'wiki', host: 'openvibe.wiki', name: 'OpenVibe.Wiki', profile: 'ugc' })); }
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'wiki', service: 'wiki', host: 'openvibe.wiki', name: 'OpenVibe.Wiki', profile: 'ugc' })); }
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-wiki', version: VERSION }));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-wiki', version: VERSION }));
- app.get('/release.json', release.handler);
+ // GET /release.json (ADR-016) and POST /release-metrics: open tabs' update reports into /metrics.
+ release.mount(app, { registry: metrics.registry });
const ready = createReadiness({
const ready = createReadiness({
service: 'wiki', release: release.release,
service: 'wiki', release: release.release,
checks: [
checks: [
@@ -68,6 +68,11 @@ const { sanitizeNext } = require('../server/auth/session');
@@ -68,6 +68,11 @@ const { sanitizeNext } = require('../server/auth/session');
const ready = await H.req(h, 'GET', '/api/ready');
const ready = await H.req(h, 'GET', '/api/ready');
assert.strictEqual(ready.status, 200);
assert.strictEqual(ready.status, 200);
assert.strictEqual(ready.json.checks.db.status, 'ok');
assert.strictEqual(ready.json.checks.db.status, 'ok');
+ // The release manifest (registry.release-manifest@1) names where open tabs report updates.
+ const rel = await H.req(h, 'GET', '/release.json');
+ assert.strictEqual(rel.json.service, 'wiki');
+ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', rel.json).errors, []);
+ assert.strictEqual(rel.json.metrics_url, '/release-metrics');
console.log('auth ok');
console.log('auth ok');
} finally {
} finally {
await h.stop();
await h.stop();
@@ -22,10 +22,14 @@ function userToken({ subject: sub = subject(), username = 'someone', role = 'use
@@ -22,10 +22,14 @@ function userToken({ subject: sub = subject(), username = 'someone', role = 'use
return serviceAuth.signServiceToken({ iss: issuer, aud, sub: 42, id: 42, subject_id: sub, username, display_name: username, role, iat: now, exp: now + exp }, privateKey);
return serviceAuth.signServiceToken({ iss: issuer, aud, sub: 42, id: 42, subject_id: sub, username, display_name: username, role, iat: now, exp: now + exp }, privateKey);
}
}
-/** A principal token: svc:<client> by default; `sub` + `actorType` for an app:/mod: principal, `extra` for on_behalf_of, env… */
+/**
+ * A principal token: svc:<client> by default; `sub` + `actorType` for an app:/mod: principal, `extra` for on_behalf_of, env…
+ * An app token carries its developer project and env, as Network's do (identity.service-token-claims 1.2.0).
+ */
function serviceToken({ client = 'ai', cap = [], aud = 'openvibe.wiki', exp = 300, sub, actorType = 'service', extra = {} } = {}) {
function serviceToken({ client = 'ai', cap = [], aud = 'openvibe.wiki', exp = 300, sub, actorType = 'service', extra = {} } = {}) {
const now = Math.floor(Date.now() / 1000);
const now = Math.floor(Date.now() / 1000);
- return serviceAuth.signServiceToken({ iss: ISSUER, sub: sub || `svc:${client}`, actor_type: actorType, aud: [aud], cap, ns: [], iat: now, exp: now + exp, jti: `tok_${crypto.randomBytes(8).toString('hex')}`, ...extra }, privateKey);
+ const app = actorType === 'app' ? { project_id: 'prj_01J8ZQ4Y7N3M2K1H0G9F8E7D6C', env: 'production' } : {};
+ return serviceAuth.signServiceToken({ iss: ISSUER, sub: sub || `svc:${client}`, actor_type: actorType, aud: [aud], cap, ns: [], iat: now, exp: now + exp, jti: `tok_${crypto.randomBytes(8).toString('hex')}`, ...app, ...extra }, privateKey);
}
}
const quiet = { log() {}, warn() {}, error() {} };
const quiet = { log() {}, warn() {}, error() {} };