@rebasepro/server-core 0.2.3 → 0.2.4

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.
Files changed (109) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +12 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
  5. package/dist/index-Cr1D21av.js.map +1 -0
  6. package/dist/index.es.js +2166 -208
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +2155 -193
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-core/src/api/logs-routes.d.ts +37 -0
  11. package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
  12. package/dist/server-core/src/api/types.d.ts +6 -1
  13. package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
  14. package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
  15. package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
  16. package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
  17. package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
  18. package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
  19. package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
  20. package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
  21. package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
  22. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
  23. package/dist/server-core/src/auth/index.d.ts +5 -3
  24. package/dist/server-core/src/auth/interfaces.d.ts +93 -3
  25. package/dist/server-core/src/auth/jwt.d.ts +3 -1
  26. package/dist/server-core/src/auth/mfa.d.ts +49 -0
  27. package/dist/server-core/src/auth/middleware.d.ts +7 -0
  28. package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
  29. package/dist/server-core/src/auth/routes.d.ts +3 -3
  30. package/dist/server-core/src/env.d.ts +6 -0
  31. package/dist/server-core/src/index.d.ts +1 -0
  32. package/dist/server-core/src/init.d.ts +3 -3
  33. package/dist/server-core/src/services/webhook-service.d.ts +29 -0
  34. package/dist/server-core/src/storage/image-transform.d.ts +48 -0
  35. package/dist/server-core/src/storage/index.d.ts +3 -0
  36. package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
  37. package/dist/types/src/controllers/auth.d.ts +2 -24
  38. package/dist/types/src/controllers/client.d.ts +0 -3
  39. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  40. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  41. package/dist/types/src/controllers/registry.d.ts +5 -4
  42. package/dist/types/src/rebase_context.d.ts +1 -1
  43. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  44. package/dist/types/src/types/collections.d.ts +0 -4
  45. package/dist/types/src/types/component_ref.d.ts +1 -1
  46. package/dist/types/src/types/cron.d.ts +1 -1
  47. package/dist/types/src/types/entity_views.d.ts +1 -0
  48. package/dist/types/src/types/export_import.d.ts +1 -1
  49. package/dist/types/src/types/formex.d.ts +2 -2
  50. package/dist/types/src/types/properties.d.ts +2 -2
  51. package/dist/types/src/types/translations.d.ts +28 -12
  52. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  53. package/dist/types/src/users/roles.d.ts +0 -8
  54. package/package.json +8 -6
  55. package/src/api/ast-schema-editor.ts +4 -4
  56. package/src/api/errors.ts +16 -7
  57. package/src/api/logs-routes.ts +129 -0
  58. package/src/api/rest/api-generator.ts +42 -2
  59. package/src/api/rest/query-parser.ts +37 -1
  60. package/src/api/types.ts +6 -1
  61. package/src/auth/adapter-middleware.ts +20 -4
  62. package/src/auth/admin-routes.ts +39 -14
  63. package/src/auth/api-keys/api-key-middleware.ts +126 -0
  64. package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
  65. package/src/auth/api-keys/api-key-routes.ts +183 -0
  66. package/src/auth/api-keys/api-key-store.ts +317 -0
  67. package/src/auth/api-keys/api-key-types.ts +94 -0
  68. package/src/auth/api-keys/index.ts +37 -0
  69. package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
  70. package/src/auth/builtin-auth-adapter.ts +31 -19
  71. package/src/auth/index.ts +7 -3
  72. package/src/auth/interfaces.ts +111 -3
  73. package/src/auth/jwt.ts +19 -5
  74. package/src/auth/mfa.ts +160 -0
  75. package/src/auth/middleware.ts +20 -1
  76. package/src/auth/rate-limiter.ts +92 -0
  77. package/src/auth/routes.ts +455 -24
  78. package/src/cron/cron-loader.ts +5 -10
  79. package/src/cron/cron-scheduler.ts +11 -12
  80. package/src/cron/cron-store.ts +8 -7
  81. package/src/env.ts +2 -0
  82. package/src/functions/function-loader.ts +6 -9
  83. package/src/index.ts +1 -2
  84. package/src/init.ts +37 -7
  85. package/src/serve-spa.ts +5 -4
  86. package/src/services/webhook-service.ts +155 -0
  87. package/src/storage/image-transform.ts +202 -0
  88. package/src/storage/index.ts +3 -0
  89. package/src/storage/routes.ts +56 -3
  90. package/src/storage/tus-handler.ts +315 -0
  91. package/src/utils/dev-port.ts +14 -0
  92. package/src/utils/logging.ts +9 -7
  93. package/test/admin-routes.test.ts +74 -7
  94. package/test/api-generator.test.ts +0 -1
  95. package/test/api-key-permission-guard.test.ts +132 -0
  96. package/test/ast-schema-editor.test.ts +26 -0
  97. package/test/auth-routes.test.ts +1 -2
  98. package/test/backend-hooks-admin.test.ts +3 -4
  99. package/test/email-templates.test.ts +169 -0
  100. package/test/function-loader.test.ts +124 -0
  101. package/test/jwt.test.ts +4 -2
  102. package/test/mfa.test.ts +197 -0
  103. package/test/middleware.test.ts +10 -5
  104. package/test/webhook-service.test.ts +249 -0
  105. package/vite.config.ts +3 -2
  106. package/dist/index-BZoAtuqi.js.map +0 -1
  107. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  108. package/src/bootstrappers/index.ts +0 -1
  109. package/src/singleton.test.ts +0 -28
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Database operations for Service API Keys.
3
+ *
4
+ * Uses the DataDriver's `admin.executeSql` capability (same pattern as
5
+ * the cron-store and ensure-tables modules). All data lives in the
6
+ * `rebase.api_keys` table.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { randomBytes, createHash } from "crypto";
12
+ import type { DataDriver } from "@rebasepro/types";
13
+ import { isSQLAdmin } from "@rebasepro/types";
14
+ import { logger } from "../../utils/logger";
15
+ import type {
16
+ ApiKey,
17
+ ApiKeyMasked,
18
+ ApiKeyWithSecret,
19
+ CreateApiKeyRequest,
20
+ UpdateApiKeyRequest,
21
+ } from "./api-key-types";
22
+
23
+ const TABLE = "rebase.api_keys";
24
+
25
+ /** Characters used to generate the random portion of an API key. */
26
+ const HEX_CHARS = "abcdef0123456789";
27
+
28
+ /**
29
+ * Generate a plaintext API key with the `rk_live_` prefix.
30
+ *
31
+ * Format: `rk_live_` + 32 random hex characters.
32
+ */
33
+ function generateApiKey(): string {
34
+ const random = randomBytes(16).toString("hex"); // 32 hex chars
35
+ return `rk_live_${random}`;
36
+ }
37
+
38
+ /**
39
+ * SHA-256 hash a plaintext API key for database storage.
40
+ */
41
+ function hashKey(plaintext: string): string {
42
+ return createHash("sha256").update(plaintext).digest("hex");
43
+ }
44
+
45
+ /**
46
+ * Extract the display prefix from a plaintext key (first 12 chars).
47
+ */
48
+ function keyPrefix(plaintext: string): string {
49
+ return plaintext.substring(0, 12);
50
+ }
51
+
52
+ /**
53
+ * Strip the `key_hash` field and return a safe-to-expose masked key.
54
+ */
55
+ function toMasked(row: ApiKey): ApiKeyMasked {
56
+ return {
57
+ id: row.id,
58
+ name: row.name,
59
+ key_prefix: row.key_prefix,
60
+ permissions: row.permissions,
61
+ rate_limit: row.rate_limit,
62
+ created_by: row.created_by,
63
+ created_at: row.created_at,
64
+ updated_at: row.updated_at,
65
+ last_used_at: row.last_used_at,
66
+ expires_at: row.expires_at,
67
+ revoked_at: row.revoked_at,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Parse a raw DB row into the typed `ApiKey` shape.
73
+ */
74
+ function rowToApiKey(row: Record<string, unknown>): ApiKey {
75
+ const permissions = typeof row.permissions === "string"
76
+ ? JSON.parse(row.permissions)
77
+ : (row.permissions ?? []);
78
+
79
+ return {
80
+ id: row.id as string,
81
+ name: row.name as string,
82
+ key_prefix: row.key_prefix as string,
83
+ key_hash: row.key_hash as string,
84
+ permissions,
85
+ rate_limit: row.rate_limit !== null && row.rate_limit !== undefined
86
+ ? Number(row.rate_limit)
87
+ : null,
88
+ created_by: row.created_by as string,
89
+ created_at: new Date(row.created_at as string).toISOString(),
90
+ updated_at: new Date(row.updated_at as string).toISOString(),
91
+ last_used_at: row.last_used_at ? new Date(row.last_used_at as string).toISOString() : null,
92
+ expires_at: row.expires_at ? new Date(row.expires_at as string).toISOString() : null,
93
+ revoked_at: row.revoked_at ? new Date(row.revoked_at as string).toISOString() : null,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Escape a string for safe inline SQL usage.
99
+ */
100
+ function esc(value: string): string {
101
+ return value.replace(/'/g, "''");
102
+ }
103
+
104
+ // ─── Public API ──────────────────────────────────────────────────────
105
+
106
+ export interface ApiKeyStore {
107
+ /** Ensure the `rebase.api_keys` table exists. Called once on startup. */
108
+ ensureTable(): Promise<void>;
109
+
110
+ /** Create a new API key. Returns the full plaintext key exactly once. */
111
+ createApiKey(request: CreateApiKeyRequest, createdBy: string): Promise<ApiKeyWithSecret>;
112
+
113
+ /** Look up an API key by its SHA-256 hash. Returns `null` if not found. */
114
+ findByKeyHash(hash: string): Promise<ApiKey | null>;
115
+
116
+ /** List all API keys (masked, never includes hash). */
117
+ listApiKeys(): Promise<ApiKeyMasked[]>;
118
+
119
+ /** Get a single API key by ID (masked). */
120
+ getApiKeyById(id: string): Promise<ApiKeyMasked | null>;
121
+
122
+ /** Update name, permissions, rate_limit, or expires_at. */
123
+ updateApiKey(id: string, updates: UpdateApiKeyRequest): Promise<ApiKeyMasked | null>;
124
+
125
+ /** Soft-delete: set `revoked_at` to now. */
126
+ revokeApiKey(id: string): Promise<boolean>;
127
+
128
+ /** Touch `last_used_at` to the current timestamp. */
129
+ updateLastUsed(id: string): Promise<void>;
130
+ }
131
+
132
+ /**
133
+ * Create an `ApiKeyStore` backed by the driver's SQL admin capability.
134
+ *
135
+ * Returns `undefined` if the driver does not support `executeSql`.
136
+ */
137
+ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
138
+ const admin = driver.admin;
139
+ if (!isSQLAdmin(admin)) {
140
+ logger.warn("⚠️ [api-key-store] DataDriver does not support SQL admin — API keys will not be available.");
141
+ return undefined;
142
+ }
143
+
144
+ const exec = admin.executeSql.bind(admin);
145
+
146
+ return {
147
+ // ── Schema bootstrap ────────────────────────────────────────
148
+ async ensureTable(): Promise<void> {
149
+ try {
150
+ await exec("CREATE SCHEMA IF NOT EXISTS rebase");
151
+ await exec(`
152
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
153
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
154
+ name TEXT NOT NULL,
155
+ key_prefix TEXT NOT NULL,
156
+ key_hash TEXT NOT NULL UNIQUE,
157
+ permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
158
+ rate_limit INTEGER,
159
+ created_by TEXT NOT NULL,
160
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
161
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
162
+ last_used_at TIMESTAMPTZ,
163
+ expires_at TIMESTAMPTZ,
164
+ revoked_at TIMESTAMPTZ
165
+ )
166
+ `);
167
+
168
+ await exec(`
169
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash
170
+ ON ${TABLE}(key_hash)
171
+ `);
172
+
173
+ await exec(`
174
+ CREATE INDEX IF NOT EXISTS idx_api_keys_prefix
175
+ ON ${TABLE}(key_prefix)
176
+ `);
177
+
178
+ logger.info("✅ API keys table ready");
179
+ } catch (err) {
180
+ logger.error("❌ Failed to create API keys table", { error: err });
181
+ logger.warn("⚠️ Continuing without API keys support.");
182
+ }
183
+ },
184
+
185
+ // ── Create ──────────────────────────────────────────────────
186
+ async createApiKey(request: CreateApiKeyRequest, createdBy: string): Promise<ApiKeyWithSecret> {
187
+ const plaintext = generateApiKey();
188
+ const hash = hashKey(plaintext);
189
+ const prefix = keyPrefix(plaintext);
190
+ const permissionsJson = JSON.stringify(request.permissions);
191
+ const rateLimitSql = request.rate_limit !== null && request.rate_limit !== undefined
192
+ ? String(request.rate_limit)
193
+ : "NULL";
194
+ const expiresAtSql = request.expires_at
195
+ ? `'${esc(request.expires_at)}'`
196
+ : "NULL";
197
+
198
+ const rows = await exec(`
199
+ INSERT INTO ${TABLE} (name, key_prefix, key_hash, permissions, rate_limit, created_by, expires_at)
200
+ VALUES (
201
+ '${esc(request.name)}',
202
+ '${esc(prefix)}',
203
+ '${esc(hash)}',
204
+ '${esc(permissionsJson)}'::jsonb,
205
+ ${rateLimitSql},
206
+ '${esc(createdBy)}',
207
+ ${expiresAtSql}
208
+ )
209
+ RETURNING *
210
+ `);
211
+
212
+ const apiKey = rowToApiKey(rows[0]);
213
+ return {
214
+ ...toMasked(apiKey),
215
+ key: plaintext,
216
+ };
217
+ },
218
+
219
+ // ── Lookup by hash ──────────────────────────────────────────
220
+ async findByKeyHash(hash: string): Promise<ApiKey | null> {
221
+ const rows = await exec(`
222
+ SELECT * FROM ${TABLE}
223
+ WHERE key_hash = '${esc(hash)}'
224
+ LIMIT 1
225
+ `);
226
+ if (rows.length === 0) return null;
227
+ return rowToApiKey(rows[0]);
228
+ },
229
+
230
+ // ── List all (masked) ───────────────────────────────────────
231
+ async listApiKeys(): Promise<ApiKeyMasked[]> {
232
+ const rows = await exec(`
233
+ SELECT * FROM ${TABLE}
234
+ ORDER BY created_at DESC
235
+ `);
236
+ return rows.map(r => toMasked(rowToApiKey(r)));
237
+ },
238
+
239
+ // ── Get by ID (masked) ──────────────────────────────────────
240
+ async getApiKeyById(id: string): Promise<ApiKeyMasked | null> {
241
+ const rows = await exec(`
242
+ SELECT * FROM ${TABLE}
243
+ WHERE id = '${esc(id)}'
244
+ LIMIT 1
245
+ `);
246
+ if (rows.length === 0) return null;
247
+ return toMasked(rowToApiKey(rows[0]));
248
+ },
249
+
250
+ // ── Update ──────────────────────────────────────────────────
251
+ async updateApiKey(id: string, updates: UpdateApiKeyRequest): Promise<ApiKeyMasked | null> {
252
+ const setClauses: string[] = [];
253
+
254
+ if (updates.name !== undefined) {
255
+ setClauses.push(`name = '${esc(updates.name)}'`);
256
+ }
257
+ if (updates.permissions !== undefined) {
258
+ setClauses.push(`permissions = '${esc(JSON.stringify(updates.permissions))}'::jsonb`);
259
+ }
260
+ if (updates.rate_limit !== undefined) {
261
+ setClauses.push(
262
+ updates.rate_limit !== null
263
+ ? `rate_limit = ${updates.rate_limit}`
264
+ : "rate_limit = NULL"
265
+ );
266
+ }
267
+ if (updates.expires_at !== undefined) {
268
+ setClauses.push(
269
+ updates.expires_at !== null
270
+ ? `expires_at = '${esc(updates.expires_at)}'`
271
+ : "expires_at = NULL"
272
+ );
273
+ }
274
+
275
+ if (setClauses.length === 0) {
276
+ return this.getApiKeyById(id);
277
+ }
278
+
279
+ setClauses.push("updated_at = NOW()");
280
+
281
+ const rows = await exec(`
282
+ UPDATE ${TABLE}
283
+ SET ${setClauses.join(", ")}
284
+ WHERE id = '${esc(id)}'
285
+ RETURNING *
286
+ `);
287
+
288
+ if (rows.length === 0) return null;
289
+ return toMasked(rowToApiKey(rows[0]));
290
+ },
291
+
292
+ // ── Revoke (soft-delete) ────────────────────────────────────
293
+ async revokeApiKey(id: string): Promise<boolean> {
294
+ const rows = await exec(`
295
+ UPDATE ${TABLE}
296
+ SET revoked_at = NOW(), updated_at = NOW()
297
+ WHERE id = '${esc(id)}' AND revoked_at IS NULL
298
+ RETURNING id
299
+ `);
300
+ return rows.length > 0;
301
+ },
302
+
303
+ // ── Touch last_used_at ──────────────────────────────────────
304
+ async updateLastUsed(id: string): Promise<void> {
305
+ try {
306
+ await exec(`
307
+ UPDATE ${TABLE}
308
+ SET last_used_at = NOW()
309
+ WHERE id = '${esc(id)}'
310
+ `);
311
+ } catch (err) {
312
+ // Non-blocking — don't fail requests because of a usage timestamp update
313
+ logger.error("[api-key-store] Failed to update last_used_at", { error: err });
314
+ }
315
+ },
316
+ };
317
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Type definitions for Service API Keys.
3
+ *
4
+ * API keys provide machine-to-machine authentication for scripts, cron jobs,
5
+ * and third-party integrations. Each key is scoped to specific collections
6
+ * and operations via the `ApiKeyPermission` model.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ /**
12
+ * A single permission entry scoping an API key to a collection and set of operations.
13
+ *
14
+ * Use `"*"` as the collection value to grant access to all collections.
15
+ */
16
+ export interface ApiKeyPermission {
17
+ /** Collection slug, or `"*"` for all collections. */
18
+ collection: string;
19
+ /** Allowed operations on the collection. */
20
+ operations: ("read" | "write" | "delete")[];
21
+ }
22
+
23
+ /**
24
+ * Full database row for an API key.
25
+ * The `key_hash` is never exposed via the API — only stored for lookup.
26
+ */
27
+ export interface ApiKey {
28
+ id: string;
29
+ name: string;
30
+ /** First 12 characters of the plaintext key, for display only. */
31
+ key_prefix: string;
32
+ /** SHA-256 hash of the full plaintext key. */
33
+ key_hash: string;
34
+ permissions: ApiKeyPermission[];
35
+ /** Requests per 15-minute window. `null` means unlimited. */
36
+ rate_limit: number | null;
37
+ created_by: string;
38
+ created_at: string;
39
+ updated_at: string;
40
+ last_used_at: string | null;
41
+ expires_at: string | null;
42
+ revoked_at: string | null;
43
+ }
44
+
45
+ /**
46
+ * Masked version of an API key, safe for API responses.
47
+ * Omits `key_hash` and shows only the prefix.
48
+ */
49
+ export interface ApiKeyMasked {
50
+ id: string;
51
+ name: string;
52
+ key_prefix: string;
53
+ permissions: ApiKeyPermission[];
54
+ rate_limit: number | null;
55
+ created_by: string;
56
+ created_at: string;
57
+ updated_at: string;
58
+ last_used_at: string | null;
59
+ expires_at: string | null;
60
+ revoked_at: string | null;
61
+ }
62
+
63
+ /**
64
+ * Request body for creating a new API key.
65
+ */
66
+ export interface CreateApiKeyRequest {
67
+ name: string;
68
+ permissions: ApiKeyPermission[];
69
+ /** Requests per 15-minute window. Omit or `null` for unlimited. */
70
+ rate_limit?: number | null;
71
+ /** ISO-8601 expiration timestamp. Omit for no expiration. */
72
+ expires_at?: string | null;
73
+ }
74
+
75
+ /**
76
+ * Request body for updating an existing API key.
77
+ * All fields are optional — only provided fields are updated.
78
+ */
79
+ export interface UpdateApiKeyRequest {
80
+ name?: string;
81
+ permissions?: ApiKeyPermission[];
82
+ rate_limit?: number | null;
83
+ expires_at?: string | null;
84
+ }
85
+
86
+ /**
87
+ * Returned exactly once when a key is created.
88
+ * The `key` field contains the full plaintext key — it is never stored
89
+ * or returned again after creation.
90
+ */
91
+ export interface ApiKeyWithSecret extends ApiKeyMasked {
92
+ /** Full plaintext API key (e.g. `rk_live_abc123...`). */
93
+ key: string;
94
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Service API Keys module.
3
+ *
4
+ * Re-exports types, store, middleware, permission guard, and routes
5
+ * for the API key authentication system.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ // Types
11
+ export type {
12
+ ApiKey,
13
+ ApiKeyMasked,
14
+ ApiKeyPermission,
15
+ ApiKeyWithSecret,
16
+ CreateApiKeyRequest,
17
+ UpdateApiKeyRequest,
18
+ } from "./api-key-types";
19
+
20
+ // Store
21
+ export { createApiKeyStore } from "./api-key-store";
22
+ export type { ApiKeyStore } from "./api-key-store";
23
+
24
+ // Middleware
25
+ export { isApiKeyToken, validateApiKey } from "./api-key-middleware";
26
+ export type { ApiKeyAuthOptions } from "./api-key-middleware";
27
+
28
+ // Permission guard
29
+ export {
30
+ httpMethodToOperation,
31
+ isOperationAllowed,
32
+ } from "./api-key-permission-guard";
33
+ export type { ApiKeyOperation } from "./api-key-permission-guard";
34
+
35
+ // Routes
36
+ export { createApiKeyRoutes } from "./api-key-routes";
37
+ export type { ApiKeyRouteOptions } from "./api-key-routes";
@@ -1,20 +1,20 @@
1
1
  /**
2
- * AuthOverrides
2
+ * AuthHooks
3
3
  *
4
- * Override specific behaviors of the built-in Rebase auth system.
4
+ * Customize specific behaviors of the built-in Rebase auth system.
5
5
  *
6
6
  * Each method replaces one piece of the default implementation.
7
7
  * Unset methods fall through to the built-in defaults (scrypt passwords,
8
8
  * standard validation rules, etc.).
9
9
  *
10
- * This interface is intentionally open for extension — new overrides
10
+ * This interface is intentionally open for extension — new hooks
11
11
  * can be added as optional methods without breaking existing configurations.
12
12
  *
13
13
  * @example bcrypt password support
14
14
  * ```ts
15
15
  * import bcrypt from "bcrypt";
16
16
  *
17
- * const overrides: AuthOverrides = {
17
+ * const hooks: AuthHooks = {
18
18
  * hashPassword: (pw) => bcrypt.hash(pw, 12),
19
19
  * verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
20
20
  * validatePasswordStrength: (pw) => ({
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * @example Override the entire login credential check
28
28
  * ```ts
29
- * const overrides: AuthOverrides = {
29
+ * const hooks: AuthHooks = {
30
30
  * verifyCredentials: async (email, password, repo) => {
31
31
  * const user = await repo.getUserByEmail(email);
32
32
  * if (!user || !user.passwordHash) return null;
@@ -48,15 +48,15 @@ import type { AuthRepository, UserData, CreateUserData } from "./interfaces";
48
48
  /**
49
49
  * Authentication method identifier for lifecycle hooks.
50
50
  */
51
- export type AuthMethod = "login" | "register" | "oauth" | "refresh" | "password-reset";
51
+ export type AuthMethod = "login" | "register" | "oauth" | "refresh" | "password-reset" | "anonymous" | "magic-link" | "mfa";
52
52
 
53
53
  /**
54
- * Override specific parts of the built-in Rebase auth implementation.
54
+ * Hook specific parts of the built-in Rebase auth implementation.
55
55
  *
56
56
  * Every method is optional. The built-in defaults apply for any method
57
57
  * that is not provided.
58
58
  */
59
- export interface AuthOverrides {
59
+ export interface AuthHooks {
60
60
  // ─── Password Operations ──────────────────────────────────────────────
61
61
 
62
62
  /**
@@ -138,11 +138,78 @@ export interface AuthOverrides {
138
138
  * This is fire-and-forget — errors are logged but do not fail the request.
139
139
  */
140
140
  afterUserCreate?(user: UserData): Promise<void>;
141
+
142
+ // ─── Extended Lifecycle Hooks ─────────────────────────────────────────
143
+
144
+ /**
145
+ * Pre-login validation. Called before credential verification.
146
+ *
147
+ * Throw an error to reject the login attempt (e.g. for account lockout,
148
+ * IP-based restrictions, etc.).
149
+ */
150
+ beforeLogin?(email: string, method: AuthMethod): Promise<void>;
151
+
152
+ /**
153
+ * Post-logout cleanup.
154
+ *
155
+ * Called after a user's session has been invalidated.
156
+ * Use for audit logging, cleanup of temporary resources, etc.
157
+ *
158
+ * This is fire-and-forget — errors are logged but do not fail the request.
159
+ */
160
+ afterLogout?(userId: string): Promise<void>;
161
+
162
+ /**
163
+ * Called after successful MFA verification.
164
+ *
165
+ * Use for audit logging, notifying external systems, etc.
166
+ *
167
+ * This is fire-and-forget — errors are logged but do not fail the request.
168
+ */
169
+ onMfaVerified?(userId: string, factorId: string): Promise<void>;
170
+
171
+ /**
172
+ * Customize JWT access token claims before signing.
173
+ *
174
+ * Return the modified claims object. The returned claims are merged
175
+ * into the JWT payload alongside standard claims (userId, roles).
176
+ *
177
+ * @param claims - The default claims that would be included.
178
+ * @param user - The authenticated user data.
179
+ * @returns Modified claims to include in the JWT.
180
+ */
181
+ customizeAccessToken?(claims: Record<string, unknown>, user: UserData): Promise<Record<string, unknown>>;
182
+
183
+ /**
184
+ * Called after a successful password reset.
185
+ *
186
+ * Use for audit logging, sending confirmation notifications, etc.
187
+ *
188
+ * This is fire-and-forget — errors are logged but do not fail the request.
189
+ */
190
+ onPasswordReset?(userId: string): Promise<void>;
191
+
192
+ /**
193
+ * Called before a user is deleted.
194
+ *
195
+ * Throw an error to prevent deletion (e.g. for users with active
196
+ * subscriptions, pending transactions, etc.).
197
+ */
198
+ beforeUserDelete?(userId: string): Promise<void>;
199
+
200
+ /**
201
+ * Called after a user is deleted.
202
+ *
203
+ * Use for cleaning up external resources, audit logging, etc.
204
+ *
205
+ * This is fire-and-forget — errors are logged but do not fail the request.
206
+ */
207
+ afterUserDelete?(userId: string): Promise<void>;
141
208
  }
142
209
 
143
210
  /**
144
211
  * Resolved auth operations — every method is guaranteed to exist.
145
- * Created by `resolveAuthOverrides()` which merges user overrides
212
+ * Created by `resolveAuthHooks()` which merges user hooks
146
213
  * with built-in defaults.
147
214
  */
148
215
  export interface ResolvedAuthOperations {
@@ -152,21 +219,21 @@ export interface ResolvedAuthOperations {
152
219
  }
153
220
 
154
221
  /**
155
- * Merge user-provided overrides with the built-in defaults to produce
222
+ * Merge user-provided hooks with the built-in defaults to produce
156
223
  * a complete set of resolved operations.
157
224
  *
158
225
  * This is the single point where defaults are applied — all consumers
159
226
  * call this once and use the resolved operations throughout.
160
227
  */
161
- export function resolveAuthOverrides(overrides?: AuthOverrides): ResolvedAuthOperations {
228
+ export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthOperations {
162
229
  return {
163
- hashPassword: overrides?.hashPassword
230
+ hashPassword: hooks?.hashPassword
164
231
  ?? defaultHashPassword,
165
232
 
166
- verifyPassword: overrides?.verifyPassword
233
+ verifyPassword: hooks?.verifyPassword
167
234
  ?? defaultVerifyPassword,
168
235
 
169
- validatePasswordStrength: overrides?.validatePasswordStrength
236
+ validatePasswordStrength: hooks?.validatePasswordStrength
170
237
  ?? defaultValidatePasswordStrength,
171
238
  };
172
239
  }