@rebasepro/server 0.25.1 → 0.26.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/{GCSStorageController-CRd9bFrc.js → GCSStorageController-40FyFeSx.js} +6 -2
- package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
- package/dist/{S3StorageController-D00EFa5P.js → S3StorageController-Bl5shp94.js} +6 -2
- package/dist/S3StorageController-Bl5shp94.js.map +1 -0
- package/dist/access-BFAUvsSs.js +1897 -0
- package/dist/access-BFAUvsSs.js.map +1 -0
- package/dist/api/branch-routes.d.ts +25 -0
- package/dist/api/live-schema-routes.d.ts +10 -0
- package/dist/api/rest/api-generator.d.ts +9 -1
- package/dist/api/rest/idempotency.d.ts +15 -1
- package/dist/api/root-error-handler.d.ts +6 -5
- package/dist/api/types.d.ts +12 -0
- package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
- package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
- package/dist/auth/access.d.ts +10 -0
- package/dist/auth/adapter-middleware.d.ts +2 -1
- package/dist/auth/adapter-service-key.d.ts +24 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
- package/dist/auth/api-keys/api-key-store.d.ts +8 -1
- package/dist/auth/api-keys/key-grant.d.ts +18 -1
- package/dist/auth/expired-token-sweep.d.ts +5 -0
- package/dist/auth/impersonation.d.ts +3 -1
- package/dist/auth/jwt.d.ts +8 -2
- package/dist/auth/oauth-signin-policy.d.ts +21 -35
- package/dist/auth/rls-scope.d.ts +17 -7
- package/dist/auth/rls-simulator-routes.d.ts +51 -0
- package/dist/auth/simulation-read-only.d.ts +28 -0
- package/dist/auth/simulation.d.ts +141 -0
- package/dist/auth/sql-rate-limit-store.d.ts +15 -1
- package/dist/{auth-Dkvo2tTJ.js → auth-izI7AEzC.js} +370 -1880
- package/dist/auth-izI7AEzC.js.map +1 -0
- package/dist/auth_adapter-B7RUMk6e.js +89 -0
- package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
- package/dist/backend/create-backend.d.ts +6 -0
- package/dist/backend/options.d.ts +9 -4
- package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
- package/dist/backend-CaG_Jyso.js.map +1 -0
- package/dist/{backup-B9m3cb3X.js → backup-BVbHoKo8.js} +6 -6
- package/dist/{backup-B9m3cb3X.js.map → backup-BVbHoKo8.js.map} +1 -1
- package/dist/boot/boot.d.ts +2 -15
- package/dist/boot/ddl-bootstrap.d.ts +34 -0
- package/dist/boot/env.d.ts +6 -0
- package/dist/boot/fetch-bundle.d.ts +5 -1
- package/dist/boot/provision-only.d.ts +23 -0
- package/dist/boot/provision.d.ts +53 -2
- package/dist/boot/role.d.ts +2 -0
- package/dist/boot/schema-change-recorder.d.ts +45 -0
- package/dist/boot/security-headers.d.ts +22 -0
- package/dist/branch-routes-B2vvEx53.js +84 -0
- package/dist/branch-routes-B2vvEx53.js.map +1 -0
- package/dist/{contract-routes-CsFXW7j4.js → contract-routes-CU2A3JnF.js} +2 -2
- package/dist/{contract-routes-CsFXW7j4.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
- package/dist/cron/cron-store.d.ts +8 -1
- package/dist/{cron-loader-Dj6lA4HQ.js → cron-loader-mMhqbVsl.js} +2 -2
- package/dist/{cron-loader-Dj6lA4HQ.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
- package/dist/{cron-routes-CwbLZguO.js → cron-routes-Cs33j_Kr.js} +3 -3
- package/dist/{cron-routes-CwbLZguO.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
- package/dist/{cron-scheduler-B04ACoUE.js → cron-scheduler-Be7CNghL.js} +2 -2
- package/dist/{cron-scheduler-B04ACoUE.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
- package/dist/{cron-store-DkyfuLOH.js → cron-store-DLn11M8H.js} +10 -9
- package/dist/cron-store-DLn11M8H.js.map +1 -0
- package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
- package/dist/data_driver-D0mg1Y_x.js.map +1 -0
- package/dist/{ddl-bootstrap-Ck20GSIX.js → ddl-bootstrap-CjRrn12l.js} +91 -2
- package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +1 -1
- package/dist/email/dev-sink.d.ts +6 -0
- package/dist/email/index.d.ts +1 -1
- package/dist/{soft-delete-params-BmlVwecC.js → field-access-DYsvjI_P.js} +27 -57
- package/dist/field-access-DYsvjI_P.js.map +1 -0
- package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
- package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
- package/dist/{function-loader-KKDqTcIr.js → function-loader-B8YzXEsQ.js} +21 -14
- package/dist/function-loader-B8YzXEsQ.js.map +1 -0
- package/dist/functions/hono-like.d.ts +12 -0
- package/dist/functions/index.js +19 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/request-timeout.d.ts +3 -3
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/history/retention-sweep.d.ts +5 -0
- package/dist/{history-recorder-B2rkuhuy.js → history-recorder-Bsau7h4X.js} +6 -6
- package/dist/history-recorder-Bsau7h4X.js.map +1 -0
- package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
- package/dist/history-store-BQz5YCkW.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.es.js +2449 -543
- package/dist/index.es.js.map +1 -1
- package/dist/init/process-safety.d.ts +4 -3
- package/dist/init/shutdown.d.ts +8 -0
- package/dist/init.d.ts +6 -1
- package/dist/jobs/job-store.d.ts +8 -1
- package/dist/{jobs-h1WRq3cp.js → jobs-Co6Ni5HP.js} +6 -6
- package/dist/jobs-Co6Ni5HP.js.map +1 -0
- package/dist/{jwt-D3oFauZI.js → jwt-rDS0884_.js} +4 -3
- package/dist/{jwt-D3oFauZI.js.map → jwt-rDS0884_.js.map} +1 -1
- package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
- package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
- package/dist/mcp/consent-page.d.ts +38 -1
- package/dist/mcp/mcp-routes.d.ts +9 -0
- package/dist/mcp/mcp-tools.d.ts +7 -1
- package/dist/mcp/oauth-store.d.ts +12 -5
- package/dist/metrics/history-recorder.d.ts +4 -1
- package/dist/metrics/history-store.d.ts +8 -0
- package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
- package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
- package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
- package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
- package/dist/{openapi-generator-BjHr6upl.js → openapi-generator-CyxEtYEX.js} +61 -22
- package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
- package/dist/{proxy-CH8Jsovg.js → proxy-BYir8tWK.js} +4 -6
- package/dist/proxy-BYir8tWK.js.map +1 -0
- package/dist/{query-parser-otFZqJsC.js → query-parser-CDXWf2hH.js} +6 -5
- package/dist/{query-parser-otFZqJsC.js.map → query-parser-CDXWf2hH.js.map} +1 -1
- package/dist/{request-timeout-59crS-9u.js → request-timeout-DVjo-c_8.js} +4 -4
- package/dist/request-timeout-DVjo-c_8.js.map +1 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
- package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
- package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
- package/dist/{schema-editor-routes-Bsut0bCC.js → schema-editor-routes-Dc3DU_jm.js} +3 -3
- package/dist/{schema-editor-routes-Bsut0bCC.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
- package/dist/soft-delete-params-BRONCOA2.js +59 -0
- package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
- package/dist/{sql-export-routes-DmB_uZ4l.js → sql-export-routes-QycX8W8S.js} +38 -4
- package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
- package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
- package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +2 -0
- package/dist/storage/LocalStorageController.d.ts +8 -3
- package/dist/storage/S3StorageController.d.ts +2 -0
- package/dist/storage/maintenance-keys.d.ts +17 -0
- package/dist/storage/maintenance-routes.d.ts +28 -5
- package/dist/storage/maintenance.d.ts +136 -4
- package/dist/storage/orphan-scans.d.ts +120 -0
- package/dist/storage/types.d.ts +11 -0
- package/dist/strings.es-B7QLnHh2.js +96 -0
- package/dist/strings.es-B7QLnHh2.js.map +1 -0
- package/dist/upload-validation-Bgo626uY.js.map +1 -1
- package/package.json +5 -5
- package/dist/GCSStorageController-CRd9bFrc.js.map +0 -1
- package/dist/S3StorageController-D00EFa5P.js.map +0 -1
- package/dist/auth-Dkvo2tTJ.js.map +0 -1
- package/dist/auth_adapter-CIItCTc8.js +0 -21
- package/dist/auth_adapter-CIItCTc8.js.map +0 -1
- package/dist/backend-DTAOsLQc.js.map +0 -1
- package/dist/cron-store-DkyfuLOH.js.map +0 -1
- package/dist/ddl-bootstrap-Ck20GSIX.js.map +0 -1
- package/dist/function-loader-KKDqTcIr.js.map +0 -1
- package/dist/history-recorder-B2rkuhuy.js.map +0 -1
- package/dist/history-store-DPofGVDp.js.map +0 -1
- package/dist/jobs-h1WRq3cp.js.map +0 -1
- package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
- package/dist/openapi-generator-BjHr6upl.js.map +0 -1
- package/dist/proxy-CH8Jsovg.js.map +0 -1
- package/dist/request-timeout-59crS-9u.js.map +0 -1
- package/dist/soft-delete-params-BmlVwecC.js.map +0 -1
- package/dist/sql-export-routes-DmB_uZ4l.js.map +0 -1
- package/dist/strings.es-BeZWAodn.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-BFAUvsSs.js","names":[],"sources":["../../types/src/controllers/storage.ts","../src/auth/api-keys/http-operation.ts","../src/auth/simulation-read-only.ts","../src/auth/token-revocation.ts","../src/auth/rls-scope.ts","../src/auth/crypto-utils.ts","../src/auth/bearer-token.ts","../src/auth/api-keys/api-key-middleware.ts","../src/auth/simulation.ts","../src/auth/impersonation.ts","../src/storage/requested-object.ts","../src/storage/request-principals.ts","../src/auth/middleware.ts","../src/auth/access.ts"],"sourcesContent":["/**\n * Path prefix that marks an object as **public**. Files stored under this\n * prefix are served without any auth token via a stable, permanent,\n * CDN-cacheable URL (see {@link StorageSource.getSignedUrl}). Shared by the\n * client SDK and the backend so both agree on which objects are public.\n *\n * @group Models\n */\nexport const PUBLIC_STORAGE_PREFIX = \"public/\";\n\n/**\n * True when a storage **key** is public: it lies under\n * {@link PUBLIC_STORAGE_PREFIX}.\n *\n * The key is read exactly as it is — the canonical key within its bucket, the\n * one the server stores, serves, and hands a `storageAuthorize` hook as\n * `key` — and nothing is parsed out of it. That is the difference from\n * {@link isPublicStoragePath}, which reads a client-side *path* and so strips\n * a `scheme://` and a leading `default/` bucket segment first. On a key both are\n * wrong: `default/public/x` is a key in a folder called `default`, and a\n * request path `notes://public/x` names the key `notes:/public/x`. Neither is\n * public, and the server decides on this function so that it never says they\n * are.\n *\n * @group Models\n */\nexport function isPublicStorageKey(key: string | null | undefined): boolean {\n if (!key || !key.startsWith(PUBLIC_STORAGE_PREFIX)) return false;\n // A canonical key never carries a `..` segment; one that does is not a key\n // the server would serve, and is never public.\n return !key.split(/[\\\\/]/).includes(\"..\");\n}\n\n/**\n * True when a storage key or client-side path points at a public object\n * (lives under {@link PUBLIC_STORAGE_PREFIX}).\n *\n * For a path as a client holds it: a `scheme://` prefix and a single leading\n * `default/` bucket segment are stripped first. The server and a\n * `storageAuthorize` hook, which hold the canonical key, use\n * {@link isPublicStorageKey}.\n *\n * @group Models\n */\nexport function isPublicStoragePath(path: string | null | undefined): boolean {\n if (!path) return false;\n let p = path;\n const scheme = p.indexOf(\"://\");\n if (scheme !== -1) p = p.substring(scheme + 3);\n p = p.replace(/^\\/+/, \"\");\n\n // Defense-in-depth: a path containing traversal segments is never public,\n // so an attacker can't reach a private object via `public/../secret`.\n if (p.split(\"/\").some((seg) => seg === \"..\")) return false;\n\n // Public iff the object **key** starts with the public prefix. A single\n // leading `default/` bucket segment is tolerated (the default bucket).\n // A substring match is deliberately NOT used — a private object under a\n // folder literally named `public` (e.g. `reports/public/q3.pdf`) must stay\n // private. Named buckets: pass the key (not `bucket/key`) so the prefix is\n // anchored; otherwise it falls back to a private, token-scoped URL (safe).\n return p.startsWith(PUBLIC_STORAGE_PREFIX) || p.startsWith(`default/${PUBLIC_STORAGE_PREFIX}`);\n}\n\n/**\n * The path the storage routes address an object by, from a key or from the\n * `storageUrl` an upload returned (`<scheme>://<bucket>/<key>`, which is what a\n * property with `includeBucketUrl: true` keeps in its column).\n *\n * The routes address a source's objects by key. On S3 and GCS the bucket in a\n * `storageUrl` is always the source's own — the server refuses any other — so\n * it is dropped: sent along, `s3://acme-media/products/a.png` reached the\n * server as the key `acme-media/products/a.png`, the preview found nothing and\n * a delete reported success while the object stayed. It worked on local disk\n * only, because `local://default/<key>` happens to carry the one bucket\n * segment the routes recognise — which is kept, as is any other local bucket.\n *\n * A bare key whose first folder is `default` is sent behind that bucket\n * segment. The routes read one leading `default/` (in any case) as the bucket,\n * so the key `default/photo.png` sent as it is names the root `photo.png`:\n * {@link defaultBucketPath} is the spelling that names the key itself.\n *\n * Shared by every storage source that talks to those routes, so they address\n * an object alike.\n *\n * @group Models\n */\nexport function storageObjectPath(keyOrUrl: string, bucket?: string): string {\n let filePath = keyOrUrl;\n // Whether `filePath` already carries a bucket segment, rather than being a key.\n let bucketQualified = false;\n const scheme = /^(local|s3|gs):\\/\\//.exec(filePath);\n if (scheme) {\n filePath = filePath.substring(scheme[0].length);\n if (scheme[1] !== \"local\") {\n const slash = filePath.indexOf(\"/\");\n if (slash > 0) filePath = filePath.substring(slash + 1);\n } else {\n bucketQualified = true;\n }\n }\n if (bucket && filePath) {\n // Already behind its bucket, or not: `defaults/x` is not in `default`.\n if (!filePath.startsWith(`${bucket}/`)) filePath = `${bucket}/${filePath}`;\n bucketQualified = true;\n }\n return bucketQualified ? filePath : defaultBucketPath(filePath);\n}\n\n/**\n * The request path of a key in the default bucket: the key itself, unless the\n * routes would read its first folder as the bucket — a first folder named\n * `default`, matched as the routes match it (case-insensitively, and only when\n * something follows it). Then the bucket segment goes in front, so exactly the\n * one segment the routes strip is the one added.\n */\nfunction defaultBucketPath(key: string): string {\n const segments = key.split(\"/\");\n return segments.length > 1 && segments[0].toLowerCase() === \"default\" ? `default/${key}` : key;\n}\n\n/**\n * @group Models\n */\nexport interface UploadFileProps {\n file: File,\n key: string,\n metadata?: Record<string, unknown>,\n bucket?: string,\n /**\n * Store this object as **public**: it is placed under\n * {@link PUBLIC_STORAGE_PREFIX} and served via a stable, token-less,\n * permanent URL (safe to persist in a database and cache on a CDN).\n * Defaults to `false` (private, short-lived signed URLs).\n */\n public?: boolean,\n /**\n * Which property this file is being uploaded *for* — the collection's slug\n * and the property path within it (`coverImage`, `meta.avatar`,\n * `gallery` for an array of files).\n *\n * The server holds the upload to that property's own `storage.maxSize`\n * and `storage.acceptedFiles`. It holds every upload to the limits of the\n * properties whose `storagePath` its key falls in anyway, named or not;\n * naming the property adds that property's rule, which matters for a\n * property whose `storagePath` is a function the server cannot read.\n *\n * Advisory in one direction only. The rules are resolved from the server's\n * own registry by slug, so naming a property can make an upload *stricter*;\n * it can never widen anything.\n */\n context?: UploadPropertyContext\n}\n\n/**\n * The property an upload is destined for.\n *\n * @group Models\n */\nexport interface UploadPropertyContext {\n /** The collection's slug, as the server registered it. */\n collection: string;\n /** Dotted path to the property — `coverImage`, `meta.avatar`. */\n property: string;\n}\n\n/**\n * @group Models\n */\nexport interface UploadFileResult {\n /**\n * Storage key including the file name where the file was uploaded.\n */\n key: string;\n /**\n * Bucket where the file was uploaded\n */\n bucket: string;\n\n /**\n * Fully qualified storage URL for the uploaded file.\n *\n * For example: `s3://my-bucket/path/to/file.png`. Every controller in the\n * framework returns one — S3, GCS and local alike — and a caller that stores\n * the reference needs it, so it is part of the result rather than a maybe.\n */\n storageUrl: string;\n}\n\n/**\n * @group Models\n */\nexport interface DownloadConfig {\n /**\n * Temporal url that can be used to download the file\n */\n url: string | null;\n\n metadata?: DownloadMetadata;\n\n fileNotFound?: boolean;\n}\n\n/**\n * The full set of object metadata, including read-only properties.\n * @public\n */\nexport declare interface DownloadMetadata {\n /**\n * The bucket this object is contained in.\n */\n bucket: string;\n /**\n * The full path of this object.\n */\n fullPath: string;\n /**\n * The short name of this object, which is the last component of the full path.\n * For example, if path is 'full/path/image.png', name is 'image.png'.\n */\n name: string;\n /**\n * The size of this object, in bytes.\n */\n size: number;\n /**\n * Type of the uploaded file\n * e.g. \"image/jpeg\"\n */\n contentType: string;\n\n customMetadata: Record<string, unknown>;\n /**\n * Optional short-lived download token (for local/server-mediated storage).\n * Absent for public objects, which need no token.\n */\n token?: string;\n /**\n * Optional remaining lifetime of the token, in seconds.\n */\n tokenExpiresIn?: number;\n /**\n * True when this object is public: it is served without a token via a\n * stable, permanent, CDN-cacheable URL. When set, the client builds a\n * token-less URL and caches it indefinitely.\n */\n public?: boolean;\n}\n\n/**\n * @group Models\n */\nexport interface StorageSource {\n /**\n * Upload an object, specifying a key\n * @param file\n * @param key\n * @param metadata\n * @param bucket\n */\n putObject: ({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps) => Promise<UploadFileResult>;\n\n /**\n * Convert a storage key or URL into a download configuration (signed URL equivalent)\n * @param keyOrUrl\n * @param bucket\n */\n getSignedUrl: (keyOrUrl: string, bucket?: string) => Promise<DownloadConfig>;\n\n /**\n * Get an object from a storage key.\n * It returns null if the object does not exist.\n * @param key\n * @param bucket\n */\n getObject: (key: string, bucket?: string) => Promise<File | null>;\n\n /**\n * Delete an object.\n * @param key\n * @param bucket\n */\n deleteObject: (key: string, bucket?: string) => Promise<void>;\n\n /**\n * List the contents of a prefix.\n * @param prefix\n * @param options\n */\n listObjects: (prefix: string, options?: {\n bucket?: string,\n maxResults?: number,\n pageToken?: string\n }) => Promise<StorageListResult>;\n\n}\n\n/**\n * Result returned by list().\n * @public\n */\nexport declare interface StorageListResult {\n /**\n * References to prefixes (sub-folders). You can call list() on them to\n * get its contents.\n *\n * Folders are implicit based on '/' in the object paths.\n * For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a')\n * will return '/a/b' as a prefix.\n */\n prefixes: StorageReference[];\n /**\n * Objects in this directory.\n * You can call getMetadata() and getDownloadUrl() on them.\n */\n items: StorageReference[];\n /**\n * If set, there might be more results for this list. Use this token to resume the list.\n */\n nextPageToken?: string;\n}\n\n/**\n * Represents a reference to an S3-compatible storage object. Developers can\n * upload, download, and delete objects, as well as get/set object metadata.\n * @public\n */\nexport declare interface StorageReference {\n /**\n * Returns a s3:// URL for this object in the form\n * `s3://<bucket>/<path>/<to>/<object>`\n * @returns The s3:// URL.\n */\n toString(): string;\n\n /**\n * A reference to the root of this object's bucket.\n */\n root: StorageReference;\n /**\n * The name of the bucket containing this reference's object.\n */\n bucket: string;\n /**\n * The full path of this object.\n */\n fullPath: string;\n /**\n * The short name of this object, which is the last component of the full path.\n * For example, if path is 'full/path/image.png', name is 'image.png'.\n */\n name: string;\n\n /**\n * A reference pointing to the parent location of this reference, or null if\n * this reference is the root.\n */\n parent: StorageReference | null;\n\n /**\n * The object's size in bytes, as the listing read it. Set on the objects\n * of a listing from the built-in local, S3 and GCS sources; absent on\n * folders and from a source that cannot list it without a request per\n * object. Since 0.24.\n */\n size?: number;\n\n /**\n * When the object was last written, as an ISO 8601 timestamp — set where\n * {@link size} is.\n */\n updated?: string;\n\n /**\n * The object's stored content type, as the listing read it: set by local\n * storage and GCS. An S3 listing does not carry it, so on S3 it is absent\n * and `getSignedUrl()` (or the metadata route) still says.\n */\n contentType?: string;\n}\n","/**\n * Which data-plane operation an HTTP request performs, for the scope it needs:\n * `data:read`, `data:write` or `data:delete` (and the same for storage).\n *\n * @module\n */\n\n/** The operation part of a data-plane scope. */\nexport type DataOperation = \"read\" | \"write\" | \"delete\";\n\n/**\n * Map an HTTP method to the operation it performs.\n *\n * - `GET`, `HEAD`, `OPTIONS` → `\"read\"`\n * - `POST`, `PUT`, `PATCH` → `\"write\"`\n * - `DELETE` → `\"delete\"`\n *\n * Any other method is a `\"write\"`: a verb this does not know is not assumed\n * to be harmless.\n */\nexport function httpMethodToOperation(method: string): DataOperation {\n switch (method.toUpperCase()) {\n case \"GET\":\n case \"HEAD\":\n case \"OPTIONS\":\n return \"read\";\n case \"DELETE\":\n return \"delete\";\n default:\n return \"write\";\n }\n}\n","/**\n * \"Simulate access\" is read-only.\n *\n * The simulator answers \"what does this role, user or key see\". A write made\n * while simulating ran as the simulated identity for real: it committed as\n * `rebase_user` with `app.uid` set to the simulated uid, `entity_history`\n * recorded that uid as the author, callbacks saw `context.user` holding a role\n * the account may not even have, and a role-only simulation stamped owner\n * columns with the nobody uid. Only the request log named the administrator.\n * So every door that honours the simulation headers refuses anything but a\n * read, naming the simulation, rather than performing it as someone else.\n *\n * Its own module so the REST generator, history routes and the functions\n * guard can share it without importing the simulator itself.\n *\n * @module\n */\n\nimport { ApiError } from \"../api/errors\";\nimport type { DataOperation } from \"./api-keys/http-operation\";\n\n/**\n * Refuse a write or delete on a request that simulates access.\n *\n * `c` is anything with the request context's `get` — the REST generator calls\n * this with a per-operation stand-in for each operation of a `_batch`.\n * `operation` is what the request does, not its verb: `POST …/query` is a read.\n */\nexport function refuseSimulatedWrite(c: { get: (key: string) => unknown }, operation: DataOperation): void {\n if (operation === \"read\" || !c.get(\"simulation\")) return;\n throw new ApiError(403, \"SIMULATION_READ_ONLY\",\n \"This request simulates access (x-rebase-simulate-*), and simulated access is read-only: it would \" +\n `${operation === \"delete\" ? \"delete\" : \"write\"} as the simulated identity, not as you. ` +\n \"Stop simulating to make changes.\");\n}\n","import type { AuthRepository } from \"./interfaces\";\nimport type { AccessTokenPayload } from \"./jwt\";\nimport { logger } from \"../utils/logger\";\nimport { ApiError, isDataException } from \"../api/errors\";\n\n/**\n * Has this access token been revoked?\n *\n * Everything that ends every session a user holds — a password change or reset\n * of any kind, and `DELETE /auth/sessions` — goes through\n * {@link revokeAllSessions}, which stamps a `tokensValidAfter` watermark on the\n * user. It also deletes refresh-token rows, which is what made the gap easy to\n * miss: the session really is gone, and the *refresh* path really does check\n * the watermark — so signing out looked like it worked.\n *\n * The access token was untouched. It is a bearer credential that nothing\n * consulted a database about, so it stayed valid for its full lifetime after\n * every one of those actions. \"Sign out everywhere\" left the stolen token\n * working; so did changing your password because you thought it had leaked.\n *\n * The watermark had exactly one read in the repository, on refresh. This adds\n * the other one.\n *\n * ## Cost\n *\n * One indexed lookup per request, on the paths that already make one. The\n * adapter re-reads roles for every authenticated request; the admin routes now\n * do too. Both call this with the row they were already fetching in mind — a\n * repository that wants to serve both from one query is free to cache\n * internally, but nothing here assumes it.\n *\n * ## Failure\n *\n * Fails **open**, deliberately, and this is the one place in the auth stack\n * where that is right. The watermark is an extra revocation signal layered over\n * a token that has already been verified — signature, expiry and purpose all\n * checked. Refusing every request when the database is unreachable would turn a\n * transient outage into a total sign-out of an entire deployment, and the\n * attacker this protects against needs to have already stolen a live token. The\n * failure is logged at warn so it is visible rather than silent.\n */\nexport async function isAccessTokenRevoked(\n authRepo: Pick<AuthRepository, \"getTokensValidAfter\">,\n payload: Pick<AccessTokenPayload, \"uid\" | \"iat\">\n): Promise<boolean> {\n // A repository that does not implement the watermark cannot revoke, and\n // says so by absence. Nothing to check.\n if (typeof authRepo.getTokensValidAfter !== \"function\") return false;\n\n // A token with no `iat` cannot be placed relative to the watermark. Tokens\n // minted before `iat` was carried through verification are in this class,\n // and they expire on their own; treating them as revoked would sign out\n // every live session on deploy.\n if (typeof payload.iat !== \"number\") return false;\n\n let validAfter: Date | null;\n try {\n validAfter = await authRepo.getTokensValidAfter(payload.uid);\n } catch (error) {\n logger.warn(\"[Auth] Could not read the token revocation watermark; allowing the request\", {\n uid: payload.uid,\n error\n });\n return false;\n }\n\n // `iat` is whole seconds; the watermark is a millisecond timestamp. Compare\n // in seconds and floor the watermark, so a token issued in the same second\n // as the revocation is treated as revoked rather than surviving on a\n // rounding artefact.\n return issuedBefore(payload, validAfter);\n}\n\n/**\n * The repository reads {@link judgeAccessToken} may make. All optional: a\n * repository answers what it can, and the judge says what it could not ask.\n */\nexport type AccessJudgeRepository = Partial<Pick<AuthRepository,\n \"getAccountAccessState\" | \"getUserWithRoles\" | \"getTokensValidAfter\">>;\n\n/**\n * Why an access token that verifies is not honoured.\n *\n * - `revoked`: issued before the account's revocation watermark — a sign-out\n * everywhere, a password change or reset.\n * - `session-revoked`: its own sign-in was ended — `POST /auth/logout`, or\n * `DELETE /auth/sessions/:id` from another device. Needs the token's `sid`.\n * - `account-deleted`: the account it names no longer exists.\n * - `account-disabled`: an administrator switched the account off.\n */\nexport type AccessTokenRefusal = \"revoked\" | \"session-revoked\" | \"account-deleted\" | \"account-disabled\";\n\nexport type AccessTokenVerdict =\n | {\n live: true;\n /**\n * The account's roles now, or `undefined` when the repository could\n * not say (it reads neither the account nor its roles).\n */\n roles?: string[];\n }\n | { live: false; refusal: AccessTokenRefusal };\n\n/**\n * Is the account behind this verified access token still the one that may use\n * it — and with which roles?\n *\n * A verified signature says who the token was minted for, an hour ago at\n * most. This asks the database what has happened to that account since. Every\n * door that honours an access token — the data plane, the admin gates, the\n * realtime socket — asks it here, so they cannot disagree about it.\n *\n * They did. A deleted account read as \"not revoked\": the watermark lives on\n * the user row, so once the row was gone there was no watermark, and the\n * roles lookup answered `[]` rather than \"nobody\". A token its owner had\n * revoked came back to life when an administrator deleted the account, as an\n * authenticated principal with that uid, for the rest of its lifetime. MCP\n * refresh and personal API keys already treated a missing account as revoked;\n * the main door did not.\n *\n * Throws when the repository does. Unlike the watermark read on its own, this\n * one decides the roles a request runs with, so a failure is a refusal (the\n * callers answer 503) rather than a guess.\n *\n * Except one failure that is an answer: the store refusing the uid as a value\n * its id column cannot hold — `u1` against a UUID key. That names no account,\n * exactly as a well-formed id nobody has does, so it is `account-deleted` and\n * the caller answers 401. Answered as a failure it was a 503 asking the\n * client to try again, which it would do forever.\n */\nexport async function judgeAccessToken(\n authRepo: AccessJudgeRepository,\n payload: Pick<AccessTokenPayload, \"uid\" | \"iat\" | \"sid\">\n): Promise<AccessTokenVerdict> {\n try {\n return await readAccessVerdict(authRepo, payload);\n } catch (error: unknown) {\n if (isDataException(error)) return { live: false, refusal: \"account-deleted\" };\n throw error;\n }\n}\n\nasync function readAccessVerdict(\n authRepo: AccessJudgeRepository,\n payload: Pick<AccessTokenPayload, \"uid\" | \"iat\" | \"sid\">\n): Promise<AccessTokenVerdict> {\n let roles: string[] | undefined;\n let validAfter: Date | null = null;\n\n if (typeof authRepo.getAccountAccessState === \"function\") {\n const state = await authRepo.getAccountAccessState(payload.uid, payload.sid);\n if (!state) return { live: false, refusal: \"account-deleted\" };\n if (state.disabled) return { live: false, refusal: \"account-disabled\" };\n roles = state.roles;\n validAfter = state.tokensValidAfter;\n // Only an explicit `false`: a token minted before `sid` existed, or a\n // store that cannot group sessions, is judged by the watermark alone.\n if (payload.sid && state.sessionActive === false) return { live: false, refusal: \"session-revoked\" };\n } else {\n if (typeof authRepo.getUserWithRoles === \"function\") {\n const account = await authRepo.getUserWithRoles(payload.uid);\n if (!account) return { live: false, refusal: \"account-deleted\" };\n if (account.user.disabled) return { live: false, refusal: \"account-disabled\" };\n roles = account.roles;\n }\n if (typeof authRepo.getTokensValidAfter === \"function\") {\n validAfter = await authRepo.getTokensValidAfter(payload.uid);\n }\n }\n\n if (issuedBefore(payload, validAfter)) return { live: false, refusal: \"revoked\" };\n return { live: true, roles };\n}\n\n/**\n * The 401 a door answers an access token with when {@link judgeAccessToken}\n * refuses it — one wording and one code for each reason, wherever the token\n * was presented.\n *\n * `SESSION_REVOKED` for a token whose session or account is gone: the client\n * SDK answers it with a refresh, which the refresh route refuses for the same\n * reason, and the client signs out. `ACCOUNT_DISABLED` for an account an\n * administrator switched off, which no refresh can mend.\n */\nexport function accessTokenRefusalError(refusal: AccessTokenRefusal): ApiError {\n switch (refusal) {\n case \"account-disabled\":\n return ApiError.unauthorized(\"This account has been disabled\", \"ACCOUNT_DISABLED\");\n case \"account-deleted\":\n return ApiError.unauthorized(\"This account no longer exists\", \"SESSION_REVOKED\");\n case \"revoked\":\n case \"session-revoked\":\n return ApiError.unauthorized(\"Session has been revoked\", \"SESSION_REVOKED\");\n }\n}\n\n/** The watermark comparison {@link isAccessTokenRevoked} and the judge share. */\nfunction issuedBefore(payload: Pick<AccessTokenPayload, \"iat\">, validAfter: Date | null): boolean {\n // A token with no `iat` cannot be placed relative to the watermark; it\n // expires on its own. See `isAccessTokenRevoked`.\n if (!validAfter || typeof payload.iat !== \"number\") return false;\n return payload.iat < Math.floor(validAfter.getTime() / 1000);\n}\n\n/**\n * End every session this user holds, on every device.\n *\n * Two writes, because each covers what the other cannot. Deleting the refresh\n * rows ends the sessions that exist at this instant, and on a repository\n * without the watermark it is the only revocation there is. The watermark\n * voids what the delete cannot see: a refresh already in flight that inserts\n * its rotated token a moment after the delete ran, and every access token\n * already handed out, which {@link isAccessTokenRevoked} refuses from here on.\n *\n * The watermark write does not fail the caller. By the time it runs the\n * credential has usually already changed and the rows are gone, so a 500 would\n * tell someone their password did not change when it did. It is logged, because\n * a failure here leaves access tokens working until they expire.\n */\nexport async function revokeAllSessions(\n authRepo: Pick<AuthRepository, \"deleteAllRefreshTokensForUser\" | \"setTokensValidAfter\">,\n uid: string\n): Promise<void> {\n await authRepo.deleteAllRefreshTokensForUser(uid);\n try {\n await authRepo.setTokensValidAfter?.(uid, new Date());\n } catch (error) {\n logger.warn(\"[Auth] Could not write the token revocation watermark; access tokens issued before now stay valid until they expire\", {\n uid,\n error\n });\n }\n}\n\n/**\n * Replace a user's password and end every session they hold.\n *\n * The one way a password hash is written over an existing account. A new\n * password is what someone sets when they believe the old one — or a session\n * signed in with it — is in someone else's hands, so a password change that\n * leaves those sessions alive does not do the thing it was done for.\n *\n * It used to be a pair of lines repeated after each `updatePassword`, and it\n * reached the two self-service routes and none of the admin ones: an\n * administrator resetting a phished account left the attacker's refresh token\n * minting access tokens for the rest of its lifetime.\n * `test/password-change-revokes-sessions.test.ts` holds every route that sets a\n * password to this, and fails if anything but this function calls\n * `updatePassword`.\n *\n * `null` removes the password instead, for the same reason and with the same\n * revocation: see `confirmAddressOwnership`.\n */\nexport async function replaceUserPassword(\n authRepo: Pick<AuthRepository, \"updatePassword\" | \"deleteAllRefreshTokensForUser\" | \"setTokensValidAfter\">\n & Partial<Pick<AuthRepository, \"deleteAllPasswordResetTokensForUser\">>,\n uid: string,\n passwordHash: string | null\n): Promise<void> {\n await authRepo.updatePassword(uid, passwordHash);\n await revokeAllSessions(authRepo, uid);\n // A reset link still in someone's inbox would set the password again for\n // the rest of its hour — the one just replaced, perhaps because a link\n // went to the wrong place. Every outstanding one goes with the old password.\n await authRepo.deleteAllPasswordResetTokensForUser?.(uid);\n}\n","/**\n * Shared RLS (Row-Level Security) scoping helper.\n *\n * DataDrivers may implement a `withAuth()` method that returns a scoped\n * clone of the driver with RLS policies applied for the given user.\n * This is database-specific (e.g. Postgres SET LOCAL ROLE) and is not\n * part of the core DataDriver interface.\n *\n * This module provides the shared duck-typing logic used by the\n * adapter-aware middleware.\n *\n * @module\n */\n\nimport type { CallerCredential, DataDriver } from \"@rebasepro/types\";\n\n/**\n * The identity the trusted server plane runs as: `rebase.dataAsAdmin`, and any\n * caller presenting the service key.\n *\n * Naming it makes the privilege legible, because it is narrower than it reads.\n * This is **not** an RLS bypass — a driver scoped with it is an\n * `AuthenticatedPostgresBackendDriver`, so every statement runs in a\n * transaction that has done `SET LOCAL ROLE rebase_user` with\n * `app.uid = 'service'`, and policies are evaluated. Two consequences worth\n * knowing before you write a policy:\n *\n * - It passes the default policies through their `rolesOverlap(['admin'])`\n * arm — the same arm an application user holding the `admin` role passes.\n * - `policy.serverContext()` compiles to `rebase.uid() IS NULL` and is therefore\n * **false** for it. A collection with `disableDefaultPolicies: true` whose\n * only rule is `serverContext()` denies these writes (42501) and returns\n * zero rows for these reads.\n *\n * The true bypass is `rebase.sql()`, which runs on the owner connection and\n * never goes near `withAuth`.\n */\nexport const SERVICE_IDENTITY: { uid: string; roles: string[] } = { uid: \"service\", roles: [\"admin\"] };\n\n/**\n * Who a request runs as, as a driver is scoped to it.\n *\n * `credential` is the API key the request authenticated with, when it was one.\n * It changes nothing the database sees; the driver hands it to collection\n * callbacks as `context.credential`, so a personal key's write can be told\n * from its owner's own.\n */\nexport interface ScopeIdentity {\n uid: string;\n roles?: string[];\n isAnonymous?: boolean;\n claims?: Record<string, unknown>;\n credential?: CallerCredential;\n}\n\n/**\n * A DataDriver that supports RLS scoping via `withAuth()`.\n *\n * This is not part of the public DataDriver interface because not all\n * database implementations support RLS. Drivers that do (e.g. Postgres)\n * extend DataDriver with this method.\n */\ninterface RLSScopedDriver extends DataDriver {\n withAuth(user: ScopeIdentity): Promise<DataDriver>;\n}\n\n/**\n * Returns true if the driver supports RLS scoping via `withAuth()`.\n */\nfunction isRLSScopedDriver(driver: DataDriver): driver is RLSScopedDriver {\n return \"withAuth\" in driver && typeof (driver as Record<string, unknown>).withAuth === \"function\";\n}\n\n/**\n * Can this driver run a request as a specific user?\n *\n * The same question `scopeDataDriver` asks internally, exported because one\n * caller must act on the answer BEFORE it has a request to scope.\n *\n * `scopeDataDriver` returns the driver unscoped when the answer is no, which is\n * the right behaviour for `/api/data` — Mongo has no row-level security and\n * never claimed to, and the route's own checks still apply. It is the wrong\n * behaviour for a surface whose entire promise is \"this runs as you\": there,\n * an unscoped driver silently means \"runs as the database owner\". So the MCP\n * surface asks this at boot and declines to mount rather than degrade.\n */\nexport function supportsRlsScoping(driver: DataDriver): boolean {\n return isRLSScopedDriver(driver);\n}\n\n/**\n * Scope a DataDriver via `withAuth()` for RLS.\n *\n * SECURITY: If `withAuth()` is available but fails, the error is re-thrown\n * so the request is **denied** rather than proceeding with unscoped access\n * (fail-closed behavior).\n *\n * If the driver does not support RLS, the original driver is returned.\n *\n * @param driver - The DataDriver to scope.\n * @param user - The authenticated user identity for RLS.\n * @returns The RLS-scoped DataDriver (or the original if RLS is unsupported).\n */\nexport async function scopeDataDriver(\n driver: DataDriver,\n /**\n * `isAnonymous` rides along so a policy can tell a GUEST — an anonymous\n * sign-in — from an account. It is a real user row with a real uid, so\n * without this the database sees the same principal either way. See\n * `rebase.is_anonymous()`.\n */\n user: ScopeIdentity\n): Promise<DataDriver> {\n if (isRLSScopedDriver(driver)) {\n // Fail closed — do NOT catch and swallow errors here.\n // If RLS scoping fails the request must be rejected.\n return await driver.withAuth(user);\n }\n return driver;\n}\n","/**\n * Cryptographic utility functions for auth.\n *\n * @module\n */\n\nimport { constantTimeEqual } from \"../utils/portable-crypto\";\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n *\n * Used for comparing service keys, tokens, and other secrets where\n * timing side-channels could leak information about the expected value.\n *\n * The implementation lives in `utils/portable-crypto.ts`, which does this over\n * UTF-8 bytes without `node:crypto` — this function is on the request path, and\n * the byte-level reasoning it depends on is documented there.\n *\n * @param a - First string to compare.\n * @param b - Second string to compare.\n * @returns `true` if the strings are identical, `false` otherwise.\n */\nexport function safeCompare(a: string, b: string): boolean {\n return constantTimeEqual(a, b);\n}\n","/**\n * Parsing of the `Authorization: Bearer <token>` header.\n *\n * Its own module rather than a helper inside `middleware.ts`, because the\n * API-key middleware needs it too and `middleware.ts` already imports from\n * there — putting it in either one would close an import cycle.\n *\n * @module\n */\n\n/**\n * Pull the token out of an `Authorization: Bearer <token>` header.\n *\n * RFC 7235 §2.1 makes the auth-scheme name case-insensitive, and plenty of\n * clients send `bearer` — HTTP libraries that lower-case what they emit,\n * hand-written curl, proxies that normalise headers. Matching `\"Bearer \"`\n * exactly turned those into a 401 that, from the caller's side, is\n * indistinguishable from a rejected token; people spend an afternoon\n * regenerating credentials that were fine. Only the scheme is folded — the\n * token itself is returned byte-for-byte.\n *\n * Returns `undefined` when there is no header or it names another scheme.\n * That is deliberately distinct from an empty token (`Bearer ` with nothing\n * after it), which callers still route into verification so it fails as a bad\n * token rather than as a missing header.\n */\nexport function extractBearerToken(authHeader: string | undefined | null): string | undefined {\n if (!authHeader) return undefined;\n const separator = authHeader.indexOf(\" \");\n if (separator < 0) return undefined;\n if (authHeader.slice(0, separator).toLowerCase() !== \"bearer\") return undefined;\n return authHeader.slice(separator + 1);\n}\n","/**\n * Authenticating requests that present an API key (`rk_`).\n *\n * {@link resolveApiKey} turns a presented key into the identity it acts as and\n * the scopes it holds; the HTTP middlewares, the realtime socket and `/mcp`\n * all call it, so a key means the same thing on every surface.\n *\n * - A **service key** acts as `api-key:<id>`, with the RLS roles `service`\n * plus whatever it was given, and holds exactly its scopes.\n * - A **personal key** acts as its owner — their uid and their roles as they\n * are *now*, read on every use — and holds its scopes narrowed to what the\n * owner's roles still hold. Demote the owner and the key shrinks with them;\n * delete the account and the key stops.\n *\n * A key never bypasses RLS: the request's driver is scoped to the identity\n * like any other caller's, so the scopes are one ceiling and the database's\n * policies another, independent one.\n *\n * @module\n */\n\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { intersectScopes, scopesForRoles, scopeGrants, scopeGrantsAny, type CallerCredential, type DataDriver } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../../api/types\";\nimport type { ApiKeyStore } from \"./api-key-store\";\nimport type { ApiKeyMasked } from \"./api-key-types\";\nimport { scopeDataDriver } from \"../rls-scope\";\nimport { extractBearerToken } from \"../bearer-token\";\nimport { accessOf, processAccessControl, type AccessControl } from \"../access\";\nimport { logger } from \"../../utils/logger\";\nimport { sha256Hex } from \"../../utils/portable-crypto\";\nimport { ApiError, errorHandler } from \"../../api/errors\";\nimport { refuseSimulatedWrite } from \"../simulation-read-only\";\nimport { httpMethodToOperation } from \"./http-operation\";\n\n/**\n * Check whether a token looks like a Rebase API key.\n */\nexport function isApiKeyToken(token: string): boolean {\n return token.startsWith(\"rk_\");\n}\n\n/** Who a verified key acts as, and what it may do. */\nexport interface ApiKeyIdentity {\n uid: string;\n roles: string[];\n scopes: string[];\n apiKey: ApiKeyMasked;\n}\n\n/** A presented key that does not authenticate, and why. */\nexport interface ApiKeyRefusal {\n message: string;\n}\n\n/**\n * Verify a presented key: it exists, is live, and — for a personal key —\n * personal keys are on and its owner still exists. Records the use.\n *\n * `access` is the backend the key belongs to: whether it takes personal keys,\n * how it finds their owners, and what their roles hold. The process default\n * when omitted.\n */\nexport async function resolveApiKey(\n store: ApiKeyStore,\n token: string,\n access: AccessControl = processAccessControl\n): Promise<ApiKeyIdentity | ApiKeyRefusal> {\n const apiKey = await store.findByKeyHash(await sha256Hex(token));\n\n if (!apiKey) return { message: \"Invalid API key\" };\n const identity = await identityOfKey(apiKey, access);\n if (!(\"uid\" in identity)) return identity;\n\n // Debounced: every request a busy key makes routes through here, and an\n // UPDATE per request would serialize on its one row.\n const lastTouch = lastUsedTouchedAt.get(apiKey.id);\n const now = Date.now();\n if (!lastTouch || now - lastTouch >= LAST_USED_DEBOUNCE_MS) {\n lastUsedTouchedAt.set(apiKey.id, now);\n store.updateLastUsed(apiKey.id).catch(() => {\n // Swallowed intentionally — logged inside the store\n });\n }\n\n const masked: ApiKeyMasked = {\n id: apiKey.id,\n name: apiKey.name,\n kind: apiKey.kind,\n key_prefix: apiKey.key_prefix,\n scopes: apiKey.scopes,\n roles: apiKey.roles,\n owner_uid: apiKey.owner_uid,\n rate_limit: apiKey.rate_limit,\n created_by: apiKey.created_by,\n created_at: apiKey.created_at,\n updated_at: apiKey.updated_at,\n last_used_at: apiKey.last_used_at,\n expires_at: apiKey.expires_at,\n revoked_at: apiKey.revoked_at\n };\n return { ...identity, apiKey: masked };\n}\n\n/**\n * What a collection callback is told about the key a request authenticated\n * with (`context.credential`): its kind, id and name, never the secret.\n */\nexport function credentialOf(apiKey: Pick<ApiKeyMasked, \"id\" | \"name\" | \"kind\">): CallerCredential {\n return { kind: apiKey.kind, id: apiKey.id, name: apiKey.name };\n}\n\n/**\n * Who a stored key acts as and what it holds — the half of\n * {@link resolveApiKey} that does not depend on how the key was found. The RLS\n * simulator asks it of a key named by id, so \"view as this key\" and a request\n * presenting the key cannot drift apart.\n */\nexport async function identityOfKey(\n apiKey: Pick<ApiKeyMasked, \"id\" | \"kind\" | \"owner_uid\" | \"scopes\" | \"roles\" | \"revoked_at\" | \"expires_at\">,\n access: AccessControl = processAccessControl\n): Promise<{ uid: string; roles: string[]; scopes: string[] } | ApiKeyRefusal> {\n if (apiKey.revoked_at) return { message: \"API key has been revoked\" };\n if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) return { message: \"API key has expired\" };\n\n if (apiKey.kind === \"personal\") {\n const resolveOwner = access.resolveKeyOwner;\n if (!resolveOwner || !apiKey.owner_uid) {\n return { message: \"Personal API keys are switched off on this backend\" };\n }\n const owner = await resolveOwner(apiKey.owner_uid);\n if (!owner) return { message: \"The account this API key acts as no longer exists\" };\n return {\n uid: apiKey.owner_uid,\n roles: owner.roles,\n scopes: intersectScopes(apiKey.scopes, scopesForRoles(owner.roles, access.model))\n };\n }\n return {\n uid: `api-key:${apiKey.id}`,\n roles: [\"service\", ...apiKey.roles.filter(role => role !== \"service\")],\n scopes: apiKey.scopes\n };\n}\n\n/** Per-process debounce state for last_used_at touches. */\nconst lastUsedTouchedAt = new Map<string, number>();\nconst LAST_USED_DEBOUNCE_MS = 60_000;\n\n/**\n * Options for the API key authentication handler.\n */\nexport interface ApiKeyAuthOptions {\n store: ApiKeyStore;\n driver: DataDriver;\n}\n\n/**\n * Validate an API key token and populate the Hono context: `user`, `apiKey`,\n * `scopes` and the RLS-scoped `driver`.\n *\n * Returns `true` when the context is populated, or the error Response.\n */\nexport async function validateApiKey(\n c: Context<HonoEnv>,\n token: string,\n options: ApiKeyAuthOptions\n): Promise<Response | true> {\n const resolved = await resolveApiKey(options.store, token, accessOf(c));\n if (!(\"uid\" in resolved)) {\n return errorHandler(ApiError.unauthenticated(resolved.message), c) as Response;\n }\n\n c.set(\"user\", { uid: resolved.uid, roles: resolved.roles });\n c.set(\"apiKey\", resolved.apiKey);\n c.set(\"scopes\", resolved.scopes);\n\n try {\n c.set(\"driver\", await scopeDataDriver(options.driver, {\n uid: resolved.uid,\n roles: resolved.roles,\n // So a collection callback can tell this key's write from its\n // owner's own session: `context.credential`.\n credential: credentialOf(resolved.apiKey)\n }));\n } catch (error) {\n logger.error(\"[AUTH] RLS scoping failed for API key\", { error: error });\n return errorHandler(ApiError.internal(\"Internal authentication error\"), c) as Response;\n }\n\n return true;\n}\n\n/**\n * The 403 for a credential that lacks a data-plane scope outside the REST\n * generator (storage and functions). Names the scope that would grant it.\n */\nexport function forbidScope(c: Context<HonoEnv>, scope: string, target?: string): Response {\n const wanted = target !== undefined && target !== \"\" ? `${scope}:${target}` : scope;\n return errorHandler(new ApiError(403, \"SCOPE_MISSING\",\n `This credential does not hold the \"${wanted}\" scope.`,\n { requiredScope: wanted }), c) as Response;\n}\n\n/** Whether this request's caller holds `scope` — on `target` when given. */\nfunction callerHolds(c: Context<HonoEnv>, scope: string, target?: string): boolean {\n const narrowed = c.get(\"scopes\");\n if (!narrowed) return true; // a person: the data plane is theirs, RLS and policies decide\n return target === undefined ? scopeGrantsAny(narrowed, scope) : scopeGrants(narrowed, scope, target);\n}\n\n/**\n * Scope guard for the resumable-upload routes (`/tus/:id`).\n *\n * Every step of an upload — the offset check (HEAD), the chunks (PATCH), the\n * cancel (DELETE) — is part of writing it, so all of them need\n * `storage:write`. Which source the upload writes to was checked when it was\n * created; these steps only reach an upload the same caller owns.\n */\nexport function createTusScopeGuard(): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n if (/\\/tus\\/[^/]+$/.test(c.req.path) && c.get(\"user\") && !callerHolds(c, \"storage:write\")) {\n return forbidScope(c, \"storage:write\");\n }\n return next();\n };\n}\n\n/**\n * Scope guard for the custom-functions router: a narrowed credential needs\n * `functions:invoke` on the function it calls. The functions index — the\n * listing at the mount point itself — needs the unqualified scope.\n *\n * People pass: what a person may do inside a function is the function's own\n * business, as it always was.\n *\n * @param mountPrefix - The path the functions router is mounted at\n * (e.g. `/api/functions`), used to extract the function\n * name from the request path. Omitted, it is the\n * `mountPath` the functions group recorded for this\n * request — wherever the application mounted it.\n *\n * It also keeps \"Simulate access\" read-only here: a function called with a\n * simulation header by any verb but GET, HEAD or OPTIONS is refused\n * (`SIMULATION_READ_ONLY`), since it would run as the simulated identity.\n */\nexport function createFunctionScopeGuard(mountPrefix?: string): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n try {\n refuseSimulatedWrite(c, httpMethodToOperation(c.req.method));\n } catch (error: unknown) {\n return errorHandler(error instanceof Error ? error : new Error(String(error)), c) as Response;\n }\n const narrowed = c.get(\"scopes\");\n if (!narrowed) return next();\n\n const path = c.req.path;\n const prefix = mountPrefix ?? c.get(\"mountPath\") ?? \"\";\n const idx = path.indexOf(prefix);\n const rest = idx < 0 ? \"\" : path.slice(idx + prefix.length);\n const rawName = rest.split(\"/\").filter(Boolean)[0] ?? \"\";\n let functionName: string;\n try {\n functionName = decodeURIComponent(rawName);\n } catch {\n // Malformed percent-encoding (e.g. %ZZ) — fall back to the raw\n // segment rather than throwing a 500. It won't match any\n // functions:invoke:<name> grant, so only the unqualified one passes.\n functionName = rawName;\n }\n\n const allowed = functionName === \"\"\n ? scopeGrants(narrowed, \"functions:invoke\")\n : scopeGrants(narrowed, \"functions:invoke\", functionName);\n if (!allowed) return forbidScope(c, \"functions:invoke\", functionName);\n return next();\n };\n}\n\n/**\n * Pre-auth middleware for `rk_` bearer tokens.\n *\n * Routers whose auth gate is JWT-based (`createRequireAuth` — the admin\n * surfaces, the key routes) don't know about API keys. Mounted in front of\n * them, this authenticates `rk_` tokens and populates the request context; the\n * downstream gates then see the already-resolved caller and check its scopes.\n *\n * Requests without an `rk_` bearer token pass through untouched. An invalid,\n * revoked, or expired `rk_` token is rejected here (401) rather than falling\n * through to be misparsed as a JWT.\n */\nexport function createApiKeyPreAuth(options: ApiKeyAuthOptions): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n // Already validated by an earlier instance (e.g. the app-level\n // /admin/* pre-auth composing with a router-level one) — don't hit\n // the store twice.\n if (c.get(\"apiKey\")) return next();\n\n const token = extractBearerToken(c.req.header(\"authorization\")) ?? \"\";\n if (!token || !isApiKeyToken(token)) return next();\n\n const result = await validateApiKey(c, token, options);\n if (result !== true) return result;\n return next();\n };\n}\n","/**\n * The RLS simulator: an administrator's data-plane request run as a role, a\n * user or a service key — `x-rebase-simulate-role`, `x-rebase-simulate-user-id`\n * and `x-rebase-simulate-api-key-id`.\n *\n * Impersonation ({@link ./impersonation}) answers \"what does this person see\";\n * this answers the question an author asks while writing policies, before any\n * such person exists: \"what does a `customer` see, what does `u_1` holding\n * `customer` see, what does the support agent's key see\". So a role need not be\n * held by anyone and a uid sent with a role need not name an account. Nothing\n * here widens what the caller could do: the caller is an administrator, whom\n * the default policies already let read and write every row, and every\n * simulated identity is narrower.\n *\n * The identity reaches the database exactly as a real caller's does —\n * `scopeDataDriver` → `withAuth` → `applyAuthContext`, which sets the\n * transaction-local identity (`app.uid`, `app.roles`, `app.jwt`) and switches to\n * `rebase_user`, so every policy is evaluated for it. A simulated key also\n * carries its scopes, so the data API refuses the collections the key cannot\n * reach, as it would refuse the key. Fields the identity's roles cannot read\n * are stripped by the driver as for any caller; {@link reportSimulatedContext}\n * names them on the response.\n *\n * Who may: a signed-in administrator on their own session, judged on their\n * roles as the database has them now. Never an API key, never the service\n * key, never anyone else — a 403, never an answer as themselves.\n *\n * Where: the data API and custom functions, like impersonation. Every other\n * route refuses the headers (`refuseUnhonouredImpersonation`).\n *\n * Read-only: a write, delete, batch, revert or non-GET function call that\n * carries the headers is refused (`SIMULATION_READ_ONLY`, see\n * {@link ./simulation-read-only}) rather than run as the simulated identity.\n *\n * Not simulated: the data API's `requireAuth` gate. A simulated anonymous\n * caller is let past it, so the answer is what the rows' policies give an\n * anonymous caller — a 401 there would send the admin panel to refresh a\n * session that is fine. Everywhere else it is no session, as a real anonymous\n * caller is: it carries no `user`, so a function's `requireAuth` refuses it.\n *\n * @module\n */\n\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport {\n ANONYMOUS_USER_ID,\n formatSimulatedContext,\n hasAdminRole,\n IMPERSONATE_HEADER,\n SIMULATE_API_KEY_ID_HEADER,\n SIMULATE_ROLE_HEADER,\n SIMULATE_USER_ID_HEADER,\n SIMULATED_CONTEXT_HEADER,\n type AccessSimulation,\n type ApiKeyMasked,\n type CollectionConfig,\n type DataDriver\n} from \"@rebasepro/types\";\nimport { apiHiddenFieldNames, restrictedFieldNames } from \"@rebasepro/common\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { scopeDataDriver } from \"./rls-scope\";\nimport type { ImpersonationCredential, ImpersonationUserResolver } from \"./impersonation\";\nimport type { ApiKeyStore } from \"./api-keys/api-key-store\";\nimport { identityOfKey } from \"./api-keys/api-key-middleware\";\nimport { accessOf, type AccessControl } from \"./access\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The uid a role simulated on its own runs as: a valid UUID that no row\n * names, so an ownership policy matches nothing and a policy comparing a\n * `uuid` column to `auth.uid()` still casts cleanly.\n */\nexport const SIMULATED_NOBODY_UID = \"00000000-0000-0000-0000-000000000000\";\n\n/** Roles that mean \"no session at all\". */\nconst ANONYMOUS_ROLES: ReadonlySet<string> = new Set([\"anon\", \"anonymous\"]);\n\n/** Who a granted simulation runs as. */\nexport interface SimulatedIdentity {\n uid: string;\n roles: string[];\n isAnonymous: boolean;\n claims?: Record<string, unknown>;\n /** A key's scopes. Absent for a person, whose data plane is RLS's to decide. */\n scopes?: string[];\n apiKey?: ApiKeyMasked;\n /** What the response header names: the role (or roles) and the uid. */\n role: string;\n}\n\nexport interface SimulationRequest {\n simulation: AccessSimulation;\n /** What the caller presented. Only a `session` may simulate. */\n credential: ImpersonationCredential;\n caller?: { uid: string; roles: readonly string[] };\n /** Re-reads the caller's roles, and finds a user named without a role. */\n resolveUser?: ImpersonationUserResolver;\n /** Finds a key named by id. Absent: a key simulation is refused. */\n apiKeyStore?: ApiKeyStore;\n /** Whose access model a personal key's owner is judged against. */\n access?: AccessControl;\n /** Where the request came in, for the audit line; absent writes none. */\n audit?: Record<string, unknown>;\n}\n\nexport type SimulationDecision =\n | { granted: SimulatedIdentity; simulator: string }\n | { refused: ApiError };\n\nconst ADMIN_ONLY = \"Only a signed-in administrator can simulate another role, user or API key.\";\n\n/** Read the three simulation headers, or `undefined` when none was sent. */\nexport function simulationOf(header: (name: string) => string | undefined): AccessSimulation | undefined {\n const role = header(SIMULATE_ROLE_HEADER);\n const userId = header(SIMULATE_USER_ID_HEADER);\n const apiKeyId = header(SIMULATE_API_KEY_ID_HEADER);\n if (role === undefined && userId === undefined && apiKeyId === undefined) return undefined;\n return {\n ...(role !== undefined ? { role } : {}),\n ...(userId !== undefined ? { userId } : {}),\n ...(apiKeyId !== undefined ? { apiKeyId } : {})\n };\n}\n\n/** Whether a request carries any simulation header. */\nexport function carriesSimulation(header: (name: string) => string | undefined): boolean {\n return simulationOf(header) !== undefined;\n}\n\n/**\n * Decide whether this caller may simulate what they asked for, and who the\n * request then runs as.\n *\n * The caller is judged before anything they named is looked up, so a refusal\n * says nothing about whether a user or key exists. Throws when a lookup fails.\n */\nexport async function decideSimulation(request: SimulationRequest): Promise<SimulationDecision> {\n const { credential, caller, resolveUser, apiKeyStore, audit } = request;\n const role = request.simulation.role?.trim();\n const userId = request.simulation.userId?.trim();\n const apiKeyId = request.simulation.apiKeyId?.trim();\n\n const refuse = (reason: string, error: ApiError): SimulationDecision => {\n if (audit) {\n logger.warn(\"[Security Audit] Refused a request to simulate access\", {\n eventType: \"auth.simulation.refused\",\n reason,\n callerUid: caller?.uid,\n simulation: request.simulation,\n ...audit\n });\n }\n return { refused: error };\n };\n\n if (role === \"\" || userId === \"\" || apiKeyId === \"\") {\n return refuse(\"malformed\", ApiError.badRequest(\n \"A simulation header was sent empty. Name a role, a user id or an API key id, or leave the header out.\",\n \"SIMULATION_INVALID\"\n ));\n }\n if (apiKeyId !== undefined && (role !== undefined || userId !== undefined)) {\n return refuse(\"malformed\", ApiError.badRequest(\n `${SIMULATE_API_KEY_ID_HEADER} runs as the key's own identity and roles, so it cannot be combined ` +\n `with ${SIMULATE_ROLE_HEADER} or ${SIMULATE_USER_ID_HEADER}.`,\n \"SIMULATION_INVALID\"\n ));\n }\n\n if (credential !== \"session\" || !caller || !hasAdminRole(caller.roles)) {\n return refuse(credential === \"session\" ? \"not-admin\" : credential,\n ApiError.forbidden(ADMIN_ONLY, \"SIMULATION_FORBIDDEN\"));\n }\n if (!resolveUser) {\n return refuse(\"unavailable\", new ApiError(501, \"SIMULATION_UNAVAILABLE\",\n \"This backend's auth cannot confirm an administrator's roles: its auth adapter does not implement resolveUser.\"));\n }\n // The caller as the database has them now: a token's roles may be a whole\n // token lifetime old, and a demoted administrator must not keep this.\n const self = await resolveUser(caller.uid);\n if (!self || !hasAdminRole(self.roles)) {\n return refuse(\"not-admin\", ApiError.forbidden(ADMIN_ONLY, \"SIMULATION_FORBIDDEN\"));\n }\n\n let identity: SimulatedIdentity;\n if (apiKeyId !== undefined) {\n if (!apiKeyStore) {\n return refuse(\"unavailable\", new ApiError(501, \"SIMULATION_UNAVAILABLE\",\n \"This backend has no API keys to simulate.\"));\n }\n const key = await apiKeyStore.getApiKeyById(apiKeyId);\n const keyIdentity = key ? await identityOfKey(key, request.access) : undefined;\n if (!key || !keyIdentity || !(\"uid\" in keyIdentity)) {\n return refuse(\"unknown-target\", ApiError.notFound(\n `No active API key has the id \"${apiKeyId}\", so there is nothing to simulate.`,\n \"SIMULATION_TARGET_NOT_FOUND\"\n ));\n }\n identity = {\n uid: keyIdentity.uid,\n roles: keyIdentity.roles,\n isAnonymous: false,\n scopes: keyIdentity.scopes,\n apiKey: key,\n role: keyIdentity.roles.join(\",\")\n };\n } else if (role !== undefined) {\n const anonymous = ANONYMOUS_ROLES.has(role) && userId === undefined;\n identity = anonymous\n ? { uid: ANONYMOUS_USER_ID, roles: [\"anon\"], isAnonymous: false, role }\n : { uid: userId ?? SIMULATED_NOBODY_UID, roles: [role], isAnonymous: false, role };\n } else {\n // A uid with no role: an account, as it is now.\n const target = userId === undefined ? null : await resolveUser(userId);\n if (!target) {\n return refuse(\"unknown-target\", ApiError.notFound(\n `No active user has the id \"${userId ?? \"\"}\". Send ${SIMULATE_ROLE_HEADER} as well to simulate a uid ` +\n \"that has no account.\",\n \"SIMULATION_TARGET_NOT_FOUND\"\n ));\n }\n identity = {\n uid: target.uid,\n roles: target.roles,\n isAnonymous: target.isAnonymous === true,\n ...(target.claims ? { claims: target.claims } : {}),\n role: target.roles.join(\",\")\n };\n }\n\n if (audit) {\n logger.info(\"[Security Audit] Ran a request with simulated access\", {\n eventType: \"auth.simulation\",\n simulatorUid: caller.uid,\n targetUid: identity.uid,\n targetRoles: identity.roles,\n ...(identity.apiKey ? { apiKeyId: identity.apiKey.id } : {}),\n ...audit\n });\n }\n return { granted: identity, simulator: caller.uid };\n}\n\nexport interface SimulationOptions {\n credential: ImpersonationCredential;\n /** The unscoped delegate the request's driver was scoped from. */\n driver: DataDriver;\n resolveUser?: ImpersonationUserResolver;\n apiKeyStore?: ApiKeyStore;\n}\n\n/** The caller the auth middleware put on the context, narrowed. */\nexport function callerOf(c: Context<HonoEnv>): { uid: string; roles: string[] } | undefined {\n const user = c.get(\"user\");\n if (typeof user !== \"object\" || user === null) return undefined;\n const uid = \"uid\" in user && typeof user.uid === \"string\" && user.uid !== \"\" ? user.uid : undefined;\n if (uid === undefined) return undefined;\n const roles = \"roles\" in user && Array.isArray(user.roles)\n ? user.roles.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n return { uid, roles };\n}\n\n/**\n * Honour or refuse this request's simulation headers.\n *\n * Called by the data-plane auth middlewares once the caller is on the context\n * and before impersonation, which it excludes. On a grant the context's\n * `user`, `driver`, `scopes` and `apiKey` become the simulated identity's,\n * `impersonator` names the administrator for the request log, and\n * `simulation` records what to report on the response.\n *\n * @returns The refusal to send, or `undefined` to carry on.\n */\nexport async function applySimulation(\n c: Context<HonoEnv>,\n { credential, driver, resolveUser, apiKeyStore }: SimulationOptions\n): Promise<Response | undefined> {\n const simulation = simulationOf(name => c.req.header(name));\n if (simulation === undefined) return undefined;\n\n if (c.req.header(IMPERSONATE_HEADER) !== undefined) {\n return errorHandler(ApiError.badRequest(\n `A request either impersonates a user (${IMPERSONATE_HEADER}) or simulates access, not both.`,\n \"SIMULATION_INVALID\"\n ), c) as Response;\n }\n\n let decision: SimulationDecision;\n try {\n decision = await decideSimulation({\n simulation,\n credential,\n caller: callerOf(c),\n resolveUser,\n apiKeyStore,\n access: accessOf(c),\n audit: { door: \"http\", method: c.req.method, path: c.req.path, requestId: c.get(\"requestId\") }\n });\n } catch (error: unknown) {\n // Refused, never run as the caller.\n return errorHandler(error instanceof Error ? error : new Error(String(error)), c) as Response;\n }\n if (\"refused\" in decision) return errorHandler(decision.refused, c) as Response;\n\n const identity = decision.granted;\n // A simulated anonymous caller has no session, as a real one has none: no\n // `user`, so a function's `requireAuth` refuses it and `callerScopes`\n // gives it nothing. As a user it was let through both, and the simulator\n // over-reported what anonymous callers reach. Only the driver is anonymous.\n c.set(\"user\", identity.uid === ANONYMOUS_USER_ID\n ? undefined\n : { uid: identity.uid, roles: identity.roles, isAnonymous: identity.isAnonymous });\n c.set(\"impersonator\", { uid: decision.simulator });\n c.set(\"scopes\", identity.scopes);\n c.set(\"apiKey\", identity.apiKey);\n c.set(\"simulation\", {\n role: identity.role,\n sub: identity.uid,\n roles: identity.roles,\n ...(identity.apiKey ? { apiKeyId: identity.apiKey.id } : {})\n });\n try {\n c.set(\"driver\", await scopeDataDriver(driver, {\n uid: identity.uid,\n roles: identity.roles,\n isAnonymous: identity.isAnonymous,\n ...(identity.claims ? { claims: identity.claims } : {})\n }));\n } catch (error: unknown) {\n logger.error(\"[AUTH] RLS scoping failed for a simulated request\", { error });\n return errorHandler(ApiError.internal(\"Internal authentication error\"), c) as Response;\n }\n return undefined;\n}\n\n/**\n * The fields of `collection` the roles cannot read: what the driver strips\n * from every row it hands them. Fields hidden from everyone (`excludeFromApi`)\n * are left out — no role sees those, so they say nothing about this one.\n */\nexport function maskedFieldsFor(collection: CollectionConfig, roles: readonly string[]): string[] {\n const hiddenFromAll = apiHiddenFieldNames(collection);\n return restrictedFieldNames(collection, { roles }, \"read\").declared\n .filter(name => !hiddenFromAll.has(name));\n}\n\n/**\n * Name what a simulated request ran as on its response\n * ({@link SIMULATED_CONTEXT_HEADER}), refusals included.\n *\n * Mounted ahead of the data-plane groups; it reads, after the request is\n * answered, what {@link applySimulation} and the REST generator recorded on\n * the context — the identity, and which collection the request was about.\n */\nexport function reportSimulatedContext(): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n await next();\n const simulation = c.get(\"simulation\");\n if (!simulation) return;\n const slug = c.get(\"collection\");\n const collection = slug ? c.get(\"services\")?.collections().find(entry => entry.slug === slug) : undefined;\n const value = formatSimulatedContext({\n role: simulation.role,\n sub: simulation.sub,\n maskedColumns: collection ? maskedFieldsFor(collection, simulation.roles) : [],\n ...(simulation.apiKeyId ? { apiKeyId: simulation.apiKeyId } : {}),\n ...(slug ? { collection: slug } : {})\n });\n try {\n c.res.headers.set(SIMULATED_CONTEXT_HEADER, value);\n } catch {\n // An immutable response (a proxied fetch) keeps its headers.\n }\n };\n}\n","/**\n * Running a request as another user: the `x-rebase-impersonate` header, and\n * the `impersonate` field of the realtime socket's `AUTHENTICATE`.\n *\n * It exists for an administrator checking what one user can see and do — \"can\n * B read A's rows, can B write them?\" — against the real policies, from\n * Studio's API explorer and JS editor. A granted request runs as one of B's\n * own would: B's uid, B's roles as they are now, B's guest flag and the claims\n * a token minted for B now would carry reach `withAuth`, so the statement runs\n * as `rebase_user` with B's identity and every policy is evaluated for B.\n *\n * Who may: a signed-in administrator, on their own session, judged on their\n * roles as the database has them now rather than as their token claims. Never\n * an API key — not a personal key whose owner is an administrator either — and\n * never the service key. Impersonation is a person checking on another person,\n * and the audit line has to name one.\n *\n * Where: the data API, custom functions and the realtime socket. Every other\n * route refuses the header ({@link refuseUnhonouredImpersonation}).\n *\n * Fail closed: a request asking to act as someone either runs as the user it\n * names or is refused. It never runs as its caller, because that is the\n * failure this exists to end — a response showing the administrator's rows\n * under a label saying they were B's.\n *\n * @module\n */\n\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { hasAdminRole, IMPERSONATE_HEADER, type AuthenticatedUser, type DataDriver } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport { scopeDataDriver } from \"./rls-scope\";\nimport { logger } from \"../utils/logger\";\nimport { carriesSimulation } from \"./simulation\";\n\n/** How the caller authenticated, as far as impersonation is concerned. */\nexport type ImpersonationCredential = \"session\" | \"api-key\" | \"service-key\" | \"none\";\n\n/** How the auth in use turns a uid into a request identity — `AuthAdapter.resolveUser`. */\nexport type ImpersonationUserResolver = (uid: string) => Promise<AuthenticatedUser | null>;\n\nexport interface ImpersonationRequest {\n /** The uid the request asked to act as, as sent. */\n requestedUid: string;\n /** What the caller presented. Only a `session` may impersonate. */\n credential: ImpersonationCredential;\n /** The caller, as their credential verified. */\n caller?: { uid: string; roles: readonly string[] };\n /** Absent when the auth in use has no `resolveUser`: then nobody may. */\n resolveUser?: ImpersonationUserResolver;\n /**\n * Where the request came in, for the security audit line — or absent to\n * write none, for a decision that only re-confirms one already logged\n * (the socket re-asks before every frame).\n */\n audit?: Record<string, unknown>;\n}\n\n/** The answer to a request to act as another user. */\nexport type ImpersonationDecision =\n | { granted: AuthenticatedUser; impersonator: string }\n | { refused: ApiError };\n\n/** Why a request asking to impersonate was turned away, for the audit line. */\ntype Refusal = \"malformed\" | \"api-key\" | \"service-key\" | \"anonymous\" | \"not-admin\" | \"unavailable\" | \"unknown-target\";\n\nconst ADMIN_ONLY = \"Only a signed-in administrator can run a request as another user.\";\n\nconst NOT_A_PERSON: Record<\"api-key\" | \"service-key\" | \"none\", { reason: Refusal; message: string }> = {\n \"api-key\": {\n reason: \"api-key\",\n message: \"An API key cannot run a request as another user. Send it with an administrator's session instead.\"\n },\n \"service-key\": {\n reason: \"service-key\",\n message: \"The service key cannot run a request as another user. Send it with an administrator's session instead.\"\n },\n none: { reason: \"anonymous\", message: ADMIN_ONLY }\n};\n\n/**\n * Decide whether this caller may act as the user they named, and who that\n * user is now.\n *\n * The one decision both doors take — the HTTP data plane through\n * {@link applyImpersonation}, the realtime socket at `AUTHENTICATE` and before\n * every frame after it. The caller is judged before the named user is looked\n * up, so the answer to a caller who may not impersonate says nothing about\n * whether the uid exists.\n *\n * Throws when a lookup fails: that is not an answer about anyone, and each\n * door refuses it its own way.\n */\nexport async function decideImpersonation(request: ImpersonationRequest): Promise<ImpersonationDecision> {\n const { credential, caller, resolveUser, audit } = request;\n const targetUid = request.requestedUid.trim();\n\n const refuse = (reason: Refusal, error: ApiError): ImpersonationDecision => {\n if (audit) {\n logger.warn(\"[Security Audit] Refused a request to act as another user\", {\n eventType: \"auth.impersonation.refused\",\n reason,\n callerUid: caller?.uid,\n requestedUid: targetUid,\n ...audit\n });\n }\n return { refused: error };\n };\n\n if (targetUid === \"\") {\n return refuse(\"malformed\", ApiError.badRequest(\n `The ${IMPERSONATE_HEADER} header names no user. Send the uid of the user to act as, or leave the header out.`,\n \"IMPERSONATION_INVALID\"\n ));\n }\n\n if (credential !== \"session\") {\n const { reason, message } = NOT_A_PERSON[credential];\n return refuse(reason, ApiError.forbidden(message, \"IMPERSONATION_FORBIDDEN\"));\n }\n\n if (!caller || !hasAdminRole(caller.roles)) {\n return refuse(\"not-admin\", ApiError.forbidden(ADMIN_ONLY, \"IMPERSONATION_FORBIDDEN\"));\n }\n\n if (!resolveUser) {\n return refuse(\"unavailable\", new ApiError(501, \"IMPERSONATION_UNAVAILABLE\",\n \"This backend's auth cannot run a request as another user: its auth adapter does not implement resolveUser.\"));\n }\n\n // The caller again, as the database has them now. The roles above came\n // from whatever verified the credential, which for a JWT may be a whole\n // token lifetime old — and a demoted administrator must not keep this.\n const self = await resolveUser(caller.uid);\n if (!self || !hasAdminRole(self.roles)) {\n return refuse(\"not-admin\", ApiError.forbidden(ADMIN_ONLY, \"IMPERSONATION_FORBIDDEN\"));\n }\n\n const target = await resolveUser(targetUid);\n if (!target) {\n return refuse(\"unknown-target\", ApiError.notFound(\n `No active user has the id \"${targetUid}\", so there is nobody to run this request as.`,\n \"IMPERSONATION_TARGET_NOT_FOUND\"\n ));\n }\n\n if (audit) {\n logger.info(\"[Security Audit] Ran a request as another user\", {\n eventType: \"auth.impersonation\",\n impersonatorUid: caller.uid,\n targetUid: target.uid,\n targetRoles: target.roles,\n ...audit\n });\n }\n return { granted: target, impersonator: caller.uid };\n}\n\nexport interface ImpersonationOptions {\n /** What the caller presented. Only a `session` may impersonate. */\n credential: ImpersonationCredential;\n /** The unscoped delegate the request's driver was scoped from. */\n driver: DataDriver;\n /** The auth adapter's `resolveUser`. Absent: the header is refused for everyone. */\n resolveUser?: ImpersonationUserResolver;\n}\n\n/** The caller the auth middleware put on the context, narrowed. */\nfunction callerOf(c: Context<HonoEnv>): { uid: string; roles: string[] } | undefined {\n const user = c.get(\"user\");\n if (typeof user !== \"object\" || user === null) return undefined;\n const uid = \"uid\" in user && typeof user.uid === \"string\" && user.uid !== \"\" ? user.uid : undefined;\n if (uid === undefined) return undefined;\n const roles = \"roles\" in user && Array.isArray(user.roles)\n ? user.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n return { uid, roles };\n}\n\n/**\n * Honour or refuse this request's {@link IMPERSONATE_HEADER}.\n *\n * Called by the data-plane auth middlewares once the caller is on the context\n * and its driver is scoped. When the header names a user the caller may act\n * as, the context's `user` and `driver` are replaced with that user's and\n * `impersonator` names the caller.\n *\n * @returns The refusal to send, or `undefined` to carry on — as the caller\n * when the header is absent, as the named user when it was granted.\n */\nexport async function applyImpersonation(\n c: Context<HonoEnv>,\n { credential, driver, resolveUser }: ImpersonationOptions\n): Promise<Response | undefined> {\n const requested = c.req.header(IMPERSONATE_HEADER);\n if (requested === undefined) return undefined;\n\n const caller = callerOf(c);\n let decision: ImpersonationDecision;\n try {\n decision = await decideImpersonation({\n requestedUid: requested,\n credential,\n caller,\n resolveUser,\n audit: { door: \"http\", method: c.req.method, path: c.req.path, requestId: c.get(\"requestId\") }\n });\n } catch (error: unknown) {\n // Refused, never run as the caller.\n return errorHandler(error instanceof Error ? error : new Error(String(error)), c) as Response;\n }\n if (\"refused\" in decision) return errorHandler(decision.refused, c) as Response;\n\n const target = decision.granted;\n const isAnonymous = target.isAnonymous === true;\n c.set(\"user\", { uid: target.uid, email: target.email, roles: target.roles, isAnonymous });\n c.set(\"impersonator\", { uid: decision.impersonator });\n try {\n c.set(\"driver\", await scopeDataDriver(driver, {\n uid: target.uid,\n roles: target.roles,\n isAnonymous,\n ...(target.claims ? { claims: target.claims } : {})\n }));\n } catch (error: unknown) {\n logger.error(\"[AUTH] RLS scoping failed for an impersonated request\", { error });\n return errorHandler(ApiError.internal(\"Internal authentication error\"), c) as Response;\n }\n return undefined;\n}\n\n/**\n * Refuse {@link IMPERSONATE_HEADER} — and the RLS simulator's\n * `x-rebase-simulate-*` headers (`auth/simulation.ts`) — on every route that\n * does not honour it.\n *\n * Only the data API and custom functions run a request as another user; the\n * rest — storage, auth, the admin surfaces — authenticate the caller and\n * ignore the header. Ignored, a request asking to act as B would be answered\n * as the administrator who sent it, which is the one outcome impersonation\n * must never have. Mounted ahead of every route, so a surface added later is\n * refused until it is listed here as honouring the header.\n *\n * @param honouredMounts - The mount points whose auth middleware applies the\n * header (`/api/data`, `/api/functions`). A path is under one when it\n * is the mount itself or continues it with a `/`.\n */\nexport function refuseUnhonouredImpersonation(honouredMounts: readonly string[]): MiddlewareHandler<HonoEnv> {\n return async (c, next) => {\n const impersonating = c.req.header(IMPERSONATE_HEADER) !== undefined;\n const simulating = carriesSimulation(name => c.req.header(name));\n if (!impersonating && !simulating) return next();\n const path = c.req.path;\n if (honouredMounts.some(mount => path === mount || path.startsWith(`${mount}/`))) return next();\n logger.warn(impersonating\n ? \"[Security Audit] Refused a request to act as another user\"\n : \"[Security Audit] Refused a request to simulate access\", {\n eventType: impersonating ? \"auth.impersonation.refused\" : \"auth.simulation.refused\",\n reason: \"unsupported-route\",\n method: c.req.method,\n path,\n requestId: c.get(\"requestId\")\n });\n return errorHandler(impersonating\n ? ApiError.badRequest(\n `This route cannot run as another user, so it refuses the ${IMPERSONATE_HEADER} header. ` +\n \"Only the data API and custom functions honour it.\",\n \"IMPERSONATION_UNSUPPORTED\"\n )\n : ApiError.badRequest(\n \"This route cannot run with simulated access, so it refuses the x-rebase-simulate-* headers. \" +\n \"Only the data API and custom functions honour them.\",\n \"SIMULATION_UNSUPPORTED\"\n ), c) as Response;\n };\n}\n","/**\n * Which object a storage request's URL path names.\n *\n * Three parties read the path of a `GET /file/*`, `GET /metadata/*` or\n * `DELETE /file/*`: the route, which serves or deletes an object;\n * `publicObjectAuth`, which lets an anonymous caller through when that object\n * is public; and `fileTokenAuth`, which compares it with the path a download\n * token grants. A decision is only about the object acted on if it is made on\n * the same key, so all three derive it here — the wildcard, the decoding, the\n * bucket segment and the canonical key — and none of them spells a step of its\n * own.\n *\n * They used to. `publicObjectAuth` judged the raw path with a check that strips\n * everything up to a `://`, while the route canonicalized it, folding `//` to\n * `/`: `GET /file/notes://public/secret.txt` was public to the one and served\n * the private key `notes:/public/secret.txt` from the other, with the authorize\n * hook never asked.\n *\n * Nothing here parses a scheme. A request path is a key, optionally behind the\n * one bucket segment the routes recognise; a `://` in it is a `:` followed by\n * an empty segment, and canonicalization folds the empty segment away.\n */\n\nimport { canonicalStorageKey, InvalidStorageKeyError } from \"./keys\";\n\n/** The object a request path names. */\nexport interface RequestedStorageObject {\n /** The bucket. Always `\"default\"`: the only bucket a request path can name. */\n bucket: string;\n /** The canonical key — the one the hook is asked about and the controller acts on. */\n key: string;\n}\n\n/**\n * The wildcard part of a storage route's path, still percent-encoded.\n *\n * Hono's `c.req.param('*')` does not work reliably in sub-routers mounted\n * via `app.route(prefix, subRouter)`. Instead it is derived from the\n * fully-resolved `c.req.path` and `c.req.routePath`.\n *\n * For a route `/metadata/*` mounted at `/api/storage`, a request to\n * `/api/storage/metadata/default/file.jpg` yields routePath\n * `/api/storage/metadata/*`. The prefix (everything before `/*`) is stripped,\n * plus one character for the trailing `/`, to obtain `default/file.jpg`.\n */\nexport function storageRequestWildcard(c: { req: { path: string; routePath: string } }): string {\n const prefix = c.req.routePath.replace(\"/*\", \"\");\n const fullPath = c.req.path;\n const idx = fullPath.indexOf(prefix);\n if (idx < 0) return \"\";\n // +1 to skip the '/' after the prefix\n return fullPath.substring(idx + prefix.length + 1);\n}\n\n/**\n * The object a decoded path names: a key, behind an optional `default/` bucket\n * segment (matched case-insensitively). Throws {@link InvalidStorageKeyError}\n * when the key cannot be canonicalized.\n *\n * For a path that is already text — the folder route's JSON body. A URL path\n * goes through {@link requestedStorageObject}, which decodes it first.\n */\nexport function storageObjectOfPath(decodedPath: string): RequestedStorageObject {\n const parts = decodedPath.split(\"/\");\n const rawKey = parts.length > 1 && parts[0].toLowerCase() === \"default\"\n ? parts.slice(1).join(\"/\")\n : decodedPath;\n return { bucket: \"default\", key: canonicalStorageKey(rawKey) };\n}\n\n/**\n * The object a request's wildcard path names, or {@link InvalidStorageKeyError}.\n *\n * The wildcard arrives as Hono leaves it — `decodeURI`d, with the reserved\n * characters (`/`, `:`, `%25` among them) still escaped — so one\n * `decodeURIComponent` yields the path the client encoded. A `%` that does not\n * begin an escape names no key, and is refused like any other key that cannot be\n * canonicalized rather than thrown as a `URIError`.\n */\nexport function requestedStorageObject(wildcard: string): RequestedStorageObject {\n let decoded: string;\n try {\n decoded = decodeURIComponent(wildcard);\n } catch {\n throw new InvalidStorageKeyError(\n \"Storage path is not valid percent-encoding: a '%' must begin an escape such as '%25' (a literal '%').\"\n );\n }\n return storageObjectOfPath(decoded);\n}\n\n/**\n * {@link requestedStorageObject}, or `null` when the path names no object.\n *\n * For the middleware, which must fail closed without an exception: a path that\n * names no object is neither public nor covered by any download token, and the\n * route answers it with a 400 of its own.\n */\nexport function tryRequestedStorageObject(wildcard: string): RequestedStorageObject | null {\n try {\n return requestedStorageObject(wildcard);\n } catch (err) {\n if (err instanceof InvalidStorageKeyError) return null;\n throw err;\n }\n}\n","/**\n * The two principals a storage read can be granted without a user: the reader\n * of a declared-public object, and the bearer of a download token.\n *\n * `checkAuthorized` does not ask the `storageAuthorize` hook about either — the\n * public prefix and the token are the authorization. That skip used to be\n * decided on the principal's `uid` string (`\"public\"`, `\"download-token\"`), so\n * any identity carrying that uid skipped the hook: a custom `AuthAdapter` whose\n * `verifyRequest` returns `{ uid: \"public\" }` for a guest could then upload,\n * delete and list anywhere, with the hook never asked.\n *\n * Each principal is now one object, set only by the middleware that granted\n * it (`publicObjectAuth`, `fileTokenAuth`) and recognised by identity. An\n * adapter builds a new object per request, so it can name the uid but never be\n * the grant.\n */\n\n/** The principal `publicObjectAuth` grants an anonymous read of a public object. */\nexport const PUBLIC_OBJECT_PRINCIPAL: { readonly uid: string; readonly roles: string[] } = Object.freeze({\n uid: \"public\",\n roles: [\"public\"]\n});\n\n/** The principal `fileTokenAuth` grants a read covered by a valid download token. */\nexport const DOWNLOAD_TOKEN_PRINCIPAL: { readonly uid: string; readonly roles: string[] } = Object.freeze({\n uid: \"download-token\",\n roles: [\"reader\"]\n});\n\n/** Whether `user` is the grant `publicObjectAuth` made — the object, not the uid. */\nexport function isPublicObjectGrant(user: unknown): boolean {\n return user === PUBLIC_OBJECT_PRINCIPAL;\n}\n\n/** Whether `user` is the grant `fileTokenAuth` made — the object, not the uid. */\nexport function isDownloadTokenGrant(user: unknown): boolean {\n return user === DOWNLOAD_TOKEN_PRINCIPAL;\n}\n","import { MiddlewareHandler, Context } from \"hono\";\nimport { accessTokenRefusalError, judgeAccessToken, type AccessJudgeRepository } from \"./token-revocation\";\nimport { ANONYMOUS_USER_ID, DataDriver, hasAdminRole, isPublicStorageKey } from \"@rebasepro/types\";\nimport { jwtOf, processJwtService, AccessTokenPayload, type DownloadTokenPayload, type JwtService } from \"./jwt\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler, isDatabaseUnreachable } from \"../api/errors\";\nimport { warnIfDatabaseClosed } from \"../api/closed-database\";\nimport { scopeDataDriver } from \"./rls-scope\";\nimport { safeCompare } from \"./crypto-utils\";\nimport { isApiKeyToken, validateApiKey } from \"./api-keys/api-key-middleware\";\nimport type { ApiKeyStore } from \"./api-keys/api-key-store\";\nimport { logger } from \"../utils/logger\";\nimport { extractBearerToken } from \"./bearer-token\";\nimport { applyImpersonation, type ImpersonationCredential } from \"./impersonation\";\nimport { applySimulation } from \"./simulation\";\n// Leaf modules (`@rebasepro/types` only), so these do not close an import cycle\n// with `storage/routes.ts` — which is the point: the route, the public check and\n// both sides of the token comparison derive the object, and the source it\n// belongs to, with the same functions, not with copies of one rule.\nimport { canonicalStorageId } from \"../storage/keys\";\nimport { storageRequestWildcard, tryRequestedStorageObject } from \"../storage/requested-object\";\nimport { DOWNLOAD_TOKEN_PRINCIPAL, PUBLIC_OBJECT_PRINCIPAL } from \"../storage/request-principals\";\n\n// Re-exported from here because this is where callers look for it; the separate\n// module exists only so `api-key-middleware` can use it without closing an\n// import cycle with this file.\nexport { extractBearerToken };\n\n/**\n * Result from a custom auth validator.\n * - `false`/`null`/`undefined` = not authenticated\n * - `true` = authenticated as default user\n * - object with `uid` = authenticated with user info\n */\nexport type AuthResult = boolean | null | undefined | { uid?: string; roles?: string[]; [key: string]: unknown };\n\n/**\n * Options for creating an auth middleware via createAuthMiddleware()\n */\nexport interface AuthMiddlewareOptions {\n /** DataDriver to scope via withAuth() for RLS */\n driver: DataDriver;\n /**\n * Optional per-request driver resolver for multi-data-source backends.\n * Given the request context, returns the unscoped delegate to use (e.g.\n * Postgres vs Mongo, picked by the request's collection data source).\n * When omitted, `driver` is used for every request.\n */\n resolveDriver?: (c: Context<HonoEnv>) => DataDriver;\n /**\n * If true, return 401 when no valid token is present.\n *\n * **Defaults to `true` (secure by default).** Set to `false` only for\n * intentionally public endpoints where access control is fully delegated\n * to Postgres Row-Level Security policies.\n */\n requireAuth?: boolean;\n /** Optional custom validator (for non-JWT auth, e.g. external auth providers) */\n validator?: (c: Context<HonoEnv>) => Promise<AuthResult>;\n /**\n * A static secret key for server-to-server / script authentication.\n *\n * When a request sends `Authorization: Bearer <key>` and the key matches\n * this value, the request is granted admin-level access (uid: `service`,\n * roles: `[\"admin\"]`) **without** JWT verification. The driver is scoped\n * via `withAuth()` with the service identity.\n *\n * This is the Rebase equivalent of a Service Account key.\n * Set via `REBASE_SERVICE_KEY` in `.env` and pass through the backend config.\n *\n * **Security:** The comparison uses constant-time equality to prevent\n * timing attacks. The key must be at least 32 characters.\n */\n serviceKey?: string;\n /**\n * API key store for authenticating `rk_` prefixed tokens.\n * When set, tokens starting with `rk_` are validated against the\n * database instead of being treated as JWTs.\n */\n apiKeyStore?: ApiKeyStore;\n}\n\n/**\n * Answer a refusal with the canonical envelope, through the same formatter\n * every route uses.\n *\n * These middlewares used to hand-build `c.json({ error: { message, code } },\n * 401)` — seven near-copies of it, which is how they came to omit `requestId`\n * while every other error on the server carried it: the one string a bug report\n * quotes was missing from precisely the errors an app hits most. It also meant\n * a routine 401 (a client refreshing before it knows whether a session exists)\n * logged like an incident, because `ApiError.expected` never reached the\n * logger.\n *\n * `errorHandler` is *called* rather than thrown to. A throw only becomes an\n * envelope where an `onError` is registered, and these middlewares are mounted\n * by callers this package does not control — `defineFunction` hands users a\n * Hono app and invites them to put `requireAuth` on a route of it. Making the\n * status depend on the mount point would turn a 401 into a 500 for exactly\n * those callers. Calling the formatter directly gets the envelope with none of\n * that.\n */\nexport function refuse(c: Context<HonoEnv>, error: ApiError): Response {\n return errorHandler(error, c) as Response;\n}\n\n/**\n * Hono middleware that requires a valid JWT token via Authorization header.\n * Returns 401 if token is missing or invalid.\n *\n * **Security:** Tokens are only accepted via the `Authorization: Bearer`\n * header. Query-string tokens (`?token=`) are intentionally NOT accepted\n * here because URLs leak into access logs, proxies, Referer headers, and\n * browser history. Use {@link queryTokenAuth} on routes that legitimately\n * need query-string tokens (e.g. storage file serving for `<img src>`).\n */\nexport const requireAuth: MiddlewareHandler<HonoEnv> = async (\n c,\n next\n) => {\n // A prior middleware (queryTokenAuth, API-key pre-auth) may have already\n // resolved the user — respect it instead of re-parsing the token as a JWT.\n if (c.get(\"user\")) return next();\n\n const token = extractBearerToken(c.req.header(\"authorization\"));\n\n if (token === undefined) {\n return refuse(c, ApiError.unauthenticated(\"Authorization header missing or invalid\"));\n }\n\n const payload = await jwtOf(c).verifyAccessToken(token);\n\n if (!payload) {\n return refuse(c, ApiError.unauthenticated(\"Invalid or expired token\"));\n }\n\n c.set(\"user\", payload);\n return next();\n};\n\n/**\n * Factory that creates a requireAuth middleware with optional service key support.\n *\n * When `serviceKey` is provided, the middleware will check if the Bearer token\n * matches the service key using constant-time comparison. If it matches, the\n * request is authenticated as a service user with admin privileges.\n *\n * This allows admin routes (which use standalone requireAuth + requireAdmin)\n * to be accessed via service keys for scripts and server-to-server calls.\n */\nexport function createRequireAuth(options?: {\n serviceKey?: string;\n /**\n * Read this user's roles from the database, replacing whatever the token\n * claims.\n *\n * Without it, `requireAdmin` downstream trusts the `roles` array inside the\n * access token — so demoting an administrator does nothing until that token\n * expires, and in the meantime the demoted admin can call\n * `PUT /api/admin/users/<self>` and put the role back for good. The data\n * plane already re-reads roles per request (`builtin-auth-adapter`); the\n * admin routes, which are the ones that can grant roles, did not.\n *\n * Optional because two call sites have no repository in scope. A route that\n * can supply one should: the cost is one indexed lookup on requests that\n * are already administrative.\n */\n resolveRoles?: (uid: string) => Promise<string[]>;\n /**\n * Repository used to check the token-revocation watermark.\n *\n * Separate from {@link resolveRoles} only because that one is a narrow\n * function; a caller with a repository should pass it here so a token\n * invalidated by `logout` or a password reset stops working on admin routes\n * too, not just on the data plane.\n */\n revocationRepo?: AccessJudgeRepository;\n}): MiddlewareHandler<HonoEnv> {\n // Only when there is nothing at all to add. Keyed on the service key alone,\n // this dropped `resolveRoles` and `revocationRepo` silently for any caller\n // that passed them without one — which reads as \"the fresh-role check is\n // wired\" at the call site and behaves as if it were not.\n if (!options?.serviceKey && !options?.resolveRoles && !options?.revocationRepo) {\n return requireAuth;\n }\n\n const key = options.serviceKey;\n const resolveRoles = options.resolveRoles;\n const revocationRepo = options.revocationRepo;\n return async (c, next) => {\n // Respect a user already resolved upstream (e.g. API-key pre-auth).\n if (c.get(\"user\")) return next();\n\n const token = extractBearerToken(c.req.header(\"authorization\"));\n\n if (token === undefined) {\n return refuse(c, ApiError.unauthenticated(\"Authorization header missing or invalid\"));\n }\n\n // Check service key first (constant-time comparison)\n if (key !== undefined && safeCompare(token, key)) {\n c.set(\"user\", { uid: \"service\",\nroles: [\"admin\"] } as AccessTokenPayload);\n return next();\n }\n\n // Fall back to JWT verification, by the backend answering the request\n const payload = await jwtOf(c).verifyAccessToken(token);\n\n if (!payload) {\n return refuse(c, ApiError.unauthenticated(\"Invalid or expired token\"));\n }\n\n if (resolveRoles || revocationRepo) {\n try {\n // The same judgement the data plane makes: revoked, or an\n // account that no longer exists. An admin route is the last\n // place such a token should still work. When the judgement\n // already read the roles, they are the roles — one read, not two.\n let judgedRoles: string[] | undefined;\n if (revocationRepo) {\n const verdict = await judgeAccessToken(revocationRepo, payload);\n if (!verdict.live) return refuse(c, accessTokenRefusalError(verdict.refusal));\n judgedRoles = verdict.roles;\n }\n c.set(\"user\", resolveRoles\n ? { ...payload, roles: judgedRoles ?? await resolveRoles(payload.uid) }\n : payload);\n return next();\n } catch (error) {\n // Fail closed. The token's own claim is exactly what must not be\n // trusted here, so falling back to it would reinstate the bug\n // the lookup exists to close. A database that cannot be reached\n // is said as that, the answer every other door gives then.\n if (isDatabaseUnreachable(error)) {\n if (!warnIfDatabaseClosed(error)) {\n logger.warn(\"[Auth] Could not resolve roles for an admin-gated request: the database is unreachable\", {\n uid: payload.uid\n });\n }\n return refuse(c, ApiError.databaseUnavailable());\n }\n logger.warn(\"[Auth] Could not resolve roles for an admin-gated request\", {\n uid: payload.uid,\n error\n });\n return refuse(c, ApiError.serviceUnavailable(\"Could not verify your permissions. Please try again.\", \"ROLE_LOOKUP_FAILED\"));\n }\n }\n\n c.set(\"user\", payload);\n return next();\n };\n}\n\n/**\n * Middleware that requires the user to hold the `admin` role, which holds\n * every scope. Must be used AFTER requireAuth or on a route where user is\n * guaranteed.\n *\n * Prefer `requireScope` from `./access` for anything a scope names: a scope\n * can be granted to a narrower role and to a key, and `admin` cannot.\n */\nexport const requireAdmin: MiddlewareHandler<HonoEnv> = async (\n c,\n next\n) => {\n const user = c.get(\"user\");\n if (!user) {\n return refuse(c, ApiError.unauthorized(\"User not authenticated. requireAuth middleware is missing?\"));\n }\n\n const roles = typeof user === \"object\" && user !== null && \"roles\" in user && Array.isArray(user.roles)\n ? user.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n const isAdmin = hasAdminRole(roles);\n\n if (!isAdmin) {\n return refuse(c, ApiError.forbidden(\"Admin privileges required for this operation\"));\n }\n\n return next();\n};\n\n\n/**\n * Middleware that optionally extracts user from JWT via Authorization header.\n * Does not return 401 if token is missing — allows anonymous access.\n *\n * Query-string tokens are NOT accepted here. Use {@link queryTokenAuth}\n * on routes that need them.\n */\nexport const optionalAuth: MiddlewareHandler<HonoEnv> = async (\n c,\n next\n) => {\n // Skip if a prior middleware (e.g. queryTokenAuth) already set the user.\n if (c.get(\"user\")) return next();\n\n const token = extractBearerToken(c.req.header(\"authorization\"));\n\n // A backend that authenticates through an adapter (Firebase, Clerk) has\n // no JWT secret, so verifying here would throw — turning a route that had\n // already accepted anonymous callers into a 500 for every request that\n // happens to carry a bearer token. Anonymous is the answer.\n const jwt = jwtOf(c);\n if (token !== undefined && jwt.isConfigured()) {\n const payload = await jwt.verifyAccessToken(token);\n if (payload) {\n c.set(\"user\", payload);\n }\n }\n\n return next();\n};\n\n/**\n * Who a session token names, as `jwt` verifies it — or \"nobody\" rather than a\n * throw on a backend with no JWT of its own, so a socket closes as\n * unauthenticated, which is the outcome this describes.\n */\nexport async function userFromToken(jwt: JwtService, token: string): Promise<AccessTokenPayload | null> {\n if (!jwt.isConfigured()) return null;\n return jwt.verifyAccessToken(token);\n}\n\n/**\n * Extract user from token - for WebSocket authentication. Answers for the\n * process default; a backend's own socket verifies with its own keys.\n */\nexport async function extractUserFromToken(token: string): Promise<AccessTokenPayload | null> {\n return userFromToken(processJwtService, token);\n}\n\n/**\n * Create a configurable auth middleware that handles:\n * 1. Token extraction (via custom validator or JWT Bearer token)\n * 2. RLS-scoped DataDriver via withAuth()\n * 3. Enforcement (401 when requireAuth is true and no user)\n *\n * **Secure by default:** `requireAuth` defaults to `true`. Anonymous\n * access is only allowed when the developer explicitly opts out by\n * setting `requireAuth: false`, indicating that Postgres RLS policies\n * fully control access.\n *\n * **Fail-closed:** The raw unscoped driver is never placed in the\n * request context. Every code path either scopes via `withAuth()` or\n * rejects the request. This prevents silent RLS bypass.\n *\n * This is the single source of truth for HTTP auth in Rebase.\n * Use this instead of manually parsing tokens in route handlers.\n */\n\nexport function createAuthMiddleware(options: AuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {\n const { driver: baseDriver, resolveDriver, requireAuth: enforceAuth = true, validator, serviceKey, apiKeyStore } = options;\n\n return async (c, next) => {\n // Pick the per-request delegate (multi-data-source) before scoping.\n const driver = resolveDriver ? resolveDriver(c) : baseDriver;\n // What the caller presented — read by `applyImpersonation` below.\n let credential: ImpersonationCredential = \"none\";\n if (validator) {\n // Custom validator path (e.g., API keys, external auth)\n try {\n const authResult = await validator(c);\n if (authResult && typeof authResult === \"object\") {\n // `uid`, and only `uid`: the spelling every other surface\n // uses. This clause used to be written twice — once for\n // `uid` and once, after a half-finished rename, for `uid`\n // again — so the `userId` the type advertised had already\n // stopped working here while still being documented.\n const id = \"uid\" in authResult ? authResult.uid : undefined;\n if (id) {\n const roles = authResult.roles || [];\n credential = \"session\";\n c.set(\"user\", { uid: id,\nroles });\n const user = { uid: id,\nroles,\n...authResult };\n c.set(\"driver\", await scopeDataDriver(driver, user));\n } else {\n // Validator returned an object but without an ID — scope as anon\n c.set(\"driver\", await scopeDataDriver(driver, { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] }));\n }\n } else if (authResult === true) {\n credential = \"session\";\n c.set(\"user\", { uid: \"default\",\nroles: [] });\n c.set(\"driver\", await scopeDataDriver(driver, { uid: \"default\",\nroles: [] }));\n } else {\n // Not authenticated — scope as anon so RLS can evaluate.\n // Fail closed: if anon scoping fails, reject instead of\n // falling back to the raw driver.\n c.set(\"driver\", await scopeDataDriver(driver, { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] }));\n }\n } catch (error) {\n return refuse(c, ApiError.unauthenticated(\"Unauthorized\"));\n }\n } else {\n // Default JWT path (with optional service key support)\n const token = extractBearerToken(c.req.header(\"authorization\"));\n\n if (token !== undefined) {\n // ── Service Key check ──────────────────────────────────\n // Check BEFORE JWT verification. Service keys are static\n // secrets (like Service Account keys) that grant admin access\n // for scripts, cron jobs, and server-to-server calls.\n if (serviceKey && safeCompare(token, serviceKey)) {\n const serviceUser: AccessTokenPayload = {\n uid: \"service\",\n roles: [\"admin\"]\n };\n credential = \"service-key\";\n c.set(\"user\", serviceUser);\n try {\n c.set(\"driver\", await scopeDataDriver(driver, {\n uid: \"service\",\n roles: [\"admin\"]\n }));\n } catch (error) {\n logger.error(\"[AUTH] RLS scoping failed for service key\", { error: error });\n return c.json({ error: { message: \"Internal authentication error\",\ncode: \"INTERNAL_ERROR\" } }, 500);\n }\n } else if (apiKeyStore && isApiKeyToken(token)) {\n // ── API Key verification ──────────────────────────────\n // Tokens starting with `rk_` are validated against the\n // api_keys table instead of JWT verification.\n const result = await validateApiKey(c, token, {\n store: apiKeyStore,\n driver\n });\n if (result !== true) {\n return result;\n }\n credential = \"api-key\";\n } else {\n // ── JWT verification ───────────────────────────────────\n const payload = await userFromToken(jwtOf(c), token);\n\n if (payload) {\n credential = \"session\";\n c.set(\"user\", payload);\n try {\n // `isAnonymous` comes from the token, and reaches\n // the RLS identity as `rebase.is_anonymous()`.\n // Without it a guest — a real user row minted by\n // anonymous sign-in — is the same principal as an\n // account inside every policy.\n const user = {\n uid: payload.uid,\n roles: payload.roles,\n isAnonymous: payload.isAnonymous === true,\n // The token's custom claims, so a policy can\n // read one — `rebase.jwt() ->> 'org_id'`, which\n // is what multi-tenancy's `claim` form compiles\n // to — and so the write path can stamp the\n // caller's tenant. Verified claims only: the\n // identity ones are stripped in\n // `verifyAccessToken`.\n claims: payload.claims\n };\n c.set(\"driver\", await scopeDataDriver(driver, user));\n } catch (error) {\n // withAuth() failed for a valid token — reject (fail closed)\n logger.error(\"[AUTH] RLS scoping failed for authenticated user\", { error: error });\n return c.json({ error: { message: \"Internal authentication error\",\ncode: \"INTERNAL_ERROR\" } }, 500);\n }\n } else {\n // Token present but invalid — always reject.\n // Providing a malformed token should never grant access,\n // regardless of requireAuth setting.\n return refuse(c, ApiError.unauthenticated(\"Invalid or expired token\"));\n }\n }\n } else {\n // No token provided — scope as anon for RLS evaluation.\n // Fail closed: if anon scoping fails, return 500 rather\n // than silently proceeding with an unscoped driver.\n try {\n c.set(\"driver\", await scopeDataDriver(driver, { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] }));\n } catch (error) {\n logger.error(\"[AUTH] Failed to create anon-scoped driver\", { error: error });\n return c.json({ error: { message: \"Server configuration error\",\ncode: \"INTERNAL_ERROR\" } }, 500);\n }\n }\n }\n\n // No `resolveUser` here: this middleware serves a backend with no\n // account store to read another user from, so a request carrying the\n // header is refused — never run as its caller.\n // Simulated access (the RLS simulator) is decided first: it refuses a\n // request that also asks to impersonate. Without `resolveUser` an\n // administrator's roles cannot be re-read, so it is refused too.\n const refusedSimulation = await applySimulation(c, { credential, driver, apiKeyStore });\n if (refusedSimulation) return refusedSimulation;\n const refused = await applyImpersonation(c, { credential, driver });\n if (refused) return refused;\n\n // The API-level gate, and the one place a caller learns it exists.\n //\n // This fires for an anonymous request when `requireAuth` is on, whatever\n // the collection's rules say — `access: \"public\"` widens which ROWS a\n // caller sees, not who may call. Read as a bare \"Unauthorized\" it looks\n // like broken RLS on a collection whose author wrote `public`, and gets\n // debugged as one: the same declaration serves anonymously against a\n // local server with `AUTH_REQUIRE=false` in its `.env` and 401s against a\n // deployment without it, which reads as the two behaving differently for\n // the same config. Naming the switch is what makes that one fact instead\n // of two mysteries.\n // A simulated anonymous caller has no `user` but is let past: see\n // `auth/simulation.ts`.\n if (enforceAuth && !c.get(\"user\") && !c.get(\"simulation\")) {\n return refuse(c, ApiError.unauthenticated(\n \"Unauthorized: Authentication required. This is the API-level gate, not a row \" +\n \"rule — a collection granting public reads still needs a caller. Set \" +\n \"AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide.\"\n ));\n }\n\n return next();\n };\n}\n\n/**\n * Middleware that authenticates via a `?token=` query parameter.\n *\n * **Use sparingly.** Tokens in URLs leak into access logs, proxy logs,\n * Referer headers, and browser history. This middleware exists solely for\n * routes where the consumer cannot set HTTP headers — e.g. `<img src>`,\n * `<a href>` for file downloads, or similar browser-native requests.\n *\n * Apply it **before** `requireAuth` or `optionalAuth` on the specific\n * route that needs it. Those middlewares will see the user context this\n * middleware sets and skip their own 401 check.\n *\n * @example\n * ```ts\n * router.get(\"/file/*\", queryTokenAuth, readAuthMiddleware, handler);\n * ```\n */\nexport const queryTokenAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n // Only activate when no Authorization header is present and a query token exists.\n if (extractBearerToken(c.req.header(\"authorization\")) !== undefined) {\n // Authorization header takes precedence — let downstream middleware handle it.\n return next();\n }\n\n const queryToken = c.req.query(\"token\");\n const jwt = jwtOf(c);\n if (queryToken && jwt.isConfigured()) {\n const payload = await jwt.verifyAccessToken(queryToken);\n if (payload) {\n c.set(\"user\", payload);\n }\n // If the token is invalid, we don't reject here — let the downstream\n // requireAuth / optionalAuth middleware decide the enforcement policy.\n }\n\n return next();\n};\n\n/**\n * Authorizes anonymous access to **public** storage objects (those under the\n * public prefix), which are served token-less via stable, permanent URLs.\n *\n * Runs on the storage `/file/*` and `/metadata/*` routes, *after* the token\n * middleware and *before* the read-auth gate. If the request is already\n * authenticated (Bearer or scoped token), it does nothing. Otherwise, when the\n * requested object path is public, it sets a minimal \"public\" principal so the\n * downstream `requireAuth` gate lets the read through. Private paths are left\n * untouched, so they still require a valid token.\n *\n * Public means the object the route will serve is public, so the decision is\n * made on that object's canonical key, derived by the route's own function\n * (`requestedStorageObject`). It used to be made on the raw path, through a\n * check that strips a `scheme://`: `notes://public/secret.txt` read as\n * `public/secret.txt` here while the route served the private\n * `notes:/public/secret.txt`, and the authorize hook — not asked about this\n * principal — never saw it.\n */\nexport const publicObjectAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n // Already authenticated (Bearer / scoped token) — nothing to authorize.\n if (c.get(\"user\")) return next();\n\n const requested = tryRequestedStorageObject(storageRequestWildcard(c));\n if (requested && isPublicStorageKey(requested.key)) {\n c.set(\"user\", PUBLIC_OBJECT_PRINCIPAL);\n }\n\n return next();\n};\n\n/**\n * Helper to match paths for scoped file tokens.\n * Matches exact paths or prefixes (exact folder prefixes or ending with /).\n *\n * The traversal check is defence in depth, not the load-bearing defence.\n *\n * A prefix comparison is a lie for any path containing `..`: `user1/../user2/x`\n * starts with `user1/`, so a token scoped to `user1/` would authorize reading\n * user2's file — and the storage controller would not object, because its own\n * guard stops a path escaping the *bucket*, and that one resolves to `user2/x`,\n * comfortably inside it. The grant is what would be escaped, and this is the\n * only layer that knows what was granted.\n *\n * Nothing arrives here in that shape today: the URL parser resolves `..` (and\n * `%2e%2e`, which the WHATWG spec treats as a dot for normalization) before\n * Hono routes the request, so this comparison already runs on a resolved path.\n * But that is a guarantee of the runtime rather than of this code, and it is\n * one line to not depend on it. The same rule already guards the public-object\n * path — see `isPublicStorageKey`.\n */\nexport function isPathMatch(requested: string, allowed: string): boolean {\n if (requested.split(\"/\").some((seg) => seg === \"..\")) return false;\n if (requested === allowed) return true;\n if (allowed.endsWith(\"/\")) {\n return requested.startsWith(allowed);\n }\n return requested.startsWith(allowed + \"/\");\n}\n\n/**\n * Middleware that authenticates file-serving routes using scoped download tokens.\n * It enforces that only scoped \"file-read\" tokens can access \"/file/*\" and \"?token=\" query params.\n * Full access JWTs are explicitly rejected on \"/file/*\" routes, and in the \"?token=\" query param.\n */\nexport const fileTokenAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n const path = c.req.path;\n const isFileRoute = path.includes(\"/storage/file/\");\n const authHeader = c.req.header(\"authorization\");\n const queryToken = c.req.query(\"token\");\n\n /**\n * Decide what a valid download token entitles this request to.\n *\n * One function rather than a copy per token location: a Bearer token and a\n * `?token=` are the same grant presented two ways, and the whole failure\n * this guards against is two places disagreeing about what was granted.\n *\n * `\"no-path\"` is distinct from the two denials because the caller treats it\n * differently — a request with no object path at all is not a denied read,\n * it is a request this middleware has nothing to say about, and on\n * `/metadata/*` it must still reach the downstream auth gate.\n */\n const evaluateGrant = (payload: DownloadTokenPayload): \"grant\" | \"deny-path\" | \"deny-storage\" | \"no-path\" => {\n // A key is unique only within its source, so the same path in another\n // source is another object. Checked first: it is the cheaper test, and\n // it holds even for a path the token genuinely covers.\n if (canonicalStorageId(c.req.query(\"storageId\")) !== payload.storageId) return \"deny-storage\";\n\n const rawPath = storageRequestWildcard(c);\n if (!rawPath) return \"no-path\";\n\n // The path a token GRANTS is minted by `/metadata` from the object the\n // route derives, and the path a request ASKS FOR is derived here — by\n // the same function, so a key whose URL form is not already canonical\n // (`a//b.txt`, `a/./b.txt`) matches the grant it was minted with. A\n // path that names no object matches no grant, the same fail-closed\n // answer `isPathMatch` gives a `..` segment.\n const requested = tryRequestedStorageObject(rawPath);\n if (requested === null) return \"deny-path\";\n\n return isPathMatch(`${requested.bucket}/${requested.key}`, payload.path) ? \"grant\" : \"deny-path\";\n };\n\n /**\n * Which half of the scope failed is named in the message. The holder of the\n * token already knows both values, so this reveals nothing to an attacker —\n * and without it, a client that forgets to forward `?storageId=` on the file\n * URL reports a \"path mismatch\" for a path that matches perfectly.\n */\n const denyMismatch = (outcome: \"deny-path\" | \"deny-storage\") =>\n refuse(c, ApiError.forbidden(outcome === \"deny-storage\"\n ? \"Forbidden: Scoped token storage mismatch\"\n : \"Forbidden: Scoped token path mismatch\"));\n\n // 1. Authorization: Bearer <token>\n const bearerToken = extractBearerToken(authHeader);\n if (bearerToken !== undefined) {\n const token = bearerToken;\n const payload = await jwtOf(c).verifyDownloadToken(token);\n\n if (payload) {\n const outcome = evaluateGrant(payload);\n if (outcome === \"grant\") {\n c.set(\"user\", DOWNLOAD_TOKEN_PRINCIPAL);\n return next();\n }\n if (outcome !== \"no-path\") return denyMismatch(outcome);\n }\n\n // If it's a file serving route, explicitly reject full access JWTs\n if (isFileRoute) {\n return refuse(c, ApiError.unauthenticated(\"Unauthorized: Access JWT not allowed on file routes\"));\n }\n\n // Otherwise (e.g. /metadata/*) let it pass to downstream requireAuth\n return next();\n }\n\n // 2. Query param: ?token=<token>\n if (queryToken) {\n const payload = await jwtOf(c).verifyDownloadToken(queryToken);\n\n if (payload) {\n const outcome = evaluateGrant(payload);\n if (outcome === \"grant\") {\n c.set(\"user\", DOWNLOAD_TOKEN_PRINCIPAL);\n return next();\n }\n if (outcome !== \"no-path\") return denyMismatch(outcome);\n }\n\n // Explicitly reject query-token if it is not a valid scoped download token (e.g. if it is a full access JWT)\n return refuse(c, ApiError.unauthenticated(\"Unauthorized: Invalid or unauthorized token\"));\n }\n\n return next();\n};\n\n","/**\n * The access model this backend serves, and the one gate every scope check\n * goes through.\n *\n * The model — the app's declared roles and scopes beside the built-in ones —\n * comes from the users collection's `auth` block. Each backend holds its own\n * ({@link AccessControl}) and its route groups put it on the request, so the\n * checks below judge a caller against the backend answering them.\n *\n * Who holds what:\n *\n * - A credential that narrows its person — an API key, an MCP token — puts\n * its scopes in the request context (`c.set(\"scopes\", …)`) when it is\n * verified. That list is the whole answer for it.\n * - Anyone else is a person (or the service key, which is the `admin` role),\n * and holds what their roles hold: `scopesForRoles`.\n *\n * @module\n */\n\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport {\n ADMIN_ROLE,\n ADMIN_SCOPES,\n BUILT_IN_SCOPES,\n EMPTY_ACCESS_MODEL,\n hasAdminRole,\n isDataPlaneScope,\n parseScope,\n roleNameProblem,\n scopeGrants,\n scopesBeyond,\n scopesForRoles,\n validateScopes,\n type AccessModel,\n type CollectionConfig,\n type RoleDefinition,\n type ScopeDefinition\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { refuse } from \"./middleware\";\nimport { scopeGuardProblem } from \"./admin-roles\";\nimport type { AuthRepository } from \"./interfaces\";\n\n/** The account a personal key acts as, as it is now. Null when it no longer exists. */\nexport type KeyOwnerResolver = (uid: string) => Promise<{ roles: string[] } | null>;\n\n/**\n * How a personal key finds its owner in an account store: the account as it\n * is now — a missing or disabled one is no owner — and its roles.\n *\n * One read where the store answers both at once: `getAccountAccessState`, the\n * statement every access token is already judged against, which reads roles\n * and `disabled_at` together. Otherwise the account, then its roles — the two\n * reads this always made, one round trip more per request.\n */\nexport function keyOwnerResolverFor(\n repo: Pick<AuthRepository, \"getUserById\" | \"getUserRoleIds\" | \"getAccountAccessState\">\n): KeyOwnerResolver {\n if (typeof repo.getAccountAccessState === \"function\") {\n const readAccount = repo.getAccountAccessState.bind(repo);\n return async (uid) => {\n const account = await readAccount(uid);\n // A disabled owner's keys stop with them.\n if (!account || account.disabled) return null;\n return { roles: account.roles };\n };\n }\n return async (uid) => {\n const user = await repo.getUserById(uid);\n if (!user || user.disabled) return null;\n return { roles: await repo.getUserRoleIds(uid) };\n };\n}\n\n/**\n * One backend's access model — the app's declared roles and scopes beside the\n * built-in ones — and, when `auth.personalKeys` is on and the backend has an\n * account store to read owners from, how a personal key finds its owner.\n *\n * Every backend holds its own (`backend.services.access`), so a role one\n * backend declares grants nothing on another in the same process.\n */\nexport interface AccessControl {\n readonly model: AccessModel;\n /**\n * How a personal key finds its owner, or undefined when personal keys are\n * off — in which case every personal key, however valid, is refused.\n */\n readonly resolveKeyOwner?: KeyOwnerResolver;\n}\n\n/**\n * What the module-level API answers for: {@link getAccessModel},\n * {@link getKeyOwnerResolver}, and every scope check on a route mounted\n * outside any backend. Built-ins only until {@link configureAccess} or a\n * backend installed as the process default sets it.\n */\nlet processAccess: AccessControl = { model: EMPTY_ACCESS_MODEL };\n\n/** The process default's access model. Built-ins only until one is configured. */\nexport function getAccessModel(): AccessModel {\n return processAccess.model;\n}\n\n/**\n * How a personal key finds its owner in the process default, or undefined\n * when personal keys are off there.\n */\nexport function getKeyOwnerResolver(): KeyOwnerResolver | undefined {\n return processAccess.resolveKeyOwner;\n}\n\n/**\n * Set the process default's access model, and — when the app enables\n * `auth.personalKeys` and has an account store to read owners from — how\n * personal keys find their owner. For a test, or a server wired by hand that\n * uses the module-level API; a backend built by `createRebaseBackend` carries\n * its own.\n */\nexport function configureAccess(options: { model: AccessModel; resolveKeyOwner?: KeyOwnerResolver }): void {\n processAccess = { model: options.model, resolveKeyOwner: options.resolveKeyOwner };\n}\n\n/** @internal The process default itself, to install another or to restore it. */\nexport function defaultAccessControl(): AccessControl {\n return processAccess;\n}\n\n/** @internal See {@link defaultAccessControl}. */\nexport function setDefaultAccessControl(access: AccessControl): void {\n processAccess = access;\n}\n\n/**\n * The process default as an {@link AccessControl} that follows it — what a\n * route or a key check built outside any backend falls back to.\n */\nexport const processAccessControl: AccessControl = {\n get model() {\n return processAccess.model;\n },\n get resolveKeyOwner() {\n return processAccess.resolveKeyOwner;\n }\n};\n\n/**\n * The access control serving this request: the backend's whose route group is\n * answering it, or the process default for a route mounted outside any.\n */\nexport function accessOf(c: Context<HonoEnv>): AccessControl {\n return c.get(\"services\")?.access ?? processAccessControl;\n}\n\n/** The resource names Rebase's own scopes use; an app scope may not reuse one. */\nconst BUILT_IN_RESOURCES = new Set(BUILT_IN_SCOPES.map(scope => scope.slice(0, scope.indexOf(\":\"))));\n\n/** A declaration the access model cannot be built from. Fails the boot. */\nexport class AccessModelError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AccessModelError\";\n }\n}\n\n/**\n * Build the access model from the users collection's `auth` block, refusing\n * anything that would read as a grant it is not.\n *\n * - `admin` may not be declared: it is built in and holds everything, and a\n * declaration would either repeat that or quietly narrow it.\n * - An app scope may not reuse a built-in resource name (`logs:export`): the\n * built-in resources are Rebase's, and a reader of `logs:*` should not have\n * to know which half is whose.\n * - A role may not list a data-plane scope: a person's data access is the\n * collection's `securityRules`, so `data:write` on a role would grant nothing\n * and look like it granted something.\n */\nexport function accessModelFromCollections(collections: readonly CollectionConfig[]): AccessModel {\n const authCollection = collections.find(collection => {\n const auth = collection.auth;\n return auth === true || (!!auth && typeof auth === \"object\" && auth.enabled === true);\n });\n const auth = authCollection && typeof authCollection.auth === \"object\" ? authCollection.auth : undefined;\n const declaredScopes: Record<string, ScopeDefinition> = { ...(auth?.scopes ?? {}) };\n const declaredRoles: Record<string, RoleDefinition> = { ...(auth?.roles ?? {}) };\n const where = `the \"${authCollection?.slug ?? \"users\"}\" collection's auth block`;\n\n for (const [name, definition] of Object.entries(declaredScopes)) {\n const parsed = parseScope(name);\n if (!parsed || parsed.target !== undefined) {\n throw new AccessModelError(\n `auth.scopes: \"${name}\" in ${where} is not a scope name. Name scopes resource:action, ` +\n \"lower-case, without a target — for example \\\"project:deploy\\\".\");\n }\n const resource = name.slice(0, name.indexOf(\":\"));\n if (BUILT_IN_RESOURCES.has(resource)) {\n throw new AccessModelError(\n `auth.scopes: \"${name}\" in ${where} uses the built-in resource \"${resource}\". ` +\n `App scopes need their own resource name; the built-in ones are ${[...BUILT_IN_RESOURCES].join(\", \")}.`);\n }\n if (typeof definition?.label !== \"string\" || definition.label.trim() === \"\") {\n throw new AccessModelError(`auth.scopes: \"${name}\" in ${where} needs a label — it is what a person granting it reads.`);\n }\n }\n\n const model: AccessModel = { roles: declaredRoles, scopes: declaredScopes };\n\n for (const [role, definition] of Object.entries(declaredRoles)) {\n if (role === ADMIN_ROLE) {\n throw new AccessModelError(\n `auth.roles: \"admin\" in ${where} is built in — it holds every scope — and cannot be declared. ` +\n \"Declare a role with another name for a narrower set.\");\n }\n if (!Array.isArray(definition?.scopes)) {\n throw new AccessModelError(`auth.roles: \"${role}\" in ${where} needs a scopes list.`);\n }\n const dataPlane = definition.scopes.filter(scope => isDataPlaneScope(parseScope(scope)?.scope ?? \"\"));\n if (dataPlane.length > 0) {\n throw new AccessModelError(\n `auth.roles: \"${role}\" in ${where} lists ${dataPlane.join(\", \")}. ` +\n \"Every signed-in person already holds the data plane; what they may do with rows is the \" +\n \"collection's securityRules. List admin-plane and app scopes only.\");\n }\n const problems = validateScopes(definition.scopes, model);\n if (problems.length > 0) {\n throw new AccessModelError(\n `auth.roles: \"${role}\" in ${where} lists ${problems.map(p => `\"${p.scope}\" (${p.reason})`).join(\", \")}. ` +\n `Role scopes are ${ADMIN_SCOPES.join(\", \")} and the app's own auth.scopes.`);\n }\n }\n\n return model;\n}\n\n/** Narrow a context's user to the roles it carries. */\nfunction rolesOf(user: unknown): string[] {\n if (typeof user !== \"object\" || user === null || !(\"roles\" in user)) return [];\n const roles = user.roles;\n return Array.isArray(roles) ? roles.filter((role): role is string => typeof role === \"string\") : [];\n}\n\n/**\n * Everything the caller of this request may do. Empty for a request with no\n * caller.\n */\nexport function callerScopes(c: Context<HonoEnv>): string[] {\n const narrowed = c.get(\"scopes\");\n if (narrowed) return narrowed;\n const user = c.get(\"user\");\n if (!user) return [];\n return scopesForRoles(rolesOf(user), accessOf(c).model);\n}\n\n/**\n * Whether this request's caller holds the `admin` role. The service key signs\n * in as one.\n */\nexport function callerIsAdmin(c: Context<HonoEnv>): boolean {\n return hasAdminRole(rolesOf(c.get(\"user\")));\n}\n\n/** Does this request's caller hold `scope`, on `target` when one is given? */\nexport function hasScope(c: Context<HonoEnv>, scope: string, target?: string): boolean {\n return scopeGrants(callerScopes(c), scope, target);\n}\n\n/**\n * The refusal for a missing scope, worded for whoever is holding the\n * credential. `details.requiredScope` names it for clients that branch.\n */\nexport function scopeMissing(c: Context<HonoEnv>, scope: string, target?: string): ApiError {\n const wanted = target !== undefined ? `${scope}:${target}` : scope;\n const viaKey = c.get(\"apiKey\") !== undefined;\n const message = viaKey\n ? `This API key does not hold the \"${wanted}\" scope. Create a key that includes it.`\n : c.get(\"scopes\")\n ? `This token does not hold the \"${wanted}\" scope. Reconnect and grant it.`\n : `Your account does not hold the \"${wanted}\" scope. A role that lists it (auth.roles on the users collection) grants it; the admin role holds every scope.`;\n return new ApiError(403, \"SCOPE_MISSING\", message, { requiredScope: wanted });\n}\n\n/**\n * Refuse a request whose caller does not hold `scope`.\n *\n * `scope` is a built-in scope or one this backend declares under\n * `auth.scopes`. A name no caller could ever hold — not a scope name, a typo\n * of a built-in one (`data:raed`) — throws as the route is registered; an app\n * scope the backend answering does not declare is answered\n * `500 SCOPE_NOT_DECLARED`, naming the declaration to add.\n *\n * `target` narrows the check to one collection, bucket, function or app\n * resource, and may be read from the request. A guard, not an authenticator:\n * it judges the caller an auth middleware identified, so mount it after one —\n * `requireAuth`, or a route group's own. A request with no caller is refused\n * as unauthenticated, not as unscoped.\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 // Refused as the route is registered, as an unknown scope in `auth.roles`\n // is refused at boot: a guard no caller could ever pass is a mistake in\n // the code, and answering every request 403 hid it.\n const problem = scopeGuardProblem(scope, typeof target === \"string\" ? target : undefined);\n if (problem) throw new Error(problem);\n return async (c, next) => {\n if (!c.get(\"user\")) {\n return refuse(c, noCallerRefusal(c));\n }\n // An app scope is declared per backend, so only the one answering can\n // say whether it exists. One it does not declare nobody holds, an\n // administrator neither: a server mistake, said as one.\n const undeclared = scopeGuardProblem(scope, undefined, Object.keys(accessOf(c).model.scopes));\n if (undeclared) return refuse(c, new ApiError(500, \"SCOPE_NOT_DECLARED\", undeclared));\n const resolvedTarget = typeof target === \"function\" ? target(c) : target;\n if (!hasScope(c, scope, resolvedTarget)) {\n return refuse(c, scopeMissing(c, scope, resolvedTarget));\n }\n return next();\n };\n}\n\n/**\n * The 401 for a guard that found no caller.\n *\n * A request that sent a credential, on a route where no auth middleware ran —\n * it sets `driver` for every caller, anonymous included — never had that\n * credential read. \"Authentication required\" sends whoever holds a valid\n * session to check it, so this names the wiring instead.\n */\nfunction noCallerRefusal(c: Context<HonoEnv>): ApiError {\n if (c.req.header(\"authorization\") !== undefined && c.get(\"driver\") === undefined) {\n return ApiError.unauthenticated(\n \"Authentication required: this request carries a credential, but nothing on this route verified it. \" +\n \"requireScope judges the caller an auth middleware identified; put requireAuth in front of it — \" +\n \"app.post(path, requireAuth, requireScope(scope), handler).\"\n );\n }\n return ApiError.unauthenticated(\"Authentication required\");\n}\n\n/**\n * Refuse a request unless its caller holds the scope for its HTTP method:\n * `read` for GET and HEAD, `write` for everything else. For an admin surface\n * whose reads and writes split exactly along the method.\n */\nexport function requireScopeByMethod(scopes: { read: string; write: string }): MiddlewareHandler<HonoEnv> {\n const read = requireScope(scopes.read);\n const write = requireScope(scopes.write);\n return async (c, next) => {\n const method = c.req.method.toUpperCase();\n return (method === \"GET\" || method === \"HEAD\" ? read : write)(c, next);\n };\n}\n\n/**\n * What a set of roles holds beyond this request's caller — the scopes, plus\n * `admin` itself when the roles include it and the caller does not hold it.\n * Empty when the caller covers every one.\n *\n * `admin` is named separately because it is more than its scopes: it is the\n * RLS role that reads every row, so a role listing every admin-plane scope\n * still cannot hand it out.\n */\nfunction beyondCaller(c: Context<HonoEnv>, roles: readonly string[]): string[] {\n const beyond = scopesBeyond(scopesForRoles(roles, accessOf(c).model), callerScopes(c));\n if (hasAdminRole(roles) && !hasAdminRole(rolesOf(c.get(\"user\")))) beyond.unshift(ADMIN_ROLE);\n return beyond;\n}\n\n/**\n * Refuse to let the caller act on an account — edit it, reset its password,\n * delete it — when that account holds anything the caller does not.\n *\n * `users:write` is the scope that lets a support role manage accounts, and\n * without this it would also let that role reset an administrator's password\n * and sign in as them.\n */\nexport function assertMayManageAccount(c: Context<HonoEnv>, targetRoles: readonly string[]): void {\n const beyond = beyondCaller(c, targetRoles);\n if (beyond.length > 0) {\n throw new ApiError(403, \"ACCOUNT_OUTRANKS_CALLER\",\n `This account holds ${beyond.join(\", \")}, which you do not, so you cannot change it.`,\n { beyond });\n }\n}\n\n/**\n * Read a `roles` body field as the exact list that is then judged and written.\n *\n * Every entry must be a string `roleNameProblem` accepts. Filtering out the bad\n * entries instead is how the ceiling once judged one list and the write stored\n * another: `[\"support\", [\"admin\"]]` was judged as `[\"support\"]` and written as\n * `{support,admin}`, and `\"support,admin\"` was one undeclared role to the check\n * and two roles to Postgres.\n */\nexport function readRoleNames(value: unknown): string[] {\n if (!Array.isArray(value)) {\n throw new ApiError(400, \"INVALID_ROLE\", \"roles must be an array of role names, e.g. [\\\"editor\\\"]\");\n }\n const problems = value.flatMap(entry => {\n const problem = roleNameProblem(entry);\n return problem ? [`${JSON.stringify(entry)} ${problem}`] : [];\n });\n if (problems.length > 0) {\n throw new ApiError(400, \"INVALID_ROLE\",\n `Not valid role names: ${problems.join(\"; \")}. A role name is a non-empty string without , { } \" \\\\ or control characters.`,\n { problems });\n }\n return [...new Set<string>(value)];\n}\n\n/** Refuse to let the caller grant roles that hold anything the caller does not. */\nexport function assertMayGrantRoles(c: Context<HonoEnv>, roles: readonly string[]): void {\n const beyond = beyondCaller(c, roles);\n if (beyond.length > 0) {\n throw new ApiError(403, \"ROLE_EXCEEDS_CALLER\",\n `These roles hold ${beyond.join(\", \")}, which you do not, so you cannot grant them.`,\n { beyond });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { type DatabaseAdmin } from "@rebasepro/types";
|
|
3
|
+
import type { HonoEnv } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Studio's review of a database branch: what it changed, and merging it.
|
|
6
|
+
*
|
|
7
|
+
* Mounted under `/api/admin/branches` behind the admin gate — `database:write`
|
|
8
|
+
* for both: the diff's sample rows are read as the database owner, outside
|
|
9
|
+
* row-level security, from both databases.
|
|
10
|
+
*
|
|
11
|
+
* Engine-agnostic: the comparison is the driver's (`BranchAdmin.diffBranch`,
|
|
12
|
+
* `mergeBranch`), and a driver without it answers 501. The driver's refusals
|
|
13
|
+
* are `ApiError`s that carry their own status — 404 for a branch that is not
|
|
14
|
+
* there, 409 for a diff that went stale or a merge that would conflict.
|
|
15
|
+
*
|
|
16
|
+
* ```
|
|
17
|
+
* GET /:name/diff?sampleLimit=100&schemas=public,app&maxRowsPerTable=100000
|
|
18
|
+
* POST /:name/merge { expectedFingerprint, onConflict?, skipBlocked?, schemas?, maxRowsPerTable? }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface BranchRoutesOptions {
|
|
22
|
+
/** The default data source's admin capabilities, read per request. */
|
|
23
|
+
getAdmin: () => DatabaseAdmin | undefined;
|
|
24
|
+
}
|
|
25
|
+
export declare function createBranchRoutes(options: BranchRoutesOptions): Hono<HonoEnv>;
|
|
@@ -74,6 +74,16 @@ export interface LiveSchemaRoutesConfig {
|
|
|
74
74
|
code: string;
|
|
75
75
|
message: string;
|
|
76
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* Set when this backend changes nothing in its database (provisioning is
|
|
79
|
+
* off): an apply that would run the change against the database is
|
|
80
|
+
* refused with this, while a source-only commit — which touches no
|
|
81
|
+
* database and goes wherever the project's own changes go — still works.
|
|
82
|
+
*/
|
|
83
|
+
databaseChangesOff?: {
|
|
84
|
+
code: string;
|
|
85
|
+
message: string;
|
|
86
|
+
};
|
|
77
87
|
/**
|
|
78
88
|
* Which paths {@link writeSource} will touch, without touching them.
|
|
79
89
|
*
|
|
@@ -40,12 +40,20 @@ export declare class RestApiGenerator {
|
|
|
40
40
|
private maxBulkRows;
|
|
41
41
|
private listLimits;
|
|
42
42
|
private authAdapter?;
|
|
43
|
-
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter, maxBulkRows?: number, listLimits?: ListLimitOptions
|
|
43
|
+
constructor(collections: CollectionConfig[], driver: DataDriver, authAdapter?: AuthAdapter, maxBulkRows?: number, listLimits?: ListLimitOptions,
|
|
44
|
+
/**
|
|
45
|
+
* `provision: false` is a backend that changes nothing in its database:
|
|
46
|
+
* the idempotency-key table is used if it is there, never created.
|
|
47
|
+
*/
|
|
48
|
+
storeOptions?: {
|
|
49
|
+
provision?: boolean;
|
|
50
|
+
});
|
|
44
51
|
/**
|
|
45
52
|
* Built on first use rather than in the constructor: it probes the driver
|
|
46
53
|
* for SQL support and creates a table, and most requests never send a key.
|
|
47
54
|
*/
|
|
48
55
|
private idempotencyStore?;
|
|
56
|
+
private readonly provision;
|
|
49
57
|
private idempotency;
|
|
50
58
|
/**
|
|
51
59
|
* Parse request query params into QueryOptions, applying this generator's
|
|
@@ -68,6 +68,20 @@ export interface IdempotencyStore {
|
|
|
68
68
|
* is the floor, not the mechanism: it only covers the exits no code runs on.
|
|
69
69
|
*/
|
|
70
70
|
release(key: string, uid: string | undefined): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Make the table ready now rather than on the first keyed request — what a
|
|
73
|
+
* provisioning run calls, so the table exists before a backend that may
|
|
74
|
+
* not create it ever needs it. Resolves to whether it is usable.
|
|
75
|
+
*/
|
|
76
|
+
ensureTable(): Promise<boolean>;
|
|
77
|
+
}
|
|
78
|
+
export interface IdempotencyStoreOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Default `true`. `false` is a backend that changes nothing in its
|
|
81
|
+
* database: the table is used if it is there and never created, and its
|
|
82
|
+
* absence turns the mechanism off, said once.
|
|
83
|
+
*/
|
|
84
|
+
provision?: boolean;
|
|
71
85
|
}
|
|
72
86
|
/**
|
|
73
87
|
* Returns `undefined` when the driver cannot run SQL, which disables the whole
|
|
@@ -79,6 +93,6 @@ export interface IdempotencyStore {
|
|
|
79
93
|
* `remember` is the duplicate we already have today, while a thrown error would
|
|
80
94
|
* reject a write the database has already accepted.
|
|
81
95
|
*/
|
|
82
|
-
export declare function createIdempotencyStore(driver: DataDriver): IdempotencyStore | undefined;
|
|
96
|
+
export declare function createIdempotencyStore(driver: DataDriver, options?: IdempotencyStoreOptions): IdempotencyStore | undefined;
|
|
83
97
|
/** The header the client sends. Matches the widely used Stripe/IETF spelling. */
|
|
84
98
|
export declare const IDEMPOTENCY_HEADER = "Idempotency-Key";
|
|
@@ -47,11 +47,12 @@ export declare function installRootErrorHandler(app: Hono<HonoEnv>): boolean;
|
|
|
47
47
|
* `boot.ts` registers `/api/health` *after* `initializeRebaseBackend` returns.
|
|
48
48
|
* A catch-all would have swallowed the health probe.
|
|
49
49
|
*
|
|
50
|
-
* Post-processing has neither problem: it converts
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* Post-processing has neither problem: it converts Hono's own default answer —
|
|
51
|
+
* `404 Not Found`, `text/plain` — and leaves every 404 something wrote exactly
|
|
52
|
+
* as it was written: the envelopes routes answer (a storage miss, an unknown
|
|
53
|
+
* collection, an unknown function), a function's own text or HTML 404, and an
|
|
54
|
+
* application's own `notFound`. Scoped to `basePath`, so an app serving its own
|
|
55
|
+
* pages at `/` keeps answering its own way for the pages.
|
|
55
56
|
*
|
|
56
57
|
* `expected`, so it logs at debug. A 404 from a frontend holding a stale link is
|
|
57
58
|
* routine, and a `⚠️` per request would trade a missing envelope for noise
|
package/dist/api/types.d.ts
CHANGED
|
@@ -50,6 +50,18 @@ export type HonoEnv = {
|
|
|
50
50
|
impersonator?: {
|
|
51
51
|
uid: string;
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* What an administrator's simulated request runs as (the RLS
|
|
55
|
+
* simulator's `x-rebase-simulate-*` headers): the role label, the uid,
|
|
56
|
+
* the roles, and the key when one was named. Read after the request by
|
|
57
|
+
* `reportSimulatedContext`. See `auth/simulation.ts`.
|
|
58
|
+
*/
|
|
59
|
+
simulation?: {
|
|
60
|
+
role: string;
|
|
61
|
+
sub: string;
|
|
62
|
+
roles: string[];
|
|
63
|
+
apiKeyId?: string;
|
|
64
|
+
};
|
|
53
65
|
/** Unique request correlation ID (generated or propagated from X-Request-ID header). */
|
|
54
66
|
requestId?: string;
|
|
55
67
|
/**
|
|
@@ -2,7 +2,7 @@ import { createRequire as __rebaseCreateRequire } from "module";
|
|
|
2
2
|
import __rebaseProcess from "process";
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
import "./src-
|
|
5
|
+
import "./src-Bh9LRPb9.js";
|
|
6
6
|
import { a as nestCollectionPatchPaths, l as nestAdminCollectionKeys, n as collectionPatchProblems, o as runtimeOnlyKeyIn, u as nestAdminPropertyKeys } from "./collection_patch-BRu-BvDv.js";
|
|
7
7
|
import * as fs$1 from "fs";
|
|
8
8
|
import * as path$1 from "path";
|
|
@@ -860,4 +860,4 @@ var AstSchemaEditor = class AstSchemaEditor {
|
|
|
860
860
|
//#endregion
|
|
861
861
|
export { AstSchemaEditor, nestAdminKeys, nestAdminKeysDeep };
|
|
862
862
|
|
|
863
|
-
//# sourceMappingURL=ast-schema-editor-
|
|
863
|
+
//# sourceMappingURL=ast-schema-editor-madTa9Hh.js.map
|