@enbox/dwn-server 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/src/admin/admin-api.d.ts +5 -1
- package/dist/esm/src/admin/admin-api.d.ts.map +1 -1
- package/dist/esm/src/admin/admin-api.js +327 -7
- package/dist/esm/src/admin/admin-api.js.map +1 -1
- package/dist/esm/src/admin/admin-auth.d.ts +21 -3
- package/dist/esm/src/admin/admin-auth.d.ts.map +1 -1
- package/dist/esm/src/admin/admin-auth.js +17 -9
- package/dist/esm/src/admin/admin-auth.js.map +1 -1
- package/dist/esm/src/admin/admin-passkey-store.d.ts +68 -0
- package/dist/esm/src/admin/admin-passkey-store.d.ts.map +1 -0
- package/dist/esm/src/admin/admin-passkey-store.js +132 -0
- package/dist/esm/src/admin/admin-passkey-store.js.map +1 -0
- package/dist/esm/src/admin/admin-session.d.ts +35 -0
- package/dist/esm/src/admin/admin-session.d.ts.map +1 -0
- package/dist/esm/src/admin/admin-session.js +91 -0
- package/dist/esm/src/admin/admin-session.js.map +1 -0
- package/dist/esm/src/admin/audit-log.d.ts.map +1 -1
- package/dist/esm/src/admin/audit-log.js +5 -43
- package/dist/esm/src/admin/audit-log.js.map +1 -1
- package/dist/esm/src/admin/index.d.ts +5 -1
- package/dist/esm/src/admin/index.d.ts.map +1 -1
- package/dist/esm/src/admin/index.js +2 -0
- package/dist/esm/src/admin/index.js.map +1 -1
- package/dist/esm/src/admin/types.d.ts +22 -0
- package/dist/esm/src/admin/types.d.ts.map +1 -1
- package/dist/esm/src/admin/webhook-manager.d.ts.map +1 -1
- package/dist/esm/src/admin/webhook-manager.js +11 -10
- package/dist/esm/src/admin/webhook-manager.js.map +1 -1
- package/dist/esm/src/config.d.ts +18 -0
- package/dist/esm/src/config.d.ts.map +1 -1
- package/dist/esm/src/config.js +18 -0
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/dwn-server.d.ts +20 -1
- package/dist/esm/src/dwn-server.d.ts.map +1 -1
- package/dist/esm/src/dwn-server.js +111 -52
- package/dist/esm/src/dwn-server.js.map +1 -1
- package/dist/esm/src/http-api.d.ts +4 -0
- package/dist/esm/src/http-api.d.ts.map +1 -1
- package/dist/esm/src/http-api.js +14 -4
- package/dist/esm/src/http-api.js.map +1 -1
- package/dist/esm/src/index.d.ts +2 -0
- package/dist/esm/src/index.d.ts.map +1 -1
- package/dist/esm/src/index.js +2 -0
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/migrations/001-initial-server-schema.d.ts +21 -0
- package/dist/esm/src/migrations/001-initial-server-schema.d.ts.map +1 -0
- package/dist/esm/src/migrations/001-initial-server-schema.js +97 -0
- package/dist/esm/src/migrations/001-initial-server-schema.js.map +1 -0
- package/dist/esm/src/migrations/index.d.ts +13 -0
- package/dist/esm/src/migrations/index.d.ts.map +1 -0
- package/dist/esm/src/migrations/index.js +5 -0
- package/dist/esm/src/migrations/index.js.map +1 -0
- package/dist/esm/src/registration/registration-store.d.ts +4 -0
- package/dist/esm/src/registration/registration-store.d.ts.map +1 -1
- package/dist/esm/src/registration/registration-store.js +11 -34
- package/dist/esm/src/registration/registration-store.js.map +1 -1
- package/dist/esm/src/server-migration-runner.d.ts +23 -0
- package/dist/esm/src/server-migration-runner.d.ts.map +1 -0
- package/dist/esm/src/server-migration-runner.js +57 -0
- package/dist/esm/src/server-migration-runner.js.map +1 -0
- package/dist/esm/src/storage.d.ts +15 -0
- package/dist/esm/src/storage.d.ts.map +1 -1
- package/dist/esm/src/storage.js +135 -17
- package/dist/esm/src/storage.js.map +1 -1
- package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts +11 -1
- package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts.map +1 -1
- package/dist/esm/src/web5-connect/sql-ttl-cache.js +19 -20
- package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -1
- package/dist/esm/src/web5-connect/web5-connect-server.d.ts +10 -3
- package/dist/esm/src/web5-connect/web5-connect-server.d.ts.map +1 -1
- package/dist/esm/src/web5-connect/web5-connect-server.js +10 -4
- package/dist/esm/src/web5-connect/web5-connect-server.js.map +1 -1
- package/package.json +3 -2
- package/src/admin/admin-api.ts +403 -10
- package/src/admin/admin-auth.ts +38 -9
- package/src/admin/admin-passkey-store.ts +190 -0
- package/src/admin/admin-session.ts +116 -0
- package/src/admin/audit-log.ts +7 -44
- package/src/admin/index.ts +5 -0
- package/src/admin/types.ts +28 -0
- package/src/admin/webhook-manager.ts +12 -10
- package/src/config.ts +21 -0
- package/src/dwn-server.ts +137 -55
- package/src/http-api.ts +20 -5
- package/src/index.ts +2 -0
- package/src/migrations/001-initial-server-schema.ts +114 -0
- package/src/migrations/index.ts +18 -0
- package/src/registration/registration-store.ts +13 -36
- package/src/server-migration-runner.ts +74 -0
- package/src/storage.ts +145 -17
- package/src/web5-connect/sql-ttl-cache.ts +21 -22
- package/src/web5-connect/web5-connect-server.ts +14 -5
package/src/dwn-server.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { ProviderAuthPlugin } from './registration/provider-auth-plugin.js'
|
|
|
16
16
|
|
|
17
17
|
import { ActivityLog } from './admin/activity-log.js';
|
|
18
18
|
import { AdminApi } from './admin/admin-api.js';
|
|
19
|
+
import { AdminPasskeyStore } from './admin/admin-passkey-store.js';
|
|
20
|
+
import { AdminSessionManager } from './admin/admin-session.js';
|
|
19
21
|
import { AdminStore } from './admin/admin-store.js';
|
|
20
22
|
import { AuditLog } from './admin/audit-log.js';
|
|
21
23
|
import { config as defaultConfig } from './config.js';
|
|
@@ -29,7 +31,7 @@ import { RateLimiter } from './rate-limiter.js';
|
|
|
29
31
|
import { RegistrationManager } from './registration/registration-manager.js';
|
|
30
32
|
import { WebhookManager } from './admin/webhook-manager.js';
|
|
31
33
|
import { WsApi } from './ws-api.js';
|
|
32
|
-
import { getDialectFromUrl, getDwnConfig } from './storage.js';
|
|
34
|
+
import { getDialectFromUrl, getDwnConfig, runServerMigrationsIfNeeded } from './storage.js';
|
|
33
35
|
import { removeProcessHandlers, setProcessHandlers } from './process-handlers.js';
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -52,6 +54,24 @@ export type DwnServerOptions = {
|
|
|
52
54
|
* are appended after the DeliveryService hook.
|
|
53
55
|
*/
|
|
54
56
|
messageProcessedHooks?: MessageProcessedHook[];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An externally created RegistrationManager to use as the tenant gate and
|
|
60
|
+
* registration endpoint handler. When a pre-built `dwn` is provided, the
|
|
61
|
+
* server cannot create its own RegistrationManager (because it doesn't
|
|
62
|
+
* control the DWN's TenantGate). Pass one here to enable registration
|
|
63
|
+
* endpoints (`POST /registration`, etc.) with a pre-built DWN.
|
|
64
|
+
*/
|
|
65
|
+
registrationManager?: RegistrationManager;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* An externally created OpenAuthHandler for the built-in provider-auth
|
|
69
|
+
* endpoints (`/provider-auth/authorize`, `/provider-auth/token`,
|
|
70
|
+
* `/provider-auth/refresh`). When a pre-built `dwn` is provided, the
|
|
71
|
+
* server skips creating this handler. Pass one here to enable the
|
|
72
|
+
* open-auth flow with a pre-built DWN.
|
|
73
|
+
*/
|
|
74
|
+
openAuthHandler?: OpenAuthHandler;
|
|
55
75
|
};
|
|
56
76
|
|
|
57
77
|
/**
|
|
@@ -79,10 +99,16 @@ export class DwnServer {
|
|
|
79
99
|
#ipRateLimiter: RateLimiter | undefined;
|
|
80
100
|
#tenantRateLimiter: RateLimiter | undefined;
|
|
81
101
|
#auditLog: AuditLog | undefined;
|
|
102
|
+
#passkeyStore: AdminPasskeyStore | undefined;
|
|
103
|
+
#sessionManager: AdminSessionManager | undefined;
|
|
82
104
|
#externalHooks: MessageProcessedHook[];
|
|
105
|
+
#externalRegistrationManager: RegistrationManager | undefined;
|
|
106
|
+
#externalOpenAuthHandler: OpenAuthHandler | undefined;
|
|
83
107
|
|
|
84
108
|
/**
|
|
85
|
-
* @param options.dwn - Dwn instance to use as an override.
|
|
109
|
+
* @param options.dwn - Dwn instance to use as an override.
|
|
110
|
+
* @param options.registrationManager - External RegistrationManager to use with a pre-built DWN.
|
|
111
|
+
* @param options.openAuthHandler - External OpenAuthHandler to use with a pre-built DWN.
|
|
86
112
|
*/
|
|
87
113
|
constructor(options: DwnServerOptions = {}) {
|
|
88
114
|
this.config = options.config ?? defaultConfig;
|
|
@@ -90,6 +116,8 @@ export class DwnServer {
|
|
|
90
116
|
this.didResolver = options.didResolver;
|
|
91
117
|
this.dwn = options.dwn;
|
|
92
118
|
this.#externalHooks = options.messageProcessedHooks ?? [];
|
|
119
|
+
this.#externalRegistrationManager = options.registrationManager;
|
|
120
|
+
this.#externalOpenAuthHandler = options.openAuthHandler;
|
|
93
121
|
|
|
94
122
|
log.setLevel(this.config.logLevel as log.LogLevelDesc);
|
|
95
123
|
|
|
@@ -116,49 +144,18 @@ export class DwnServer {
|
|
|
116
144
|
*/
|
|
117
145
|
async #setupServer(): Promise<void> {
|
|
118
146
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Custom external plugin.
|
|
126
|
-
providerAuthPlugin = await loadProviderAuthPlugin(this.config.providerAuthPluginPath);
|
|
127
|
-
log.info('Provider auth plugin loaded from path');
|
|
128
|
-
} else if (this.config.providerAuthJwtSecret || this.config.providerAuthJwtJwksUrl) {
|
|
129
|
-
// Built-in JWT plugin.
|
|
130
|
-
providerAuthPlugin = await JwtProviderAuthPlugin.create({
|
|
131
|
-
secret : this.config.providerAuthJwtSecret,
|
|
132
|
-
jwksUrl : this.config.providerAuthJwtJwksUrl,
|
|
133
|
-
issuer : this.config.baseUrl,
|
|
134
|
-
audience : this.config.baseUrl,
|
|
135
|
-
});
|
|
136
|
-
log.info('Built-in JWT provider auth plugin created');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
147
|
+
// Run server migrations (admin stores, registration, TTL cache) FIRST,
|
|
148
|
+
// before creating any stores that depend on the server schema. The
|
|
149
|
+
// returned dialect is reused for the TTL cache and admin stores so that
|
|
150
|
+
// in-memory SQLite shares a single database instance across migrations
|
|
151
|
+
// and stores.
|
|
152
|
+
const serverDialect = await runServerMigrationsIfNeeded(this.config);
|
|
139
153
|
|
|
140
|
-
|
|
141
|
-
registrationManager = await RegistrationManager.create({
|
|
142
|
-
registrationStoreUrl : this.config.registrationStoreUrl,
|
|
143
|
-
termsOfServiceFilePath : this.config.termsOfServiceFilePath,
|
|
144
|
-
proofOfWorkChallengeNonceSeed : this.config.registrationProofOfWorkSeed,
|
|
145
|
-
proofOfWorkInitialMaximumAllowedHash : this.config.registrationProofOfWorkInitialMaxHash,
|
|
146
|
-
providerAuthPlugin,
|
|
147
|
-
});
|
|
154
|
+
let registrationManager: RegistrationManager | undefined;
|
|
148
155
|
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
if (this.config.registrationStoreUrl
|
|
153
|
-
&& !this.config.registrationProofOfWorkEnabled
|
|
154
|
-
&& !providerAuthPlugin) {
|
|
155
|
-
log.warn(
|
|
156
|
-
'*** WARNING: DWN_REGISTRATION_STORE_URL is set (tenant gate active) but neither ' +
|
|
157
|
-
'proof-of-work (DWN_REGISTRATION_PROOF_OF_WORK_ENABLED) nor provider auth ' +
|
|
158
|
-
'(DWN_PROVIDER_AUTH_ENABLED + secret/plugin) is configured. ' +
|
|
159
|
-
'New tenants will be unable to register. ***',
|
|
160
|
-
);
|
|
161
|
-
}
|
|
156
|
+
if (!this.dwn) {
|
|
157
|
+
// No pre-built DWN — create everything from scratch including registration.
|
|
158
|
+
registrationManager = await this.#createRegistrationManager();
|
|
162
159
|
|
|
163
160
|
let eventLog: EventLog | undefined;
|
|
164
161
|
if (this.config.webSocketSupport) {
|
|
@@ -186,6 +183,11 @@ export class DwnServer {
|
|
|
186
183
|
eventLog,
|
|
187
184
|
});
|
|
188
185
|
this.dwn = await Dwn.create(dwnConfig);
|
|
186
|
+
} else if (this.#externalRegistrationManager) {
|
|
187
|
+
// Pre-built DWN with an externally-provided RegistrationManager.
|
|
188
|
+
// The caller is responsible for passing this RegistrationManager as the
|
|
189
|
+
// TenantGate when creating the DWN instance.
|
|
190
|
+
registrationManager = this.#externalRegistrationManager;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
// Assemble message-processed hooks.
|
|
@@ -232,34 +234,45 @@ export class DwnServer {
|
|
|
232
234
|
let adminStore: AdminStore | undefined;
|
|
233
235
|
let auditLog: AuditLog | undefined;
|
|
234
236
|
let webhookManager: WebhookManager | undefined;
|
|
237
|
+
let passkeyStore: AdminPasskeyStore | undefined;
|
|
238
|
+
let sessionManager: AdminSessionManager | undefined;
|
|
235
239
|
|
|
236
240
|
if (this.config.adminToken) {
|
|
237
241
|
const storageUrl = this.config.messageStore;
|
|
238
242
|
adminStore = AdminStore.create(storageUrl);
|
|
239
243
|
activityLog = new ActivityLog(this.config.adminActivityLogCapacity);
|
|
240
244
|
|
|
241
|
-
//
|
|
242
|
-
//
|
|
245
|
+
// Reuse the dialect returned by server migrations when available — this
|
|
246
|
+
// is critical for in-memory SQLite where every `getDialectFromUrl` call
|
|
247
|
+
// creates a separate database. For Postgres, `serverDialect` points at
|
|
248
|
+
// the same shared pool, so reusing it also avoids pool proliferation.
|
|
243
249
|
if (this.config.registrationStoreUrl) {
|
|
250
|
+
const adminDialect = serverDialect ?? getDialectFromUrl(new URL(this.config.registrationStoreUrl));
|
|
251
|
+
|
|
244
252
|
try {
|
|
245
|
-
|
|
246
|
-
auditLog = await AuditLog.create(auditDialect, {
|
|
253
|
+
auditLog = await AuditLog.create(adminDialect, {
|
|
247
254
|
maxAgeDays : this.config.auditLogMaxAgeDays,
|
|
248
255
|
maxRows : this.config.auditLogMaxRows,
|
|
249
256
|
});
|
|
250
257
|
} catch (err) {
|
|
251
258
|
log.warn('Failed to create audit log:', err);
|
|
252
259
|
}
|
|
253
|
-
}
|
|
254
260
|
|
|
255
|
-
// Create webhook manager using the same dialect as the audit log.
|
|
256
|
-
if (this.config.registrationStoreUrl) {
|
|
257
261
|
try {
|
|
258
|
-
|
|
259
|
-
webhookManager = await WebhookManager.create(webhookDialect);
|
|
262
|
+
webhookManager = await WebhookManager.create(adminDialect);
|
|
260
263
|
} catch (err) {
|
|
261
264
|
log.warn('Failed to create webhook manager:', err);
|
|
262
265
|
}
|
|
266
|
+
|
|
267
|
+
// Create passkey store and session manager for WebAuthn admin auth.
|
|
268
|
+
// @see https://github.com/enboxorg/enbox/issues/546
|
|
269
|
+
try {
|
|
270
|
+
passkeyStore = await AdminPasskeyStore.create(adminDialect);
|
|
271
|
+
sessionManager = new AdminSessionManager(this.config.adminSessionTtlSeconds);
|
|
272
|
+
log.info('Admin passkey authentication enabled');
|
|
273
|
+
} catch (err) {
|
|
274
|
+
log.warn('Failed to create passkey store:', err);
|
|
275
|
+
}
|
|
263
276
|
}
|
|
264
277
|
|
|
265
278
|
adminApi = AdminApi.create({
|
|
@@ -273,6 +286,8 @@ export class DwnServer {
|
|
|
273
286
|
ipRateLimiter,
|
|
274
287
|
tenantRateLimiter,
|
|
275
288
|
webhookManager,
|
|
289
|
+
passkeyStore,
|
|
290
|
+
sessionManager,
|
|
276
291
|
});
|
|
277
292
|
|
|
278
293
|
// Record server start event in audit log.
|
|
@@ -288,17 +303,22 @@ export class DwnServer {
|
|
|
288
303
|
this.#ipRateLimiter = ipRateLimiter;
|
|
289
304
|
this.#tenantRateLimiter = tenantRateLimiter;
|
|
290
305
|
this.#auditLog = auditLog;
|
|
306
|
+
this.#passkeyStore = passkeyStore;
|
|
307
|
+
this.#sessionManager = sessionManager;
|
|
291
308
|
|
|
292
309
|
// Create open-auth handler if provider auth is enabled with a JWT secret
|
|
293
310
|
// and authorize/token URLs point to this server (or are not set — defaulting to built-in).
|
|
294
|
-
|
|
295
|
-
|
|
311
|
+
// An externally-provided handler (e.g. from the relay) takes precedence.
|
|
312
|
+
let openAuthHandler: OpenAuthHandler | undefined = this.#externalOpenAuthHandler;
|
|
313
|
+
if (!openAuthHandler && this.config.providerAuthEnabled && this.config.providerAuthJwtSecret && !this.config.providerAuthPluginPath) {
|
|
296
314
|
openAuthHandler = OpenAuthHandler.create(
|
|
297
315
|
this.config.providerAuthJwtSecret,
|
|
298
316
|
this.config.baseUrl,
|
|
299
317
|
);
|
|
300
318
|
log.info('Built-in open-auth endpoints enabled');
|
|
319
|
+
}
|
|
301
320
|
|
|
321
|
+
if (openAuthHandler) {
|
|
302
322
|
// Auto-configure authorize/token/refresh URLs if not explicitly set.
|
|
303
323
|
if (!this.config.providerAuthAuthorizeUrl) {
|
|
304
324
|
this.config.providerAuthAuthorizeUrl = `${this.config.baseUrl}/provider-auth/authorize`;
|
|
@@ -313,7 +333,11 @@ export class DwnServer {
|
|
|
313
333
|
|
|
314
334
|
this.#httpApi = await HttpApi.create(
|
|
315
335
|
this.config, this.dwn, registrationManager, adminApi, activityLog,
|
|
316
|
-
{
|
|
336
|
+
{
|
|
337
|
+
adminStore, registrationStore, ipRateLimiter, tenantRateLimiter,
|
|
338
|
+
messageProcessedHooks, openAuthHandler, sessionManager,
|
|
339
|
+
ttlCacheDialect: serverDialect,
|
|
340
|
+
},
|
|
317
341
|
);
|
|
318
342
|
|
|
319
343
|
await this.#httpApi.start(this.config.port);
|
|
@@ -339,6 +363,56 @@ export class DwnServer {
|
|
|
339
363
|
}
|
|
340
364
|
}
|
|
341
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Creates a RegistrationManager based on the server config. Factored out of
|
|
368
|
+
* `#setupServer()` so the same logic can be reused regardless of whether the
|
|
369
|
+
* DWN is created internally or externally.
|
|
370
|
+
*/
|
|
371
|
+
async #createRegistrationManager(): Promise<RegistrationManager> {
|
|
372
|
+
// Load provider auth plugin if configured.
|
|
373
|
+
let providerAuthPlugin: ProviderAuthPlugin | undefined;
|
|
374
|
+
if (this.config.providerAuthEnabled) {
|
|
375
|
+
if (this.config.providerAuthPluginPath) {
|
|
376
|
+
// Custom external plugin.
|
|
377
|
+
providerAuthPlugin = await loadProviderAuthPlugin(this.config.providerAuthPluginPath);
|
|
378
|
+
log.info('Provider auth plugin loaded from path');
|
|
379
|
+
} else if (this.config.providerAuthJwtSecret || this.config.providerAuthJwtJwksUrl) {
|
|
380
|
+
// Built-in JWT plugin.
|
|
381
|
+
providerAuthPlugin = await JwtProviderAuthPlugin.create({
|
|
382
|
+
secret : this.config.providerAuthJwtSecret,
|
|
383
|
+
jwksUrl : this.config.providerAuthJwtJwksUrl,
|
|
384
|
+
issuer : this.config.baseUrl,
|
|
385
|
+
audience : this.config.baseUrl,
|
|
386
|
+
});
|
|
387
|
+
log.info('Built-in JWT provider auth plugin created');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// undefined registrationStoreUrl is used as a signal that there is no need
|
|
392
|
+
// for tenant registration, DWN is open for all.
|
|
393
|
+
const registrationManager = await RegistrationManager.create({
|
|
394
|
+
registrationStoreUrl : this.config.registrationStoreUrl,
|
|
395
|
+
termsOfServiceFilePath : this.config.termsOfServiceFilePath,
|
|
396
|
+
proofOfWorkChallengeNonceSeed : this.config.registrationProofOfWorkSeed,
|
|
397
|
+
proofOfWorkInitialMaximumAllowedHash : this.config.registrationProofOfWorkInitialMaxHash,
|
|
398
|
+
providerAuthPlugin,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Warn if the tenant gate is active but no registration method is enabled.
|
|
402
|
+
if (this.config.registrationStoreUrl
|
|
403
|
+
&& !this.config.registrationProofOfWorkEnabled
|
|
404
|
+
&& !providerAuthPlugin) {
|
|
405
|
+
log.warn(
|
|
406
|
+
'*** WARNING: DWN_REGISTRATION_STORE_URL is set (tenant gate active) but neither ' +
|
|
407
|
+
'proof-of-work (DWN_REGISTRATION_PROOF_OF_WORK_ENABLED) nor provider auth ' +
|
|
408
|
+
'(DWN_PROVIDER_AUTH_ENABLED + secret/plugin) is configured. ' +
|
|
409
|
+
'New tenants will be unable to register. ***',
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return registrationManager;
|
|
414
|
+
}
|
|
415
|
+
|
|
342
416
|
/**
|
|
343
417
|
* Stops the DWN server.
|
|
344
418
|
*/
|
|
@@ -356,6 +430,14 @@ export class DwnServer {
|
|
|
356
430
|
await this.#auditLog.close();
|
|
357
431
|
}
|
|
358
432
|
|
|
433
|
+
// Clean up passkey store and session manager.
|
|
434
|
+
if (this.#passkeyStore) {
|
|
435
|
+
await this.#passkeyStore.close();
|
|
436
|
+
}
|
|
437
|
+
if (this.#sessionManager) {
|
|
438
|
+
this.#sessionManager.destroy();
|
|
439
|
+
}
|
|
440
|
+
|
|
359
441
|
// Clean up rate limiters (stops their interval timers).
|
|
360
442
|
if (this.#ipRateLimiter) {
|
|
361
443
|
this.#ipRateLimiter.destroy();
|
package/src/http-api.ts
CHANGED
|
@@ -3,8 +3,11 @@ import type { RecordsReadReply } from '@enbox/dwn-sdk-js';
|
|
|
3
3
|
import type { ServerInfo } from '@enbox/dwn-clients';
|
|
4
4
|
import type { Server, ServerWebSocket } from 'bun';
|
|
5
5
|
|
|
6
|
+
import type { Dialect } from '@enbox/dwn-sql-store';
|
|
7
|
+
|
|
6
8
|
import type { ActivityLog } from './admin/activity-log.js';
|
|
7
9
|
import type { AdminApi } from './admin/admin-api.js';
|
|
10
|
+
import type { AdminSessionManager } from './admin/admin-session.js';
|
|
8
11
|
import type { AdminStore } from './admin/admin-store.js';
|
|
9
12
|
import type { DwnServerConfig } from './config.js';
|
|
10
13
|
import type { DwnServerError } from './dwn-error.js';
|
|
@@ -27,6 +30,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
27
30
|
import { join, resolve } from 'path';
|
|
28
31
|
|
|
29
32
|
import { config } from './config.js';
|
|
33
|
+
import { getDialectFromUrl } from './storage.js';
|
|
30
34
|
import { jsonRpcRouter } from './json-rpc-api.js';
|
|
31
35
|
import { validateAdminAuth } from './admin/admin-auth.js';
|
|
32
36
|
import { Web5ConnectServer } from './web5-connect/web5-connect-server.js';
|
|
@@ -62,6 +66,7 @@ export class HttpApi {
|
|
|
62
66
|
#tenantRateLimiter: RateLimiter | undefined;
|
|
63
67
|
#messageProcessedHooks: MessageProcessedHook[];
|
|
64
68
|
#openAuthHandler: OpenAuthHandler | undefined;
|
|
69
|
+
#sessionManager: AdminSessionManager | undefined;
|
|
65
70
|
#adminUiPath: string | undefined;
|
|
66
71
|
web5ConnectServer: Web5ConnectServer;
|
|
67
72
|
registrationManager: RegistrationManager;
|
|
@@ -82,6 +87,8 @@ export class HttpApi {
|
|
|
82
87
|
tenantRateLimiter? : RateLimiter;
|
|
83
88
|
messageProcessedHooks? : MessageProcessedHook[];
|
|
84
89
|
openAuthHandler? : OpenAuthHandler;
|
|
90
|
+
sessionManager? : AdminSessionManager;
|
|
91
|
+
ttlCacheDialect? : Dialect;
|
|
85
92
|
},
|
|
86
93
|
): Promise<HttpApi> {
|
|
87
94
|
const httpApi = new HttpApi();
|
|
@@ -119,15 +126,20 @@ export class HttpApi {
|
|
|
119
126
|
httpApi.#tenantRateLimiter = options?.tenantRateLimiter;
|
|
120
127
|
httpApi.#messageProcessedHooks = options?.messageProcessedHooks ?? [];
|
|
121
128
|
httpApi.#openAuthHandler = options?.openAuthHandler;
|
|
129
|
+
httpApi.#sessionManager = options?.sessionManager;
|
|
122
130
|
httpApi.#adminUiPath = resolvedAdminUiPath;
|
|
123
131
|
|
|
124
132
|
if (registrationManager !== undefined) {
|
|
125
133
|
httpApi.registrationManager = registrationManager;
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
// Use an externally provided dialect when available (required for
|
|
137
|
+
// in-memory SQLite so that migrations and the TTL cache share the same
|
|
138
|
+
// database instance). Falls back to creating a dialect from the URL.
|
|
139
|
+
const ttlDialect = options?.ttlCacheDialect ?? getDialectFromUrl(new URL(config.ttlCacheUrl));
|
|
128
140
|
httpApi.web5ConnectServer = await Web5ConnectServer.create({
|
|
129
|
-
baseUrl
|
|
130
|
-
|
|
141
|
+
baseUrl : config.baseUrl,
|
|
142
|
+
sqlDialect : ttlDialect,
|
|
131
143
|
});
|
|
132
144
|
|
|
133
145
|
return httpApi;
|
|
@@ -258,6 +270,9 @@ export class HttpApi {
|
|
|
258
270
|
if (this.#openAuthHandler) {
|
|
259
271
|
this.#openAuthHandler.destroy();
|
|
260
272
|
}
|
|
273
|
+
if (this.web5ConnectServer) {
|
|
274
|
+
this.web5ConnectServer.close();
|
|
275
|
+
}
|
|
261
276
|
if (this.#server) {
|
|
262
277
|
this.#server.stop(true); // close all connections immediately
|
|
263
278
|
}
|
|
@@ -321,9 +336,9 @@ export class HttpApi {
|
|
|
321
336
|
if (method === 'GET' && path === '/metrics') {
|
|
322
337
|
// Metrics require admin authentication when an admin token is configured.
|
|
323
338
|
if (this.#config.adminToken) {
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
326
|
-
return
|
|
339
|
+
const authResult = validateAdminAuth(req, this.#config, this.#sessionManager);
|
|
340
|
+
if (authResult.error) {
|
|
341
|
+
return authResult.error;
|
|
327
342
|
}
|
|
328
343
|
}
|
|
329
344
|
try {
|
package/src/index.ts
CHANGED
|
@@ -13,5 +13,7 @@ export { DwnServer, DwnServerOptions } from './dwn-server.js';
|
|
|
13
13
|
export { HttpApi } from './http-api.js';
|
|
14
14
|
export { jsonRpcRouter } from './json-rpc-api.js';
|
|
15
15
|
export type { MessageProcessedContext, MessageProcessedHook } from './message-processed-hook.js';
|
|
16
|
+
export { OpenAuthHandler } from './registration/open-auth-handler.js';
|
|
17
|
+
export { RegistrationManager } from './registration/registration-manager.js';
|
|
16
18
|
export { getDwnConfig, StoreType, BackendTypes, DwnStore } from './storage.js';
|
|
17
19
|
export { WsApi } from './ws-api.js';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Dialect } from '@enbox/dwn-sql-store';
|
|
2
|
+
import type { Kysely, Migration } from 'kysely';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory type for server migrations. Mirrors the `DwnMigrationFactory` from
|
|
6
|
+
* `@enbox/dwn-sql-store` — receives the {@link Dialect} so migrations can use
|
|
7
|
+
* dialect-specific helpers like `addAutoIncrementingColumn()`.
|
|
8
|
+
*/
|
|
9
|
+
export type ServerMigrationFactory = (dialect: Dialect) => Migration;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Baseline server migration: creates all DWN server admin and cache tables.
|
|
13
|
+
*
|
|
14
|
+
* Tables:
|
|
15
|
+
* - `registeredTenants`: tenant registration data
|
|
16
|
+
* - `tenantQuotas`: per-tenant storage quotas
|
|
17
|
+
* - `adminAuditLog`: append-only admin event log
|
|
18
|
+
* - `adminWebhooks`: webhook registrations
|
|
19
|
+
* - `adminPasskeys`: WebAuthn admin passkeys
|
|
20
|
+
* - `cacheEntries`: TTL-based key/value cache (Web5 Connect state, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export const migration001InitialServerSchema: ServerMigrationFactory = (dialect): Migration => ({
|
|
23
|
+
|
|
24
|
+
async up(db: Kysely<any>): Promise<void> {
|
|
25
|
+
|
|
26
|
+
// ─── registeredTenants ────────────────────────────────────────────
|
|
27
|
+
await db.schema
|
|
28
|
+
.createTable('registeredTenants')
|
|
29
|
+
.ifNotExists()
|
|
30
|
+
.addColumn('did', 'text', (col) => col.primaryKey())
|
|
31
|
+
.addColumn('termsOfServiceHash', 'text')
|
|
32
|
+
.addColumn('suspended', 'integer', (col) => col.defaultTo(0))
|
|
33
|
+
.addColumn('accountId', 'text')
|
|
34
|
+
.addColumn('registrationType', 'text')
|
|
35
|
+
.addColumn('registeredAt', 'text')
|
|
36
|
+
.addColumn('metadata', 'text')
|
|
37
|
+
.execute();
|
|
38
|
+
|
|
39
|
+
// ─── tenantQuotas ─────────────────────────────────────────────────
|
|
40
|
+
await db.schema
|
|
41
|
+
.createTable('tenantQuotas')
|
|
42
|
+
.ifNotExists()
|
|
43
|
+
.addColumn('did', 'text', (col) => col.primaryKey())
|
|
44
|
+
.addColumn('maxMessages', 'integer', (col) => col.defaultTo(0))
|
|
45
|
+
.addColumn('maxStorageBytes', 'bigint', (col) => col.defaultTo(0))
|
|
46
|
+
.execute();
|
|
47
|
+
|
|
48
|
+
// ─── adminAuditLog ────────────────────────────────────────────────
|
|
49
|
+
let auditTable = db.schema
|
|
50
|
+
.createTable('adminAuditLog')
|
|
51
|
+
.ifNotExists()
|
|
52
|
+
.addColumn('timestamp', 'text', (col) => col.notNull())
|
|
53
|
+
.addColumn('actor', 'text', (col) => col.notNull())
|
|
54
|
+
.addColumn('action', 'text', (col) => col.notNull())
|
|
55
|
+
.addColumn('target', 'text')
|
|
56
|
+
.addColumn('detail', 'text');
|
|
57
|
+
|
|
58
|
+
auditTable = dialect.addAutoIncrementingColumn(auditTable, 'id', (col) => col.primaryKey());
|
|
59
|
+
await auditTable.execute();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await db.schema.createIndex('index_audit_timestamp')
|
|
63
|
+
.ifNotExists().on('adminAuditLog').column('timestamp').execute();
|
|
64
|
+
} catch { /* index already exists */ }
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await db.schema.createIndex('index_audit_target')
|
|
68
|
+
.ifNotExists().on('adminAuditLog').column('target').execute();
|
|
69
|
+
} catch { /* index already exists */ }
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await db.schema.createIndex('index_audit_action')
|
|
73
|
+
.ifNotExists().on('adminAuditLog').column('action').execute();
|
|
74
|
+
} catch { /* index already exists */ }
|
|
75
|
+
|
|
76
|
+
// ─── adminWebhooks ────────────────────────────────────────────────
|
|
77
|
+
await db.schema
|
|
78
|
+
.createTable('adminWebhooks')
|
|
79
|
+
.ifNotExists()
|
|
80
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
81
|
+
.addColumn('url', 'text', (col) => col.notNull())
|
|
82
|
+
.addColumn('events', 'text', (col) => col.notNull())
|
|
83
|
+
.addColumn('secret', 'text')
|
|
84
|
+
.addColumn('createdAt', 'text', (col) => col.notNull())
|
|
85
|
+
.execute();
|
|
86
|
+
|
|
87
|
+
// ─── adminPasskeys ────────────────────────────────────────────────
|
|
88
|
+
await db.schema
|
|
89
|
+
.createTable('adminPasskeys')
|
|
90
|
+
.ifNotExists()
|
|
91
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
92
|
+
.addColumn('name', 'text', (col) => col.notNull())
|
|
93
|
+
.addColumn('publicKey', 'text', (col) => col.notNull())
|
|
94
|
+
.addColumn('counter', 'integer', (col) => col.notNull().defaultTo(0))
|
|
95
|
+
.addColumn('transports', 'text', (col) => col.notNull().defaultTo('[]'))
|
|
96
|
+
.addColumn('createdAt', 'text', (col) => col.notNull())
|
|
97
|
+
.addColumn('lastUsedAt', 'text')
|
|
98
|
+
.execute();
|
|
99
|
+
|
|
100
|
+
// ─── cacheEntries (TTL cache) ─────────────────────────────────────
|
|
101
|
+
await db.schema
|
|
102
|
+
.createTable('cacheEntries')
|
|
103
|
+
.ifNotExists()
|
|
104
|
+
.addColumn('key', 'varchar(512)', (col) => col.primaryKey())
|
|
105
|
+
.addColumn('value', 'text', (col) => col.notNull())
|
|
106
|
+
.addColumn('expiry', 'bigint', (col) => col.notNull())
|
|
107
|
+
.execute();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await db.schema.createIndex('index_expiry')
|
|
111
|
+
.ifNotExists().on('cacheEntries').column('expiry').execute();
|
|
112
|
+
} catch { /* index already exists */ }
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ServerMigrationFactory } from './001-initial-server-schema.js';
|
|
2
|
+
|
|
3
|
+
import { migration001InitialServerSchema } from './001-initial-server-schema.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* All DWN server migrations in sequential order.
|
|
7
|
+
*
|
|
8
|
+
* Each entry is a `[name, factory]` tuple where the factory receives the
|
|
9
|
+
* `Dialect` and returns a standard Kysely `Migration`. This mirrors the
|
|
10
|
+
* pattern used by DWN store migrations in `@enbox/dwn-sql-store`.
|
|
11
|
+
*
|
|
12
|
+
* **Ordering contract:** Entries MUST be sorted by name (lexicographic).
|
|
13
|
+
*/
|
|
14
|
+
export type { ServerMigrationFactory };
|
|
15
|
+
|
|
16
|
+
export const allServerMigrations: ReadonlyArray<readonly [name: string, factory: ServerMigrationFactory]> = [
|
|
17
|
+
['001-initial-server-schema', migration001InitialServerSchema],
|
|
18
|
+
];
|
|
@@ -29,47 +29,24 @@ export class RegistrationStore {
|
|
|
29
29
|
return store;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Verifies that the required tables exist. Throws a clear error directing
|
|
34
|
+
* the caller to run server migrations first.
|
|
35
|
+
*/
|
|
32
36
|
private async initialize(): Promise<void> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.addColumn('suspended', 'integer', (column) => column.defaultTo(0))
|
|
39
|
-
.execute();
|
|
40
|
-
|
|
41
|
-
// Add the `suspended` column to existing tables that don't have it yet.
|
|
42
|
-
// Kysely doesn't support `ADD COLUMN IF NOT EXISTS` across all dialects, so we
|
|
43
|
-
// catch and ignore the "column already exists" error.
|
|
44
|
-
try {
|
|
45
|
-
await this.db.schema
|
|
46
|
-
.alterTable(RegistrationStore.registeredTenantTableName)
|
|
47
|
-
.addColumn('suspended', 'integer', (column) => column.defaultTo(0))
|
|
48
|
-
.execute();
|
|
49
|
-
} catch {
|
|
50
|
-
// Column already exists — expected for new installations.
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Add provider-auth columns (idempotent migration). https://github.com/enboxorg/enbox/issues/404
|
|
54
|
-
for (const col of ['accountId', 'registrationType', 'registeredAt', 'metadata']) {
|
|
37
|
+
const tables = [
|
|
38
|
+
RegistrationStore.registeredTenantTableName,
|
|
39
|
+
RegistrationStore.tenantQuotasTableName,
|
|
40
|
+
];
|
|
41
|
+
for (const table of tables) {
|
|
55
42
|
try {
|
|
56
|
-
await this.db
|
|
57
|
-
.alterTable(RegistrationStore.registeredTenantTableName)
|
|
58
|
-
.addColumn(col, 'text')
|
|
59
|
-
.execute();
|
|
43
|
+
await sql`SELECT 1 FROM ${sql.table(table)} LIMIT 0`.execute(this.db);
|
|
60
44
|
} catch {
|
|
61
|
-
|
|
45
|
+
throw new Error(
|
|
46
|
+
`RegistrationStore: table '${table}' does not exist. Run server migrations before starting.`
|
|
47
|
+
);
|
|
62
48
|
}
|
|
63
49
|
}
|
|
64
|
-
|
|
65
|
-
// Per-tenant storage quotas table.
|
|
66
|
-
await this.db.schema
|
|
67
|
-
.createTable(RegistrationStore.tenantQuotasTableName)
|
|
68
|
-
.ifNotExists()
|
|
69
|
-
.addColumn('did', 'text', (column) => column.primaryKey())
|
|
70
|
-
.addColumn('maxMessages', 'integer', (column) => column.defaultTo(0))
|
|
71
|
-
.addColumn('maxStorageBytes', 'bigint', (column) => column.defaultTo(0))
|
|
72
|
-
.execute();
|
|
73
50
|
}
|
|
74
51
|
|
|
75
52
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Dialect } from '@enbox/dwn-sql-store';
|
|
2
|
+
import type { ServerMigrationFactory } from './migrations/index.js';
|
|
3
|
+
import type { Kysely, Migration, MigrationProvider, MigrationResultSet } from 'kysely';
|
|
4
|
+
|
|
5
|
+
import { allServerMigrations } from './migrations/index.js';
|
|
6
|
+
import { Migrator } from 'kysely';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* {@link MigrationProvider} for server migrations. Wraps an ordered list of
|
|
10
|
+
* `(name, factory)` pairs. At resolution time each factory is called with the
|
|
11
|
+
* dialect, producing the concrete Kysely {@link Migration} objects.
|
|
12
|
+
*/
|
|
13
|
+
class ServerMigrationProvider implements MigrationProvider {
|
|
14
|
+
#dialect: Dialect;
|
|
15
|
+
#factories: ReadonlyArray<readonly [name: string, factory: ServerMigrationFactory]>;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
dialect: Dialect,
|
|
19
|
+
factories: ReadonlyArray<readonly [name: string, factory: ServerMigrationFactory]>,
|
|
20
|
+
) {
|
|
21
|
+
this.#dialect = dialect;
|
|
22
|
+
this.#factories = factories;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async getMigrations(): Promise<Record<string, Migration>> {
|
|
26
|
+
const migrations: Record<string, Migration> = {};
|
|
27
|
+
for (const [name, factory] of this.#factories) {
|
|
28
|
+
migrations[name] = factory(this.#dialect);
|
|
29
|
+
}
|
|
30
|
+
return migrations;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runs all pending DWN server migrations against the given database.
|
|
36
|
+
*
|
|
37
|
+
* Uses Kysely's native {@link Migrator} with a separate migration table
|
|
38
|
+
* (`dwn_server_migration`) to avoid collisions with the DWN store
|
|
39
|
+
* migrations that use the default `kysely_migration` table.
|
|
40
|
+
*
|
|
41
|
+
* Call this once during server startup, before creating admin stores,
|
|
42
|
+
* registration stores, or the TTL cache.
|
|
43
|
+
*
|
|
44
|
+
* @param db - An open Kysely instance connected to the target database.
|
|
45
|
+
* @param dialect - The dialect for the target database. Passed to each
|
|
46
|
+
* migration factory so it can use dialect-specific DDL helpers.
|
|
47
|
+
* @param migrations - Optional custom migration list; defaults to the
|
|
48
|
+
* built-in {@link allServerMigrations}.
|
|
49
|
+
* @returns The names of newly applied migrations (empty if already up-to-date).
|
|
50
|
+
* @throws If any migration fails.
|
|
51
|
+
*/
|
|
52
|
+
export async function runServerMigrations(
|
|
53
|
+
db: Kysely<any>,
|
|
54
|
+
dialect: Dialect,
|
|
55
|
+
migrations?: ReadonlyArray<readonly [name: string, factory: ServerMigrationFactory]>,
|
|
56
|
+
): Promise<string[]> {
|
|
57
|
+
const provider = new ServerMigrationProvider(dialect, migrations ?? allServerMigrations);
|
|
58
|
+
const migrator = new Migrator({
|
|
59
|
+
db,
|
|
60
|
+
provider,
|
|
61
|
+
migrationTableName : 'dwn_server_migration',
|
|
62
|
+
migrationLockTableName : 'dwn_server_migration_lock',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const resultSet: MigrationResultSet = await migrator.migrateToLatest();
|
|
66
|
+
|
|
67
|
+
if (resultSet.error) {
|
|
68
|
+
throw resultSet.error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (resultSet.results ?? [])
|
|
72
|
+
.filter((r) => r.status === 'Success')
|
|
73
|
+
.map((r) => r.migrationName);
|
|
74
|
+
}
|