@rebasepro/server 0.22.0 → 0.24.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/README.md +1 -1
- package/bin/rebase-server.js +4 -2
- package/dist/{GCSStorageController-CLIJXwGS.js → GCSStorageController-BSiP1c-f.js} +57 -29
- package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
- package/dist/{S3StorageController-Dcuf8lMA.js → S3StorageController-CAwFRgjV.js} +19 -7
- package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +127 -1
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/live-schema-routes.d.ts +52 -8
- package/dist/api/logs-routes.d.ts +39 -1
- package/dist/api/openapi-generator.d.ts +17 -0
- package/dist/api/rest/api-generator.d.ts +158 -42
- package/dist/api/rest/auth-collection-writes.d.ts +85 -0
- package/dist/api/rest/field-access-query.d.ts +6 -2
- package/dist/api/rest/idempotency.d.ts +7 -1
- package/dist/api/rest/nested-write-access.d.ts +46 -0
- package/dist/api/rest/write-validation.d.ts +34 -2
- package/dist/api/types.d.ts +17 -1
- package/dist/{ast-schema-editor-CslO8Oje.js → ast-schema-editor-CWqS_sLJ.js} +411 -13
- package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
- package/dist/auth/access.d.ts +105 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/address-ownership.d.ts +68 -0
- package/dist/auth/admin-roles-route.d.ts +4 -2
- package/dist/auth/admin-roles.d.ts +17 -20
- package/dist/auth/admin-user-ops.d.ts +35 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
- package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
- package/dist/auth/api-keys/api-key-store.d.ts +31 -8
- package/dist/auth/api-keys/api-key-types.d.ts +14 -16
- package/dist/auth/api-keys/http-operation.d.ts +19 -0
- package/dist/auth/api-keys/index.d.ts +11 -11
- package/dist/auth/api-keys/key-grant.d.ts +41 -0
- package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
- package/dist/auth/auth-hooks.d.ts +50 -7
- package/dist/auth/builtin-auth-adapter.d.ts +8 -0
- package/dist/auth/captcha.d.ts +5 -0
- package/dist/auth/cookie-utils.d.ts +7 -0
- package/dist/auth/deliverable-address.d.ts +6 -0
- package/dist/auth/email-change-routes.d.ts +41 -0
- package/dist/auth/expired-token-sweep.d.ts +67 -0
- package/dist/auth/impersonation.d.ts +110 -0
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/interfaces.d.ts +146 -65
- package/dist/auth/jwt.d.ts +66 -3
- package/dist/auth/magic-link-routes.d.ts +2 -6
- package/dist/auth/mfa-routes.d.ts +2 -9
- package/dist/auth/middleware.d.ts +17 -5
- package/dist/auth/oauth-signin-policy.d.ts +25 -8
- package/dist/auth/otp-routes.d.ts +2 -6
- package/dist/auth/passwordless-signup.d.ts +27 -0
- package/dist/auth/platform-token.d.ts +122 -0
- package/dist/auth/rate-limiter.d.ts +72 -1
- package/dist/auth/routes.d.ts +45 -0
- package/dist/auth/scope-routes.d.ts +22 -0
- package/dist/auth/session-routes.d.ts +18 -6
- package/dist/auth/token-revocation.d.ts +53 -1
- package/dist/auth/verify-credential.d.ts +28 -0
- package/dist/{auth-CCDpk2rn.js → auth-DMLngxn_.js} +2712 -711
- package/dist/auth-DMLngxn_.js.map +1 -0
- package/dist/backend-DTAOsLQc.js +30 -0
- package/dist/backend-DTAOsLQc.js.map +1 -0
- package/dist/backup/backup-common.d.ts +29 -0
- package/dist/backup/backup-routes.d.ts +24 -4
- package/dist/backup/backup-schedule.d.ts +33 -0
- package/dist/backup/backup-storage.d.ts +14 -0
- package/dist/backup/index.d.ts +2 -0
- package/dist/backup-CN0s50D2.js +444 -0
- package/dist/backup-CN0s50D2.js.map +1 -0
- package/dist/boot/bundle.d.ts +19 -0
- package/dist/boot/driver.d.ts +10 -0
- package/dist/boot/env.d.ts +51 -6
- package/dist/boot/fetch-bundle.d.ts +18 -1
- package/dist/boot/rls-audit-option.d.ts +26 -0
- package/dist/boot/security-headers.d.ts +26 -0
- package/dist/boot/sources.d.ts +1 -0
- package/dist/boot/static-routing.d.ts +56 -0
- package/dist/collection_patch-BRu-BvDv.js +472 -0
- package/dist/collection_patch-BRu-BvDv.js.map +1 -0
- package/dist/{contract-routes-eLxV0le1.js → contract-routes-fz8i4pxs.js} +17 -4
- package/dist/contract-routes-fz8i4pxs.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +7 -2
- package/dist/cron/cron-scheduler.d.ts +146 -21
- package/dist/cron/cron-store.d.ts +76 -8
- package/dist/cron/index.d.ts +1 -1
- package/dist/{cron-loader-CQjvjpEw.js → cron-loader-CwaANlOG.js} +4 -4
- package/dist/cron-loader-CwaANlOG.js.map +1 -0
- package/dist/cron-routes-Bc-SB0Se.js +96 -0
- package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
- package/dist/{cron-scheduler-COPQxlEq.js → cron-scheduler-CYQgco86.js} +427 -83
- package/dist/cron-scheduler-CYQgco86.js.map +1 -0
- package/dist/{cron-store-BYGZFNWk.js → cron-store-D2Q9-Aco.js} +139 -23
- package/dist/cron-store-D2Q9-Aco.js.map +1 -0
- package/dist/{ddl-bootstrap-CfNvxMuK.js → ddl-bootstrap-BaqMSa4Y.js} +3 -26
- package/dist/ddl-bootstrap-BaqMSa4Y.js.map +1 -0
- package/dist/email/index.d.ts +2 -2
- package/dist/email/link-base.d.ts +5 -4
- package/dist/email/smtp-email-service.d.ts +13 -1
- package/dist/email/templates.d.ts +31 -0
- package/dist/email/types.d.ts +29 -2
- package/dist/env.d.ts +25 -7
- package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
- package/dist/errors-D6_y86c5.js.map +1 -0
- package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
- package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
- package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
- package/dist/function-routes-CaNG4waN.js.map +1 -0
- package/dist/functions/context.d.ts +17 -6
- package/dist/functions/guards.d.ts +22 -5
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.js +90 -36
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-BQmB0P_j.js → history-recorder-Nr8zLvoU.js} +9 -7
- package/dist/history-recorder-Nr8zLvoU.js.map +1 -0
- package/dist/{history-store-CetkrBBD.js → history-store-rcAm_xFR.js} +2 -2
- package/dist/{history-store-CetkrBBD.js.map → history-store-rcAm_xFR.js.map} +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.es.js +5733 -1551
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +5 -2
- package/dist/init/health.d.ts +17 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +54 -0
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/job-queue.d.ts +23 -2
- package/dist/jobs/job-store.d.ts +37 -5
- package/dist/jobs/types.d.ts +8 -6
- package/dist/{jobs-Bjr8DZAi.js → jobs-DqYNfquG.js} +306 -167
- package/dist/jobs-DqYNfquG.js.map +1 -0
- package/dist/{jwt-C4OW-DNq.js → jwt-R6bSPMjk.js} +114 -38
- package/dist/{jwt-C4OW-DNq.js.map → jwt-R6bSPMjk.js.map} +1 -1
- package/dist/{keys-Qfc4XieN.js → keys-GAVZqbqx.js} +18 -17
- package/dist/{keys-Qfc4XieN.js.map → keys-GAVZqbqx.js.map} +1 -1
- package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
- package/dist/logger-D-S-hO5e.js.map +1 -0
- package/dist/{logs-routes-3EEzPjhl.js → logs-routes-DAdv37GI.js} +54 -11
- package/dist/logs-routes-DAdv37GI.js.map +1 -0
- package/dist/mcp/consent-page.d.ts +1 -1
- package/dist/mcp/mcp-routes.d.ts +45 -2
- package/dist/mcp/mcp-tools.d.ts +22 -10
- package/dist/mcp/oauth-metadata.d.ts +21 -16
- package/dist/mcp/oauth-routes.d.ts +34 -1
- package/dist/mcp/oauth-store.d.ts +29 -13
- package/dist/metrics/history-recorder.d.ts +1 -1
- package/dist/{openapi-generator-D8uFz-LW.js → openapi-generator-DAq_XVDu.js} +135 -22
- package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
- package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
- package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
- package/dist/{query-parser-BleZmY18.js → query-parser-BgiKJKvc.js} +41 -82
- package/dist/query-parser-BgiKJKvc.js.map +1 -0
- package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
- package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
- package/dist/rls-audit/index.d.ts +4 -0
- package/dist/schema-edit/apply-schema-change.d.ts +63 -3
- package/dist/schema-edit/project-root.d.ts +3 -2
- package/dist/schema-edit/remote-source.d.ts +9 -4
- package/dist/{schema-editor-routes-DdLihzp0.js → schema-editor-routes-oIyuWl3L.js} +12 -7
- package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
- package/dist/serve-spa.d.ts +58 -0
- package/dist/services/routed-realtime-service.d.ts +11 -0
- package/dist/soft-delete-params-BWPilMPF.js +59 -0
- package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
- package/dist/{src-Br6ARbs6.js → src-CatHFUym.js} +439 -45
- package/dist/src-CatHFUym.js.map +1 -0
- package/dist/{src-1vL-I1Po.js → src-I3aG1PcY.js} +371 -81
- package/dist/src-I3aG1PcY.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +13 -1
- package/dist/storage/LocalStorageController.d.ts +2 -0
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/keys.d.ts +12 -0
- package/dist/storage/property-limits.d.ts +41 -6
- package/dist/storage/rendition-cache.d.ts +11 -1
- package/dist/storage/request-keys.d.ts +82 -0
- package/dist/storage/requested-object.d.ts +74 -0
- package/dist/storage/routes.d.ts +36 -18
- package/dist/storage/tus-handler.d.ts +30 -5
- package/dist/storage/types.d.ts +36 -1
- package/dist/types-BfKcm9do.js.map +1 -1
- package/dist/utils/logger.d.ts +12 -0
- package/package.json +9 -9
- package/dist/GCSStorageController-CLIJXwGS.js.map +0 -1
- package/dist/S3StorageController-Dcuf8lMA.js.map +0 -1
- package/dist/admin-roles-vYdp_Pil.js +0 -36
- package/dist/admin-roles-vYdp_Pil.js.map +0 -1
- package/dist/admin_block-DxKLmdiv.js +0 -206
- package/dist/admin_block-DxKLmdiv.js.map +0 -1
- package/dist/ast-schema-editor-CslO8Oje.js.map +0 -1
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
- package/dist/auth-CCDpk2rn.js.map +0 -1
- package/dist/backup-DzI9jLwc.js +0 -192
- package/dist/backup-DzI9jLwc.js.map +0 -1
- package/dist/contract-routes-eLxV0le1.js.map +0 -1
- package/dist/cron-loader-CQjvjpEw.js.map +0 -1
- package/dist/cron-routes-B7CRGfiq.js +0 -72
- package/dist/cron-routes-B7CRGfiq.js.map +0 -1
- package/dist/cron-scheduler-COPQxlEq.js.map +0 -1
- package/dist/cron-store-BYGZFNWk.js.map +0 -1
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +0 -1
- package/dist/errors-DWsX4yTd.js.map +0 -1
- package/dist/function-routes-Chet4-lB.js.map +0 -1
- package/dist/history-recorder-BQmB0P_j.js.map +0 -1
- package/dist/jobs-Bjr8DZAi.js.map +0 -1
- package/dist/logger-DO2PZc4i.js.map +0 -1
- package/dist/logs-routes-3EEzPjhl.js.map +0 -1
- package/dist/openapi-generator-D8uFz-LW.js.map +0 -1
- package/dist/query-parser-BleZmY18.js.map +0 -1
- package/dist/schema-editor-routes-DdLihzp0.js.map +0 -1
- package/dist/src-1vL-I1Po.js.map +0 -1
- package/dist/src-Br6ARbs6.js.map +0 -1
|
@@ -3,9 +3,28 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
-
import { t as ApiError } from "./errors-
|
|
7
|
-
import { n as hasAdministrativeRole } from "./admin-roles-vYdp_Pil.js";
|
|
6
|
+
import { t as ApiError } from "./errors-D6_y86c5.js";
|
|
8
7
|
import { Hono } from "hono";
|
|
8
|
+
//#region src/auth/admin-roles.ts
|
|
9
|
+
/**
|
|
10
|
+
* The admin role and scope matching, for code that may not import
|
|
11
|
+
* `@rebasepro/types`.
|
|
12
|
+
*
|
|
13
|
+
* The custom-functions surface (`@rebasepro/server/functions`) must bundle for
|
|
14
|
+
* a runtime with no Node built-ins and imports nothing but `hono`, so its
|
|
15
|
+
* guards cannot reach the canonical definitions in `@rebasepro/types`
|
|
16
|
+
* (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,
|
|
17
|
+
* restated, and `test/admin-roles.test.ts` holds them equal.
|
|
18
|
+
*
|
|
19
|
+
* Everything else imports the `@rebasepro/types` versions.
|
|
20
|
+
*/
|
|
21
|
+
/** The one built-in role. It holds every scope. */
|
|
22
|
+
var ADMIN_ROLE_NAME = "admin";
|
|
23
|
+
/** Does this list of roles include the admin role? */
|
|
24
|
+
function holdsAdminRole(roles) {
|
|
25
|
+
return !!roles?.includes(ADMIN_ROLE_NAME);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
9
28
|
//#region src/functions/context.ts
|
|
10
29
|
function read(c, key) {
|
|
11
30
|
return c.get(key);
|
|
@@ -56,16 +75,9 @@ function hasRole(c, ...roles) {
|
|
|
56
75
|
const held = new Set(getRoles(c));
|
|
57
76
|
return roles.some((role) => held.has(role));
|
|
58
77
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Whether the caller holds an administrative role.
|
|
61
|
-
*
|
|
62
|
-
* Delegates to the single definition in `auth/admin-roles.ts` — which is
|
|
63
|
-
* `admin` **or** `schema-admin` — rather than comparing against `"admin"`.
|
|
64
|
-
* Those two lists disagreed once, and the gap made every public registrant an
|
|
65
|
-
* administrator; see that file.
|
|
66
|
-
*/
|
|
78
|
+
/** Whether the caller holds the `admin` role, which holds every scope. */
|
|
67
79
|
function isAdmin(c) {
|
|
68
|
-
return
|
|
80
|
+
return holdsAdminRole(getRoles(c));
|
|
69
81
|
}
|
|
70
82
|
/** Whether the request carries an identity at all. */
|
|
71
83
|
function isAuthenticated(c) {
|
|
@@ -264,4 +276,4 @@ function createFunctionRoutes(functions, problems = [], mountPath = "/functions"
|
|
|
264
276
|
//#endregion
|
|
265
277
|
export { getDriver as a, getUser as c, identityResolved as d, isAdmin as f, getApiKey as i, getUserId as l, requireDriver as m, function_routes_exports as n, getRequestId as o, isAuthenticated as p, requireRole as r, getRoles as s, createFunctionRoutes as t, hasRole as u };
|
|
266
278
|
|
|
267
|
-
//# sourceMappingURL=function-routes-
|
|
279
|
+
//# sourceMappingURL=function-routes-CaNG4waN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"function-routes-CaNG4waN.js","names":[],"sources":["../src/auth/admin-roles.ts","../src/functions/context.ts","../src/functions/guards.ts","../src/functions/function-routes.ts"],"sourcesContent":["/**\n * The admin role and scope matching, for code that may not import\n * `@rebasepro/types`.\n *\n * The custom-functions surface (`@rebasepro/server/functions`) must bundle for\n * a runtime with no Node built-ins and imports nothing but `hono`, so its\n * guards cannot reach the canonical definitions in `@rebasepro/types`\n * (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,\n * restated, and `test/admin-roles.test.ts` holds them equal.\n *\n * Everything else imports the `@rebasepro/types` versions.\n */\n\n/** The one built-in role. It holds every scope. */\nexport const ADMIN_ROLE_NAME = \"admin\";\n\n/** Does this list of roles include the admin role? */\nexport function holdsAdminRole(roles: readonly string[] | null | undefined): boolean {\n return !!roles?.includes(ADMIN_ROLE_NAME);\n}\n\n/**\n * Does a set of held scopes grant `scope`, optionally on one `target`? The\n * unqualified grant covers every target; `scope:target` covers its own.\n */\nexport function heldScopesGrant(held: readonly string[], scope: string, target?: string): boolean {\n for (const entry of held) {\n if (entry === scope) return true;\n if (target !== undefined && entry === `${scope}:${target}`) return true;\n }\n return false;\n}\n","/**\n * Reading the request context from inside a custom function.\n *\n * The functions router resolves the caller's identity before any handler runs\n * and leaves the result on the Hono context. Getting it back out used to be the\n * user's problem, and the shape made that worse than it sounds: `HonoEnv`\n * types `user` as `AuthResult`, a union that includes `boolean`, `null` and an\n * index signature, because the same slot is filled by four different middlewares\n * — JWT, service key, API key, and a user-supplied validator that may return\n * `true`. Every example in the documentation therefore opened with\n *\n * const user = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n *\n * and an assertion in a security-relevant position is exactly the kind of line\n * that gets copied once and then never re-examined. It is also wrong in one\n * case that occurs in practice: a custom validator returning `true` stores\n * `{ uid: \"default\", roles: [] }`, which the assertion above types as having a\n * `uid` — true here, but nothing checks it.\n *\n * These accessors do the narrowing once, in the framework, where it can be\n * tested. They are also **runtime-neutral by construction** — no crypto, no\n * token parsing, no I/O, nothing but property reads on an object another\n * middleware already populated. That is what lets them live in\n * `@rebasepro/server/functions` and run unchanged on a host that has no Node\n * built-ins.\n *\n * @module\n */\nimport type { Context } from \"hono\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { ApiKeyMasked } from \"../auth/api-keys/api-key-types\";\nimport { heldScopesGrant, holdsAdminRole } from \"../auth/admin-roles\";\n\n/**\n * The caller, as a custom function sees them.\n *\n * A narrowed view of whatever the auth middleware resolved: `uid` and `roles`\n * are guaranteed, and the index signature keeps any extra claims the token or\n * the adapter carried (`email`, `org_id`, anything a custom validator added)\n * reachable without a cast.\n */\nexport interface FunctionUser {\n /** Stable id of the caller. `\"service\"` for service-key and API-key callers. */\n uid: string;\n /** Roles as resolved for this request. Never `undefined` — an empty array instead. */\n roles: string[];\n /** Present when the identity carried one. Not every auth method does. */\n email?: string;\n /** Any further claim the token, adapter or validator supplied. */\n [claim: string]: unknown;\n}\n\n/** Anything with a Hono-style `.get`, so these work on any `Context` shape. */\ntype CtxLike = Context<HonoEnv> | Context;\n\nfunction read<K extends keyof HonoEnv[\"Variables\"]>(\n c: CtxLike,\n key: K\n): HonoEnv[\"Variables\"][K] | undefined {\n // `c.get` is typed against the app's own Env, which a handler mounted\n // through `app.route()` may have declared more loosely. The cast is\n // confined to this one function rather than repeated at every call site.\n return (c as Context<HonoEnv>).get(key);\n}\n\n/**\n * The authenticated caller, or `undefined` for an anonymous request.\n *\n * **`undefined` is not a permission decision.** The functions router mounts its\n * auth middleware with `requireAuth: false` on purpose — a webhook receiver has\n * no token to send — so an anonymous caller reaches the handler and reads\n * `undefined` here while the handler runs on regardless. Use {@link requireAuth}\n * (or a `!user` branch that returns 401) to make it a decision.\n *\n * A caller who presented a *bad* token never gets this far: both auth\n * middlewares reject an unverifiable token with 401 before the router is\n * reached, precisely so an expired session cannot be silently downgraded to an\n * anonymous one.\n */\nexport function getUser(c: CtxLike): FunctionUser | undefined {\n const raw = read(c, \"user\");\n if (!raw || typeof raw !== \"object\") return undefined;\n\n const record = raw as Record<string, unknown>;\n const uid = typeof record.uid === \"string\" ? record.uid : undefined;\n if (uid === undefined) return undefined;\n\n const roles = Array.isArray(record.roles)\n ? record.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return { ...record,\n uid,\n roles } as FunctionUser;\n}\n\n/** The caller's id, or `undefined` when nobody is signed in. */\nexport function getUserId(c: CtxLike): string | undefined {\n return getUser(c)?.uid;\n}\n\n/** The caller's roles. Empty for an anonymous request — never `undefined`. */\nexport function getRoles(c: CtxLike): string[] {\n return getUser(c)?.roles ?? [];\n}\n\n/**\n * Whether the caller holds **any** of the named roles.\n *\n * Any rather than all, because that is what a route guard means by a list of\n * roles; require several by calling this more than once.\n */\nexport function hasRole(c: CtxLike, ...roles: string[]): boolean {\n if (roles.length === 0) return false;\n const held = new Set(getRoles(c));\n return roles.some(role => held.has(role));\n}\n\n/** Whether the caller holds the `admin` role, which holds every scope. */\nexport function isAdmin(c: CtxLike): boolean {\n return holdsAdminRole(getRoles(c));\n}\n\n/**\n * Everything the caller may do, as `resource:action[:target]` scope strings —\n * the data plane, the app's own `auth.scopes`, and whatever their roles (or\n * their API key) hold. Empty for an anonymous request.\n *\n * Resolved by the framework before the handler runs; `undefined` only when no\n * Rebase auth middleware ran (see {@link identityResolved}).\n */\nexport function getScopes(c: CtxLike): string[] | undefined {\n return read(c, \"scopes\");\n}\n\n/**\n * Whether the caller holds `scope`, on `target` when one is given.\n *\n * For an app scope declared under `auth.scopes` — `project:deploy` — a\n * signed-in person always holds it, so this narrows only API keys and tokens.\n * Whether the *person* may deploy *this* project is still the handler's to\n * decide.\n */\nexport function hasScope(c: CtxLike, scope: string, target?: string): boolean {\n return heldScopesGrant(getScopes(c) ?? [], scope, target);\n}\n\n/** Whether the request carries an identity at all. */\nexport function isAuthenticated(c: CtxLike): boolean {\n return getUser(c) !== undefined;\n}\n\n/**\n * The request-scoped data driver: reads and writes run as **the caller**, with\n * your row-level security policies evaluated against their identity.\n *\n * This is the accessor to reach for when a function serves user-facing data.\n * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs\n * as `{ uid: \"service\", roles: [\"admin\"] }` for every caller alike, which is\n * correct for trusted background work and wrong for a request.\n *\n * `undefined` only when no Rebase auth middleware ran (see\n * {@link identityResolved}); inside a function mounted by the framework it is\n * always present, anonymous requests included — they get an anon-scoped driver\n * so policies still have an identity to evaluate.\n */\nexport function getDriver(c: CtxLike): DataDriver | undefined {\n return read(c, \"driver\");\n}\n\n/**\n * {@link getDriver}, but throws instead of handing back `undefined`.\n *\n * For the common case where a handler cannot proceed without it and would\n * otherwise write `c.get(\"driver\")!` — an assertion that turns a wiring problem\n * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty\n * lines away from the cause.\n */\nexport function requireDriver(c: CtxLike): DataDriver {\n const driver = getDriver(c);\n if (!driver) {\n throw new Error(\n \"No request-scoped driver on this context. A Rebase auth middleware \" +\n \"populates it before any custom function runs, so this means the handler \" +\n \"was mounted outside the functions router — e.g. added to your own Hono \" +\n \"app directly. Mount it from the functions directory, or use \" +\n \"`rebase.dataAsAdmin` if the work is genuinely service-scoped.\"\n );\n }\n return driver;\n}\n\n/**\n * The API key this request authenticated with, masked, or `undefined` when it\n * did not use one.\n *\n * Useful for attribution and for per-key behaviour. The permission check itself\n * has already happened — reaching a handler means the key was allowed to.\n */\nexport function getApiKey(c: CtxLike): ApiKeyMasked | undefined {\n return read(c, \"apiKey\");\n}\n\n/**\n * The correlation id for this request — generated, or taken from an inbound\n * `X-Request-ID`.\n *\n * Log it. It is the only thing that ties a line written inside a function to\n * the framework's own lines for the same request.\n */\nexport function getRequestId(c: CtxLike): string | undefined {\n return read(c, \"requestId\");\n}\n\n/**\n * Whether a Rebase auth middleware has run on this request.\n *\n * Both middlewares populate `driver` for *every* outcome, anonymous included,\n * and populate `user` whenever there is one. So \"neither is set\" does not mean\n * \"anonymous\" — it means nothing resolved the identity, and treating that as\n * anonymous is the dangerous reading. The guards use this to tell a genuinely\n * anonymous caller (401) from a misconfigured mount (500), because answering\n * 401 to the second sends whoever is debugging it to look at the token.\n */\nexport function identityResolved(c: CtxLike): boolean {\n return read(c, \"user\") !== undefined || read(c, \"driver\") !== undefined;\n}\n","/**\n * Route guards for custom functions.\n *\n * These decide access from the identity the platform already resolved. They do\n * **not** verify tokens, and that division is the point rather than a\n * limitation:\n *\n * - Verifying a token needs a signing key, constant-time comparison and a\n * revocation lookup. That is host work, it belongs to the process that holds\n * the secret, and it is the part of the stack that cannot be made\n * runtime-neutral without rewriting it against WebCrypto.\n * - Deciding whether *this* caller may call *this* route is application work.\n * It needs nothing but the resolved identity, so it costs nothing to make it\n * portable — and it is the half that lives in user code.\n *\n * Splitting there is what lets a function file compile and run unchanged on a\n * host with no Node built-ins, and it is why these live in\n * `@rebasepro/server/functions` while `verifyAccessToken` does not.\n *\n * **Inside the functions router these are equivalent to the guards exported\n * from the package root.** Both auth middlewares resolve the identity before\n * any handler runs: a valid credential populates `user`, an invalid one is\n * rejected with 401 by the middleware itself, and a missing one leaves `user`\n * unset. So the root `requireAuth`'s token-parsing branch is unreachable from a\n * function, and removing it changes no outcome. The one difference is a handler\n * mounted **outside** the framework's router, where no middleware ran: the root\n * guard would parse the `Authorization` header itself, and these refuse the\n * request with a 500 that names the wiring problem. Fail-closed, and legible.\n *\n * @module\n */\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { getUser, isAdmin, getRoles, identityResolved, hasScope } from \"./context\";\n\n/**\n * The answer to \"a guard ran, but no middleware had resolved anything\".\n *\n * Deliberately a 500 and not a 401. A 401 tells the caller their credential is\n * the problem, and here the caller's credential was never looked at — sending\n * them to check their token is sending them to the one place the answer is not.\n */\nfunction unresolvedIdentity(): { error: { message: string; code: string } } {\n return {\n error: {\n message:\n \"This route's identity was never resolved: no Rebase auth middleware ran \" +\n \"before the guard. A function loaded from the functions directory always \" +\n \"has one. This usually means the Hono app was mounted onto your own \" +\n \"server directly, bypassing the functions router.\",\n code: \"AUTH_MIDDLEWARE_MISSING\"\n }\n };\n}\n\n/**\n * Reject anonymous callers with 401.\n *\n * Put it in the route's own middleware slot rather than `app.use(\"/*\", …)`:\n * `use()` covers only the routes declared *below* it, so a route appended later\n * — by you, months from now, at the bottom of the file — is silently\n * unprotected. The per-route form cannot drift that way.\n *\n * @example\n * ```ts\n * app.post(\"/\", requireAuth, async (c) => {\n * const user = getUser(c)!; // guaranteed by the guard\n * return c.json({ uid: user.uid });\n * });\n * ```\n */\nexport const requireAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n if (getUser(c)) return next();\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n};\n\n/**\n * Reject callers without the `admin` role with 403.\n *\n * Must come **after** {@link requireAuth}: on its own it answers 401 for an\n * anonymous caller, which is right, but pairing them keeps the two failures\n * distinguishable — 401 \"who are you\", 403 \"not you\".\n *\n * Prefer {@link requireScope} for anything an app declares a scope for: a\n * scope can be granted to a narrower role and to a key, and `admin` cannot.\n */\nexport const requireAdmin: MiddlewareHandler<HonoEnv> = async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!isAdmin(c)) {\n return c.json({\n error: {\n message: \"Admin privileges required for this operation\",\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n};\n\n/**\n * Reject callers holding none of the named roles with 403.\n *\n * Any of them, not all — require several by chaining the guard twice. Naming no\n * role at all is a programming error and throws at module load rather than at\n * request time, because `requireRole()` with an empty list would otherwise read\n * as a guard while admitting everyone.\n *\n * @example\n * ```ts\n * app.post(\"/publish\", requireAuth, requireRole(\"editor\", \"admin\"), handler);\n * ```\n */\nexport function requireRole(...roles: string[]): MiddlewareHandler<HonoEnv> {\n if (roles.length === 0) {\n throw new Error(\n \"requireRole() needs at least one role. An empty list would admit every \" +\n \"signed-in caller while reading as a restriction.\"\n );\n }\n\n const allowed = new Set(roles);\n return async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!getRoles(c).some(role => allowed.has(role))) {\n return c.json({\n error: {\n message: `This operation requires one of these roles: ${roles.join(\", \")}`,\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n };\n}\n\n/**\n * Reject callers who do not hold `scope` with 403.\n *\n * `scope` is a built-in scope or one the app declares under `auth.scopes` on\n * the users collection. `target` narrows it to one resource — a key holding\n * `project:deploy:p1` passes `requireScope(\"project:deploy\", c => c.req.param(\"project\"))`\n * for `p1` only — and may be read from the request.\n *\n * A signed-in person holds every app scope, so for a person this is no\n * authorization at all: it narrows API keys and tokens. Decide whether the\n * person may act in the handler, as you would without it.\n *\n * @example\n * ```ts\n * app.post(\"/deploy/:project\", requireAuth, requireScope(\"project:deploy\", c => c.req.param(\"project\")), handler);\n * ```\n */\nexport function requireScope(\n scope: string,\n target?: string | ((c: Context<HonoEnv>) => string | undefined)\n): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (!getUser(c)) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n const resolvedTarget = typeof target === \"function\" ? target(c) : target;\n if (!hasScope(c, scope, resolvedTarget)) {\n const wanted = resolvedTarget !== undefined ? `${scope}:${resolvedTarget}` : scope;\n return c.json({\n error: {\n message: `This credential does not hold the \"${wanted}\" scope.`,\n code: \"SCOPE_MISSING\",\n details: { requiredScope: wanted }\n }\n }, 403);\n }\n return next();\n };\n}\n","import { Hono } from \"hono\";\nimport { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { LoadedFunction } from \"./function-loader\";\nimport { requireAuth } from \"./guards\";\n\n/** The file a loader problem names, without its extension: `broken.ts (threw: …)` → `broken`. */\nfunction problemName(problem: string): string {\n return problem.split(\" \")[0].replace(/\\/$/, \"\").replace(/\\.[cm]?[jt]s$/, \"\");\n}\n\n/**\n * Mount all loaded function routes under a single Hono router.\n *\n * Each function is mounted at `/<function-name>`, preserving\n * whatever HTTP methods and middleware the Hono sub-app defines.\n *\n * @param functions What loaded. May be empty — the router still mounts, so\n * \"no functions are served\" answers 200 with an empty list instead of 404.\n * @param problems The files the loader saw and could not serve, as\n * `\"<file> (<reason>)\"`. The listing reports a count and a pointer to the log,\n * not the reasons: those carry import errors, and the listing is one guard\n * away from anyone. The unmatched-route handler uses the *names* to answer the\n * one question a 404 on a function nobody can find should answer — \"there is a\n * file for this and it did not load\" — which is the difference between a typo\n * and a broken deploy, and the loader was the only thing that knew.\n */\nexport function createFunctionRoutes(\n functions: LoadedFunction[],\n problems: string[] = [],\n /**\n * Where this router is mounted, so the listing can report a path a caller\n * can actually request. It used to hardcode `/functions/<name>`, which is\n * wrong under every `basePath` including the default `/api`.\n */\n mountPath = \"/functions\"\n): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const skipped = problems.length;\n\n // Listing endpoint: GET / → list available functions.\n //\n // Functions themselves stay anonymous-callable by default — a webhook\n // receiver has to be — but the index of them does not: it is an inventory\n // of every custom endpoint, for whoever asks. `requireAuth` admits any\n // resolved identity (a signed-in user, an API key, the service key), so\n // `rebase doctor` and `rebase cloud debug` keep their answer; the latter\n // already reads a 401 here as \"mounted\".\n router.get(\"/\", requireAuth, (c) => {\n return c.json({\n functions: functions.map((fn) => ({\n name: fn.name,\n endpoint: `${mountPath}/${fn.name}`\n })),\n ...(skipped > 0 && {\n skipped,\n note: `${skipped} function file(s) failed to load and are NOT served — see the server log for the reason.`\n })\n });\n });\n\n for (const fn of functions) {\n router.route(`/${fn.name}`, fn.app);\n }\n\n // A name that matches nothing, answered in the envelope.\n //\n // Registered last so every real route wins it. Without it, a typo'd\n // function name — the single most likely 404 a developer meets on this\n // surface — fell through to Hono's default `404 Not Found` as `text/plain`,\n // and through the SDK arrived as `RebaseApiError { code: undefined }`, so\n // the documented `e.code === \"FUNCTION_NOT_FOUND\"` branch never ran.\n //\n // What the message may say depends on who is asking. The mounted names are\n // an inventory of every custom endpoint, which is exactly what the listing\n // above requires an identity to see — so an anonymous caller is told their\n // name is unknown and nothing more, and a resolved caller gets the list\n // that turns the 404 into a fix.\n const mounted = new Set(functions.map(fn => fn.name));\n const failedToLoad = new Map(problems.map(p => [problemName(p), p.split(\" \")[0]]));\n\n router.all(\"/:name{.*}\", (c): never => {\n const requested = (c.req.param(\"name\") ?? \"\").split(\"/\").filter(Boolean);\n const name = requested[0] ?? \"\";\n const rest = requested.slice(1).join(\"/\");\n const identified = Boolean(c.get(\"user\"));\n\n const refuse = (message: string): never => {\n throw new ApiError(404, \"FUNCTION_NOT_FOUND\", message, { function: name }, true);\n };\n\n if (!name) {\n refuse(`No function in the request path. Expected ${mountPath}/<function>.`);\n }\n if (mounted.has(name)) {\n refuse(\n `The function '${name}' is served, and has no ${c.req.method} route at '/${rest}'. ` +\n \"The path after the function name is routed by the function's own Hono app.\"\n );\n }\n const file = failedToLoad.get(name);\n if (file) {\n refuse(\n `The function '${name}' is not served: '${file}' failed to load. ` +\n \"The server log records why, at boot.\"\n );\n }\n return refuse(\n `No function named '${name}' on this backend.` +\n (identified\n ? (mounted.size > 0\n ? ` This backend serves: ${[...mounted].sort().join(\", \")}.`\n : \" This backend serves no functions.\")\n : \"\") +\n (skipped > 0 && identified\n ? ` ${skipped} function file(s) failed to load and are not served — see the server log.`\n : \"\")\n );\n });\n\n return router;\n}\n"],"mappings":"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"}
|
|
@@ -76,15 +76,26 @@ export declare function getRoles(c: CtxLike): string[];
|
|
|
76
76
|
* roles; require several by calling this more than once.
|
|
77
77
|
*/
|
|
78
78
|
export declare function hasRole(c: CtxLike, ...roles: string[]): boolean;
|
|
79
|
+
/** Whether the caller holds the `admin` role, which holds every scope. */
|
|
80
|
+
export declare function isAdmin(c: CtxLike): boolean;
|
|
79
81
|
/**
|
|
80
|
-
*
|
|
82
|
+
* Everything the caller may do, as `resource:action[:target]` scope strings —
|
|
83
|
+
* the data plane, the app's own `auth.scopes`, and whatever their roles (or
|
|
84
|
+
* their API key) hold. Empty for an anonymous request.
|
|
81
85
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* Those two lists disagreed once, and the gap made every public registrant an
|
|
85
|
-
* administrator; see that file.
|
|
86
|
+
* Resolved by the framework before the handler runs; `undefined` only when no
|
|
87
|
+
* Rebase auth middleware ran (see {@link identityResolved}).
|
|
86
88
|
*/
|
|
87
|
-
export declare function
|
|
89
|
+
export declare function getScopes(c: CtxLike): string[] | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Whether the caller holds `scope`, on `target` when one is given.
|
|
92
|
+
*
|
|
93
|
+
* For an app scope declared under `auth.scopes` — `project:deploy` — a
|
|
94
|
+
* signed-in person always holds it, so this narrows only API keys and tokens.
|
|
95
|
+
* Whether the *person* may deploy *this* project is still the handler's to
|
|
96
|
+
* decide.
|
|
97
|
+
*/
|
|
98
|
+
export declare function hasScope(c: CtxLike, scope: string, target?: string): boolean;
|
|
88
99
|
/** Whether the request carries an identity at all. */
|
|
89
100
|
export declare function isAuthenticated(c: CtxLike): boolean;
|
|
90
101
|
/**
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* @module
|
|
31
31
|
*/
|
|
32
|
-
import type { MiddlewareHandler } from "hono";
|
|
32
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
33
33
|
import type { HonoEnv } from "../api/types.js";
|
|
34
34
|
/**
|
|
35
35
|
* Reject anonymous callers with 401.
|
|
@@ -49,15 +49,14 @@ import type { HonoEnv } from "../api/types.js";
|
|
|
49
49
|
*/
|
|
50
50
|
export declare const requireAuth: MiddlewareHandler<HonoEnv>;
|
|
51
51
|
/**
|
|
52
|
-
* Reject callers without
|
|
52
|
+
* Reject callers without the `admin` role with 403.
|
|
53
53
|
*
|
|
54
54
|
* Must come **after** {@link requireAuth}: on its own it answers 401 for an
|
|
55
55
|
* anonymous caller, which is right, but pairing them keeps the two failures
|
|
56
56
|
* distinguishable — 401 "who are you", 403 "not you".
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* divergence that list exists to prevent.
|
|
58
|
+
* Prefer {@link requireScope} for anything an app declares a scope for: a
|
|
59
|
+
* scope can be granted to a narrower role and to a key, and `admin` cannot.
|
|
61
60
|
*/
|
|
62
61
|
export declare const requireAdmin: MiddlewareHandler<HonoEnv>;
|
|
63
62
|
/**
|
|
@@ -74,3 +73,21 @@ export declare const requireAdmin: MiddlewareHandler<HonoEnv>;
|
|
|
74
73
|
* ```
|
|
75
74
|
*/
|
|
76
75
|
export declare function requireRole(...roles: string[]): MiddlewareHandler<HonoEnv>;
|
|
76
|
+
/**
|
|
77
|
+
* Reject callers who do not hold `scope` with 403.
|
|
78
|
+
*
|
|
79
|
+
* `scope` is a built-in scope or one the app declares under `auth.scopes` on
|
|
80
|
+
* the users collection. `target` narrows it to one resource — a key holding
|
|
81
|
+
* `project:deploy:p1` passes `requireScope("project:deploy", c => c.req.param("project"))`
|
|
82
|
+
* for `p1` only — and may be read from the request.
|
|
83
|
+
*
|
|
84
|
+
* A signed-in person holds every app scope, so for a person this is no
|
|
85
|
+
* authorization at all: it narrows API keys and tokens. Decide whether the
|
|
86
|
+
* person may act in the handler, as you would without it.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* app.post("/deploy/:project", requireAuth, requireScope("project:deploy", c => c.req.param("project")), handler);
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export declare function requireScope(scope: string, target?: string | ((c: Context<HonoEnv>) => string | undefined)): MiddlewareHandler<HonoEnv>;
|
|
@@ -72,9 +72,9 @@ export type { RebaseFunctionContext } from "./define-function.js";
|
|
|
72
72
|
* `../singleton.ts`.
|
|
73
73
|
*/
|
|
74
74
|
export { rebase } from "../singleton.js";
|
|
75
|
-
export { getUser, getUserId, getRoles, hasRole, isAdmin, isAuthenticated, getDriver, requireDriver, getApiKey, getRequestId, identityResolved } from "./context.js";
|
|
75
|
+
export { getUser, getUserId, getRoles, hasRole, isAdmin, getScopes, hasScope, isAuthenticated, getDriver, requireDriver, getApiKey, getRequestId, identityResolved } from "./context.js";
|
|
76
76
|
export type { FunctionUser } from "./context.js";
|
|
77
|
-
export { requireAuth, requireAdmin, requireRole } from "./guards.js";
|
|
77
|
+
export { requireAuth, requireAdmin, requireRole, requireScope } from "./guards.js";
|
|
78
78
|
export { getEnv, env, requireEnv, runtimeKey, isNodeRuntime, lazyResource } from "./runtime-env.js";
|
|
79
79
|
export { waitUntil } from "./wait-until.js";
|
|
80
80
|
/**
|
package/dist/functions/index.js
CHANGED
|
@@ -233,31 +233,33 @@ function defineFunction(definition) {
|
|
|
233
233
|
//#endregion
|
|
234
234
|
//#region src/auth/admin-roles.ts
|
|
235
235
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* a
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* neither of them owns it.
|
|
247
|
-
*
|
|
248
|
-
* `schema-admin` is here deliberately: it can reach the schema editor and the
|
|
249
|
-
* SQL surfaces, which is administrative by any definition that matters. If a
|
|
250
|
-
* genuinely lesser role is added later, it does not belong in this list — it
|
|
251
|
-
* belongs in a capability check of its own.
|
|
236
|
+
* The admin role and scope matching, for code that may not import
|
|
237
|
+
* `@rebasepro/types`.
|
|
238
|
+
*
|
|
239
|
+
* The custom-functions surface (`@rebasepro/server/functions`) must bundle for
|
|
240
|
+
* a runtime with no Node built-ins and imports nothing but `hono`, so its
|
|
241
|
+
* guards cannot reach the canonical definitions in `@rebasepro/types`
|
|
242
|
+
* (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,
|
|
243
|
+
* restated, and `test/admin-roles.test.ts` holds them equal.
|
|
244
|
+
*
|
|
245
|
+
* Everything else imports the `@rebasepro/types` versions.
|
|
252
246
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
247
|
+
/** The one built-in role. It holds every scope. */
|
|
248
|
+
var ADMIN_ROLE_NAME = "admin";
|
|
249
|
+
/** Does this list of roles include the admin role? */
|
|
250
|
+
function holdsAdminRole(roles) {
|
|
251
|
+
return !!roles?.includes(ADMIN_ROLE_NAME);
|
|
257
252
|
}
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Does a set of held scopes grant `scope`, optionally on one `target`? The
|
|
255
|
+
* unqualified grant covers every target; `scope:target` covers its own.
|
|
256
|
+
*/
|
|
257
|
+
function heldScopesGrant(held, scope, target) {
|
|
258
|
+
for (const entry of held) {
|
|
259
|
+
if (entry === scope) return true;
|
|
260
|
+
if (target !== void 0 && entry === `${scope}:${target}`) return true;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
261
263
|
}
|
|
262
264
|
//#endregion
|
|
263
265
|
//#region src/functions/context.ts
|
|
@@ -310,16 +312,31 @@ function hasRole(c, ...roles) {
|
|
|
310
312
|
const held = new Set(getRoles(c));
|
|
311
313
|
return roles.some((role) => held.has(role));
|
|
312
314
|
}
|
|
315
|
+
/** Whether the caller holds the `admin` role, which holds every scope. */
|
|
316
|
+
function isAdmin(c) {
|
|
317
|
+
return holdsAdminRole(getRoles(c));
|
|
318
|
+
}
|
|
313
319
|
/**
|
|
314
|
-
*
|
|
320
|
+
* Everything the caller may do, as `resource:action[:target]` scope strings —
|
|
321
|
+
* the data plane, the app's own `auth.scopes`, and whatever their roles (or
|
|
322
|
+
* their API key) hold. Empty for an anonymous request.
|
|
315
323
|
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* Those two lists disagreed once, and the gap made every public registrant an
|
|
319
|
-
* administrator; see that file.
|
|
324
|
+
* Resolved by the framework before the handler runs; `undefined` only when no
|
|
325
|
+
* Rebase auth middleware ran (see {@link identityResolved}).
|
|
320
326
|
*/
|
|
321
|
-
function
|
|
322
|
-
return
|
|
327
|
+
function getScopes(c) {
|
|
328
|
+
return read(c, "scopes");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Whether the caller holds `scope`, on `target` when one is given.
|
|
332
|
+
*
|
|
333
|
+
* For an app scope declared under `auth.scopes` — `project:deploy` — a
|
|
334
|
+
* signed-in person always holds it, so this narrows only API keys and tokens.
|
|
335
|
+
* Whether the *person* may deploy *this* project is still the handler's to
|
|
336
|
+
* decide.
|
|
337
|
+
*/
|
|
338
|
+
function hasScope(c, scope, target) {
|
|
339
|
+
return heldScopesGrant(getScopes(c) ?? [], scope, target);
|
|
323
340
|
}
|
|
324
341
|
/** Whether the request carries an identity at all. */
|
|
325
342
|
function isAuthenticated(c) {
|
|
@@ -428,15 +445,14 @@ var requireAuth = async (c, next) => {
|
|
|
428
445
|
} }, 401);
|
|
429
446
|
};
|
|
430
447
|
/**
|
|
431
|
-
* Reject callers without
|
|
448
|
+
* Reject callers without the `admin` role with 403.
|
|
432
449
|
*
|
|
433
450
|
* Must come **after** {@link requireAuth}: on its own it answers 401 for an
|
|
434
451
|
* anonymous caller, which is right, but pairing them keeps the two failures
|
|
435
452
|
* distinguishable — 401 "who are you", 403 "not you".
|
|
436
453
|
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* divergence that list exists to prevent.
|
|
454
|
+
* Prefer {@link requireScope} for anything an app declares a scope for: a
|
|
455
|
+
* scope can be granted to a narrower role and to a key, and `admin` cannot.
|
|
440
456
|
*/
|
|
441
457
|
var requireAdmin = async (c, next) => {
|
|
442
458
|
if (!getUser(c)) {
|
|
@@ -483,6 +499,44 @@ function requireRole(...roles) {
|
|
|
483
499
|
return next();
|
|
484
500
|
};
|
|
485
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Reject callers who do not hold `scope` with 403.
|
|
504
|
+
*
|
|
505
|
+
* `scope` is a built-in scope or one the app declares under `auth.scopes` on
|
|
506
|
+
* the users collection. `target` narrows it to one resource — a key holding
|
|
507
|
+
* `project:deploy:p1` passes `requireScope("project:deploy", c => c.req.param("project"))`
|
|
508
|
+
* for `p1` only — and may be read from the request.
|
|
509
|
+
*
|
|
510
|
+
* A signed-in person holds every app scope, so for a person this is no
|
|
511
|
+
* authorization at all: it narrows API keys and tokens. Decide whether the
|
|
512
|
+
* person may act in the handler, as you would without it.
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* ```ts
|
|
516
|
+
* app.post("/deploy/:project", requireAuth, requireScope("project:deploy", c => c.req.param("project")), handler);
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
function requireScope(scope, target) {
|
|
520
|
+
return async (c, next) => {
|
|
521
|
+
if (!getUser(c)) {
|
|
522
|
+
if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
|
|
523
|
+
return c.json({ error: {
|
|
524
|
+
message: "Authentication required",
|
|
525
|
+
code: "UNAUTHORIZED"
|
|
526
|
+
} }, 401);
|
|
527
|
+
}
|
|
528
|
+
const resolvedTarget = typeof target === "function" ? target(c) : target;
|
|
529
|
+
if (!hasScope(c, scope, resolvedTarget)) {
|
|
530
|
+
const wanted = resolvedTarget !== void 0 ? `${scope}:${resolvedTarget}` : scope;
|
|
531
|
+
return c.json({ error: {
|
|
532
|
+
message: `This credential does not hold the "${wanted}" scope.`,
|
|
533
|
+
code: "SCOPE_MISSING",
|
|
534
|
+
details: { requiredScope: wanted }
|
|
535
|
+
} }, 403);
|
|
536
|
+
}
|
|
537
|
+
return next();
|
|
538
|
+
};
|
|
539
|
+
}
|
|
486
540
|
//#endregion
|
|
487
541
|
//#region src/functions/runtime-env.ts
|
|
488
542
|
/**
|
|
@@ -634,7 +688,7 @@ function isProduction() {
|
|
|
634
688
|
var configuredLevel;
|
|
635
689
|
function getMinLevel() {
|
|
636
690
|
if (configuredLevel) return configuredLevel;
|
|
637
|
-
const env = (hostEnv().LOG_LEVEL || "info").toLowerCase();
|
|
691
|
+
const env = (hostEnv().LOG_LEVEL || "info").trim().toLowerCase();
|
|
638
692
|
if (env in LOG_PRIORITY) return env;
|
|
639
693
|
return "info";
|
|
640
694
|
}
|
|
@@ -1024,6 +1078,6 @@ var ApiError = class ApiError extends Error {
|
|
|
1024
1078
|
}
|
|
1025
1079
|
};
|
|
1026
1080
|
//#endregion
|
|
1027
|
-
export { ApiError, defineFunction, env, getApiKey, getDriver, getEnv, getRequestId, getRoles, getUser, getUserId, hasRole, identityResolved, isAdmin, isAuthenticated, isNodeRuntime, lazyResource, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, runtimeKey, waitUntil };
|
|
1081
|
+
export { ApiError, defineFunction, env, getApiKey, getDriver, getEnv, getRequestId, getRoles, getScopes, getUser, getUserId, hasRole, hasScope, identityResolved, isAdmin, isAuthenticated, isNodeRuntime, lazyResource, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, requireScope, runtimeKey, waitUntil };
|
|
1028
1082
|
|
|
1029
1083
|
//# sourceMappingURL=index.js.map
|