@rebasepro/server-core 0.2.3 → 0.3.0
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/common/src/collections/default-collections.d.ts +12 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
- package/dist/index-Cr1D21av.js.map +1 -0
- package/dist/index.es.js +2166 -208
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2155 -193
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/logs-routes.d.ts +37 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
- package/dist/server-core/src/api/types.d.ts +6 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
- package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
- package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
- package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
- package/dist/server-core/src/auth/index.d.ts +5 -3
- package/dist/server-core/src/auth/interfaces.d.ts +93 -3
- package/dist/server-core/src/auth/jwt.d.ts +3 -1
- package/dist/server-core/src/auth/mfa.d.ts +49 -0
- package/dist/server-core/src/auth/middleware.d.ts +7 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
- package/dist/server-core/src/auth/routes.d.ts +3 -3
- package/dist/server-core/src/env.d.ts +6 -0
- package/dist/server-core/src/index.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +3 -3
- package/dist/server-core/src/services/webhook-service.d.ts +29 -0
- package/dist/server-core/src/storage/image-transform.d.ts +48 -0
- package/dist/server-core/src/storage/index.d.ts +3 -0
- package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
- package/dist/types/src/controllers/auth.d.ts +2 -24
- package/dist/types/src/controllers/client.d.ts +0 -3
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data_driver.d.ts +18 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +2 -4
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +2 -2
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +6 -4
- package/dist/types/src/users/roles.d.ts +0 -8
- package/package.json +8 -6
- package/src/api/ast-schema-editor.ts +4 -4
- package/src/api/errors.ts +16 -7
- package/src/api/logs-routes.ts +129 -0
- package/src/api/rest/api-generator.ts +42 -2
- package/src/api/rest/query-parser.ts +37 -1
- package/src/api/types.ts +6 -1
- package/src/auth/adapter-middleware.ts +20 -4
- package/src/auth/admin-routes.ts +39 -14
- package/src/auth/api-keys/api-key-middleware.ts +126 -0
- package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
- package/src/auth/api-keys/api-key-routes.ts +183 -0
- package/src/auth/api-keys/api-key-store.ts +317 -0
- package/src/auth/api-keys/api-key-types.ts +94 -0
- package/src/auth/api-keys/index.ts +37 -0
- package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
- package/src/auth/builtin-auth-adapter.ts +31 -19
- package/src/auth/index.ts +7 -3
- package/src/auth/interfaces.ts +111 -3
- package/src/auth/jwt.ts +19 -5
- package/src/auth/mfa.ts +160 -0
- package/src/auth/middleware.ts +20 -1
- package/src/auth/rate-limiter.ts +92 -0
- package/src/auth/routes.ts +455 -24
- package/src/cron/cron-loader.ts +5 -10
- package/src/cron/cron-scheduler.ts +11 -12
- package/src/cron/cron-store.ts +8 -7
- package/src/env.ts +2 -0
- package/src/functions/function-loader.ts +6 -9
- package/src/index.ts +1 -2
- package/src/init.ts +37 -7
- package/src/serve-spa.ts +5 -4
- package/src/services/webhook-service.ts +155 -0
- package/src/storage/image-transform.ts +202 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/routes.ts +56 -3
- package/src/storage/tus-handler.ts +315 -0
- package/src/utils/dev-port.ts +14 -0
- package/src/utils/logging.ts +9 -7
- package/test/admin-routes.test.ts +74 -7
- package/test/api-generator.test.ts +0 -1
- package/test/api-key-permission-guard.test.ts +132 -0
- package/test/ast-schema-editor.test.ts +26 -0
- package/test/auth-routes.test.ts +1 -2
- package/test/backend-hooks-admin.test.ts +3 -4
- package/test/email-templates.test.ts +169 -0
- package/test/function-loader.test.ts +124 -0
- package/test/jwt.test.ts +4 -2
- package/test/mfa.test.ts +197 -0
- package/test/middleware.test.ts +10 -5
- package/test/webhook-service.test.ts +249 -0
- package/vite.config.ts +3 -2
- package/dist/index-BZoAtuqi.js.map +0 -1
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/src/bootstrappers/index.ts +0 -1
- 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
|
-
*
|
|
2
|
+
* AuthHooks
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
228
|
+
export function resolveAuthHooks(hooks?: AuthHooks): ResolvedAuthOperations {
|
|
162
229
|
return {
|
|
163
|
-
hashPassword:
|
|
230
|
+
hashPassword: hooks?.hashPassword
|
|
164
231
|
?? defaultHashPassword,
|
|
165
232
|
|
|
166
|
-
verifyPassword:
|
|
233
|
+
verifyPassword: hooks?.verifyPassword
|
|
167
234
|
?? defaultVerifyPassword,
|
|
168
235
|
|
|
169
|
-
validatePasswordStrength:
|
|
236
|
+
validatePasswordStrength: hooks?.validatePasswordStrength
|
|
170
237
|
?? defaultValidatePasswordStrength,
|
|
171
238
|
};
|
|
172
239
|
}
|