@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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Dialect } from '@enbox/dwn-sql-store';
|
|
2
|
+
|
|
3
|
+
import { Kysely, sql } from 'kysely';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A registered WebAuthn credential (passkey) for admin access.
|
|
7
|
+
*/
|
|
8
|
+
export type AdminPasskeyRecord = {
|
|
9
|
+
/** Base64url-encoded credential ID. */
|
|
10
|
+
id : string;
|
|
11
|
+
/** Human-readable name for the passkey (e.g. "MacBook Touch ID"). */
|
|
12
|
+
name : string;
|
|
13
|
+
/** Base64url-encoded COSE public key. */
|
|
14
|
+
publicKey : string;
|
|
15
|
+
/** Monotonically increasing counter for replay protection. */
|
|
16
|
+
counter : number;
|
|
17
|
+
/** WebAuthn transports the credential supports (JSON-encoded string array). */
|
|
18
|
+
transports : string;
|
|
19
|
+
/** ISO-8601 timestamp when the passkey was registered. */
|
|
20
|
+
createdAt : string;
|
|
21
|
+
/** ISO-8601 timestamp of the most recent successful authentication. */
|
|
22
|
+
lastUsedAt : string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SQL-backed credential store for WebAuthn passkeys used by the admin panel.
|
|
27
|
+
*
|
|
28
|
+
* Follows the same Kysely + Dialect pattern as {@link AuditLog} and
|
|
29
|
+
* {@link WebhookManager}. The `adminPasskeys` table is created automatically
|
|
30
|
+
* on first use.
|
|
31
|
+
*
|
|
32
|
+
* Future: migrate to DID/DWN-based auth (see https://github.com/enboxorg/enbox/issues/546).
|
|
33
|
+
*/
|
|
34
|
+
export class AdminPasskeyStore {
|
|
35
|
+
static readonly #tableName = 'adminPasskeys';
|
|
36
|
+
|
|
37
|
+
#db: Kysely<PasskeyDatabase>;
|
|
38
|
+
|
|
39
|
+
private constructor(dialect: Dialect) {
|
|
40
|
+
this.#db = new Kysely<PasskeyDatabase>({ dialect });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates and initializes an `AdminPasskeyStore` instance.
|
|
45
|
+
* Creates the `adminPasskeys` table if it does not already exist.
|
|
46
|
+
*/
|
|
47
|
+
public static async create(dialect: Dialect): Promise<AdminPasskeyStore> {
|
|
48
|
+
const store = new AdminPasskeyStore(dialect);
|
|
49
|
+
await store.#initialize();
|
|
50
|
+
return store;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verifies that the required table exists. Throws a clear error directing
|
|
55
|
+
* the caller to run server migrations first.
|
|
56
|
+
*/
|
|
57
|
+
async #initialize(): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
await sql`SELECT 1 FROM ${sql.table(AdminPasskeyStore.#tableName)} LIMIT 0`.execute(this.#db);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`AdminPasskeyStore: table '${AdminPasskeyStore.#tableName}' does not exist. Run server migrations before starting.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stores a new passkey credential.
|
|
69
|
+
*/
|
|
70
|
+
public async save(record: AdminPasskeyRecord): Promise<void> {
|
|
71
|
+
await this.#db
|
|
72
|
+
.insertInto(AdminPasskeyStore.#tableName)
|
|
73
|
+
.values({
|
|
74
|
+
id : record.id,
|
|
75
|
+
name : record.name,
|
|
76
|
+
publicKey : record.publicKey,
|
|
77
|
+
counter : record.counter,
|
|
78
|
+
transports : record.transports,
|
|
79
|
+
createdAt : record.createdAt,
|
|
80
|
+
lastUsedAt : record.lastUsedAt ?? null,
|
|
81
|
+
})
|
|
82
|
+
.execute();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Retrieves a passkey by its credential ID.
|
|
87
|
+
*/
|
|
88
|
+
public async getById(id: string): Promise<AdminPasskeyRecord | undefined> {
|
|
89
|
+
const row = await this.#db
|
|
90
|
+
.selectFrom(AdminPasskeyStore.#tableName)
|
|
91
|
+
.selectAll()
|
|
92
|
+
.where('id', '=', id)
|
|
93
|
+
.executeTakeFirst();
|
|
94
|
+
|
|
95
|
+
if (!row) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.#toRecord(row);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns all registered passkeys, ordered by creation date (newest first).
|
|
104
|
+
*/
|
|
105
|
+
public async list(): Promise<AdminPasskeyRecord[]> {
|
|
106
|
+
const rows = await this.#db
|
|
107
|
+
.selectFrom(AdminPasskeyStore.#tableName)
|
|
108
|
+
.selectAll()
|
|
109
|
+
.orderBy('createdAt', 'desc')
|
|
110
|
+
.execute();
|
|
111
|
+
|
|
112
|
+
return rows.map((row): AdminPasskeyRecord => this.#toRecord(row));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the count of registered passkeys.
|
|
117
|
+
*/
|
|
118
|
+
public async count(): Promise<number> {
|
|
119
|
+
const result = await this.#db
|
|
120
|
+
.selectFrom(AdminPasskeyStore.#tableName)
|
|
121
|
+
.select(this.#db.fn.countAll<number>().as('count'))
|
|
122
|
+
.executeTakeFirstOrThrow();
|
|
123
|
+
|
|
124
|
+
return Number(result.count);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Updates the counter and last-used timestamp after a successful authentication.
|
|
129
|
+
*/
|
|
130
|
+
public async updateCounter(id: string, counter: number): Promise<void> {
|
|
131
|
+
await this.#db
|
|
132
|
+
.updateTable(AdminPasskeyStore.#tableName)
|
|
133
|
+
.set({
|
|
134
|
+
counter,
|
|
135
|
+
lastUsedAt: new Date().toISOString(),
|
|
136
|
+
})
|
|
137
|
+
.where('id', '=', id)
|
|
138
|
+
.execute();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Deletes a passkey by its credential ID.
|
|
143
|
+
* @returns `true` if the passkey was found and deleted.
|
|
144
|
+
*/
|
|
145
|
+
public async delete(id: string): Promise<boolean> {
|
|
146
|
+
const result = await this.#db
|
|
147
|
+
.deleteFrom(AdminPasskeyStore.#tableName)
|
|
148
|
+
.where('id', '=', id)
|
|
149
|
+
.executeTakeFirstOrThrow();
|
|
150
|
+
|
|
151
|
+
return Number(result.numDeletedRows) > 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Closes the underlying database connection.
|
|
156
|
+
*/
|
|
157
|
+
public async close(): Promise<void> {
|
|
158
|
+
await this.#db.destroy();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#toRecord(row: PasskeyRow): AdminPasskeyRecord {
|
|
162
|
+
return {
|
|
163
|
+
id : row.id,
|
|
164
|
+
name : row.name,
|
|
165
|
+
publicKey : row.publicKey,
|
|
166
|
+
counter : Number(row.counter),
|
|
167
|
+
transports : row.transports,
|
|
168
|
+
createdAt : row.createdAt,
|
|
169
|
+
lastUsedAt : row.lastUsedAt ?? null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Kysely type definitions
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
interface PasskeyRow {
|
|
179
|
+
id : string;
|
|
180
|
+
name : string;
|
|
181
|
+
publicKey : string;
|
|
182
|
+
counter : number;
|
|
183
|
+
transports : string;
|
|
184
|
+
createdAt : string;
|
|
185
|
+
lastUsedAt : string | null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface PasskeyDatabase {
|
|
189
|
+
adminPasskeys : PasskeyRow;
|
|
190
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A lightweight in-memory session entry.
|
|
5
|
+
*/
|
|
6
|
+
type SessionEntry = {
|
|
7
|
+
/** Opaque session token (hex string). */
|
|
8
|
+
token : string;
|
|
9
|
+
/** Timestamp (ms) when the session was created. */
|
|
10
|
+
createdAt : number;
|
|
11
|
+
/** Timestamp (ms) when the session expires. */
|
|
12
|
+
expiresAt : number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Manages short-lived sessions for admin passkey authentication.
|
|
17
|
+
*
|
|
18
|
+
* Sessions are opaque hex tokens stored in an in-memory `Map`. They are
|
|
19
|
+
* validated in {@link validateAdminAuth} as an alternative to the static
|
|
20
|
+
* bearer token. Expired sessions are lazily pruned on every create/validate
|
|
21
|
+
* call and periodically via a cleanup interval.
|
|
22
|
+
*
|
|
23
|
+
* Future: migrate to DID/DWN-based auth (see https://github.com/enboxorg/enbox/issues/546).
|
|
24
|
+
*/
|
|
25
|
+
export class AdminSessionManager {
|
|
26
|
+
/** Default session time-to-live: 24 hours (in seconds). */
|
|
27
|
+
static readonly #DEFAULT_TTL_SECONDS = 86400;
|
|
28
|
+
/** Cleanup runs every 10 minutes. */
|
|
29
|
+
static readonly #CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
#sessions: Map<string, SessionEntry> = new Map();
|
|
32
|
+
#ttlMs: number;
|
|
33
|
+
#cleanupInterval: ReturnType<typeof setInterval> | undefined;
|
|
34
|
+
|
|
35
|
+
constructor(ttlSeconds?: number) {
|
|
36
|
+
this.#ttlMs = (ttlSeconds ?? AdminSessionManager.#DEFAULT_TTL_SECONDS) * 1000;
|
|
37
|
+
this.#startCleanup();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new session and returns the opaque token.
|
|
42
|
+
*/
|
|
43
|
+
public create(): string {
|
|
44
|
+
this.#prune();
|
|
45
|
+
|
|
46
|
+
const token = randomBytes(32).toString('hex');
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
this.#sessions.set(token, {
|
|
49
|
+
token,
|
|
50
|
+
createdAt : now,
|
|
51
|
+
expiresAt : now + this.#ttlMs,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates a session token. Returns `true` if the token is valid and not expired.
|
|
59
|
+
*/
|
|
60
|
+
public validate(token: string): boolean {
|
|
61
|
+
const entry = this.#sessions.get(token);
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (Date.now() >= entry.expiresAt) {
|
|
67
|
+
this.#sessions.delete(token);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Revokes a session token.
|
|
76
|
+
*/
|
|
77
|
+
public revoke(token: string): void {
|
|
78
|
+
this.#sessions.delete(token);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns the number of active (non-expired) sessions.
|
|
83
|
+
*/
|
|
84
|
+
public get size(): number {
|
|
85
|
+
return this.#sessions.size;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stops the periodic cleanup timer.
|
|
90
|
+
*/
|
|
91
|
+
public destroy(): void {
|
|
92
|
+
if (this.#cleanupInterval) {
|
|
93
|
+
clearInterval(this.#cleanupInterval);
|
|
94
|
+
this.#cleanupInterval = undefined;
|
|
95
|
+
}
|
|
96
|
+
this.#sessions.clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes all expired sessions.
|
|
101
|
+
*/
|
|
102
|
+
#prune(): void {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
for (const [token, entry] of this.#sessions) {
|
|
105
|
+
if (now >= entry.expiresAt) {
|
|
106
|
+
this.#sessions.delete(token);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#startCleanup(): void {
|
|
112
|
+
this.#cleanupInterval = setInterval((): void => {
|
|
113
|
+
this.#prune();
|
|
114
|
+
}, AdminSessionManager.#CLEANUP_INTERVAL_MS);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/admin/audit-log.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Dialect } from '@enbox/dwn-sql-store';
|
|
2
2
|
|
|
3
|
-
import { Kysely } from 'kysely';
|
|
4
3
|
import log from 'loglevel';
|
|
4
|
+
import { Kysely, sql } from 'kysely';
|
|
5
5
|
|
|
6
6
|
import { escapeLikeWildcards } from '../lib/sql-utils.js';
|
|
7
7
|
|
|
@@ -96,53 +96,16 @@ export class AuditLog {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
99
|
+
* Verifies that the required table exists. Throws a clear error directing
|
|
100
|
+
* the caller to run server migrations first.
|
|
100
101
|
*/
|
|
101
102
|
async #initialize(): Promise<void> {
|
|
102
|
-
await this.#db.schema
|
|
103
|
-
.createTable(AuditLog.#tableName)
|
|
104
|
-
.ifNotExists()
|
|
105
|
-
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
|
|
106
|
-
.addColumn('timestamp', 'text', (col) => col.notNull())
|
|
107
|
-
.addColumn('actor', 'text', (col) => col.notNull())
|
|
108
|
-
.addColumn('action', 'text', (col) => col.notNull())
|
|
109
|
-
.addColumn('target', 'text')
|
|
110
|
-
.addColumn('detail', 'text')
|
|
111
|
-
.execute();
|
|
112
|
-
|
|
113
|
-
// Indices for common query patterns. Wrapped in try/catch because
|
|
114
|
-
// `CREATE INDEX IF NOT EXISTS` syntax varies across dialects.
|
|
115
|
-
try {
|
|
116
|
-
await this.#db.schema
|
|
117
|
-
.createIndex('index_audit_timestamp')
|
|
118
|
-
.ifNotExists()
|
|
119
|
-
.on(AuditLog.#tableName)
|
|
120
|
-
.column('timestamp')
|
|
121
|
-
.execute();
|
|
122
|
-
} catch {
|
|
123
|
-
// Index already exists.
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
await this.#db.schema
|
|
128
|
-
.createIndex('index_audit_target')
|
|
129
|
-
.ifNotExists()
|
|
130
|
-
.on(AuditLog.#tableName)
|
|
131
|
-
.column('target')
|
|
132
|
-
.execute();
|
|
133
|
-
} catch {
|
|
134
|
-
// Index already exists.
|
|
135
|
-
}
|
|
136
|
-
|
|
137
103
|
try {
|
|
138
|
-
await this.#db
|
|
139
|
-
.createIndex('index_audit_action')
|
|
140
|
-
.ifNotExists()
|
|
141
|
-
.on(AuditLog.#tableName)
|
|
142
|
-
.column('action')
|
|
143
|
-
.execute();
|
|
104
|
+
await sql`SELECT 1 FROM ${sql.table(AuditLog.#tableName)} LIMIT 0`.execute(this.#db);
|
|
144
105
|
} catch {
|
|
145
|
-
|
|
106
|
+
throw new Error(
|
|
107
|
+
`AuditLog: table '${AuditLog.#tableName}' does not exist. Run server migrations before starting.`
|
|
108
|
+
);
|
|
146
109
|
}
|
|
147
110
|
}
|
|
148
111
|
|
package/src/admin/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { ActivityLog } from './activity-log.js';
|
|
2
2
|
export { AdminApi } from './admin-api.js';
|
|
3
|
+
export { AdminPasskeyStore } from './admin-passkey-store.js';
|
|
4
|
+
export { AdminSessionManager } from './admin-session.js';
|
|
3
5
|
export { AdminStore } from './admin-store.js';
|
|
4
6
|
export { AuditLog } from './audit-log.js';
|
|
5
7
|
export { validateAdminAuth } from './admin-auth.js';
|
|
@@ -9,6 +11,7 @@ export type {
|
|
|
9
11
|
AdminConnectionSnapshot,
|
|
10
12
|
AdminHealthCheck,
|
|
11
13
|
AdminMessageSummary,
|
|
14
|
+
AdminPasskeySummary,
|
|
12
15
|
AdminProtocolSummary,
|
|
13
16
|
AdminServerStats,
|
|
14
17
|
AdminSubscriptionSnapshot,
|
|
@@ -30,5 +33,7 @@ export type {
|
|
|
30
33
|
TenantQuotaStatus,
|
|
31
34
|
TenantStats,
|
|
32
35
|
} from './types.js';
|
|
36
|
+
export type { AdminAuthResult } from './admin-auth.js';
|
|
37
|
+
export type { AdminPasskeyRecord } from './admin-passkey-store.js';
|
|
33
38
|
export type { AuditEvent, AuditEventInput, AuditQueryOptions, AuditRetentionConfig } from './audit-log.js';
|
|
34
39
|
export type { WebhookPayload } from './webhook-manager.js';
|
package/src/admin/types.ts
CHANGED
|
@@ -350,3 +350,31 @@ export type AdminWebhookInput = {
|
|
|
350
350
|
events : string[];
|
|
351
351
|
secret? : string;
|
|
352
352
|
};
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Passkey (WebAuthn) authentication
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Summary of a registered passkey for the admin API response.
|
|
360
|
+
*
|
|
361
|
+
* Future: migrate to DID/DWN-based auth (see https://github.com/enboxorg/enbox/issues/546).
|
|
362
|
+
*/
|
|
363
|
+
export type AdminPasskeySummary = {
|
|
364
|
+
/** Base64url-encoded credential ID. */
|
|
365
|
+
id : string;
|
|
366
|
+
/** Human-readable name (e.g. "MacBook Touch ID"). */
|
|
367
|
+
name : string;
|
|
368
|
+
/** ISO-8601 timestamp when the passkey was registered. */
|
|
369
|
+
createdAt : string;
|
|
370
|
+
/** ISO-8601 timestamp of the most recent successful authentication. */
|
|
371
|
+
lastUsedAt : string | null;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Request body for registering a new passkey.
|
|
376
|
+
*/
|
|
377
|
+
export type AdminPasskeyRegisterInput = {
|
|
378
|
+
/** Human-readable name for the passkey. */
|
|
379
|
+
name? : string;
|
|
380
|
+
};
|
|
@@ -2,9 +2,9 @@ import type { Dialect } from '@enbox/dwn-sql-store';
|
|
|
2
2
|
import type { AdminWebhook, AdminWebhookInput } from './types.js';
|
|
3
3
|
|
|
4
4
|
import { createHmac } from 'crypto';
|
|
5
|
-
import { Kysely } from 'kysely';
|
|
6
5
|
import log from 'loglevel';
|
|
7
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import { Kysely, sql } from 'kysely';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Payload delivered to webhook endpoints.
|
|
@@ -50,16 +50,18 @@ export class WebhookManager {
|
|
|
50
50
|
return manager;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Verifies that the required table exists. Throws a clear error directing
|
|
55
|
+
* the caller to run server migrations first.
|
|
56
|
+
*/
|
|
53
57
|
async #initialize(): Promise<void> {
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.addColumn('createdAt', 'text', (col) => col.notNull())
|
|
62
|
-
.execute();
|
|
58
|
+
try {
|
|
59
|
+
await sql`SELECT 1 FROM ${sql.table(WebhookManager.#tableName)} LIMIT 0`.execute(this.#db);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`WebhookManager: table '${WebhookManager.#tableName}' does not exist. Run server migrations before starting.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// ---------------------------------------------------------------------------
|
package/src/config.ts
CHANGED
|
@@ -122,6 +122,27 @@ export const config = {
|
|
|
122
122
|
*/
|
|
123
123
|
adminMetricsUpdateIntervalSeconds: parseInt(process.env.DWN_ADMIN_METRICS_UPDATE_INTERVAL || '30'),
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* WebAuthn Relying Party ID for admin passkey authentication. Typically
|
|
127
|
+
* the hostname of the DWN server (e.g. `dev.aws.dwn.enbox.id`). When not
|
|
128
|
+
* set, the hostname is extracted from `DWN_BASE_URL` at runtime.
|
|
129
|
+
*
|
|
130
|
+
* @see https://github.com/enboxorg/enbox/issues/546
|
|
131
|
+
*/
|
|
132
|
+
adminWebAuthnRpId: process.env.DWN_ADMIN_WEBAUTHN_RP_ID || undefined,
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Human-readable Relying Party name shown during passkey registration.
|
|
136
|
+
* Defaults to `"DWN Admin"`.
|
|
137
|
+
*/
|
|
138
|
+
adminWebAuthnRpName: process.env.DWN_ADMIN_WEBAUTHN_RP_NAME || 'DWN Admin',
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Session time-to-live (in seconds) for passkey-authenticated sessions.
|
|
142
|
+
* Defaults to 86400 (24 hours).
|
|
143
|
+
*/
|
|
144
|
+
adminSessionTtlSeconds: parseInt(process.env.DWN_ADMIN_SESSION_TTL || '86400'),
|
|
145
|
+
|
|
125
146
|
// ---------------------------------------------------------------------------
|
|
126
147
|
// Per-tenant storage quotas
|
|
127
148
|
// ---------------------------------------------------------------------------
|