@rebasepro/server 0.24.0 → 0.24.1-canary.gcb918fc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/GCSStorageController-fv6JAZ87.js +402 -0
- package/dist/GCSStorageController-fv6JAZ87.js.map +1 -0
- package/dist/S3StorageController-BNg-pXiY.js +480 -0
- package/dist/S3StorageController-BNg-pXiY.js.map +1 -0
- package/dist/api/mount-path.d.ts +30 -0
- package/dist/api/mount.d.ts +6 -13
- package/dist/api/rest/api-generator.d.ts +53 -1
- package/dist/api/rest/auth-collection-writes.d.ts +28 -21
- package/dist/api/rest/field-access-query.d.ts +28 -8
- package/dist/api/rest/field-ops.d.ts +19 -7
- package/dist/api/rest/query-body.d.ts +10 -0
- package/dist/api/rest/write-validation.d.ts +7 -5
- package/dist/api/schema-editor-routes.d.ts +10 -1
- package/dist/api/sql-export-routes.d.ts +8 -0
- package/dist/api/types.d.ts +35 -0
- package/dist/{ast-schema-editor-CWqS_sLJ.js → ast-schema-editor-mCeSC03x.js} +3 -3
- package/dist/{ast-schema-editor-CWqS_sLJ.js.map → ast-schema-editor-mCeSC03x.js.map} +1 -1
- package/dist/auth/access.d.ts +57 -8
- package/dist/auth/admin-user-ops.d.ts +24 -2
- package/dist/auth/admin-users-route.d.ts +1 -0
- package/dist/auth/api-keys/api-key-middleware.d.ts +10 -3
- package/dist/auth/api-keys/api-key-routes.d.ts +4 -1
- package/dist/auth/builtin-auth-adapter.d.ts +13 -0
- package/dist/auth/email-change-routes.d.ts +3 -0
- package/dist/auth/expired-token-sweep.d.ts +5 -1
- package/dist/auth/interfaces.d.ts +10 -0
- package/dist/auth/jwks-routes.d.ts +5 -1
- package/dist/auth/jwt-crypto.d.ts +7 -0
- package/dist/auth/jwt-keys.d.ts +4 -0
- package/dist/auth/jwt.d.ts +282 -166
- package/dist/auth/magic-link-routes.d.ts +3 -0
- package/dist/auth/mfa-crypto.d.ts +9 -3
- package/dist/auth/mfa-gate.d.ts +4 -2
- package/dist/auth/mfa-routes.d.ts +6 -0
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/otp-routes.d.ts +3 -0
- package/dist/auth/rate-limiter.d.ts +61 -34
- package/dist/auth/registration-policy.d.ts +17 -0
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/routes.d.ts +16 -0
- package/dist/auth/session-routes.d.ts +6 -0
- package/dist/auth/user-picking.d.ts +37 -0
- package/dist/auth/verify-credential.d.ts +25 -4
- package/dist/{auth-DMLngxn_.js → auth-GnONzteu.js} +2869 -1871
- package/dist/auth-GnONzteu.js.map +1 -0
- package/dist/auth_adapter-CIItCTc8.js +21 -0
- package/dist/auth_adapter-CIItCTc8.js.map +1 -0
- package/dist/backend/create-backend.d.ts +281 -0
- package/dist/backend/default-backend.d.ts +35 -0
- package/dist/backend/options.d.ts +912 -0
- package/dist/backend/scope.d.ts +23 -0
- package/dist/backend/services.d.ts +54 -0
- package/dist/backend-DTAOsLQc.js.map +1 -1
- package/dist/backup/backup-common.d.ts +14 -3
- package/dist/backup/backup-routes.d.ts +23 -2
- package/dist/backup/backup-storage.d.ts +3 -1
- package/dist/backup/index.d.ts +3 -1
- package/dist/{backup-CN0s50D2.js → backup-BnvkSLHi.js} +176 -35
- package/dist/backup-BnvkSLHi.js.map +1 -0
- package/dist/boot/boot.d.ts +2 -2
- package/dist/boot/driver.d.ts +3 -0
- package/dist/boot/env.d.ts +9 -2
- package/dist/boot/http-server.d.ts +22 -0
- package/dist/boot/options.d.ts +16 -1
- package/dist/boot/provision.d.ts +4 -2
- package/dist/boot/runtime-cors.d.ts +17 -0
- package/dist/boot/sources.d.ts +2 -0
- package/dist/boot/static-routing.d.ts +9 -0
- package/dist/{contract-routes-fz8i4pxs.js → contract-routes-Ey6ftnKH.js} +4 -20
- package/dist/contract-routes-Ey6ftnKH.js.map +1 -0
- package/dist/cron/cron-loader.d.ts +77 -18
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +69 -3
- package/dist/cron/cron-store.d.ts +33 -5
- package/dist/cron/define-cron.d.ts +6 -2
- package/dist/cron/index.d.ts +3 -3
- package/dist/cron-loader-B-u0snqq.js +202 -0
- package/dist/cron-loader-B-u0snqq.js.map +1 -0
- package/dist/{cron-routes-Bc-SB0Se.js → cron-routes-OD6BstUj.js} +27 -4
- package/dist/cron-routes-OD6BstUj.js.map +1 -0
- package/dist/{cron-scheduler-CYQgco86.js → cron-scheduler-idtLPoN-.js} +299 -33
- package/dist/cron-scheduler-idtLPoN-.js.map +1 -0
- package/dist/cron-store-CZ30ZBi-.js +577 -0
- package/dist/cron-store-CZ30ZBi-.js.map +1 -0
- package/dist/email/smtp-email-service.d.ts +23 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/env.d.ts +0 -37
- package/dist/filter-dialect-O1HK6Nvr.js +1628 -0
- package/dist/filter-dialect-O1HK6Nvr.js.map +1 -0
- package/dist/{function-loader-D7o5Epjj.js → function-loader-iKhOZatS.js} +68 -38
- package/dist/function-loader-iKhOZatS.js.map +1 -0
- package/dist/{function-routes-CaNG4waN.js → function-routes-j9TwpvQe.js} +5 -4
- package/dist/function-routes-j9TwpvQe.js.map +1 -0
- package/dist/functions/context.d.ts +9 -0
- package/dist/functions/define-function.d.ts +10 -6
- package/dist/functions/function-loader.d.ts +75 -18
- package/dist/functions/function-routes.d.ts +2 -1
- package/dist/functions/index.d.ts +5 -4
- package/dist/functions/index.js +42 -13
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/internal.d.ts +2 -2
- package/dist/functions/proxy.d.ts +6 -2
- package/dist/functions/request-timeout.d.ts +7 -5
- package/dist/history/history-routes.d.ts +9 -0
- package/dist/history/retention-sweep.d.ts +81 -0
- package/dist/{history-recorder-Nr8zLvoU.js → history-recorder-BfWRwmPy.js} +3 -3
- package/dist/{history-recorder-Nr8zLvoU.js.map → history-recorder-BfWRwmPy.js.map} +1 -1
- package/dist/{history-store-rcAm_xFR.js → history-store-DPofGVDp.js} +2 -2
- package/dist/{history-store-rcAm_xFR.js.map → history-store-DPofGVDp.js.map} +1 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.es.js +24423 -20038
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +27 -10
- package/dist/init/process-safety.d.ts +7 -2
- package/dist/init/shutdown.d.ts +18 -3
- package/dist/init.d.ts +49 -865
- package/dist/internal-tables-L2t39RdT.js +136 -0
- package/dist/internal-tables-L2t39RdT.js.map +1 -0
- package/dist/jobs/job-queue.d.ts +8 -0
- package/dist/jobs/job-store.d.ts +24 -0
- package/dist/jobs/types.d.ts +25 -4
- package/dist/{jobs-DqYNfquG.js → jobs-BCbflZj2.js} +234 -24
- package/dist/jobs-BCbflZj2.js.map +1 -0
- package/dist/{jwt-R6bSPMjk.js → jwt-BlvuKoR6.js} +529 -454
- package/dist/jwt-BlvuKoR6.js.map +1 -0
- package/dist/{keys-GAVZqbqx.js → keys-COzQ2aE8.js} +2 -2
- package/dist/{keys-GAVZqbqx.js.map → keys-COzQ2aE8.js.map} +1 -1
- package/dist/load-failure-L5hvfDoG.js +80 -0
- package/dist/load-failure-L5hvfDoG.js.map +1 -0
- package/dist/mcp/mcp-routes.d.ts +6 -0
- package/dist/mcp/mcp-tools.d.ts +8 -0
- package/dist/mcp/oauth-routes.d.ts +7 -0
- package/dist/metrics/index.d.ts +18 -0
- package/dist/metrics/pool-wait.d.ts +50 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js +3305 -0
- package/dist/offline-QNQgsPOq-B2yGeXkn.js.map +1 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js +1915 -0
- package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +1 -0
- package/dist/{openapi-generator-DAq_XVDu.js → openapi-generator-D9zUQmEp.js} +366 -82
- package/dist/openapi-generator-D9zUQmEp.js.map +1 -0
- package/dist/policy-PdzX-Opd.js +121 -0
- package/dist/policy-PdzX-Opd.js.map +1 -0
- package/dist/project_manifest-R6y_KHms.js +59 -0
- package/dist/project_manifest-R6y_KHms.js.map +1 -0
- package/dist/{proxy-qRlqeUmO.js → proxy-CkTlMGmh.js} +4 -3
- package/dist/proxy-CkTlMGmh.js.map +1 -0
- package/dist/{query-parser-BgiKJKvc.js → query-parser-4WkMoA_K.js} +510 -15
- package/dist/query-parser-4WkMoA_K.js.map +1 -0
- package/dist/{request-timeout-DgH7j8qO.js → request-timeout-Dnl2_OBc.js} +15 -8
- package/dist/request-timeout-Dnl2_OBc.js.map +1 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js +24 -0
- package/dist/resumable-upload-threshold-CKv-Ry0l.js.map +1 -0
- package/dist/schema-edit/apply-schema-change.d.ts +10 -0
- package/dist/schema-edit/local-source.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +13 -3
- package/dist/schema-edit/source-editor.d.ts +28 -0
- package/dist/{schema-editor-routes-oIyuWl3L.js → schema-editor-routes-C11Tumyd.js} +54 -10
- package/dist/schema-editor-routes-C11Tumyd.js.map +1 -0
- package/dist/services/outbound-post.d.ts +50 -0
- package/dist/services/outbound-url-guard.d.ts +34 -5
- package/dist/services/routed-realtime-service.d.ts +5 -0
- package/dist/services/webhook-service.d.ts +66 -14
- package/dist/singleton.d.ts +9 -5
- package/dist/soft-delete-params-DfV3vVN5.js +668 -0
- package/dist/soft-delete-params-DfV3vVN5.js.map +1 -0
- package/dist/sql-export-routes-ClRwzIsC.js +240 -0
- package/dist/sql-export-routes-ClRwzIsC.js.map +1 -0
- package/dist/{src-CatHFUym.js → src-D5Y3N-S4.js} +2 -1
- package/dist/{src-CatHFUym.js.map → src-D5Y3N-S4.js.map} +1 -1
- package/dist/storage/GCSStorageController.d.ts +46 -2
- package/dist/storage/LocalStorageController.d.ts +67 -2
- package/dist/storage/S3StorageController.d.ts +37 -3
- package/dist/storage/image-transform.d.ts +5 -0
- package/dist/storage/maintenance-routes.d.ts +23 -0
- package/dist/storage/maintenance.d.ts +126 -0
- package/dist/storage/rendition-cache.d.ts +9 -11
- package/dist/storage/request-principals.d.ts +30 -0
- package/dist/storage/routes.d.ts +20 -0
- package/dist/storage/source-variables.d.ts +10 -0
- package/dist/storage/streams.d.ts +42 -0
- package/dist/storage/tus-handler.d.ts +46 -2
- package/dist/storage/types.d.ts +82 -7
- package/dist/storage/upload-validation.d.ts +13 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js +125 -0
- package/dist/storage-resumable-VO-cMpse-C_YX71pO.js.map +1 -0
- package/dist/strings.es-BeZWAodn.js +256 -0
- package/dist/strings.es-BeZWAodn.js.map +1 -0
- package/dist/upload-validation-Dd-fnsbA.js +172 -0
- package/dist/upload-validation-Dd-fnsbA.js.map +1 -0
- package/dist/utils/byte-size.d.ts +10 -0
- package/dist/utils/compression.d.ts +6 -0
- package/dist/utils/load-failure.d.ts +14 -0
- package/package.json +5 -5
- package/dist/GCSStorageController-BSiP1c-f.js +0 -256
- package/dist/GCSStorageController-BSiP1c-f.js.map +0 -1
- package/dist/S3StorageController-CAwFRgjV.js +0 -276
- package/dist/S3StorageController-CAwFRgjV.js.map +0 -1
- package/dist/auth-DMLngxn_.js.map +0 -1
- package/dist/backup-CN0s50D2.js.map +0 -1
- package/dist/contract-routes-fz8i4pxs.js.map +0 -1
- package/dist/cron-loader-CwaANlOG.js +0 -118
- package/dist/cron-loader-CwaANlOG.js.map +0 -1
- package/dist/cron-routes-Bc-SB0Se.js.map +0 -1
- package/dist/cron-scheduler-CYQgco86.js.map +0 -1
- package/dist/cron-store-D2Q9-Aco.js +0 -300
- package/dist/cron-store-D2Q9-Aco.js.map +0 -1
- package/dist/dynamic-import-X40pTZUQ.js +0 -23
- package/dist/dynamic-import-X40pTZUQ.js.map +0 -1
- package/dist/function-loader-D7o5Epjj.js.map +0 -1
- package/dist/function-routes-CaNG4waN.js.map +0 -1
- package/dist/jobs-DqYNfquG.js.map +0 -1
- package/dist/jwt-R6bSPMjk.js.map +0 -1
- package/dist/openapi-generator-DAq_XVDu.js.map +0 -1
- package/dist/proxy-qRlqeUmO.js.map +0 -1
- package/dist/query-parser-BgiKJKvc.js.map +0 -1
- package/dist/request-timeout-DgH7j8qO.js.map +0 -1
- package/dist/schema-editor-routes-oIyuWl3L.js.map +0 -1
- package/dist/soft-delete-params-BWPilMPF.js +0 -59
- package/dist/soft-delete-params-BWPilMPF.js.map +0 -1
- package/dist/src-I3aG1PcY.js +0 -6870
- package/dist/src-I3aG1PcY.js.map +0 -1
- package/dist/types-BfKcm9do.js +0 -39
- package/dist/types-BfKcm9do.js.map +0 -1
|
@@ -2,10 +2,10 @@ 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 { i as __toESM,
|
|
5
|
+
import { i as __toESM, r as __require, t as __commonJSMin } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
6
|
import { r as logger } from "./logger-D-S-hO5e.js";
|
|
7
|
-
import { a as canonicalStorageId } from "./keys-
|
|
8
|
-
import { createPrivateKey, createPublicKey } from "crypto";
|
|
7
|
+
import { a as canonicalStorageId } from "./keys-COzQ2aE8.js";
|
|
8
|
+
import { createPrivateKey, createPublicKey, createSecretKey } from "crypto";
|
|
9
9
|
//#region src/utils/portable-crypto.ts
|
|
10
10
|
/**
|
|
11
11
|
* The cryptography the request path needs, on primitives every runtime has.
|
|
@@ -2386,7 +2386,7 @@ var require_verify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2386
2386
|
var validateAsymmetricKey = require_validateAsymmetricKey();
|
|
2387
2387
|
var PS_SUPPORTED = require_psSupported();
|
|
2388
2388
|
var jws = require_jws();
|
|
2389
|
-
var { KeyObject: KeyObject$1, createSecretKey: createSecretKey$
|
|
2389
|
+
var { KeyObject: KeyObject$1, createSecretKey: createSecretKey$2, createPublicKey: createPublicKey$1 } = __require("crypto");
|
|
2390
2390
|
var PUB_KEY_ALGS = [
|
|
2391
2391
|
"RS256",
|
|
2392
2392
|
"RS384",
|
|
@@ -2457,7 +2457,7 @@ var require_verify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2457
2457
|
secretOrPublicKey = createPublicKey$1(secretOrPublicKey);
|
|
2458
2458
|
} catch (_) {
|
|
2459
2459
|
try {
|
|
2460
|
-
secretOrPublicKey = createSecretKey$
|
|
2460
|
+
secretOrPublicKey = createSecretKey$2(typeof secretOrPublicKey === "string" ? Buffer.from(secretOrPublicKey) : secretOrPublicKey);
|
|
2461
2461
|
} catch (_) {
|
|
2462
2462
|
return done(new JsonWebTokenError("secretOrPublicKey is not valid key material"));
|
|
2463
2463
|
}
|
|
@@ -4015,7 +4015,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4015
4015
|
var isPlainObject = require_lodash_isplainobject();
|
|
4016
4016
|
var isString = require_lodash_isstring();
|
|
4017
4017
|
var once = require_lodash_once();
|
|
4018
|
-
var { KeyObject, createSecretKey, createPrivateKey: createPrivateKey$1 } = __require("crypto");
|
|
4018
|
+
var { KeyObject, createSecretKey: createSecretKey$1, createPrivateKey: createPrivateKey$1 } = __require("crypto");
|
|
4019
4019
|
var SUPPORTED_ALGS = [
|
|
4020
4020
|
"RS256",
|
|
4021
4021
|
"RS384",
|
|
@@ -4159,7 +4159,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4159
4159
|
secretOrPrivateKey = createPrivateKey$1(secretOrPrivateKey);
|
|
4160
4160
|
} catch (_) {
|
|
4161
4161
|
try {
|
|
4162
|
-
secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === "string" ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey);
|
|
4162
|
+
secretOrPrivateKey = createSecretKey$1(typeof secretOrPrivateKey === "string" ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey);
|
|
4163
4163
|
} catch (_) {
|
|
4164
4164
|
return failure(/* @__PURE__ */ new Error("secretOrPrivateKey is not valid key material"));
|
|
4165
4165
|
}
|
|
@@ -4258,61 +4258,8 @@ var require_jsonwebtoken = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4258
4258
|
};
|
|
4259
4259
|
}));
|
|
4260
4260
|
//#endregion
|
|
4261
|
-
//#region src/auth/jwt-crypto.ts
|
|
4262
|
-
var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
|
|
4263
|
-
/** Sign a set of claims. Rejects if the key and algorithm disagree. */
|
|
4264
|
-
async function signJwt(payload, key, options) {
|
|
4265
|
-
return import_jsonwebtoken.default.sign(payload, key, {
|
|
4266
|
-
algorithm: options.algorithm,
|
|
4267
|
-
...options.expiresIn === void 0 ? {} : { expiresIn: options.expiresIn },
|
|
4268
|
-
...options.keyid === void 0 ? {} : { keyid: options.keyid }
|
|
4269
|
-
});
|
|
4270
|
-
}
|
|
4271
|
-
/**
|
|
4272
|
-
* Verify a token against one key and an explicit algorithm list, and return its
|
|
4273
|
-
* claims. Rejects — never returns null — when the token is not valid.
|
|
4274
|
-
*
|
|
4275
|
-
* `algorithms` has no default and must not acquire one. Letting the verifier
|
|
4276
|
-
* read `alg` out of the token's own header is the canonical JWT vulnerability:
|
|
4277
|
-
* an attacker takes a published RSA public key, HMACs a payload of their
|
|
4278
|
-
* choosing with it, sets `alg: HS256`, and a verifier holding that same public
|
|
4279
|
-
* key as "the secret" agrees. The caller decides the algorithm from the key it
|
|
4280
|
-
* chose, and {@link decodeProtectedHeader} exists so it can choose that key
|
|
4281
|
-
* without trusting anything the token asserts.
|
|
4282
|
-
*/
|
|
4283
|
-
async function verifyJwt(token, key, options) {
|
|
4284
|
-
return import_jsonwebtoken.default.verify(token, key, { algorithms: options.algorithms });
|
|
4285
|
-
}
|
|
4286
|
-
/**
|
|
4287
|
-
* The token's unverified header — `kid` and `alg` — or an empty object.
|
|
4288
|
-
*
|
|
4289
|
-
* Unverified is the point: this is read *in order to* pick the key that will
|
|
4290
|
-
* verify it, so it cannot itself be verified first. Nothing but key selection
|
|
4291
|
-
* may depend on it, and key selection then pins the algorithm.
|
|
4292
|
-
*
|
|
4293
|
-
* Implemented here rather than via the JWT library because it is plain
|
|
4294
|
-
* base64url and JSON, needs no cryptography, and there is no reason for the
|
|
4295
|
-
* portable half of this module to wait on the non-portable half.
|
|
4296
|
-
*/
|
|
4297
|
-
function decodeProtectedHeader(token) {
|
|
4298
|
-
const encoded = token.split(".")[0];
|
|
4299
|
-
if (!encoded) return {};
|
|
4300
|
-
try {
|
|
4301
|
-
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
4302
|
-
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
4303
|
-
const header = JSON.parse(atob(padded));
|
|
4304
|
-
if (typeof header !== "object" || header === null) return {};
|
|
4305
|
-
const { kid, alg } = header;
|
|
4306
|
-
return {
|
|
4307
|
-
...typeof kid === "string" ? { kid } : {},
|
|
4308
|
-
...typeof alg === "string" ? { alg } : {}
|
|
4309
|
-
};
|
|
4310
|
-
} catch {
|
|
4311
|
-
return {};
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
//#endregion
|
|
4315
4261
|
//#region src/auth/jwt-keys.ts
|
|
4262
|
+
var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
|
|
4316
4263
|
/**
|
|
4317
4264
|
* The algorithm a key type implies.
|
|
4318
4265
|
*
|
|
@@ -4416,36 +4363,162 @@ function toJwks(keys) {
|
|
|
4416
4363
|
use: "sig"
|
|
4417
4364
|
})) };
|
|
4418
4365
|
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Key strings handed to the JWT library, imported once each.
|
|
4368
|
+
*
|
|
4369
|
+
* `jsonwebtoken` turns every key that is not a `KeyObject` into one on every
|
|
4370
|
+
* call: `createPublicKey` (verify) or `createPrivateKey` (sign) first, and
|
|
4371
|
+
* when that throws — as it does for every HMAC secret, which is not PEM —
|
|
4372
|
+
* `createSecretKey`. The failed parse and its exception were about 15 of the
|
|
4373
|
+
* 21 µs of each access-token verify. These are that same conversion, in the
|
|
4374
|
+
* same order, done once per string; `jwt-crypto.ts` hands the library the
|
|
4375
|
+
* `KeyObject` it produces. Here rather than there because key parsing lives
|
|
4376
|
+
* here (see `jwt-crypto.ts`), and so does this module's `node:crypto`.
|
|
4377
|
+
*
|
|
4378
|
+
* Keyed by the key string itself, so a rotated secret (`configureJwt`) is a
|
|
4379
|
+
* new entry and the old one is simply no longer asked for; bounded, oldest
|
|
4380
|
+
* first, so a process cycling through secrets does not keep all of them. Only
|
|
4381
|
+
* keys live here — never a verification result. Empty strings and anything
|
|
4382
|
+
* not a string pass through untouched, so `jsonwebtoken` refuses them exactly
|
|
4383
|
+
* as it did ("secret or public key must be provided" and the like), and so
|
|
4384
|
+
* does a string no conversion accepts.
|
|
4385
|
+
*/
|
|
4386
|
+
var IMPORTED_KEY_LIMIT = 16;
|
|
4387
|
+
var importedVerificationKeys = /* @__PURE__ */ new Map();
|
|
4388
|
+
var importedSigningKeys = /* @__PURE__ */ new Map();
|
|
4389
|
+
function importedKey(key, cache, asymmetric) {
|
|
4390
|
+
if (typeof key !== "string" || key === "") return key;
|
|
4391
|
+
const cached = cache.get(key);
|
|
4392
|
+
if (cached) return cached;
|
|
4393
|
+
let keyObject;
|
|
4394
|
+
try {
|
|
4395
|
+
keyObject = asymmetric(key);
|
|
4396
|
+
} catch {
|
|
4397
|
+
try {
|
|
4398
|
+
keyObject = createSecretKey(Buffer.from(key));
|
|
4399
|
+
} catch {
|
|
4400
|
+
return key;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
if (cache.size >= IMPORTED_KEY_LIMIT) {
|
|
4404
|
+
const oldest = cache.keys().next();
|
|
4405
|
+
if (!oldest.done) cache.delete(oldest.value);
|
|
4406
|
+
}
|
|
4407
|
+
cache.set(key, keyObject);
|
|
4408
|
+
return keyObject;
|
|
4409
|
+
}
|
|
4410
|
+
/** A verification key, as `jsonwebtoken.verify` would import it, once per string. */
|
|
4411
|
+
function importedVerificationKey(key) {
|
|
4412
|
+
return importedKey(key, importedVerificationKeys, createPublicKey);
|
|
4413
|
+
}
|
|
4414
|
+
/** A signing key, as `jsonwebtoken.sign` would import it, once per string. */
|
|
4415
|
+
function importedSigningKey(key) {
|
|
4416
|
+
return importedKey(key, importedSigningKeys, createPrivateKey);
|
|
4417
|
+
}
|
|
4418
|
+
//#endregion
|
|
4419
|
+
//#region src/auth/jwt-crypto.ts
|
|
4420
|
+
/**
|
|
4421
|
+
* The one module in the request path that knows what signs a JWT.
|
|
4422
|
+
*
|
|
4423
|
+
* `jsonwebtoken` is built on `node:crypto` and is synchronous. Every portable
|
|
4424
|
+
* JWT implementation — WebCrypto directly, or `jose` on top of it — is
|
|
4425
|
+
* asynchronous, because `crypto.subtle` is. So the shape of the boundary
|
|
4426
|
+
* matters more than what is behind it right now: **these functions are async
|
|
4427
|
+
* and `jsonwebtoken` is confined to this file.**
|
|
4428
|
+
*
|
|
4429
|
+
* That is the expensive half of the change, and it is why it was made before
|
|
4430
|
+
* anything actually needed it. Swapping the implementation touches one file.
|
|
4431
|
+
* Going from synchronous to asynchronous verification touches every caller of
|
|
4432
|
+
* every function that verifies a token — which, at the time of writing, is
|
|
4433
|
+
* eleven call sites in `src` and about a hundred and eighty in the suite. Doing
|
|
4434
|
+
* that as a line item inside a runtime port, on top of everything else moving
|
|
4435
|
+
* at once, is how a port stalls.
|
|
4436
|
+
*
|
|
4437
|
+
* What is **not** here: key parsing. `jwt-keys.ts` turns PEM into
|
|
4438
|
+
* `node:crypto` `KeyObject`s, and the portable replacement for that is
|
|
4439
|
+
* `jose`'s `importSPKI`/`importPKCS8` — a dependency this package does not
|
|
4440
|
+
* carry yet. It stays recorded in `contracts/portable-core.txt` rather than
|
|
4441
|
+
* wrapped in something that would imply it had moved.
|
|
4442
|
+
*
|
|
4443
|
+
* ## Replacing this with jose
|
|
4444
|
+
*
|
|
4445
|
+
* Add `jose` (a lockfile change, so it happens in the primary checkout), then:
|
|
4446
|
+
* `signJwt` becomes `new SignJWT(payload).setProtectedHeader({ alg, kid })
|
|
4447
|
+
* .setExpirationTime(...).sign(key)`, `verifyJwt` becomes `jwtVerify(token,
|
|
4448
|
+
* key, { algorithms })`, and `decodeProtectedHeader` is already what jose's
|
|
4449
|
+
* function of that name does. Nothing outside this file changes — with one
|
|
4450
|
+
* exception worth writing down, because nothing would fail loudly:
|
|
4451
|
+
* **`jsonwebtoken` stamps `iat` on every token it signs and jose does not**
|
|
4452
|
+
* (it wants an explicit `.setIssuedAt()`). `iat` is what the revocation
|
|
4453
|
+
* watermark compares against — `logout`, `change-password`, `reset-password`
|
|
4454
|
+
* and `DELETE /auth/sessions` all stamp `tokensValidAfter` on the user, and
|
|
4455
|
+
* every access token issued before that mark is void. Tokens minted without
|
|
4456
|
+
* `iat` would verify perfectly well and simply stop being revocable.
|
|
4457
|
+
*
|
|
4458
|
+
* @module
|
|
4459
|
+
*/
|
|
4460
|
+
/** Sign a set of claims. Rejects if the key and algorithm disagree. */
|
|
4461
|
+
async function signJwt(payload, key, options) {
|
|
4462
|
+
return import_jsonwebtoken.default.sign(payload, importedSigningKey(key), {
|
|
4463
|
+
algorithm: options.algorithm,
|
|
4464
|
+
...options.expiresIn === void 0 ? {} : { expiresIn: options.expiresIn },
|
|
4465
|
+
...options.keyid === void 0 ? {} : { keyid: options.keyid }
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
/**
|
|
4469
|
+
* Verify a token against one key and an explicit algorithm list, and return its
|
|
4470
|
+
* claims. Rejects — never returns null — when the token is not valid.
|
|
4471
|
+
*
|
|
4472
|
+
* `algorithms` has no default and must not acquire one. Letting the verifier
|
|
4473
|
+
* read `alg` out of the token's own header is the canonical JWT vulnerability:
|
|
4474
|
+
* an attacker takes a published RSA public key, HMACs a payload of their
|
|
4475
|
+
* choosing with it, sets `alg: HS256`, and a verifier holding that same public
|
|
4476
|
+
* key as "the secret" agrees. The caller decides the algorithm from the key it
|
|
4477
|
+
* chose, and {@link decodeProtectedHeader} exists so it can choose that key
|
|
4478
|
+
* without trusting anything the token asserts.
|
|
4479
|
+
*/
|
|
4480
|
+
async function verifyJwt(token, key, options) {
|
|
4481
|
+
return import_jsonwebtoken.default.verify(token, importedVerificationKey(key), { algorithms: options.algorithms });
|
|
4482
|
+
}
|
|
4483
|
+
/**
|
|
4484
|
+
* Whether a {@link verifyJwt} rejection says only that the token's `exp` has
|
|
4485
|
+
* passed — the routine refusal that sends a client to refresh — rather than
|
|
4486
|
+
* that the token is not one we signed. Here because the library decides how
|
|
4487
|
+
* that is spelled.
|
|
4488
|
+
*/
|
|
4489
|
+
function isExpiredJwtError(error) {
|
|
4490
|
+
return error instanceof import_jsonwebtoken.default.TokenExpiredError;
|
|
4491
|
+
}
|
|
4492
|
+
/**
|
|
4493
|
+
* The token's unverified header — `kid` and `alg` — or an empty object.
|
|
4494
|
+
*
|
|
4495
|
+
* Unverified is the point: this is read *in order to* pick the key that will
|
|
4496
|
+
* verify it, so it cannot itself be verified first. Nothing but key selection
|
|
4497
|
+
* may depend on it, and key selection then pins the algorithm.
|
|
4498
|
+
*
|
|
4499
|
+
* Implemented here rather than via the JWT library because it is plain
|
|
4500
|
+
* base64url and JSON, needs no cryptography, and there is no reason for the
|
|
4501
|
+
* portable half of this module to wait on the non-portable half.
|
|
4502
|
+
*/
|
|
4503
|
+
function decodeProtectedHeader(token) {
|
|
4504
|
+
const encoded = token.split(".")[0];
|
|
4505
|
+
if (!encoded) return {};
|
|
4506
|
+
try {
|
|
4507
|
+
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
4508
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
4509
|
+
const header = JSON.parse(atob(padded));
|
|
4510
|
+
if (typeof header !== "object" || header === null) return {};
|
|
4511
|
+
const { kid, alg } = header;
|
|
4512
|
+
return {
|
|
4513
|
+
...typeof kid === "string" ? { kid } : {},
|
|
4514
|
+
...typeof alg === "string" ? { alg } : {}
|
|
4515
|
+
};
|
|
4516
|
+
} catch {
|
|
4517
|
+
return {};
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4419
4520
|
//#endregion
|
|
4420
4521
|
//#region src/auth/jwt.ts
|
|
4421
|
-
var jwt_exports = /* @__PURE__ */ __exportAll({
|
|
4422
|
-
MAX_COOKIE_AGE_MS: () => MAX_COOKIE_AGE_MS,
|
|
4423
|
-
MCP_ACCESS_PURPOSE: () => MCP_ACCESS_PURPOSE,
|
|
4424
|
-
MFA_PENDING_PURPOSE: () => MFA_PENDING_PURPOSE,
|
|
4425
|
-
configureJwt: () => configureJwt,
|
|
4426
|
-
configuredJwtSecret: () => configuredJwtSecret,
|
|
4427
|
-
customClaimsOf: () => customClaimsOf,
|
|
4428
|
-
generateAccessToken: () => generateAccessToken,
|
|
4429
|
-
generateDownloadToken: () => generateDownloadToken,
|
|
4430
|
-
generateMcpAccessToken: () => generateMcpAccessToken,
|
|
4431
|
-
generateMfaPendingToken: () => generateMfaPendingToken,
|
|
4432
|
-
generateRefreshToken: () => generateRefreshToken,
|
|
4433
|
-
getAccessTokenExpiry: () => getAccessTokenExpiry,
|
|
4434
|
-
getAccessTokenExpiryMs: () => getAccessTokenExpiryMs,
|
|
4435
|
-
getJwks: () => getJwks,
|
|
4436
|
-
getRefreshTokenExpiry: () => getRefreshTokenExpiry,
|
|
4437
|
-
getRefreshTokenTtlMs: () => getRefreshTokenTtlMs,
|
|
4438
|
-
hasAsymmetricSigningKey: () => hasAsymmetricSigningKey,
|
|
4439
|
-
hashRefreshToken: () => hashRefreshToken,
|
|
4440
|
-
isJwtConfigured: () => isJwtConfigured,
|
|
4441
|
-
parseDurationMs: () => parseDurationMs,
|
|
4442
|
-
signPurposeToken: () => signPurposeToken,
|
|
4443
|
-
verifyAccessToken: () => verifyAccessToken,
|
|
4444
|
-
verifyDownloadToken: () => verifyDownloadToken,
|
|
4445
|
-
verifyMcpAccessToken: () => verifyMcpAccessToken,
|
|
4446
|
-
verifyMfaPendingToken: () => verifyMfaPendingToken,
|
|
4447
|
-
verifyPurposeToken: () => verifyPurposeToken
|
|
4448
|
-
});
|
|
4449
4522
|
/**
|
|
4450
4523
|
* Claims the token's own identity owns, and which therefore never appear in
|
|
4451
4524
|
* {@link AccessTokenPayload.claims}.
|
|
@@ -4485,135 +4558,343 @@ function customClaimsOf(decoded) {
|
|
|
4485
4558
|
}
|
|
4486
4559
|
return Object.keys(claims).length > 0 ? claims : void 0;
|
|
4487
4560
|
}
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4561
|
+
/** How long an access token lives when no lifetime is configured. */
|
|
4562
|
+
var DEFAULT_ACCESS_EXPIRES_IN = "1h";
|
|
4563
|
+
/** Secrets that are somebody's example, not a secret. */
|
|
4564
|
+
var WEAK_SECRETS = /* @__PURE__ */ new Set([
|
|
4565
|
+
"secret",
|
|
4566
|
+
"jwt-secret",
|
|
4567
|
+
"jwt_secret",
|
|
4568
|
+
"your-secret",
|
|
4569
|
+
"your-super-secret-jwt-key-change-in-production",
|
|
4570
|
+
"super-secret-jwt-key-change-in-production",
|
|
4571
|
+
"change-me",
|
|
4572
|
+
"changeme",
|
|
4573
|
+
"password",
|
|
4574
|
+
"test",
|
|
4575
|
+
"mysecret",
|
|
4576
|
+
"my-secret",
|
|
4577
|
+
"my_secret",
|
|
4578
|
+
"example-secret",
|
|
4579
|
+
"please-change-me",
|
|
4580
|
+
"replace-this-with-a-real-secret",
|
|
4581
|
+
"default-secret",
|
|
4582
|
+
"rebase_saas_jwt_secret_must_be_long_long_long_long",
|
|
4583
|
+
"rebase_saas_service_key_must_be_long_long_long_long"
|
|
4584
|
+
]);
|
|
4493
4585
|
/**
|
|
4494
|
-
*
|
|
4586
|
+
* Build a {@link JwtService} from a configuration, refusing one it would sign
|
|
4587
|
+
* badly with: a short or well-known secret, an `activeKid` no key carries, a
|
|
4588
|
+
* lifetime that is not one.
|
|
4495
4589
|
*
|
|
4496
|
-
*
|
|
4497
|
-
*
|
|
4498
|
-
*
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
*
|
|
4504
|
-
*
|
|
4590
|
+
* Unset lifetimes take the defaults — an hour for access tokens,
|
|
4591
|
+
* {@link DEFAULT_REFRESH_EXPIRES_IN} for refresh tokens.
|
|
4592
|
+
*
|
|
4593
|
+
* @example
|
|
4594
|
+
* ```ts
|
|
4595
|
+
* const jwt = createJwtService({ secret: process.env.JWT_SECRET! });
|
|
4596
|
+
* const token = await jwt.generateAccessToken("u1", ["editor"]);
|
|
4597
|
+
* const payload = await jwt.verifyAccessToken(token); // { uid: "u1", roles: ["editor"], … }
|
|
4598
|
+
* ```
|
|
4505
4599
|
*/
|
|
4506
|
-
function
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
"jwt_secret",
|
|
4511
|
-
"your-secret",
|
|
4512
|
-
"your-super-secret-jwt-key-change-in-production",
|
|
4513
|
-
"super-secret-jwt-key-change-in-production",
|
|
4514
|
-
"change-me",
|
|
4515
|
-
"changeme",
|
|
4516
|
-
"password",
|
|
4517
|
-
"test",
|
|
4518
|
-
"mysecret",
|
|
4519
|
-
"my-secret",
|
|
4520
|
-
"my_secret",
|
|
4521
|
-
"example-secret",
|
|
4522
|
-
"please-change-me",
|
|
4523
|
-
"replace-this-with-a-real-secret",
|
|
4524
|
-
"default-secret",
|
|
4525
|
-
"rebase_saas_jwt_secret_must_be_long_long_long_long",
|
|
4526
|
-
"rebase_saas_service_key_must_be_long_long_long_long"
|
|
4527
|
-
]);
|
|
4600
|
+
function createJwtService(config) {
|
|
4601
|
+
return jwtServiceFrom(checkJwtConfig(config));
|
|
4602
|
+
}
|
|
4603
|
+
function checkJwtConfig(config) {
|
|
4528
4604
|
if (!config.secret || config.secret.length < 32) throw new Error("JWT secret is too short. Must be at least 32 characters. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
|
|
4529
|
-
if (
|
|
4605
|
+
if (WEAK_SECRETS.has(config.secret.toLowerCase())) throw new Error("JWT secret is a known default/weak value. Please use a strong, randomly generated secret. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
|
|
4530
4606
|
const resolved = config.signingKeys ? resolveSigningKeys(config.signingKeys) : [];
|
|
4531
4607
|
let active = null;
|
|
4532
4608
|
if (resolved.length > 0) if (config.activeKid) {
|
|
4533
4609
|
active = resolved.find((key) => key.kid === config.activeKid) ?? null;
|
|
4534
4610
|
if (!active) throw new Error(`auth.activeKid is "${config.activeKid}", which is not among the configured signing keys (${resolved.map((k) => `"${k.kid}"`).join(", ")}). Signing with a key nobody published produces tokens no verifier can check.`);
|
|
4535
4611
|
} else active = resolved[0];
|
|
4536
|
-
const lifetimes = {};
|
|
4537
4612
|
for (const key of ["accessExpiresIn", "refreshExpiresIn"]) {
|
|
4538
4613
|
const value = config[key];
|
|
4539
4614
|
if (value === void 0) continue;
|
|
4540
4615
|
if (parseDurationMs(value) === null) throw new Error(`auth.${key} is "${value}", which is not a lifetime. Give a positive number and a unit — "15m", "1h", "30d", "1w", "2 weeks" — in the grammar the token signer reads (vercel/ms). A bare number is refused: the signer reads it as milliseconds.`);
|
|
4541
|
-
lifetimes[key] = value;
|
|
4542
4616
|
}
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4617
|
+
return {
|
|
4618
|
+
secret: config.secret,
|
|
4619
|
+
accessExpiresIn: config.accessExpiresIn ?? DEFAULT_ACCESS_EXPIRES_IN,
|
|
4620
|
+
refreshExpiresIn: config.refreshExpiresIn ?? "30d",
|
|
4621
|
+
signingKeys: resolved,
|
|
4622
|
+
activeSigningKey: active
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
var UNCONFIGURED = {
|
|
4626
|
+
secret: "",
|
|
4627
|
+
accessExpiresIn: DEFAULT_ACCESS_EXPIRES_IN,
|
|
4628
|
+
refreshExpiresIn: "30d",
|
|
4629
|
+
signingKeys: [],
|
|
4630
|
+
activeSigningKey: null
|
|
4631
|
+
};
|
|
4632
|
+
/** Fallback when `refreshExpiresIn` is unset. {@link createJwtService} refuses an unparseable one. */
|
|
4633
|
+
var DEFAULT_REFRESH_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
4634
|
+
function jwtServiceFrom(state) {
|
|
4635
|
+
const { secret, signingKeys, activeSigningKey } = state;
|
|
4636
|
+
const requireSecret = () => {
|
|
4637
|
+
if (!secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
4638
|
+
return secret;
|
|
4639
|
+
};
|
|
4640
|
+
const getAccessTokenExpiryMs = () => parseDurationMs(state.accessExpiresIn || DEFAULT_ACCESS_EXPIRES_IN) ?? 3600 * 1e3;
|
|
4641
|
+
const getRefreshTokenTtlMs = () => {
|
|
4642
|
+
const duration = state.refreshExpiresIn;
|
|
4643
|
+
return (duration ? parseDurationMs(duration) : null) ?? DEFAULT_REFRESH_TTL_MS;
|
|
4644
|
+
};
|
|
4645
|
+
return {
|
|
4646
|
+
accessExpiresIn: state.accessExpiresIn,
|
|
4647
|
+
refreshExpiresIn: state.refreshExpiresIn,
|
|
4648
|
+
isConfigured: () => Boolean(secret),
|
|
4649
|
+
hasAsymmetricSigningKey: () => activeSigningKey !== null,
|
|
4650
|
+
getJwks: () => toJwks(signingKeys),
|
|
4651
|
+
hmacSecret: () => secret || void 0,
|
|
4652
|
+
async generateAccessToken(uid, roles, aal = "aal1", customClaims, isAnonymous = false, sessionId) {
|
|
4653
|
+
const key = requireSecret();
|
|
4654
|
+
const payload = {
|
|
4655
|
+
...customClaims,
|
|
4656
|
+
uid,
|
|
4657
|
+
roles,
|
|
4658
|
+
aal,
|
|
4659
|
+
...isAnonymous ? { isAnonymous: true } : {},
|
|
4660
|
+
...sessionId ? { sid: sessionId } : {}
|
|
4661
|
+
};
|
|
4662
|
+
if (activeSigningKey) return signJwt(payload, activeSigningKey.privateKey, {
|
|
4663
|
+
expiresIn: state.accessExpiresIn,
|
|
4664
|
+
algorithm: activeSigningKey.algorithm,
|
|
4665
|
+
keyid: activeSigningKey.kid
|
|
4666
|
+
});
|
|
4667
|
+
return signJwt(payload, key, {
|
|
4668
|
+
expiresIn: state.accessExpiresIn,
|
|
4669
|
+
algorithm: "HS256"
|
|
4670
|
+
});
|
|
4671
|
+
},
|
|
4672
|
+
async verifyAccessToken(token) {
|
|
4673
|
+
const key = requireSecret();
|
|
4674
|
+
try {
|
|
4675
|
+
const header = decodeProtectedHeader(token);
|
|
4676
|
+
const namedKey = resolveVerificationKey(signingKeys, header.kid);
|
|
4677
|
+
const decoded = namedKey ? await verifyJwt(token, namedKey.publicKey, { algorithms: [namedKey.algorithm] }) : await verifyJwt(token, key, { algorithms: ["HS256"] });
|
|
4678
|
+
if (decoded.purpose) {
|
|
4679
|
+
logger.error("[JWT] Verification failed: a purpose-scoped token is not an access token", { purpose: decoded.purpose });
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
const id = decoded.uid || decoded.sub;
|
|
4683
|
+
if (!id) {
|
|
4684
|
+
logger.error("[JWT] Verification failed: missing id in payload", { detail: decoded });
|
|
4685
|
+
return null;
|
|
4686
|
+
}
|
|
4687
|
+
const aal = decoded.aal === "aal1" || decoded.aal === "aal2" ? decoded.aal : "aal1";
|
|
4688
|
+
return {
|
|
4689
|
+
uid: id,
|
|
4690
|
+
roles: decoded.roles || [],
|
|
4691
|
+
aal,
|
|
4692
|
+
...decoded.isAnonymous === true ? { isAnonymous: true } : {},
|
|
4693
|
+
iat: decoded.iat,
|
|
4694
|
+
...typeof decoded.exp === "number" ? { exp: decoded.exp } : {},
|
|
4695
|
+
...typeof decoded.sid === "string" && decoded.sid ? { sid: decoded.sid } : {},
|
|
4696
|
+
...(() => {
|
|
4697
|
+
const claims = customClaimsOf(decoded);
|
|
4698
|
+
return claims ? { claims } : {};
|
|
4699
|
+
})()
|
|
4700
|
+
};
|
|
4701
|
+
} catch (error) {
|
|
4702
|
+
if (isExpiredJwtError(error)) {
|
|
4703
|
+
logger.debug("[JWT] Access token expired");
|
|
4704
|
+
return null;
|
|
4705
|
+
}
|
|
4706
|
+
logger.error("[JWT] Verification failed", { error });
|
|
4707
|
+
return null;
|
|
4708
|
+
}
|
|
4709
|
+
},
|
|
4710
|
+
getAccessTokenExpiryMs,
|
|
4711
|
+
getAccessTokenExpiry: () => Date.now() + getAccessTokenExpiryMs(),
|
|
4712
|
+
getRefreshTokenTtlMs,
|
|
4713
|
+
getRefreshTokenExpiry: () => new Date(Date.now() + getRefreshTokenTtlMs()),
|
|
4714
|
+
async generateMfaPendingToken(uid, expiresInSeconds = 300) {
|
|
4715
|
+
const key = requireSecret();
|
|
4716
|
+
return signJwt({
|
|
4717
|
+
purpose: MFA_PENDING_PURPOSE,
|
|
4718
|
+
uid
|
|
4719
|
+
}, key, {
|
|
4720
|
+
expiresIn: expiresInSeconds,
|
|
4721
|
+
algorithm: "HS256"
|
|
4722
|
+
});
|
|
4723
|
+
},
|
|
4724
|
+
async verifyMfaPendingToken(token) {
|
|
4725
|
+
const key = requireSecret();
|
|
4726
|
+
try {
|
|
4727
|
+
const decoded = await verifyJwt(token, key, { algorithms: ["HS256"] });
|
|
4728
|
+
if (decoded.purpose !== "mfa-pending" || !decoded.uid) return null;
|
|
4729
|
+
return {
|
|
4730
|
+
uid: decoded.uid,
|
|
4731
|
+
iat: typeof decoded.iat === "number" ? decoded.iat : void 0
|
|
4732
|
+
};
|
|
4733
|
+
} catch {
|
|
4734
|
+
return null;
|
|
4735
|
+
}
|
|
4736
|
+
},
|
|
4737
|
+
async generateDownloadToken(path, expiresInSeconds = 300, storageId, principal) {
|
|
4738
|
+
const key = requireSecret();
|
|
4739
|
+
return signJwt({
|
|
4740
|
+
purpose: "file-read",
|
|
4741
|
+
path,
|
|
4742
|
+
storageId: canonicalStorageId(storageId),
|
|
4743
|
+
...principal ? { rl: await downloadTokenRateLimitMark(principal) } : {}
|
|
4744
|
+
}, key, {
|
|
4745
|
+
expiresIn: expiresInSeconds,
|
|
4746
|
+
algorithm: "HS256"
|
|
4747
|
+
});
|
|
4748
|
+
},
|
|
4749
|
+
async verifyDownloadToken(token) {
|
|
4750
|
+
const key = requireSecret();
|
|
4751
|
+
try {
|
|
4752
|
+
const decoded = await verifyJwt(token, key, { algorithms: ["HS256"] });
|
|
4753
|
+
if (decoded && decoded.purpose === "file-read" && typeof decoded.path === "string") return {
|
|
4754
|
+
purpose: "file-read",
|
|
4755
|
+
path: decoded.path,
|
|
4756
|
+
storageId: canonicalStorageId(typeof decoded.storageId === "string" ? decoded.storageId : null),
|
|
4757
|
+
...typeof decoded.rl === "string" ? { rl: decoded.rl } : {}
|
|
4758
|
+
};
|
|
4759
|
+
return null;
|
|
4760
|
+
} catch (error) {
|
|
4761
|
+
logger.debug("[JWT] Download token verification failed", { error });
|
|
4762
|
+
return null;
|
|
4763
|
+
}
|
|
4764
|
+
},
|
|
4765
|
+
async signPurposeToken(purpose, claims, expiresInSeconds) {
|
|
4766
|
+
const key = requireSecret();
|
|
4767
|
+
return signJwt({
|
|
4768
|
+
...claims,
|
|
4769
|
+
purpose
|
|
4770
|
+
}, key, {
|
|
4771
|
+
expiresIn: expiresInSeconds,
|
|
4772
|
+
algorithm: "HS256"
|
|
4773
|
+
});
|
|
4774
|
+
},
|
|
4775
|
+
async verifyPurposeToken(purpose, token) {
|
|
4776
|
+
const key = requireSecret();
|
|
4777
|
+
if (!token) return null;
|
|
4778
|
+
try {
|
|
4779
|
+
const decoded = await verifyJwt(token, key, { algorithms: ["HS256"] });
|
|
4780
|
+
if (!decoded || decoded.purpose !== purpose) return null;
|
|
4781
|
+
return decoded;
|
|
4782
|
+
} catch (error) {
|
|
4783
|
+
logger.error("[JWT] Purpose token verification failed", {
|
|
4784
|
+
purpose,
|
|
4785
|
+
error
|
|
4786
|
+
});
|
|
4787
|
+
return null;
|
|
4788
|
+
}
|
|
4789
|
+
},
|
|
4790
|
+
async generateMcpAccessToken(payload, expiresInSeconds) {
|
|
4791
|
+
const key = requireSecret();
|
|
4792
|
+
return signJwt({
|
|
4793
|
+
purpose: MCP_ACCESS_PURPOSE,
|
|
4794
|
+
uid: payload.uid,
|
|
4795
|
+
roles: payload.roles,
|
|
4796
|
+
scope: payload.scope,
|
|
4797
|
+
clientId: payload.clientId,
|
|
4798
|
+
aud: payload.aud,
|
|
4799
|
+
iss: payload.iss
|
|
4800
|
+
}, key, {
|
|
4801
|
+
expiresIn: expiresInSeconds,
|
|
4802
|
+
algorithm: "HS256"
|
|
4803
|
+
});
|
|
4804
|
+
},
|
|
4805
|
+
async verifyMcpAccessToken(token, expectedAudience) {
|
|
4806
|
+
const key = requireSecret();
|
|
4807
|
+
try {
|
|
4808
|
+
const decoded = await verifyJwt(token, key, { algorithms: ["HS256"] });
|
|
4809
|
+
if (!decoded || decoded.purpose !== "mcp-access") return null;
|
|
4810
|
+
if (typeof decoded.uid !== "string" || !decoded.uid) return null;
|
|
4811
|
+
if (typeof decoded.aud !== "string" || decoded.aud !== expectedAudience) {
|
|
4812
|
+
logger.error("[JWT] MCP token rejected: audience mismatch", {
|
|
4813
|
+
expected: expectedAudience,
|
|
4814
|
+
got: decoded.aud
|
|
4815
|
+
});
|
|
4816
|
+
return null;
|
|
4817
|
+
}
|
|
4818
|
+
return {
|
|
4819
|
+
purpose: MCP_ACCESS_PURPOSE,
|
|
4820
|
+
uid: decoded.uid,
|
|
4821
|
+
roles: Array.isArray(decoded.roles) ? decoded.roles.map(String) : [],
|
|
4822
|
+
scope: typeof decoded.scope === "string" ? decoded.scope : "",
|
|
4823
|
+
clientId: typeof decoded.clientId === "string" ? decoded.clientId : "",
|
|
4824
|
+
aud: decoded.aud,
|
|
4825
|
+
iss: typeof decoded.iss === "string" ? decoded.iss : ""
|
|
4826
|
+
};
|
|
4827
|
+
} catch (error) {
|
|
4828
|
+
logger.error("[JWT] MCP token verification failed", { error });
|
|
4829
|
+
return null;
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4548
4832
|
};
|
|
4549
|
-
signingKeys = resolved;
|
|
4550
|
-
activeSigningKey = active;
|
|
4551
4833
|
}
|
|
4552
4834
|
/**
|
|
4553
|
-
* The
|
|
4835
|
+
* The service the module-level functions below answer for.
|
|
4836
|
+
*
|
|
4837
|
+
* Unconfigured until {@link configureJwt} is called or a backend is installed
|
|
4838
|
+
* as the process default (`initializeRebaseBackend`, `setDefaultBackend`).
|
|
4839
|
+
* `createRebaseBackend` never touches it: a backend it builds signs and
|
|
4840
|
+
* verifies with `backend.services.jwt`.
|
|
4841
|
+
*/
|
|
4842
|
+
var processJwt = jwtServiceFrom(UNCONFIGURED);
|
|
4843
|
+
/**
|
|
4844
|
+
* Configure the process default's JWT settings — what `verifyAccessToken`,
|
|
4845
|
+
* `generateAccessToken` and the other module-level functions sign and verify
|
|
4846
|
+
* with.
|
|
4554
4847
|
*
|
|
4555
|
-
*
|
|
4556
|
-
*
|
|
4557
|
-
*
|
|
4558
|
-
*
|
|
4848
|
+
* Validates the secret strength to prevent deployment with default/weak
|
|
4849
|
+
* secrets. A lifetime left unset keeps the one already configured; a
|
|
4850
|
+
* configuration that fails validation leaves the previous one standing rather
|
|
4851
|
+
* than half-applying a new one.
|
|
4559
4852
|
*/
|
|
4853
|
+
function configureJwt(config) {
|
|
4854
|
+
processJwt = createJwtService({
|
|
4855
|
+
...config,
|
|
4856
|
+
accessExpiresIn: config.accessExpiresIn ?? processJwt.accessExpiresIn,
|
|
4857
|
+
refreshExpiresIn: config.refreshExpiresIn ?? processJwt.refreshExpiresIn
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
/** @internal See {@link defaultJwtService}. */
|
|
4861
|
+
function setDefaultJwtService(service) {
|
|
4862
|
+
processJwt = service;
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* The JWT service serving this request: the backend's whose route group is
|
|
4866
|
+
* answering it, or the process default for a route mounted outside any
|
|
4867
|
+
* backend.
|
|
4868
|
+
*/
|
|
4869
|
+
function jwtOf(c) {
|
|
4870
|
+
return c.get("services")?.jwt ?? processJwtService;
|
|
4871
|
+
}
|
|
4872
|
+
/** See {@link JwtService.getJwks}. Answers for the process default. */
|
|
4560
4873
|
function getJwks() {
|
|
4561
|
-
return
|
|
4874
|
+
return processJwt.getJwks();
|
|
4562
4875
|
}
|
|
4563
4876
|
/**
|
|
4564
|
-
* The secret
|
|
4565
|
-
*
|
|
4566
|
-
* passed in code, not only the one in `JWT_SECRET`.
|
|
4877
|
+
* The secret {@link configureJwt} was given, or undefined before it is called.
|
|
4878
|
+
* Answers for the process default — see {@link JwtService.hmacSecret}.
|
|
4567
4879
|
*/
|
|
4568
4880
|
function configuredJwtSecret() {
|
|
4569
|
-
return
|
|
4881
|
+
return processJwt.hmacSecret();
|
|
4570
4882
|
}
|
|
4571
|
-
/**
|
|
4883
|
+
/** See {@link JwtService.hasAsymmetricSigningKey}. Answers for the process default. */
|
|
4572
4884
|
function hasAsymmetricSigningKey() {
|
|
4573
|
-
return
|
|
4885
|
+
return processJwt.hasAsymmetricSigningKey();
|
|
4574
4886
|
}
|
|
4575
|
-
/**
|
|
4576
|
-
* Has this server been given a JWT secret?
|
|
4577
|
-
*
|
|
4578
|
-
* False on every backend that authenticates through an adapter — Firebase,
|
|
4579
|
-
* Clerk, anything with its own tokens — because nothing calls
|
|
4580
|
-
* {@link configureJwt} there. Signing paths still throw when it is false, since
|
|
4581
|
-
* asking for a token from a server that cannot mint one is a mistake worth
|
|
4582
|
-
* hearing about. Paths whose contract is *"tolerate the absence of auth"* must
|
|
4583
|
-
* ask first: `optionalAuth` crashing a request because this backend does not do
|
|
4584
|
-
* JWT is a 500 on a route that had already decided anonymous was acceptable.
|
|
4585
|
-
*/
|
|
4887
|
+
/** See {@link JwtService.isConfigured}. Answers for the process default. */
|
|
4586
4888
|
function isJwtConfigured() {
|
|
4587
|
-
return
|
|
4889
|
+
return processJwt.isConfigured();
|
|
4588
4890
|
}
|
|
4589
|
-
/**
|
|
4590
|
-
* Generate an access token (short-lived, 1 hour by default)
|
|
4591
|
-
*/
|
|
4891
|
+
/** See {@link JwtService.generateAccessToken}. Answers for the process default. */
|
|
4592
4892
|
async function generateAccessToken(uid, roles, aal = "aal1", customClaims, isAnonymous = false, sessionId) {
|
|
4593
|
-
|
|
4594
|
-
const payload = {
|
|
4595
|
-
...customClaims,
|
|
4596
|
-
uid,
|
|
4597
|
-
roles,
|
|
4598
|
-
aal,
|
|
4599
|
-
...isAnonymous ? { isAnonymous: true } : {},
|
|
4600
|
-
...sessionId ? { sid: sessionId } : {}
|
|
4601
|
-
};
|
|
4602
|
-
if (activeSigningKey) return signJwt(payload, activeSigningKey.privateKey, {
|
|
4603
|
-
expiresIn: jwtConfig.accessExpiresIn,
|
|
4604
|
-
algorithm: activeSigningKey.algorithm,
|
|
4605
|
-
keyid: activeSigningKey.kid
|
|
4606
|
-
});
|
|
4607
|
-
return signJwt(payload, jwtConfig.secret, {
|
|
4608
|
-
expiresIn: jwtConfig.accessExpiresIn,
|
|
4609
|
-
algorithm: "HS256"
|
|
4610
|
-
});
|
|
4893
|
+
return processJwt.generateAccessToken(uid, roles, aal, customClaims, isAnonymous, sessionId);
|
|
4611
4894
|
}
|
|
4612
|
-
/**
|
|
4613
|
-
* Get the expiration time of an access token in milliseconds from now
|
|
4614
|
-
*/
|
|
4895
|
+
/** See {@link JwtService.getAccessTokenExpiryMs}. Answers for the process default. */
|
|
4615
4896
|
function getAccessTokenExpiryMs() {
|
|
4616
|
-
return
|
|
4897
|
+
return processJwt.getAccessTokenExpiryMs();
|
|
4617
4898
|
}
|
|
4618
4899
|
var DURATION_UNIT_MS = {
|
|
4619
4900
|
ms: 1,
|
|
@@ -4667,63 +4948,17 @@ function parseDurationMs(value) {
|
|
|
4667
4948
|
const ms = parseFloat(match[1]) * unit;
|
|
4668
4949
|
return ms > 0 && Number.isFinite(ms) ? ms : null;
|
|
4669
4950
|
}
|
|
4670
|
-
/**
|
|
4671
|
-
* Get the expiration timestamp for an access token
|
|
4672
|
-
*/
|
|
4951
|
+
/** See {@link JwtService.getAccessTokenExpiry}. Answers for the process default. */
|
|
4673
4952
|
function getAccessTokenExpiry() {
|
|
4674
|
-
return
|
|
4953
|
+
return processJwt.getAccessTokenExpiry();
|
|
4675
4954
|
}
|
|
4676
|
-
/**
|
|
4677
|
-
* Verify and decode an access token.
|
|
4678
|
-
*
|
|
4679
|
-
* Every token this server issues is signed with the same secret, so what a
|
|
4680
|
-
* token *is* comes from its claims, not from its signature. A download token
|
|
4681
|
-
* ({@link generateDownloadToken}) is therefore a validly-signed string that
|
|
4682
|
-
* must never authenticate anybody: it is scoped to one file path and handed out
|
|
4683
|
-
* in URLs, which is a far weaker thing to hold than a session.
|
|
4684
|
-
*
|
|
4685
|
-
* Today it is rejected below for want of an id — but only by luck, since
|
|
4686
|
-
* nothing stops a future download token from carrying one. So the purpose is
|
|
4687
|
-
* checked explicitly: a token minted for reading a file is not a token for
|
|
4688
|
-
* being a user.
|
|
4689
|
-
*/
|
|
4955
|
+
/** See {@link JwtService.verifyAccessToken}. Answers for the process default. */
|
|
4690
4956
|
async function verifyAccessToken(token) {
|
|
4691
|
-
|
|
4692
|
-
try {
|
|
4693
|
-
const header = decodeProtectedHeader(token);
|
|
4694
|
-
const namedKey = resolveVerificationKey(signingKeys, header.kid);
|
|
4695
|
-
const decoded = namedKey ? await verifyJwt(token, namedKey.publicKey, { algorithms: [namedKey.algorithm] }) : await verifyJwt(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4696
|
-
if (decoded.purpose) {
|
|
4697
|
-
logger.error("[JWT] Verification failed: a purpose-scoped token is not an access token", { purpose: decoded.purpose });
|
|
4698
|
-
return null;
|
|
4699
|
-
}
|
|
4700
|
-
const id = decoded.uid || decoded.sub;
|
|
4701
|
-
if (!id) {
|
|
4702
|
-
logger.error("[JWT] Verification failed: missing id in payload", { detail: decoded });
|
|
4703
|
-
return null;
|
|
4704
|
-
}
|
|
4705
|
-
const aal = decoded.aal === "aal1" || decoded.aal === "aal2" ? decoded.aal : "aal1";
|
|
4706
|
-
return {
|
|
4707
|
-
uid: id,
|
|
4708
|
-
roles: decoded.roles || [],
|
|
4709
|
-
aal,
|
|
4710
|
-
...decoded.isAnonymous === true ? { isAnonymous: true } : {},
|
|
4711
|
-
iat: decoded.iat,
|
|
4712
|
-
...typeof decoded.exp === "number" ? { exp: decoded.exp } : {},
|
|
4713
|
-
...typeof decoded.sid === "string" && decoded.sid ? { sid: decoded.sid } : {},
|
|
4714
|
-
...(() => {
|
|
4715
|
-
const claims = customClaimsOf(decoded);
|
|
4716
|
-
return claims ? { claims } : {};
|
|
4717
|
-
})()
|
|
4718
|
-
};
|
|
4719
|
-
} catch (error) {
|
|
4720
|
-
logger.error("[JWT] Verification failed", { error });
|
|
4721
|
-
return null;
|
|
4722
|
-
}
|
|
4957
|
+
return processJwt.verifyAccessToken(token);
|
|
4723
4958
|
}
|
|
4724
4959
|
/**
|
|
4725
|
-
* Generate a random refresh token. Long-lived:
|
|
4726
|
-
* see {@link getRefreshTokenTtlMs}.
|
|
4960
|
+
* Generate a random refresh token. Long-lived: 30 days by default, sliding —
|
|
4961
|
+
* see {@link JwtService.getRefreshTokenTtlMs}.
|
|
4727
4962
|
*/
|
|
4728
4963
|
function generateRefreshToken() {
|
|
4729
4964
|
return randomHex(40);
|
|
@@ -4741,255 +4976,95 @@ function hashRefreshToken(token) {
|
|
|
4741
4976
|
* what is actually enforced, which is worse than knowing the ceiling.
|
|
4742
4977
|
*/
|
|
4743
4978
|
var MAX_COOKIE_AGE_MS = 400 * 24 * 60 * 60 * 1e3;
|
|
4744
|
-
/**
|
|
4745
|
-
var DEFAULT_REFRESH_TTL_MS = MAX_COOKIE_AGE_MS;
|
|
4746
|
-
/**
|
|
4747
|
-
* How long a refresh token is valid for, in milliseconds.
|
|
4748
|
-
*
|
|
4749
|
-
* Every rotation issues a token with a fresh TTL, so this is a sliding window:
|
|
4750
|
-
* a user who visits at all keeps their session indefinitely, and one who
|
|
4751
|
-
* disappears loses it this long after their last visit. That is what both
|
|
4752
|
-
* Firebase and Supabase do by default, and it is the behaviour people mean
|
|
4753
|
-
* when they say they expect to still be signed in.
|
|
4754
|
-
*/
|
|
4979
|
+
/** See {@link JwtService.getRefreshTokenTtlMs}. Answers for the process default. */
|
|
4755
4980
|
function getRefreshTokenTtlMs() {
|
|
4756
|
-
|
|
4757
|
-
return (duration ? parseDurationMs(duration) : null) ?? DEFAULT_REFRESH_TTL_MS;
|
|
4981
|
+
return processJwt.getRefreshTokenTtlMs();
|
|
4758
4982
|
}
|
|
4759
|
-
/**
|
|
4760
|
-
* Calculate refresh token expiration date
|
|
4761
|
-
*/
|
|
4983
|
+
/** See {@link JwtService.getRefreshTokenExpiry}. Answers for the process default. */
|
|
4762
4984
|
function getRefreshTokenExpiry() {
|
|
4763
|
-
return
|
|
4985
|
+
return processJwt.getRefreshTokenExpiry();
|
|
4764
4986
|
}
|
|
4765
4987
|
/**
|
|
4766
4988
|
* The `purpose` claim carried by a credential that stands between "first factor
|
|
4767
4989
|
* accepted" and "session issued".
|
|
4768
4990
|
*
|
|
4769
4991
|
* A pre-auth token is NOT a session and must never be usable as one:
|
|
4770
|
-
* {@link verifyAccessToken} refuses any token carrying a `purpose`,
|
|
4771
|
-
* value cannot authenticate a request no matter which route it is
|
|
4772
|
-
* The only thing that reads it is the MFA challenge pair, which
|
|
4773
|
-
* plus a second factor — for a real session.
|
|
4992
|
+
* {@link JwtService.verifyAccessToken} refuses any token carrying a `purpose`,
|
|
4993
|
+
* so this value cannot authenticate a request no matter which route it is
|
|
4994
|
+
* presented to. The only thing that reads it is the MFA challenge pair, which
|
|
4995
|
+
* exchanges it — plus a second factor — for a real session.
|
|
4774
4996
|
*/
|
|
4775
4997
|
var MFA_PENDING_PURPOSE = "mfa-pending";
|
|
4776
|
-
/**
|
|
4777
|
-
* Mint the short-lived credential handed back with an `MFA_REQUIRED` response.
|
|
4778
|
-
*
|
|
4779
|
-
* Short-lived on purpose: it is the window in which a caller who has proven the
|
|
4780
|
-
* first factor may present the second, not a session to be carried around. Five
|
|
4781
|
-
* minutes matches the challenge TTL.
|
|
4782
|
-
*/
|
|
4998
|
+
/** See {@link JwtService.generateMfaPendingToken}. Answers for the process default. */
|
|
4783
4999
|
async function generateMfaPendingToken(uid, expiresInSeconds = 300) {
|
|
4784
|
-
|
|
4785
|
-
return signJwt({
|
|
4786
|
-
purpose: MFA_PENDING_PURPOSE,
|
|
4787
|
-
uid
|
|
4788
|
-
}, jwtConfig.secret, {
|
|
4789
|
-
expiresIn: expiresInSeconds,
|
|
4790
|
-
algorithm: "HS256"
|
|
4791
|
-
});
|
|
5000
|
+
return processJwt.generateMfaPendingToken(uid, expiresInSeconds);
|
|
4792
5001
|
}
|
|
4793
|
-
/**
|
|
4794
|
-
* Verify a pre-auth token and return the user it was minted for.
|
|
4795
|
-
*
|
|
4796
|
-
* Returns `null` for anything else — including a perfectly valid *access*
|
|
4797
|
-
* token, which must not be interchangeable with this one in either direction.
|
|
4798
|
-
*
|
|
4799
|
-
* `iat` comes back with the uid because this token is a first factor that was
|
|
4800
|
-
* accepted at that instant, and a sign-out everywhere after it has to void it
|
|
4801
|
-
* the way it voids an access token — see `isAccessTokenRevoked`.
|
|
4802
|
-
*/
|
|
5002
|
+
/** See {@link JwtService.verifyMfaPendingToken}. Answers for the process default. */
|
|
4803
5003
|
async function verifyMfaPendingToken(token) {
|
|
4804
|
-
|
|
4805
|
-
try {
|
|
4806
|
-
const decoded = await verifyJwt(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4807
|
-
if (decoded.purpose !== "mfa-pending" || !decoded.uid) return null;
|
|
4808
|
-
return {
|
|
4809
|
-
uid: decoded.uid,
|
|
4810
|
-
iat: typeof decoded.iat === "number" ? decoded.iat : void 0
|
|
4811
|
-
};
|
|
4812
|
-
} catch {
|
|
4813
|
-
return null;
|
|
4814
|
-
}
|
|
5004
|
+
return processJwt.verifyMfaPendingToken(token);
|
|
4815
5005
|
}
|
|
4816
|
-
/**
|
|
4817
|
-
* Generate a short-lived download token scoped to a specific file path or prefix
|
|
4818
|
-
* *within one storage source*.
|
|
4819
|
-
*
|
|
4820
|
-
* Both halves of that scope are load-bearing. A key is only unique inside its
|
|
4821
|
-
* own bucket, and a project with more than one source routinely holds the same
|
|
4822
|
-
* key in several of them — `avatars/u1.png` in `(default)` and in `media` are
|
|
4823
|
-
* different objects, quite possibly with different owners. A token that names
|
|
4824
|
-
* only the path is therefore a grant on every source at once: authorize a read
|
|
4825
|
-
* on the source whose `storageAuthorize` hook says yes, then spend the token
|
|
4826
|
-
* against `?storageId=` pointing somewhere else.
|
|
4827
|
-
*
|
|
4828
|
-
* `storageId` is optional here only because omitting it *is* the default
|
|
4829
|
-
* source, which is what the overwhelming majority of deployments have. A mint
|
|
4830
|
-
* site that forgets to pass a named source produces a default-scoped token,
|
|
4831
|
-
* which fails closed at `/file/*` rather than over-granting.
|
|
4832
|
-
*/
|
|
5006
|
+
/** See {@link JwtService.generateDownloadToken}. Answers for the process default. */
|
|
4833
5007
|
async function generateDownloadToken(path, expiresInSeconds = 300, storageId, principal) {
|
|
4834
|
-
|
|
4835
|
-
return signJwt({
|
|
4836
|
-
purpose: "file-read",
|
|
4837
|
-
path,
|
|
4838
|
-
storageId: canonicalStorageId(storageId),
|
|
4839
|
-
...principal ? { rl: await downloadTokenRateLimitMark(principal) } : {}
|
|
4840
|
-
}, jwtConfig.secret, {
|
|
4841
|
-
expiresIn: expiresInSeconds,
|
|
4842
|
-
algorithm: "HS256"
|
|
4843
|
-
});
|
|
5008
|
+
return processJwt.generateDownloadToken(path, expiresInSeconds, storageId, principal);
|
|
4844
5009
|
}
|
|
4845
5010
|
/** See {@link DownloadTokenPayload.rl}. */
|
|
4846
5011
|
async function downloadTokenRateLimitMark(principal) {
|
|
4847
5012
|
return (await sha256Hex(`download-token-rl:${principal}`)).slice(0, 32);
|
|
4848
5013
|
}
|
|
4849
|
-
/**
|
|
4850
|
-
* Verify and decode a download token.
|
|
4851
|
-
*
|
|
4852
|
-
* A token minted before `storageId` existed carries no such claim. It is read
|
|
4853
|
-
* as a grant on the **default source** rather than on all of them: that is the
|
|
4854
|
-
* fail-closed reading, and it is what such a token almost always was, since a
|
|
4855
|
-
* named source has to be asked for explicitly. The cost is bounded by the
|
|
4856
|
-
* five-minute TTL — for at most that long after a deploy, an in-flight token
|
|
4857
|
-
* for a *named* source is refused and the client re-fetches `/metadata`.
|
|
4858
|
-
*/
|
|
5014
|
+
/** See {@link JwtService.verifyDownloadToken}. Answers for the process default. */
|
|
4859
5015
|
async function verifyDownloadToken(token) {
|
|
4860
|
-
|
|
4861
|
-
try {
|
|
4862
|
-
const decoded = await verifyJwt(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4863
|
-
if (decoded && decoded.purpose === "file-read" && typeof decoded.path === "string") return {
|
|
4864
|
-
purpose: "file-read",
|
|
4865
|
-
path: decoded.path,
|
|
4866
|
-
storageId: canonicalStorageId(typeof decoded.storageId === "string" ? decoded.storageId : null),
|
|
4867
|
-
...typeof decoded.rl === "string" ? { rl: decoded.rl } : {}
|
|
4868
|
-
};
|
|
4869
|
-
return null;
|
|
4870
|
-
} catch (error) {
|
|
4871
|
-
logger.error("[JWT] Download token verification failed", { error });
|
|
4872
|
-
return null;
|
|
4873
|
-
}
|
|
5016
|
+
return processJwt.verifyDownloadToken(token);
|
|
4874
5017
|
}
|
|
4875
|
-
/**
|
|
4876
|
-
* Sign a short-lived token that carries state across a redirect.
|
|
4877
|
-
*
|
|
4878
|
-
* The generalisation of the MFA-pending token: a signed blob the server hands
|
|
4879
|
-
* out and expects back, where the point is that the holder cannot edit it. The
|
|
4880
|
-
* OAuth authorize flow needs one to carry the authorization request across the
|
|
4881
|
-
* sign-in hop — without it, the endpoint the consent page posts to would mint a
|
|
4882
|
-
* code for whatever `client_id` and `redirect_uri` its caller typed, skipping
|
|
4883
|
-
* every check the authorize endpoint performs.
|
|
4884
|
-
*
|
|
4885
|
-
* The `purpose` is not decoration. {@link verifyAccessToken} refuses any token
|
|
4886
|
-
* that has one, so nothing minted here can be replayed as a session, and
|
|
4887
|
-
* {@link verifyPurposeToken} refuses a token whose purpose is not the one the
|
|
4888
|
-
* caller named — so a token minted for one hop cannot be spent on another.
|
|
4889
|
-
*/
|
|
5018
|
+
/** See {@link JwtService.signPurposeToken}. Answers for the process default. */
|
|
4890
5019
|
async function signPurposeToken(purpose, claims, expiresInSeconds) {
|
|
4891
|
-
|
|
4892
|
-
return signJwt({
|
|
4893
|
-
...claims,
|
|
4894
|
-
purpose
|
|
4895
|
-
}, jwtConfig.secret, {
|
|
4896
|
-
expiresIn: expiresInSeconds,
|
|
4897
|
-
algorithm: "HS256"
|
|
4898
|
-
});
|
|
5020
|
+
return processJwt.signPurposeToken(purpose, claims, expiresInSeconds);
|
|
4899
5021
|
}
|
|
4900
|
-
/**
|
|
5022
|
+
/** See {@link JwtService.verifyPurposeToken}. Answers for the process default. */
|
|
4901
5023
|
async function verifyPurposeToken(purpose, token) {
|
|
4902
|
-
|
|
4903
|
-
if (!token) return null;
|
|
4904
|
-
try {
|
|
4905
|
-
const decoded = await verifyJwt(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4906
|
-
if (!decoded || decoded.purpose !== purpose) return null;
|
|
4907
|
-
return decoded;
|
|
4908
|
-
} catch (error) {
|
|
4909
|
-
logger.error("[JWT] Purpose token verification failed", {
|
|
4910
|
-
purpose,
|
|
4911
|
-
error
|
|
4912
|
-
});
|
|
4913
|
-
return null;
|
|
4914
|
-
}
|
|
5024
|
+
return processJwt.verifyPurposeToken(purpose, token);
|
|
4915
5025
|
}
|
|
4916
5026
|
/** The `purpose` that marks a token as belonging to the MCP resource. */
|
|
4917
5027
|
var MCP_ACCESS_PURPOSE = "mcp-access";
|
|
4918
|
-
/**
|
|
4919
|
-
* Mint an access token for the MCP endpoint.
|
|
4920
|
-
*
|
|
4921
|
-
* Deliberately **not** an ordinary session token, and the `purpose` claim is
|
|
4922
|
-
* what enforces that: {@link verifyAccessToken} refuses any token carrying one,
|
|
4923
|
-
* so a credential issued to a third-party MCP client — Claude, an IDE, anything
|
|
4924
|
-
* that completed the OAuth flow — cannot be replayed against `/api/data`,
|
|
4925
|
-
* `/api/admin` or the websocket. It buys exactly the one resource it names.
|
|
4926
|
-
*
|
|
4927
|
-
* That asymmetry matters more than it first looks. These tokens are handed to
|
|
4928
|
-
* software the user authorized but the operator never vetted, they live in that
|
|
4929
|
-
* software's storage, and they are the only credential in this system minted
|
|
4930
|
-
* for a third party. Without the purpose separation, "connect Claude to your
|
|
4931
|
-
* data" would silently mean "give Claude a full session".
|
|
4932
|
-
*
|
|
4933
|
-
* The audience is the canonical resource URI, and {@link verifyMcpAccessToken}
|
|
4934
|
-
* requires the caller to say which resource it is checking for. A token minted
|
|
4935
|
-
* for another Rebase project — or another path on this one — fails there.
|
|
4936
|
-
*
|
|
4937
|
-
* HS256 with the shared secret, like the download token: the authorization
|
|
4938
|
-
* server and the resource server are the same process, so nothing outside it
|
|
4939
|
-
* ever needs to verify one, and an asymmetric key here would be a second key
|
|
4940
|
-
* surface bought for nobody.
|
|
4941
|
-
*/
|
|
5028
|
+
/** See {@link JwtService.generateMcpAccessToken}. Answers for the process default. */
|
|
4942
5029
|
async function generateMcpAccessToken(payload, expiresInSeconds) {
|
|
4943
|
-
|
|
4944
|
-
return signJwt({
|
|
4945
|
-
purpose: MCP_ACCESS_PURPOSE,
|
|
4946
|
-
uid: payload.uid,
|
|
4947
|
-
roles: payload.roles,
|
|
4948
|
-
scope: payload.scope,
|
|
4949
|
-
clientId: payload.clientId,
|
|
4950
|
-
aud: payload.aud,
|
|
4951
|
-
iss: payload.iss
|
|
4952
|
-
}, jwtConfig.secret, {
|
|
4953
|
-
expiresIn: expiresInSeconds,
|
|
4954
|
-
algorithm: "HS256"
|
|
4955
|
-
});
|
|
5030
|
+
return processJwt.generateMcpAccessToken(payload, expiresInSeconds);
|
|
4956
5031
|
}
|
|
4957
|
-
/**
|
|
4958
|
-
* Verify an MCP access token **for a specific resource**.
|
|
4959
|
-
*
|
|
4960
|
-
* `expectedAudience` has no default. The MCP specification requires a resource
|
|
4961
|
-
* server to validate that a token was issued for it specifically, and a default
|
|
4962
|
-
* would be the thing that quietly turns that check off — the caller has to say
|
|
4963
|
-
* what it is, every time, so a new call site cannot forget by omission.
|
|
4964
|
-
*/
|
|
5032
|
+
/** See {@link JwtService.verifyMcpAccessToken}. Answers for the process default. */
|
|
4965
5033
|
async function verifyMcpAccessToken(token, expectedAudience) {
|
|
4966
|
-
|
|
4967
|
-
try {
|
|
4968
|
-
const decoded = await verifyJwt(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4969
|
-
if (!decoded || decoded.purpose !== "mcp-access") return null;
|
|
4970
|
-
if (typeof decoded.uid !== "string" || !decoded.uid) return null;
|
|
4971
|
-
if (typeof decoded.aud !== "string" || decoded.aud !== expectedAudience) {
|
|
4972
|
-
logger.error("[JWT] MCP token rejected: audience mismatch", {
|
|
4973
|
-
expected: expectedAudience,
|
|
4974
|
-
got: decoded.aud
|
|
4975
|
-
});
|
|
4976
|
-
return null;
|
|
4977
|
-
}
|
|
4978
|
-
return {
|
|
4979
|
-
purpose: MCP_ACCESS_PURPOSE,
|
|
4980
|
-
uid: decoded.uid,
|
|
4981
|
-
roles: Array.isArray(decoded.roles) ? decoded.roles.map(String) : [],
|
|
4982
|
-
scope: typeof decoded.scope === "string" ? decoded.scope : "",
|
|
4983
|
-
clientId: typeof decoded.clientId === "string" ? decoded.clientId : "",
|
|
4984
|
-
aud: decoded.aud,
|
|
4985
|
-
iss: typeof decoded.iss === "string" ? decoded.iss : ""
|
|
4986
|
-
};
|
|
4987
|
-
} catch (error) {
|
|
4988
|
-
logger.error("[JWT] MCP token verification failed", { error });
|
|
4989
|
-
return null;
|
|
4990
|
-
}
|
|
5034
|
+
return processJwt.verifyMcpAccessToken(token, expectedAudience);
|
|
4991
5035
|
}
|
|
5036
|
+
/**
|
|
5037
|
+
* The process default as a service object that follows it — every call reads
|
|
5038
|
+
* whatever {@link configureJwt} or `setDefaultBackend` installed last. What a
|
|
5039
|
+
* route built outside any backend falls back to.
|
|
5040
|
+
*/
|
|
5041
|
+
var processJwtService = {
|
|
5042
|
+
get accessExpiresIn() {
|
|
5043
|
+
return processJwt.accessExpiresIn;
|
|
5044
|
+
},
|
|
5045
|
+
get refreshExpiresIn() {
|
|
5046
|
+
return processJwt.refreshExpiresIn;
|
|
5047
|
+
},
|
|
5048
|
+
isConfigured: isJwtConfigured,
|
|
5049
|
+
hasAsymmetricSigningKey,
|
|
5050
|
+
getJwks,
|
|
5051
|
+
hmacSecret: configuredJwtSecret,
|
|
5052
|
+
generateAccessToken,
|
|
5053
|
+
verifyAccessToken,
|
|
5054
|
+
getAccessTokenExpiryMs,
|
|
5055
|
+
getAccessTokenExpiry,
|
|
5056
|
+
getRefreshTokenTtlMs,
|
|
5057
|
+
getRefreshTokenExpiry,
|
|
5058
|
+
generateMfaPendingToken,
|
|
5059
|
+
verifyMfaPendingToken,
|
|
5060
|
+
generateDownloadToken,
|
|
5061
|
+
verifyDownloadToken,
|
|
5062
|
+
signPurposeToken,
|
|
5063
|
+
verifyPurposeToken,
|
|
5064
|
+
generateMcpAccessToken,
|
|
5065
|
+
verifyMcpAccessToken
|
|
5066
|
+
};
|
|
4992
5067
|
//#endregion
|
|
4993
|
-
export {
|
|
5068
|
+
export { randomHex as C, sha256Hex as E, constantTimeEqual as S, sha256Bytes as T, verifyAccessToken as _, generateAccessToken as a, normalizePemFromEnv as b, getAccessTokenExpiry as c, hasAsymmetricSigningKey as d, hashRefreshToken as f, setDefaultJwtService as g, processJwtService as h, customClaimsOf as i, getJwks as l, jwtOf as m, configureJwt as n, generateDownloadToken as o, isJwtConfigured as p, createJwtService as r, generateRefreshToken as s, MAX_COOKIE_AGE_MS as t, getRefreshTokenExpiry as u, verifyDownloadToken as v, randomInt as w, require_jsonwebtoken as x, verifyJwt as y };
|
|
4994
5069
|
|
|
4995
|
-
//# sourceMappingURL=jwt-
|
|
5070
|
+
//# sourceMappingURL=jwt-BlvuKoR6.js.map
|