@rebasepro/server-core 0.6.1 → 0.7.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/package.json +14 -14
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +45 -10
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +6 -0
- package/src/auth/adapter-middleware.ts +10 -2
- package/src/auth/admin-roles-route.ts +36 -0
- package/src/auth/admin-users-route.ts +302 -0
- package/src/auth/api-keys/api-key-middleware.ts +4 -3
- package/src/auth/api-keys/api-key-routes.ts +12 -2
- package/src/auth/api-keys/api-key-store.ts +83 -66
- package/src/auth/api-keys/api-key-types.ts +8 -0
- package/src/auth/apple-oauth.ts +2 -1
- package/src/auth/auth-hooks.ts +21 -0
- package/src/auth/bitbucket-oauth.ts +2 -1
- package/src/auth/builtin-auth-adapter.ts +28 -4
- package/src/auth/custom-auth-adapter.ts +2 -0
- package/src/auth/discord-oauth.ts +2 -1
- package/src/auth/facebook-oauth.ts +2 -1
- package/src/auth/github-oauth.ts +2 -1
- package/src/auth/gitlab-oauth.ts +2 -1
- package/src/auth/google-oauth.ts +8 -4
- package/src/auth/index.ts +2 -0
- package/src/auth/interfaces.ts +27 -0
- package/src/auth/linkedin-oauth.ts +2 -1
- package/src/auth/magic-link-routes.ts +167 -0
- package/src/auth/mfa-crypto.ts +91 -0
- package/src/auth/mfa-routes.ts +34 -10
- package/src/auth/microsoft-oauth.ts +2 -1
- package/src/auth/middleware.ts +10 -1
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +77 -8
- package/src/auth/session-routes.ts +15 -3
- package/src/auth/slack-oauth.ts +2 -1
- package/src/auth/spotify-oauth.ts +2 -1
- package/src/auth/twitter-oauth.ts +8 -1
- package/src/cron/cron-store.ts +25 -23
- package/src/email/index.ts +3 -2
- package/src/email/templates.ts +82 -0
- package/src/email/types.ts +16 -0
- package/src/init.ts +137 -5
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/utils/dev-port.ts +13 -7
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- package/test/env.test.ts +9 -19
- package/test/multi-datasource-routing.test.ts +113 -0
- package/test/routed-realtime-service.test.ts +86 -0
- package/test/transform-auth-response.test.ts +305 -0
- package/build-errors.txt +0 -52
- package/dist/api/ast-schema-editor.d.ts +0 -21
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +0 -2
- package/dist/api/errors.d.ts +0 -53
- package/dist/api/graphql/graphql-schema-generator.d.ts +0 -35
- package/dist/api/graphql/index.d.ts +0 -1
- package/dist/api/index.d.ts +0 -9
- package/dist/api/logs-routes.d.ts +0 -37
- package/dist/api/openapi-generator.d.ts +0 -16
- package/dist/api/rest/api-generator.d.ts +0 -88
- package/dist/api/rest/index.d.ts +0 -1
- package/dist/api/rest/query-parser.d.ts +0 -11
- package/dist/api/schema-editor-routes.d.ts +0 -3
- package/dist/api/server.d.ts +0 -40
- package/dist/api/types.d.ts +0 -98
- package/dist/auth/adapter-middleware.d.ts +0 -37
- package/dist/auth/admin-user-ops.d.ts +0 -79
- package/dist/auth/api-keys/api-key-middleware.d.ts +0 -39
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -32
- package/dist/auth/api-keys/api-key-routes.d.ts +0 -20
- package/dist/auth/api-keys/api-key-store.d.ts +0 -35
- package/dist/auth/api-keys/api-key-types.d.ts +0 -88
- package/dist/auth/api-keys/index.d.ts +0 -17
- package/dist/auth/apple-oauth.d.ts +0 -30
- package/dist/auth/auth-hooks.d.ts +0 -225
- package/dist/auth/bitbucket-oauth.d.ts +0 -11
- package/dist/auth/builtin-auth-adapter.d.ts +0 -49
- package/dist/auth/crypto-utils.d.ts +0 -16
- package/dist/auth/custom-auth-adapter.d.ts +0 -39
- package/dist/auth/discord-oauth.d.ts +0 -14
- package/dist/auth/facebook-oauth.d.ts +0 -14
- package/dist/auth/github-oauth.d.ts +0 -15
- package/dist/auth/gitlab-oauth.d.ts +0 -13
- package/dist/auth/google-oauth.d.ts +0 -47
- package/dist/auth/index.d.ts +0 -36
- package/dist/auth/interfaces.d.ts +0 -401
- package/dist/auth/jwt.d.ts +0 -55
- package/dist/auth/linkedin-oauth.d.ts +0 -18
- package/dist/auth/mfa-routes.d.ts +0 -6
- package/dist/auth/mfa.d.ts +0 -49
- package/dist/auth/microsoft-oauth.d.ts +0 -16
- package/dist/auth/middleware.d.ts +0 -106
- package/dist/auth/password.d.ts +0 -22
- package/dist/auth/rate-limiter.d.ts +0 -50
- package/dist/auth/reset-password-admin.d.ts +0 -29
- package/dist/auth/rls-scope.d.ts +0 -31
- package/dist/auth/routes.d.ts +0 -33
- package/dist/auth/session-routes.d.ts +0 -25
- package/dist/auth/slack-oauth.d.ts +0 -12
- package/dist/auth/spotify-oauth.d.ts +0 -12
- package/dist/auth/twitter-oauth.d.ts +0 -18
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/chunk-Dze3rakg.js +0 -42
- package/dist/collections/BackendCollectionRegistry.d.ts +0 -13
- package/dist/collections/loader.d.ts +0 -5
- package/dist/cron/cron-loader.d.ts +0 -17
- package/dist/cron/cron-routes.d.ts +0 -14
- package/dist/cron/cron-scheduler.d.ts +0 -106
- package/dist/cron/cron-store.d.ts +0 -32
- package/dist/cron/index.d.ts +0 -6
- package/dist/db/interfaces.d.ts +0 -18
- package/dist/dist-CZKP-Xz4.js +0 -832
- package/dist/dist-CZKP-Xz4.js.map +0 -1
- package/dist/email/index.d.ts +0 -6
- package/dist/email/smtp-email-service.d.ts +0 -25
- package/dist/email/templates.d.ts +0 -42
- package/dist/email/types.d.ts +0 -108
- package/dist/env.d.ts +0 -102
- package/dist/from-VbwD7xRf.js +0 -3849
- package/dist/from-VbwD7xRf.js.map +0 -1
- package/dist/functions/function-loader.d.ts +0 -17
- package/dist/functions/function-routes.d.ts +0 -10
- package/dist/functions/index.d.ts +0 -3
- package/dist/history/history-routes.d.ts +0 -23
- package/dist/history/index.d.ts +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.es.js +0 -61731
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -73221
- package/dist/index.umd.js.map +0 -1
- package/dist/init/docs.d.ts +0 -4
- package/dist/init/health.d.ts +0 -2
- package/dist/init/middlewares.d.ts +0 -10
- package/dist/init/shutdown.d.ts +0 -11
- package/dist/init/storage.d.ts +0 -5
- package/dist/init.d.ts +0 -258
- package/dist/jwt-DHcQRGC3.js +0 -4168
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/dist/logger-BYU66ENZ.js +0 -94
- package/dist/logger-BYU66ENZ.js.map +0 -1
- package/dist/ms-BeBSuOXN.js +0 -125
- package/dist/ms-BeBSuOXN.js.map +0 -1
- package/dist/multipart-parser-CedBDOeC.js +0 -299
- package/dist/multipart-parser-CedBDOeC.js.map +0 -1
- package/dist/serve-spa.d.ts +0 -30
- package/dist/services/driver-registry.d.ts +0 -78
- package/dist/services/webhook-service.d.ts +0 -29
- package/dist/singleton.d.ts +0 -35
- package/dist/src-COaj0G3P.js +0 -1182
- package/dist/src-COaj0G3P.js.map +0 -1
- package/dist/storage/LocalStorageController.d.ts +0 -46
- package/dist/storage/S3StorageController.d.ts +0 -36
- package/dist/storage/image-transform.d.ts +0 -50
- package/dist/storage/index.d.ts +0 -28
- package/dist/storage/routes.d.ts +0 -38
- package/dist/storage/storage-registry.d.ts +0 -78
- package/dist/storage/tus-handler.d.ts +0 -51
- package/dist/storage/types.d.ts +0 -103
- package/dist/types/index.d.ts +0 -11
- package/dist/utils/dev-port.d.ts +0 -35
- package/dist/utils/logger.d.ts +0 -31
- package/dist/utils/logging.d.ts +0 -9
- package/dist/utils/request-id.d.ts +0 -4
- package/dist/utils/request-logger.d.ts +0 -19
- package/dist/utils/sql.d.ts +0 -27
|
@@ -59,7 +59,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
59
59
|
// POST / — Create a new API key
|
|
60
60
|
router.post("/", async (c) => {
|
|
61
61
|
const body = await c.req.json() as Record<string, unknown>;
|
|
62
|
-
const { name, permissions, rate_limit, expires_at } = body;
|
|
62
|
+
const { name, permissions, admin, rate_limit, expires_at } = body;
|
|
63
63
|
|
|
64
64
|
if (!name || typeof name !== "string" || name.trim().length === 0) {
|
|
65
65
|
throw ApiError.badRequest("Name is required", "INVALID_INPUT");
|
|
@@ -72,6 +72,10 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
if (admin !== undefined && typeof admin !== "boolean") {
|
|
76
|
+
throw ApiError.badRequest("admin must be a boolean", "INVALID_INPUT");
|
|
77
|
+
}
|
|
78
|
+
|
|
75
79
|
if (rate_limit !== undefined && rate_limit !== null) {
|
|
76
80
|
if (typeof rate_limit !== "number" || rate_limit < 1 || !Number.isInteger(rate_limit)) {
|
|
77
81
|
throw ApiError.badRequest("rate_limit must be a positive integer or null", "INVALID_INPUT");
|
|
@@ -96,6 +100,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
96
100
|
const request: CreateApiKeyRequest = {
|
|
97
101
|
name: name.trim(),
|
|
98
102
|
permissions: permissions as ApiKeyPermission[],
|
|
103
|
+
admin: (admin as boolean) ?? false,
|
|
99
104
|
rate_limit: (rate_limit as number | null) ?? null,
|
|
100
105
|
expires_at: (expires_at as string | null) ?? null
|
|
101
106
|
};
|
|
@@ -121,7 +126,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
121
126
|
router.put("/:id", async (c) => {
|
|
122
127
|
const id = c.req.param("id");
|
|
123
128
|
const body = await c.req.json() as Record<string, unknown>;
|
|
124
|
-
const { name, permissions, rate_limit, expires_at } = body;
|
|
129
|
+
const { name, permissions, admin, rate_limit, expires_at } = body;
|
|
125
130
|
|
|
126
131
|
// Validate fields if provided
|
|
127
132
|
if (name !== undefined) {
|
|
@@ -139,6 +144,10 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
if (admin !== undefined && typeof admin !== "boolean") {
|
|
148
|
+
throw ApiError.badRequest("admin must be a boolean", "INVALID_INPUT");
|
|
149
|
+
}
|
|
150
|
+
|
|
142
151
|
if (rate_limit !== undefined && rate_limit !== null) {
|
|
143
152
|
if (typeof rate_limit !== "number" || rate_limit < 1 || !Number.isInteger(rate_limit)) {
|
|
144
153
|
throw ApiError.badRequest("rate_limit must be a positive integer or null", "INVALID_INPUT");
|
|
@@ -155,6 +164,7 @@ export function createApiKeyRoutes(options: ApiKeyRouteOptions): Hono<HonoEnv> {
|
|
|
155
164
|
const updates: UpdateApiKeyRequest = {};
|
|
156
165
|
if (name !== undefined) updates.name = (name as string).trim();
|
|
157
166
|
if (permissions !== undefined) updates.permissions = permissions as ApiKeyPermission[];
|
|
167
|
+
if (admin !== undefined) updates.admin = admin as boolean;
|
|
158
168
|
if (rate_limit !== undefined) updates.rate_limit = rate_limit as number | null;
|
|
159
169
|
if (expires_at !== undefined) updates.expires_at = expires_at as string | null;
|
|
160
170
|
|
|
@@ -59,6 +59,7 @@ function toMasked(row: ApiKey): ApiKeyMasked {
|
|
|
59
59
|
name: row.name,
|
|
60
60
|
key_prefix: row.key_prefix,
|
|
61
61
|
permissions: row.permissions,
|
|
62
|
+
admin: row.admin,
|
|
62
63
|
rate_limit: row.rate_limit,
|
|
63
64
|
created_by: row.created_by,
|
|
64
65
|
created_at: row.created_at,
|
|
@@ -88,6 +89,7 @@ function rowToApiKey(row: Record<string, unknown>): ApiKey {
|
|
|
88
89
|
key_prefix: row.key_prefix as string,
|
|
89
90
|
key_hash: row.key_hash as string,
|
|
90
91
|
permissions,
|
|
92
|
+
admin: Boolean(row.admin),
|
|
91
93
|
rate_limit: row.rate_limit !== null && row.rate_limit !== undefined
|
|
92
94
|
? Number(row.rate_limit)
|
|
93
95
|
: null,
|
|
@@ -100,13 +102,6 @@ function rowToApiKey(row: Record<string, unknown>): ApiKey {
|
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
/**
|
|
104
|
-
* Escape a string for safe inline SQL usage.
|
|
105
|
-
*/
|
|
106
|
-
function esc(value: string): string {
|
|
107
|
-
return value.replace(/'/g, "''");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
// ─── Public API ──────────────────────────────────────────────────────
|
|
111
106
|
|
|
112
107
|
export interface ApiKeyStore {
|
|
@@ -147,7 +142,8 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
147
142
|
return undefined;
|
|
148
143
|
}
|
|
149
144
|
|
|
150
|
-
const exec =
|
|
145
|
+
const exec = (sqlText: string, options?: { params?: unknown[] }) =>
|
|
146
|
+
admin.executeSql(sqlText, options?.params ? { params: options.params } : undefined);
|
|
151
147
|
|
|
152
148
|
return {
|
|
153
149
|
// ── Schema bootstrap ────────────────────────────────────────
|
|
@@ -161,6 +157,7 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
161
157
|
key_prefix TEXT NOT NULL,
|
|
162
158
|
key_hash TEXT NOT NULL UNIQUE,
|
|
163
159
|
permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
160
|
+
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
164
161
|
rate_limit INTEGER,
|
|
165
162
|
created_by TEXT NOT NULL,
|
|
166
163
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
@@ -181,6 +178,12 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
181
178
|
ON ${TABLE}(key_prefix)
|
|
182
179
|
`);
|
|
183
180
|
|
|
181
|
+
// Migration: add admin column to existing tables
|
|
182
|
+
await exec(`
|
|
183
|
+
ALTER TABLE ${TABLE}
|
|
184
|
+
ADD COLUMN IF NOT EXISTS admin BOOLEAN NOT NULL DEFAULT FALSE
|
|
185
|
+
`);
|
|
186
|
+
|
|
184
187
|
logger.info("✅ API keys table ready");
|
|
185
188
|
} catch (err) {
|
|
186
189
|
logger.error("❌ Failed to create API keys table", { error: err });
|
|
@@ -194,26 +197,22 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
194
197
|
const hash = hashKey(plaintext);
|
|
195
198
|
const prefix = keyPrefix(plaintext);
|
|
196
199
|
const permissionsJson = JSON.stringify(request.permissions);
|
|
197
|
-
const rateLimitSql = request.rate_limit !== null && request.rate_limit !== undefined
|
|
198
|
-
? String(request.rate_limit)
|
|
199
|
-
: "NULL";
|
|
200
|
-
const expiresAtSql = request.expires_at
|
|
201
|
-
? `'${esc(request.expires_at)}'`
|
|
202
|
-
: "NULL";
|
|
203
200
|
|
|
204
|
-
const rows = await exec(
|
|
205
|
-
INSERT INTO ${TABLE} (name, key_prefix, key_hash, permissions, rate_limit, created_by, expires_at)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
201
|
+
const rows = await exec(
|
|
202
|
+
`INSERT INTO ${TABLE} (name, key_prefix, key_hash, permissions, admin, rate_limit, created_by, expires_at)
|
|
203
|
+
VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8)
|
|
204
|
+
RETURNING *`,
|
|
205
|
+
{ params: [
|
|
206
|
+
request.name,
|
|
207
|
+
prefix,
|
|
208
|
+
hash,
|
|
209
|
+
permissionsJson,
|
|
210
|
+
request.admin ?? false,
|
|
211
|
+
request.rate_limit ?? null,
|
|
212
|
+
createdBy,
|
|
213
|
+
request.expires_at ?? null
|
|
214
|
+
]}
|
|
215
|
+
);
|
|
217
216
|
|
|
218
217
|
const apiKey = rowToApiKey(rows[0]);
|
|
219
218
|
return {
|
|
@@ -224,11 +223,12 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
224
223
|
|
|
225
224
|
// ── Lookup by hash ──────────────────────────────────────────
|
|
226
225
|
async findByKeyHash(hash: string): Promise<ApiKey | null> {
|
|
227
|
-
const rows = await exec(
|
|
228
|
-
SELECT * FROM ${TABLE}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
const rows = await exec(
|
|
227
|
+
`SELECT * FROM ${TABLE}
|
|
228
|
+
WHERE key_hash = $1
|
|
229
|
+
LIMIT 1`,
|
|
230
|
+
{ params: [hash] }
|
|
231
|
+
);
|
|
232
232
|
if (rows.length === 0) return null;
|
|
233
233
|
return rowToApiKey(rows[0]);
|
|
234
234
|
},
|
|
@@ -244,11 +244,12 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
244
244
|
|
|
245
245
|
// ── Get by ID (masked) ──────────────────────────────────────
|
|
246
246
|
async getApiKeyById(id: string): Promise<ApiKeyMasked | null> {
|
|
247
|
-
const rows = await exec(
|
|
248
|
-
SELECT * FROM ${TABLE}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
const rows = await exec(
|
|
248
|
+
`SELECT * FROM ${TABLE}
|
|
249
|
+
WHERE id = $1
|
|
250
|
+
LIMIT 1`,
|
|
251
|
+
{ params: [id] }
|
|
252
|
+
);
|
|
252
253
|
if (rows.length === 0) return null;
|
|
253
254
|
return toMasked(rowToApiKey(rows[0]));
|
|
254
255
|
},
|
|
@@ -256,26 +257,36 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
256
257
|
// ── Update ──────────────────────────────────────────────────
|
|
257
258
|
async updateApiKey(id: string, updates: UpdateApiKeyRequest): Promise<ApiKeyMasked | null> {
|
|
258
259
|
const setClauses: string[] = [];
|
|
260
|
+
const params: unknown[] = [];
|
|
261
|
+
let paramIdx = 1;
|
|
259
262
|
|
|
260
263
|
if (updates.name !== undefined) {
|
|
261
|
-
setClauses.push(`name =
|
|
264
|
+
setClauses.push(`name = $${paramIdx++}`);
|
|
265
|
+
params.push(updates.name);
|
|
262
266
|
}
|
|
263
267
|
if (updates.permissions !== undefined) {
|
|
264
|
-
setClauses.push(`permissions =
|
|
268
|
+
setClauses.push(`permissions = $${paramIdx++}::jsonb`);
|
|
269
|
+
params.push(JSON.stringify(updates.permissions));
|
|
270
|
+
}
|
|
271
|
+
if (updates.admin !== undefined) {
|
|
272
|
+
setClauses.push(`admin = $${paramIdx++}`);
|
|
273
|
+
params.push(updates.admin);
|
|
265
274
|
}
|
|
266
275
|
if (updates.rate_limit !== undefined) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
276
|
+
if (updates.rate_limit !== null) {
|
|
277
|
+
setClauses.push(`rate_limit = $${paramIdx++}`);
|
|
278
|
+
params.push(updates.rate_limit);
|
|
279
|
+
} else {
|
|
280
|
+
setClauses.push("rate_limit = NULL");
|
|
281
|
+
}
|
|
272
282
|
}
|
|
273
283
|
if (updates.expires_at !== undefined) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
284
|
+
if (updates.expires_at !== null) {
|
|
285
|
+
setClauses.push(`expires_at = $${paramIdx++}`);
|
|
286
|
+
params.push(updates.expires_at);
|
|
287
|
+
} else {
|
|
288
|
+
setClauses.push("expires_at = NULL");
|
|
289
|
+
}
|
|
279
290
|
}
|
|
280
291
|
|
|
281
292
|
if (setClauses.length === 0) {
|
|
@@ -284,12 +295,16 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
284
295
|
|
|
285
296
|
setClauses.push("updated_at = NOW()");
|
|
286
297
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
298
|
+
// The WHERE id = $N uses the next available param index
|
|
299
|
+
params.push(id);
|
|
300
|
+
|
|
301
|
+
const rows = await exec(
|
|
302
|
+
`UPDATE ${TABLE}
|
|
303
|
+
SET ${setClauses.join(", ")}
|
|
304
|
+
WHERE id = $${paramIdx}
|
|
305
|
+
RETURNING *`,
|
|
306
|
+
{ params }
|
|
307
|
+
);
|
|
293
308
|
|
|
294
309
|
if (rows.length === 0) return null;
|
|
295
310
|
return toMasked(rowToApiKey(rows[0]));
|
|
@@ -297,23 +312,25 @@ export function createApiKeyStore(driver: DataDriver): ApiKeyStore | undefined {
|
|
|
297
312
|
|
|
298
313
|
// ── Revoke (soft-delete) ────────────────────────────────────
|
|
299
314
|
async revokeApiKey(id: string): Promise<boolean> {
|
|
300
|
-
const rows = await exec(
|
|
301
|
-
UPDATE ${TABLE}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
315
|
+
const rows = await exec(
|
|
316
|
+
`UPDATE ${TABLE}
|
|
317
|
+
SET revoked_at = NOW(), updated_at = NOW()
|
|
318
|
+
WHERE id = $1 AND revoked_at IS NULL
|
|
319
|
+
RETURNING id`,
|
|
320
|
+
{ params: [id] }
|
|
321
|
+
);
|
|
306
322
|
return rows.length > 0;
|
|
307
323
|
},
|
|
308
324
|
|
|
309
325
|
// ── Touch last_used_at ──────────────────────────────────────
|
|
310
326
|
async updateLastUsed(id: string): Promise<void> {
|
|
311
327
|
try {
|
|
312
|
-
await exec(
|
|
313
|
-
UPDATE ${TABLE}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
328
|
+
await exec(
|
|
329
|
+
`UPDATE ${TABLE}
|
|
330
|
+
SET last_used_at = NOW()
|
|
331
|
+
WHERE id = $1`,
|
|
332
|
+
{ params: [id] }
|
|
333
|
+
);
|
|
317
334
|
} catch (err) {
|
|
318
335
|
// Non-blocking — don't fail requests because of a usage timestamp update
|
|
319
336
|
logger.error("[api-key-store] Failed to update last_used_at", { error: err });
|
|
@@ -32,6 +32,8 @@ export interface ApiKey {
|
|
|
32
32
|
/** SHA-256 hash of the full plaintext key. */
|
|
33
33
|
key_hash: string;
|
|
34
34
|
permissions: ApiKeyPermission[];
|
|
35
|
+
/** When true, the key is granted the `admin` role (access to admin routes). */
|
|
36
|
+
admin: boolean;
|
|
35
37
|
/** Requests per 15-minute window. `null` means unlimited. */
|
|
36
38
|
rate_limit: number | null;
|
|
37
39
|
created_by: string;
|
|
@@ -51,6 +53,8 @@ export interface ApiKeyMasked {
|
|
|
51
53
|
name: string;
|
|
52
54
|
key_prefix: string;
|
|
53
55
|
permissions: ApiKeyPermission[];
|
|
56
|
+
/** When true, the key is granted the `admin` role (access to admin routes). */
|
|
57
|
+
admin: boolean;
|
|
54
58
|
rate_limit: number | null;
|
|
55
59
|
created_by: string;
|
|
56
60
|
created_at: string;
|
|
@@ -66,6 +70,8 @@ export interface ApiKeyMasked {
|
|
|
66
70
|
export interface CreateApiKeyRequest {
|
|
67
71
|
name: string;
|
|
68
72
|
permissions: ApiKeyPermission[];
|
|
73
|
+
/** When true, grants the `admin` role — allows access to admin routes. */
|
|
74
|
+
admin?: boolean;
|
|
69
75
|
/** Requests per 15-minute window. Omit or `null` for unlimited. */
|
|
70
76
|
rate_limit?: number | null;
|
|
71
77
|
/** ISO-8601 expiration timestamp. Omit for no expiration. */
|
|
@@ -79,6 +85,8 @@ export interface CreateApiKeyRequest {
|
|
|
79
85
|
export interface UpdateApiKeyRequest {
|
|
80
86
|
name?: string;
|
|
81
87
|
permissions?: ApiKeyPermission[];
|
|
88
|
+
/** When true, grants the `admin` role — allows access to admin routes. */
|
|
89
|
+
admin?: boolean;
|
|
82
90
|
rate_limit?: number | null;
|
|
83
91
|
expires_at?: string | null;
|
|
84
92
|
}
|
package/src/auth/apple-oauth.ts
CHANGED
|
@@ -110,7 +110,8 @@ export function createAppleProvider(config: {
|
|
|
110
110
|
providerId: decoded.sub,
|
|
111
111
|
email,
|
|
112
112
|
displayName,
|
|
113
|
-
photoUrl: null // Apple does not provide a profile photo
|
|
113
|
+
photoUrl: null, // Apple does not provide a profile photo
|
|
114
|
+
emailVerified: decoded.email_verified === true || decoded.email_verified === "true"
|
|
114
115
|
};
|
|
115
116
|
} catch (error) {
|
|
116
117
|
logger.error("Apple OAuth error", { error: error });
|
package/src/auth/auth-hooks.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import type { PasswordValidationResult } from "./password";
|
|
46
46
|
import type { AuthRepository, UserData, CreateUserData } from "./interfaces";
|
|
47
47
|
import type { EmailService, EmailConfig } from "../email";
|
|
48
|
+
import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Authentication method identifier for lifecycle hooks.
|
|
@@ -181,6 +182,26 @@ export interface AuthHooks {
|
|
|
181
182
|
*/
|
|
182
183
|
customizeAccessToken?(claims: Record<string, unknown>, user: UserData): Promise<Record<string, unknown>>;
|
|
183
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Transform the auth response before sending it to the client.
|
|
187
|
+
*
|
|
188
|
+
* Called after successful login, register, refresh, OAuth, anonymous,
|
|
189
|
+
* magic-link, and MFA flows. The hook receives the fully-formed
|
|
190
|
+
* response and returns a (potentially enriched) response.
|
|
191
|
+
*
|
|
192
|
+
* Use cases:
|
|
193
|
+
* - Inject tokens from external auth systems (Firebase Custom Tokens, etc.)
|
|
194
|
+
* - Add project-specific metadata to the response
|
|
195
|
+
* - Enrich the user object with data from external sources
|
|
196
|
+
*
|
|
197
|
+
* The hook runs in the request path — keep it fast.
|
|
198
|
+
* Heavy work should be offloaded to `onAuthenticated` (fire-and-forget).
|
|
199
|
+
*/
|
|
200
|
+
transformAuthResponse?(
|
|
201
|
+
response: AuthResponsePayload,
|
|
202
|
+
context: TransformAuthResponseContext
|
|
203
|
+
): Promise<AuthResponsePayload>;
|
|
204
|
+
|
|
184
205
|
/**
|
|
185
206
|
* Called after a successful password reset.
|
|
186
207
|
*
|
|
@@ -72,7 +72,8 @@ export function createBitbucketProvider(config: { clientId: string; clientSecret
|
|
|
72
72
|
providerId: p.uuid,
|
|
73
73
|
email,
|
|
74
74
|
displayName: p.display_name || p.username || null,
|
|
75
|
-
photoUrl: p.links?.avatar?.href || null
|
|
75
|
+
photoUrl: p.links?.avatar?.href || null,
|
|
76
|
+
emailVerified: true
|
|
76
77
|
};
|
|
77
78
|
} catch (error) {
|
|
78
79
|
logger.error("Bitbucket OAuth error", { error: error });
|
|
@@ -22,11 +22,13 @@ import type {
|
|
|
22
22
|
BootstrappedAuth
|
|
23
23
|
} from "@rebasepro/types";
|
|
24
24
|
|
|
25
|
-
import
|
|
25
|
+
import { Hono } from "hono";
|
|
26
26
|
import { verifyAccessToken } from "./jwt";
|
|
27
27
|
import type { AccessTokenPayload } from "./jwt";
|
|
28
28
|
import { createAuthRoutes } from "./routes";
|
|
29
29
|
import { createResetPasswordRoute } from "./reset-password-admin";
|
|
30
|
+
import { createAdminRolesRoute } from "./admin-roles-route";
|
|
31
|
+
import { createAdminUsersRoute } from "./admin-users-route";
|
|
30
32
|
import { prepareAdminUserValues, finalizeAdminUserCreation } from "./admin-user-ops";
|
|
31
33
|
import type { AuthRepository, OAuthProvider } from "./interfaces";
|
|
32
34
|
import type { AuthHooks, ResolvedAuthHooks } from "./auth-hooks";
|
|
@@ -62,6 +64,8 @@ export interface BuiltinAuthAdapterConfig {
|
|
|
62
64
|
authHooks?: AuthHooks;
|
|
63
65
|
/** The parsed auth config from the collection (if `auth` is an object, not just `true`). */
|
|
64
66
|
collectionAuthConfig?: import("@rebasepro/types").AuthCollectionConfig;
|
|
67
|
+
/** Enable magic link (passwordless email) login. Requires email service. */
|
|
68
|
+
enableMagicLink?: boolean;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -81,7 +85,8 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
81
85
|
oauthProviders = [],
|
|
82
86
|
serviceKey,
|
|
83
87
|
authHooks,
|
|
84
|
-
collectionAuthConfig
|
|
88
|
+
collectionAuthConfig,
|
|
89
|
+
enableMagicLink = false
|
|
85
90
|
} = config;
|
|
86
91
|
|
|
87
92
|
const resolvedOps = resolveAuthHooks(authHooks);
|
|
@@ -188,12 +193,26 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
188
193
|
allowRegistration,
|
|
189
194
|
defaultRole,
|
|
190
195
|
oauthProviders,
|
|
191
|
-
authHooks
|
|
196
|
+
authHooks,
|
|
197
|
+
enableMagicLink
|
|
192
198
|
});
|
|
193
199
|
},
|
|
194
200
|
|
|
195
201
|
createAdminRoutes(): Hono<HonoEnv> | undefined {
|
|
196
|
-
|
|
202
|
+
const router = new Hono<HonoEnv>();
|
|
203
|
+
const resetPasswordRoute = createResetPasswordRoute({
|
|
204
|
+
authRepo: authRepository,
|
|
205
|
+
emailService,
|
|
206
|
+
emailConfig,
|
|
207
|
+
serviceKey,
|
|
208
|
+
authHooks,
|
|
209
|
+
collectionAuthConfig
|
|
210
|
+
});
|
|
211
|
+
const rolesRoute = createAdminRolesRoute({
|
|
212
|
+
authRepo: authRepository,
|
|
213
|
+
serviceKey
|
|
214
|
+
});
|
|
215
|
+
const adminUsersRoute = createAdminUsersRoute({
|
|
197
216
|
authRepo: authRepository,
|
|
198
217
|
emailService,
|
|
199
218
|
emailConfig,
|
|
@@ -201,6 +220,10 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
201
220
|
authHooks,
|
|
202
221
|
collectionAuthConfig
|
|
203
222
|
});
|
|
223
|
+
router.route("/", resetPasswordRoute);
|
|
224
|
+
router.route("/", rolesRoute);
|
|
225
|
+
router.route("/", adminUsersRoute);
|
|
226
|
+
return router;
|
|
204
227
|
},
|
|
205
228
|
|
|
206
229
|
async prepareUserCreation(values, collectionAuth) {
|
|
@@ -245,6 +268,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
245
268
|
sessionManagement: true,
|
|
246
269
|
profileUpdate: true,
|
|
247
270
|
emailVerification: !!emailService?.isConfigured(),
|
|
271
|
+
magicLink: enableMagicLink && !!emailService?.isConfigured(),
|
|
248
272
|
enabledProviders,
|
|
249
273
|
needsSetup
|
|
250
274
|
};
|
|
@@ -51,6 +51,7 @@ export function createCustomAuthAdapter(options: CustomAuthAdapterOptions): Auth
|
|
|
51
51
|
sessionManagement: false,
|
|
52
52
|
profileUpdate: false,
|
|
53
53
|
emailVerification: false,
|
|
54
|
+
magicLink: false,
|
|
54
55
|
enabledProviders: [],
|
|
55
56
|
...options.capabilities
|
|
56
57
|
};
|
|
@@ -76,6 +77,7 @@ export function createCustomAuthAdapter(options: CustomAuthAdapterOptions): Auth
|
|
|
76
77
|
|
|
77
78
|
userManagement: options.userManagement,
|
|
78
79
|
|
|
80
|
+
transformAuthResponse: options.transformAuthResponse,
|
|
79
81
|
|
|
80
82
|
getCapabilities() {
|
|
81
83
|
return defaultCapabilities;
|
|
@@ -73,7 +73,8 @@ export function createDiscordProvider(config: { clientId: string; clientSecret:
|
|
|
73
73
|
providerId: profileData.id,
|
|
74
74
|
email: profileData.email,
|
|
75
75
|
displayName: profileData.global_name || profileData.username || null,
|
|
76
|
-
photoUrl
|
|
76
|
+
photoUrl,
|
|
77
|
+
emailVerified: profileData.verified === true
|
|
77
78
|
};
|
|
78
79
|
} catch (error) {
|
|
79
80
|
logger.error("Discord OAuth error", { error: error });
|
|
@@ -62,7 +62,8 @@ export function createFacebookProvider(config: { clientId: string; clientSecret:
|
|
|
62
62
|
providerId: profileData.id,
|
|
63
63
|
email: profileData.email,
|
|
64
64
|
displayName: profileData.name || null,
|
|
65
|
-
photoUrl: profileData.picture?.data?.url || null
|
|
65
|
+
photoUrl: profileData.picture?.data?.url || null,
|
|
66
|
+
emailVerified: true
|
|
66
67
|
};
|
|
67
68
|
} catch (error) {
|
|
68
69
|
logger.error("Facebook OAuth error", { error: error });
|
package/src/auth/github-oauth.ts
CHANGED
|
@@ -100,7 +100,8 @@ export function createGitHubProvider(config: { clientId: string; clientSecret: s
|
|
|
100
100
|
providerId: String(profileData.id),
|
|
101
101
|
email,
|
|
102
102
|
displayName: profileData.name || profileData.login || null,
|
|
103
|
-
photoUrl: profileData.avatar_url || null
|
|
103
|
+
photoUrl: profileData.avatar_url || null,
|
|
104
|
+
emailVerified: true
|
|
104
105
|
};
|
|
105
106
|
} catch (error) {
|
|
106
107
|
logger.error("GitHub OAuth error", { error: error });
|
package/src/auth/gitlab-oauth.ts
CHANGED
|
@@ -60,7 +60,8 @@ export function createGitLabProvider(config: {
|
|
|
60
60
|
providerId: String(p.id),
|
|
61
61
|
email: p.email,
|
|
62
62
|
displayName: p.name || p.username || null,
|
|
63
|
-
photoUrl: p.avatar_url || null
|
|
63
|
+
photoUrl: p.avatar_url || null,
|
|
64
|
+
emailVerified: true
|
|
64
65
|
};
|
|
65
66
|
} catch (error) {
|
|
66
67
|
logger.error("GitLab OAuth error", { error: error });
|
package/src/auth/google-oauth.ts
CHANGED
|
@@ -89,7 +89,8 @@ export function createGoogleProvider(config: GoogleProviderConfig | string): OAu
|
|
|
89
89
|
providerId: content.sub,
|
|
90
90
|
email: content.email || "",
|
|
91
91
|
displayName: content.name || null,
|
|
92
|
-
photoUrl: content.picture || null
|
|
92
|
+
photoUrl: content.picture || null,
|
|
93
|
+
emailVerified: true
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -115,7 +116,8 @@ export function createGoogleProvider(config: GoogleProviderConfig | string): OAu
|
|
|
115
116
|
providerId: info.sub,
|
|
116
117
|
email: info.email,
|
|
117
118
|
displayName: info.name || null,
|
|
118
|
-
photoUrl: info.picture || null
|
|
119
|
+
photoUrl: info.picture || null,
|
|
120
|
+
emailVerified: true
|
|
119
121
|
};
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -176,7 +178,8 @@ export function createGoogleProvider(config: GoogleProviderConfig | string): OAu
|
|
|
176
178
|
providerId: content.sub,
|
|
177
179
|
email: content.email || "",
|
|
178
180
|
displayName: content.name || null,
|
|
179
|
-
photoUrl: content.picture || null
|
|
181
|
+
photoUrl: content.picture || null,
|
|
182
|
+
emailVerified: true
|
|
180
183
|
};
|
|
181
184
|
}
|
|
182
185
|
|
|
@@ -202,7 +205,8 @@ export function createGoogleProvider(config: GoogleProviderConfig | string): OAu
|
|
|
202
205
|
providerId: info.sub,
|
|
203
206
|
email: info.email,
|
|
204
207
|
displayName: info.name || null,
|
|
205
|
-
photoUrl: info.picture || null
|
|
208
|
+
photoUrl: info.picture || null,
|
|
209
|
+
emailVerified: true
|
|
206
210
|
};
|
|
207
211
|
}
|
|
208
212
|
|
package/src/auth/index.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type { AuthMiddlewareOptions, AuthResult } from "./middleware";
|
|
|
35
35
|
export { createAuthRoutes } from "./routes";
|
|
36
36
|
export type { AuthModuleConfig } from "./routes";
|
|
37
37
|
|
|
38
|
+
export { mountMagicLinkRoutes } from "./magic-link-routes";
|
|
39
|
+
|
|
38
40
|
export { createResetPasswordRoute } from "./reset-password-admin";
|
|
39
41
|
export type { ResetPasswordRouteConfig } from "./reset-password-admin";
|
|
40
42
|
|
package/src/auth/interfaces.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface OAuthProviderProfile {
|
|
|
60
60
|
email: string;
|
|
61
61
|
displayName?: string | null;
|
|
62
62
|
photoUrl?: string | null;
|
|
63
|
+
/** Whether the OAuth provider has verified the user's email address */
|
|
64
|
+
emailVerified?: boolean;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -129,6 +131,14 @@ export interface PasswordResetTokenInfo {
|
|
|
129
131
|
expiresAt: Date;
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Magic link token info
|
|
136
|
+
*/
|
|
137
|
+
export interface MagicLinkTokenInfo {
|
|
138
|
+
userId: string;
|
|
139
|
+
expiresAt: Date;
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
// =============================================================================
|
|
133
143
|
// AUTH REPOSITORY INTERFACES
|
|
134
144
|
// =============================================================================
|
|
@@ -357,6 +367,23 @@ export interface TokenRepository {
|
|
|
357
367
|
* Clean up expired tokens
|
|
358
368
|
*/
|
|
359
369
|
deleteExpiredTokens(): Promise<void>;
|
|
370
|
+
|
|
371
|
+
// Magic link tokens
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Create a magic link token
|
|
375
|
+
*/
|
|
376
|
+
createMagicLinkToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Find a valid (not expired, not used) magic link token by hash
|
|
380
|
+
*/
|
|
381
|
+
findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Mark a magic link token as used
|
|
385
|
+
*/
|
|
386
|
+
markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
|
|
360
387
|
}
|
|
361
388
|
|
|
362
389
|
// =============================================================================
|
|
@@ -71,7 +71,8 @@ export function createLinkedinProvider(config: { clientId: string, clientSecret:
|
|
|
71
71
|
providerId: profileData.sub,
|
|
72
72
|
email: profileData.email,
|
|
73
73
|
displayName: profileData.name || null,
|
|
74
|
-
photoUrl: profileData.picture || null
|
|
74
|
+
photoUrl: profileData.picture || null,
|
|
75
|
+
emailVerified: profileData.email_verified === true
|
|
75
76
|
};
|
|
76
77
|
} catch (error) {
|
|
77
78
|
logger.error("LinkedIn OAuth error", { error: error });
|