@open-mercato/shared 0.7.0 → 0.7.1-develop.7102.1.b41f7e3e51
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -1
- package/dist/lib/auth/jwt.js +6 -0
- package/dist/lib/auth/jwt.js.map +2 -2
- package/dist/lib/auth/mfaPendingAccess.js +42 -0
- package/dist/lib/auth/mfaPendingAccess.js.map +7 -0
- package/dist/lib/auth/organizationAccess.js +7 -4
- package/dist/lib/auth/organizationAccess.js.map +2 -2
- package/dist/lib/auth/principal-service.js +1 -0
- package/dist/lib/auth/principal-service.js.map +7 -0
- package/dist/lib/auth/server.js +38 -7
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/commands/command-bus.js +6 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/crud/factory.js +24 -8
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/data/engine.js +8 -2
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/html/htmlToPlainText.js +16 -0
- package/dist/lib/html/htmlToPlainText.js.map +7 -0
- package/dist/lib/location/countries.js +12 -0
- package/dist/lib/location/countries.js.map +2 -2
- package/dist/lib/openapi/crud.js +4 -1
- package/dist/lib/openapi/crud.js.map +2 -2
- package/dist/lib/query/count-cap.js +11 -0
- package/dist/lib/query/count-cap.js.map +7 -0
- package/dist/lib/query/engine.js +270 -34
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/queue/dispatchOrigin.js +20 -0
- package/dist/lib/queue/dispatchOrigin.js.map +7 -0
- package/dist/lib/search/config.js +1 -0
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/entityAccess.js +44 -0
- package/dist/lib/search/entityAccess.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +69 -15
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/registry.js +15 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/component-registry.js.map +2 -2
- package/package.json +10 -3
- package/src/lib/auth/__tests__/jwt.test.ts +13 -0
- package/src/lib/auth/__tests__/mfaPendingAccess.test.ts +69 -0
- package/src/lib/auth/__tests__/organizationAccess.test.ts +36 -1
- package/src/lib/auth/__tests__/principalServiceExport.test.ts +67 -0
- package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +324 -0
- package/src/lib/auth/__tests__/server.test.ts +104 -0
- package/src/lib/auth/jwt.ts +17 -0
- package/src/lib/auth/mfaPendingAccess.ts +70 -0
- package/src/lib/auth/organizationAccess.ts +11 -3
- package/src/lib/auth/principal-service.ts +110 -0
- package/src/lib/auth/server.ts +78 -8
- package/src/lib/commands/__tests__/command-bus.test.ts +31 -0
- package/src/lib/commands/command-bus.ts +8 -1
- package/src/lib/crud/__tests__/crud-factory.test.ts +165 -0
- package/src/lib/crud/factory.ts +33 -7
- package/src/lib/data/__tests__/engine.event-validation.test.ts +9 -1
- package/src/lib/data/engine.ts +7 -1
- package/src/lib/html/__tests__/htmlToPlainText.test.ts +59 -0
- package/src/lib/html/htmlToPlainText.ts +17 -0
- package/src/lib/location/__tests__/countries.test.ts +15 -0
- package/src/lib/location/countries.ts +17 -0
- package/src/lib/openapi/crud.ts +3 -0
- package/src/lib/query/__tests__/count-cap-plan.test.ts +240 -0
- package/src/lib/query/__tests__/count-cap.test.ts +41 -0
- package/src/lib/query/__tests__/engine.count-distinct.test.ts +162 -15
- package/src/lib/query/__tests__/engine.scope-and-or.test.ts +11 -1
- package/src/lib/query/__tests__/engine.test.ts +445 -7
- package/src/lib/query/count-cap.ts +19 -0
- package/src/lib/query/engine.ts +434 -54
- package/src/lib/query/types.ts +15 -0
- package/src/lib/queue/dispatchOrigin.ts +35 -0
- package/src/lib/search/config.ts +10 -0
- package/src/lib/search/entityAccess.ts +132 -0
- package/src/modules/events/__tests__/factory.test.ts +88 -0
- package/src/modules/events/factory.ts +111 -19
- package/src/modules/events/types.ts +17 -0
- package/src/modules/registry.ts +40 -0
- package/src/modules/widgets/component-registry.ts +14 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 267 entry points
|
|
2
2
|
[build:shared] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -123,7 +123,10 @@ index stores hashes of the plaintext, so it keeps matching. Issue #2990.
|
|
|
123
123
|
predicate in that case.
|
|
124
124
|
- `matched: true` with `ids: []` is a real empty result.
|
|
125
125
|
- Queries that go through the query engine get this routing automatically; raw
|
|
126
|
-
`em.find` / Kysely list routes must wire it themselves.
|
|
126
|
+
`em.find` / Kysely list routes must wire it themselves. One carve-out: with
|
|
127
|
+
`OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS=true` (default false), a base-column
|
|
128
|
+
`like`/`ilike` on a **plaintext** column runs as exact SQL ILIKE instead of the token
|
|
129
|
+
rewrite — encrypted columns keep the token path either way. When the fallback would run
|
|
127
130
|
`ILIKE` against an encrypted column, both query engines now log a warning
|
|
128
131
|
(`lib/query/ciphertext-search-warning`) instead of degrading silently.
|
|
129
132
|
- The `…WithDecryption` helpers log the same warning outside production when the `where`
|
package/dist/lib/auth/jwt.js
CHANGED
|
@@ -247,9 +247,15 @@ function signAudienceJwt(audience, payload, expiresInSec = 60 * 60 * 8) {
|
|
|
247
247
|
function verifyAudienceJwt(audience, token) {
|
|
248
248
|
return verifyJwt(token, { audience });
|
|
249
249
|
}
|
|
250
|
+
function isMfaPendingJwtPayload(payload) {
|
|
251
|
+
if (!payload || typeof payload !== "object") return false;
|
|
252
|
+
const record = payload;
|
|
253
|
+
return record.mfa_pending === true && record.mfa_verified !== true;
|
|
254
|
+
}
|
|
250
255
|
export {
|
|
251
256
|
assertJwtSecretPolicy,
|
|
252
257
|
deriveJwtAudienceSecret,
|
|
258
|
+
isMfaPendingJwtPayload,
|
|
253
259
|
signAudienceJwt,
|
|
254
260
|
signJwt,
|
|
255
261
|
verifyAudienceJwt,
|
package/dist/lib/auth/jwt.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/auth/jwt.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport { createLogger } from '../logger'\nimport { parseNumberWithDefault } from '../number'\n\nconst logger = createLogger('auth').child({ component: 'jwt' })\n\nfunction base64url(input: Buffer | string) {\n return (typeof input === 'string' ? Buffer.from(input) : input)\n .toString('base64')\n .replace(/=/g, '')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n}\n\nexport type JwtPayload = Record<string, any>\n\nexport type JwtAudience = 'staff' | 'customer' | (string & {})\n\nexport type SignJwtOptions = {\n secret?: string\n expiresInSec?: number\n audience?: string\n issuer?: string\n}\n\nexport type VerifyJwtOptions = {\n secret?: string\n audience?: string\n issuer?: string\n}\n\nconst DEFAULT_ISSUER = 'open-mercato'\nconst DEFAULT_STAFF_AUDIENCE: JwtAudience = 'staff'\nconst AUDIENCE_SECRET_LABEL = 'open-mercato:jwt:v1'\n\nconst LEGACY_GRACE_DEFAULT_MINUTES = 480\nconst LEGACY_TOKEN_CLOCK_SKEW_SECONDS = 60\n\n/**\n * How long after a token was issued (`iat`) the raw-`JWT_SECRET` fallback in `verifyJwt` keeps\n * accepting it. The fallback exists so a rolling deployment of the audience-derived signing\n * scheme does not force-log-out every user; it is a migration window, not a permanent mode.\n *\n * Set via `JWT_LEGACY_GRACE_MINUTES`. Defaults to 480 (8 hours \u2014 one full token TTL).\n * `0`, `false` or `off` disables the fallback entirely (hard cutover). Values that do not parse\n * fall back to the default rather than silently disabling authentication.\n */\nfunction getLegacyGraceMinutes(): number {\n const raw = process.env.JWT_LEGACY_GRACE_MINUTES\n const normalized = typeof raw === 'string' ? raw.trim().toLowerCase() : raw\n if (normalized === 'false' || normalized === 'off') return 0\n return parseNumberWithDefault(normalized, LEGACY_GRACE_DEFAULT_MINUTES, { min: 0, integer: true })\n}\n\n/**\n * Required absolute deadline for the legacy fallback, as an ISO-8601 instant in\n * `JWT_LEGACY_CUTOVER_AT`. Without a valid deadline the fallback stays disabled: token `iat` is\n * attacker-controlled by anyone who knows the former raw secret, so a relative age check alone\n * cannot make the migration window finite.\n */\nfunction getLegacyCutoverEpochSeconds(): number | null {\n const raw = process.env.JWT_LEGACY_CUTOVER_AT\n if (!raw || !raw.trim()) return null\n const parsed = Date.parse(raw.trim())\n if (Number.isNaN(parsed)) {\n warnOnce(\n 'jwt-legacy-cutover-unparseable',\n 'JWT_LEGACY_CUTOVER_AT is not a valid ISO-8601 instant \u2014 legacy JWT fallback remains disabled.',\n )\n return null\n }\n return Math.floor(parsed / 1000)\n}\n\nconst MIN_SECRET_LENGTH = 32\n\n/**\n * Signing secrets that ship in this repository's own examples, compose files, and docs. A\n * deployment reaching production with one of these is not \"weakly configured\" \u2014 it is publicly\n * forgeable by anyone who has read the repository.\n */\nconst PLACEHOLDER_SECRETS = new Set([\n 'jwt',\n 'jwt-secret',\n 'jwtsecret',\n 'secret',\n 'password',\n 'changeme',\n 'change-me',\n 'change-me-dev-secret',\n 'change-me-dev-auth-secret',\n 'your-strong-jwt-secret',\n 'your-secure-jwt-secret-change-me',\n 'dev',\n 'development',\n 'test',\n])\n\nexport type JwtSecretViolation = 'missing' | 'placeholder' | 'too_short'\n\nconst warnedKeys = new Set<string>()\n\nfunction warnOnce(key: string, message: string): void {\n if (warnedKeys.has(key)) return\n warnedKeys.add(key)\n logger.warn(message)\n}\n\nfunction isProduction(): boolean {\n return process.env.NODE_ENV === 'production'\n}\n\nfunction inspectSecret(secret: string | undefined | null): JwtSecretViolation | null {\n const value = typeof secret === 'string' ? secret.trim() : ''\n if (!value) return 'missing'\n if (PLACEHOLDER_SECRETS.has(value.toLowerCase())) return 'placeholder'\n if (value.length < MIN_SECRET_LENGTH) return 'too_short'\n return null\n}\n\nfunction describeViolation(name: string, violation: JwtSecretViolation): string {\n switch (violation) {\n case 'missing':\n return `${name} is not set. Generate one with \\`openssl rand -hex 32\\`.`\n case 'placeholder':\n return `${name} is set to a placeholder value published in this repository's examples, so anyone can forge tokens for this deployment. Generate a real one with \\`openssl rand -hex 32\\`.`\n case 'too_short':\n return `${name} is shorter than ${MIN_SECRET_LENGTH} characters. Generate a stronger one with \\`openssl rand -hex 32\\`.`\n }\n}\n\n/**\n * Fail closed in production, warn in every other environment. Called on every secret read so\n * worker, scheduler, and CLI processes \u2014 which never run the app's startup hook \u2014 are covered\n * too. `assertJwtSecretPolicy` runs the same check eagerly at server startup.\n */\nfunction enforceSecretPolicy(name: string, secret: string | undefined | null): void {\n const violation = inspectSecret(secret)\n if (!violation) return\n const message = describeViolation(name, violation)\n if (isProduction()) {\n throw new Error(`[auth.jwt] Refusing to run in production with an unsafe signing secret: ${message}`)\n }\n warnOnce(`secret-policy:${name}:${violation}`, `${message} This is tolerated outside production only.`)\n}\n\n/**\n * Validate every JWT signing secret this process would use. Call it once at startup so a\n * misconfigured production deployment fails immediately and loudly instead of at the first login\n * attempt. Throws in production; logs a warning elsewhere.\n */\nexport function assertJwtSecretPolicy(): void {\n enforceSecretPolicy('JWT_SECRET', process.env.JWT_SECRET)\n for (const [key, value] of Object.entries(process.env)) {\n if (!/^JWT_[A-Z0-9]+(?:_[A-Z0-9]+)*_SECRET$/.test(key)) continue\n enforceSecretPolicy(key, value)\n }\n}\n\nfunction readBaseSecret(explicit?: string): string {\n const secret = explicit ?? process.env.JWT_SECRET\n if (!secret) throw new Error('JWT_SECRET is not set')\n if (explicit === undefined) enforceSecretPolicy('JWT_SECRET', secret)\n return secret\n}\n\nfunction normalizeAudience(audience: string): string {\n return audience.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')\n}\n\nconst derivedSecretCache = new Map<string, string>()\n\nfunction deriveAudienceSecretFromBase(normalized: string, base: string): string {\n const cacheKey = `${normalized}::${base}`\n const cached = derivedSecretCache.get(cacheKey)\n if (cached !== undefined) return cached\n const label = `${AUDIENCE_SECRET_LABEL}:${normalized}`\n const derived = crypto.createHmac('sha256', base).update(label).digest('hex')\n derivedSecretCache.set(cacheKey, derived)\n return derived\n}\n\n/**\n * Derive a per-audience signing key from the base `JWT_SECRET`.\n *\n * - If `JWT_${AUDIENCE}_SECRET` env var is set, it is used verbatim (allows operators to rotate a\n * single audience independently).\n * - Otherwise, the key is derived deterministically via HMAC-SHA256 from the base secret using a\n * versioned label. This ensures that a staff JWT signature cannot verify against the customer\n * key (and vice versa) even though both share the same base `JWT_SECRET`.\n * - Derived values are memoized per `(audience, baseSecret)` for the process lifetime.\n */\nexport function deriveJwtAudienceSecret(audience: string, baseSecret?: string): string {\n const normalized = normalizeAudience(audience)\n if (!normalized) throw new Error('Audience is required to derive a JWT secret')\n const overrideName = `JWT_${normalized.toUpperCase()}_SECRET`\n const override = process.env[overrideName]\n if (override && override.trim().length > 0) {\n enforceSecretPolicy(overrideName, override)\n return override\n }\n const base = readBaseSecret(baseSecret)\n return deriveAudienceSecretFromBase(normalized, base)\n}\n\nfunction isSignOptions(value: string | SignJwtOptions | undefined): value is SignJwtOptions {\n return typeof value === 'object' && value !== null\n}\n\nfunction isVerifyOptions(value: string | VerifyJwtOptions | undefined): value is VerifyJwtOptions {\n return typeof value === 'object' && value !== null\n}\n\nfunction toSignOptions(secretOrOptions?: string | SignJwtOptions, expiresInSec?: number): { secret: string; expiresInSec: number; audience?: string; issuer?: string } {\n if (isSignOptions(secretOrOptions)) {\n const audience = secretOrOptions.audience ?? DEFAULT_STAFF_AUDIENCE\n const secret = secretOrOptions.secret ?? deriveJwtAudienceSecret(audience)\n if (!secret) throw new Error('JWT_SECRET is not set')\n return {\n secret,\n expiresInSec: secretOrOptions.expiresInSec ?? 60 * 60 * 8,\n audience,\n issuer: secretOrOptions.issuer ?? DEFAULT_ISSUER,\n }\n }\n if (typeof secretOrOptions === 'string') {\n // Legacy: explicit raw secret supplied by caller \u2014 keep audience/issuer off by default so\n // existing tests and callers that BYO secret see unchanged behavior.\n if (!secretOrOptions) throw new Error('JWT_SECRET is not set')\n return {\n secret: secretOrOptions,\n expiresInSec: expiresInSec ?? 60 * 60 * 8,\n }\n }\n // Default path: staff-audience derived secret + iss/aud claims.\n return {\n secret: deriveJwtAudienceSecret(DEFAULT_STAFF_AUDIENCE),\n expiresInSec: expiresInSec ?? 60 * 60 * 8,\n audience: DEFAULT_STAFF_AUDIENCE,\n issuer: DEFAULT_ISSUER,\n }\n}\n\nfunction toVerifyOptions(secretOrOptions?: string | VerifyJwtOptions): { secret: string; audience?: string; issuer?: string } {\n if (isVerifyOptions(secretOrOptions)) {\n const audience = secretOrOptions.audience ?? DEFAULT_STAFF_AUDIENCE\n const secret = secretOrOptions.secret ?? deriveJwtAudienceSecret(audience)\n if (!secret) throw new Error('JWT_SECRET is not set')\n return {\n secret,\n audience,\n issuer: secretOrOptions.issuer ?? DEFAULT_ISSUER,\n }\n }\n if (typeof secretOrOptions === 'string') {\n if (!secretOrOptions) throw new Error('JWT_SECRET is not set')\n // Legacy explicit secret: no audience/issuer enforcement.\n return { secret: secretOrOptions }\n }\n return {\n secret: deriveJwtAudienceSecret(DEFAULT_STAFF_AUDIENCE),\n audience: DEFAULT_STAFF_AUDIENCE,\n issuer: DEFAULT_ISSUER,\n }\n}\n\nexport function signJwt(\n payload: JwtPayload,\n secretOrOptions?: string | SignJwtOptions,\n expiresInSec?: number,\n) {\n const options = toSignOptions(secretOrOptions, expiresInSec)\n const header = { alg: 'HS256', typ: 'JWT' }\n const now = Math.floor(Date.now() / 1000)\n const body: JwtPayload = { iat: now, exp: now + options.expiresInSec, ...payload }\n if (options.issuer && body.iss === undefined) body.iss = options.issuer\n if (options.audience && body.aud === undefined) body.aud = options.audience\n const encHeader = base64url(JSON.stringify(header))\n const encBody = base64url(JSON.stringify(body))\n const data = `${encHeader}.${encBody}`\n const sig = crypto.createHmac('sha256', options.secret).update(data).digest()\n const encSig = base64url(sig)\n return `${data}.${encSig}`\n}\n\nfunction verifyWithOptions(token: string, options: { secret: string; audience?: string; issuer?: string }): JwtPayload | null {\n const parts = token.split('.')\n if (parts.length !== 3) return null\n const [h, p, s] = parts\n const data = `${h}.${p}`\n const expected = base64url(crypto.createHmac('sha256', options.secret).update(data).digest())\n const providedSignature = Buffer.from(s)\n const expectedSignature = Buffer.from(expected)\n if (providedSignature.length !== expectedSignature.length) return null\n if (!crypto.timingSafeEqual(providedSignature, expectedSignature)) return null\n let payload: JwtPayload\n try {\n payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))\n } catch {\n return null\n }\n const now = Math.floor(Date.now() / 1000)\n if (payload.exp && now > payload.exp) return null\n if (options.audience !== undefined) {\n if (payload.aud !== options.audience) return null\n }\n if (options.issuer !== undefined) {\n if (payload.iss !== options.issuer) return null\n }\n return payload\n}\n\n/**\n * Whether a raw-secret token is still inside the migration window. A token is only ever legacy\n * for a bounded period after it was issued, so `iat` is mandatory: a token that cannot prove its\n * age cannot prove it is inside the window either, and accepting it would make the window\n * unbounded \u2014 which is exactly the defect this guard closes.\n */\nfunction isWithinLegacyWindow(payload: JwtPayload, graceMinutes: number): boolean {\n const cutoverAt = getLegacyCutoverEpochSeconds()\n const now = Math.floor(Date.now() / 1000)\n if (cutoverAt === null || now >= cutoverAt) return false\n const issuedAt = payload.iat\n if (typeof issuedAt !== 'number' || !Number.isFinite(issuedAt)) return false\n if (issuedAt > now + LEGACY_TOKEN_CLOCK_SKEW_SECONDS) return false\n return now - issuedAt <= graceMinutes * 60\n}\n\nexport function verifyJwt(token: string, secretOrOptions?: string | VerifyJwtOptions) {\n const options = toVerifyOptions(secretOrOptions)\n const result = verifyWithOptions(token, options)\n if (result) {\n // `_legacyToken` is assigned by this function alone. Strip any same-named claim carried in\n // the token body so a payload can never talk callers (staff session integrity, portal auth)\n // into treating a modern, session-bound token as a sessionless legacy one.\n if (result._legacyToken !== undefined) delete result._legacyToken\n return result\n }\n\n // Legacy fallback: when the caller used the default path (no explicit secret) and the new\n // audience-derived verification failed, try verifying with the raw JWT_SECRET. This keeps\n // pre-migration tokens working across a rolling deployment \u2014 but only until the token's own\n // `iat` leaves the configured grace window, or the configured cutover instant passes.\n if (secretOrOptions === undefined) {\n const graceMinutes = getLegacyGraceMinutes()\n const rawSecret = process.env.JWT_SECRET\n if (graceMinutes > 0 && rawSecret) {\n const legacyResult = verifyWithOptions(token, { secret: rawSecret })\n if (legacyResult && isWithinLegacyWindow(legacyResult, graceMinutes)) {\n legacyResult._legacyToken = true\n return legacyResult\n }\n }\n }\n\n return null\n}\n\n/**\n * Sign a JWT for a specific audience using an audience-derived signing key. The resulting token\n * carries `iss` and `aud` claims and cannot be verified with the base `JWT_SECRET` directly \u2014\n * callers must use `verifyAudienceJwt` with the same audience.\n */\nexport function signAudienceJwt(\n audience: string,\n payload: JwtPayload,\n expiresInSec: number = 60 * 60 * 8,\n): string {\n return signJwt(payload, { audience, expiresInSec })\n}\n\n/**\n * Verify a JWT that was signed with an audience-scoped secret. Rejects tokens that are missing\n * or carry a mismatched `aud`/`iss` claim, so a staff JWT cannot be replayed against the\n * customer portal (and vice versa) even when the base `JWT_SECRET` is shared.\n */\nexport function verifyAudienceJwt(audience: string, token: string): JwtPayload | null {\n return verifyJwt(token, { audience })\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport { createLogger } from '../logger'\nimport { parseNumberWithDefault } from '../number'\n\nconst logger = createLogger('auth').child({ component: 'jwt' })\n\nfunction base64url(input: Buffer | string) {\n return (typeof input === 'string' ? Buffer.from(input) : input)\n .toString('base64')\n .replace(/=/g, '')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n}\n\nexport type JwtPayload = Record<string, any>\n\nexport type JwtAudience = 'staff' | 'customer' | (string & {})\n\nexport type SignJwtOptions = {\n secret?: string\n expiresInSec?: number\n audience?: string\n issuer?: string\n}\n\nexport type VerifyJwtOptions = {\n secret?: string\n audience?: string\n issuer?: string\n}\n\nconst DEFAULT_ISSUER = 'open-mercato'\nconst DEFAULT_STAFF_AUDIENCE: JwtAudience = 'staff'\nconst AUDIENCE_SECRET_LABEL = 'open-mercato:jwt:v1'\n\nconst LEGACY_GRACE_DEFAULT_MINUTES = 480\nconst LEGACY_TOKEN_CLOCK_SKEW_SECONDS = 60\n\n/**\n * How long after a token was issued (`iat`) the raw-`JWT_SECRET` fallback in `verifyJwt` keeps\n * accepting it. The fallback exists so a rolling deployment of the audience-derived signing\n * scheme does not force-log-out every user; it is a migration window, not a permanent mode.\n *\n * Set via `JWT_LEGACY_GRACE_MINUTES`. Defaults to 480 (8 hours \u2014 one full token TTL).\n * `0`, `false` or `off` disables the fallback entirely (hard cutover). Values that do not parse\n * fall back to the default rather than silently disabling authentication.\n */\nfunction getLegacyGraceMinutes(): number {\n const raw = process.env.JWT_LEGACY_GRACE_MINUTES\n const normalized = typeof raw === 'string' ? raw.trim().toLowerCase() : raw\n if (normalized === 'false' || normalized === 'off') return 0\n return parseNumberWithDefault(normalized, LEGACY_GRACE_DEFAULT_MINUTES, { min: 0, integer: true })\n}\n\n/**\n * Required absolute deadline for the legacy fallback, as an ISO-8601 instant in\n * `JWT_LEGACY_CUTOVER_AT`. Without a valid deadline the fallback stays disabled: token `iat` is\n * attacker-controlled by anyone who knows the former raw secret, so a relative age check alone\n * cannot make the migration window finite.\n */\nfunction getLegacyCutoverEpochSeconds(): number | null {\n const raw = process.env.JWT_LEGACY_CUTOVER_AT\n if (!raw || !raw.trim()) return null\n const parsed = Date.parse(raw.trim())\n if (Number.isNaN(parsed)) {\n warnOnce(\n 'jwt-legacy-cutover-unparseable',\n 'JWT_LEGACY_CUTOVER_AT is not a valid ISO-8601 instant \u2014 legacy JWT fallback remains disabled.',\n )\n return null\n }\n return Math.floor(parsed / 1000)\n}\n\nconst MIN_SECRET_LENGTH = 32\n\n/**\n * Signing secrets that ship in this repository's own examples, compose files, and docs. A\n * deployment reaching production with one of these is not \"weakly configured\" \u2014 it is publicly\n * forgeable by anyone who has read the repository.\n */\nconst PLACEHOLDER_SECRETS = new Set([\n 'jwt',\n 'jwt-secret',\n 'jwtsecret',\n 'secret',\n 'password',\n 'changeme',\n 'change-me',\n 'change-me-dev-secret',\n 'change-me-dev-auth-secret',\n 'your-strong-jwt-secret',\n 'your-secure-jwt-secret-change-me',\n 'dev',\n 'development',\n 'test',\n])\n\nexport type JwtSecretViolation = 'missing' | 'placeholder' | 'too_short'\n\nconst warnedKeys = new Set<string>()\n\nfunction warnOnce(key: string, message: string): void {\n if (warnedKeys.has(key)) return\n warnedKeys.add(key)\n logger.warn(message)\n}\n\nfunction isProduction(): boolean {\n return process.env.NODE_ENV === 'production'\n}\n\nfunction inspectSecret(secret: string | undefined | null): JwtSecretViolation | null {\n const value = typeof secret === 'string' ? secret.trim() : ''\n if (!value) return 'missing'\n if (PLACEHOLDER_SECRETS.has(value.toLowerCase())) return 'placeholder'\n if (value.length < MIN_SECRET_LENGTH) return 'too_short'\n return null\n}\n\nfunction describeViolation(name: string, violation: JwtSecretViolation): string {\n switch (violation) {\n case 'missing':\n return `${name} is not set. Generate one with \\`openssl rand -hex 32\\`.`\n case 'placeholder':\n return `${name} is set to a placeholder value published in this repository's examples, so anyone can forge tokens for this deployment. Generate a real one with \\`openssl rand -hex 32\\`.`\n case 'too_short':\n return `${name} is shorter than ${MIN_SECRET_LENGTH} characters. Generate a stronger one with \\`openssl rand -hex 32\\`.`\n }\n}\n\n/**\n * Fail closed in production, warn in every other environment. Called on every secret read so\n * worker, scheduler, and CLI processes \u2014 which never run the app's startup hook \u2014 are covered\n * too. `assertJwtSecretPolicy` runs the same check eagerly at server startup.\n */\nfunction enforceSecretPolicy(name: string, secret: string | undefined | null): void {\n const violation = inspectSecret(secret)\n if (!violation) return\n const message = describeViolation(name, violation)\n if (isProduction()) {\n throw new Error(`[auth.jwt] Refusing to run in production with an unsafe signing secret: ${message}`)\n }\n warnOnce(`secret-policy:${name}:${violation}`, `${message} This is tolerated outside production only.`)\n}\n\n/**\n * Validate every JWT signing secret this process would use. Call it once at startup so a\n * misconfigured production deployment fails immediately and loudly instead of at the first login\n * attempt. Throws in production; logs a warning elsewhere.\n */\nexport function assertJwtSecretPolicy(): void {\n enforceSecretPolicy('JWT_SECRET', process.env.JWT_SECRET)\n for (const [key, value] of Object.entries(process.env)) {\n if (!/^JWT_[A-Z0-9]+(?:_[A-Z0-9]+)*_SECRET$/.test(key)) continue\n enforceSecretPolicy(key, value)\n }\n}\n\nfunction readBaseSecret(explicit?: string): string {\n const secret = explicit ?? process.env.JWT_SECRET\n if (!secret) throw new Error('JWT_SECRET is not set')\n if (explicit === undefined) enforceSecretPolicy('JWT_SECRET', secret)\n return secret\n}\n\nfunction normalizeAudience(audience: string): string {\n return audience.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')\n}\n\nconst derivedSecretCache = new Map<string, string>()\n\nfunction deriveAudienceSecretFromBase(normalized: string, base: string): string {\n const cacheKey = `${normalized}::${base}`\n const cached = derivedSecretCache.get(cacheKey)\n if (cached !== undefined) return cached\n const label = `${AUDIENCE_SECRET_LABEL}:${normalized}`\n const derived = crypto.createHmac('sha256', base).update(label).digest('hex')\n derivedSecretCache.set(cacheKey, derived)\n return derived\n}\n\n/**\n * Derive a per-audience signing key from the base `JWT_SECRET`.\n *\n * - If `JWT_${AUDIENCE}_SECRET` env var is set, it is used verbatim (allows operators to rotate a\n * single audience independently).\n * - Otherwise, the key is derived deterministically via HMAC-SHA256 from the base secret using a\n * versioned label. This ensures that a staff JWT signature cannot verify against the customer\n * key (and vice versa) even though both share the same base `JWT_SECRET`.\n * - Derived values are memoized per `(audience, baseSecret)` for the process lifetime.\n */\nexport function deriveJwtAudienceSecret(audience: string, baseSecret?: string): string {\n const normalized = normalizeAudience(audience)\n if (!normalized) throw new Error('Audience is required to derive a JWT secret')\n const overrideName = `JWT_${normalized.toUpperCase()}_SECRET`\n const override = process.env[overrideName]\n if (override && override.trim().length > 0) {\n enforceSecretPolicy(overrideName, override)\n return override\n }\n const base = readBaseSecret(baseSecret)\n return deriveAudienceSecretFromBase(normalized, base)\n}\n\nfunction isSignOptions(value: string | SignJwtOptions | undefined): value is SignJwtOptions {\n return typeof value === 'object' && value !== null\n}\n\nfunction isVerifyOptions(value: string | VerifyJwtOptions | undefined): value is VerifyJwtOptions {\n return typeof value === 'object' && value !== null\n}\n\nfunction toSignOptions(secretOrOptions?: string | SignJwtOptions, expiresInSec?: number): { secret: string; expiresInSec: number; audience?: string; issuer?: string } {\n if (isSignOptions(secretOrOptions)) {\n const audience = secretOrOptions.audience ?? DEFAULT_STAFF_AUDIENCE\n const secret = secretOrOptions.secret ?? deriveJwtAudienceSecret(audience)\n if (!secret) throw new Error('JWT_SECRET is not set')\n return {\n secret,\n expiresInSec: secretOrOptions.expiresInSec ?? 60 * 60 * 8,\n audience,\n issuer: secretOrOptions.issuer ?? DEFAULT_ISSUER,\n }\n }\n if (typeof secretOrOptions === 'string') {\n // Legacy: explicit raw secret supplied by caller \u2014 keep audience/issuer off by default so\n // existing tests and callers that BYO secret see unchanged behavior.\n if (!secretOrOptions) throw new Error('JWT_SECRET is not set')\n return {\n secret: secretOrOptions,\n expiresInSec: expiresInSec ?? 60 * 60 * 8,\n }\n }\n // Default path: staff-audience derived secret + iss/aud claims.\n return {\n secret: deriveJwtAudienceSecret(DEFAULT_STAFF_AUDIENCE),\n expiresInSec: expiresInSec ?? 60 * 60 * 8,\n audience: DEFAULT_STAFF_AUDIENCE,\n issuer: DEFAULT_ISSUER,\n }\n}\n\nfunction toVerifyOptions(secretOrOptions?: string | VerifyJwtOptions): { secret: string; audience?: string; issuer?: string } {\n if (isVerifyOptions(secretOrOptions)) {\n const audience = secretOrOptions.audience ?? DEFAULT_STAFF_AUDIENCE\n const secret = secretOrOptions.secret ?? deriveJwtAudienceSecret(audience)\n if (!secret) throw new Error('JWT_SECRET is not set')\n return {\n secret,\n audience,\n issuer: secretOrOptions.issuer ?? DEFAULT_ISSUER,\n }\n }\n if (typeof secretOrOptions === 'string') {\n if (!secretOrOptions) throw new Error('JWT_SECRET is not set')\n // Legacy explicit secret: no audience/issuer enforcement.\n return { secret: secretOrOptions }\n }\n return {\n secret: deriveJwtAudienceSecret(DEFAULT_STAFF_AUDIENCE),\n audience: DEFAULT_STAFF_AUDIENCE,\n issuer: DEFAULT_ISSUER,\n }\n}\n\nexport function signJwt(\n payload: JwtPayload,\n secretOrOptions?: string | SignJwtOptions,\n expiresInSec?: number,\n) {\n const options = toSignOptions(secretOrOptions, expiresInSec)\n const header = { alg: 'HS256', typ: 'JWT' }\n const now = Math.floor(Date.now() / 1000)\n const body: JwtPayload = { iat: now, exp: now + options.expiresInSec, ...payload }\n if (options.issuer && body.iss === undefined) body.iss = options.issuer\n if (options.audience && body.aud === undefined) body.aud = options.audience\n const encHeader = base64url(JSON.stringify(header))\n const encBody = base64url(JSON.stringify(body))\n const data = `${encHeader}.${encBody}`\n const sig = crypto.createHmac('sha256', options.secret).update(data).digest()\n const encSig = base64url(sig)\n return `${data}.${encSig}`\n}\n\nfunction verifyWithOptions(token: string, options: { secret: string; audience?: string; issuer?: string }): JwtPayload | null {\n const parts = token.split('.')\n if (parts.length !== 3) return null\n const [h, p, s] = parts\n const data = `${h}.${p}`\n const expected = base64url(crypto.createHmac('sha256', options.secret).update(data).digest())\n const providedSignature = Buffer.from(s)\n const expectedSignature = Buffer.from(expected)\n if (providedSignature.length !== expectedSignature.length) return null\n if (!crypto.timingSafeEqual(providedSignature, expectedSignature)) return null\n let payload: JwtPayload\n try {\n payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))\n } catch {\n return null\n }\n const now = Math.floor(Date.now() / 1000)\n if (payload.exp && now > payload.exp) return null\n if (options.audience !== undefined) {\n if (payload.aud !== options.audience) return null\n }\n if (options.issuer !== undefined) {\n if (payload.iss !== options.issuer) return null\n }\n return payload\n}\n\n/**\n * Whether a raw-secret token is still inside the migration window. A token is only ever legacy\n * for a bounded period after it was issued, so `iat` is mandatory: a token that cannot prove its\n * age cannot prove it is inside the window either, and accepting it would make the window\n * unbounded \u2014 which is exactly the defect this guard closes.\n */\nfunction isWithinLegacyWindow(payload: JwtPayload, graceMinutes: number): boolean {\n const cutoverAt = getLegacyCutoverEpochSeconds()\n const now = Math.floor(Date.now() / 1000)\n if (cutoverAt === null || now >= cutoverAt) return false\n const issuedAt = payload.iat\n if (typeof issuedAt !== 'number' || !Number.isFinite(issuedAt)) return false\n if (issuedAt > now + LEGACY_TOKEN_CLOCK_SKEW_SECONDS) return false\n return now - issuedAt <= graceMinutes * 60\n}\n\nexport function verifyJwt(token: string, secretOrOptions?: string | VerifyJwtOptions) {\n const options = toVerifyOptions(secretOrOptions)\n const result = verifyWithOptions(token, options)\n if (result) {\n // `_legacyToken` is assigned by this function alone. Strip any same-named claim carried in\n // the token body so a payload can never talk callers (staff session integrity, portal auth)\n // into treating a modern, session-bound token as a sessionless legacy one.\n if (result._legacyToken !== undefined) delete result._legacyToken\n return result\n }\n\n // Legacy fallback: when the caller used the default path (no explicit secret) and the new\n // audience-derived verification failed, try verifying with the raw JWT_SECRET. This keeps\n // pre-migration tokens working across a rolling deployment \u2014 but only until the token's own\n // `iat` leaves the configured grace window, or the configured cutover instant passes.\n if (secretOrOptions === undefined) {\n const graceMinutes = getLegacyGraceMinutes()\n const rawSecret = process.env.JWT_SECRET\n if (graceMinutes > 0 && rawSecret) {\n const legacyResult = verifyWithOptions(token, { secret: rawSecret })\n if (legacyResult && isWithinLegacyWindow(legacyResult, graceMinutes)) {\n legacyResult._legacyToken = true\n return legacyResult\n }\n }\n }\n\n return null\n}\n\n/**\n * Sign a JWT for a specific audience using an audience-derived signing key. The resulting token\n * carries `iss` and `aud` claims and cannot be verified with the base `JWT_SECRET` directly \u2014\n * callers must use `verifyAudienceJwt` with the same audience.\n */\nexport function signAudienceJwt(\n audience: string,\n payload: JwtPayload,\n expiresInSec: number = 60 * 60 * 8,\n): string {\n return signJwt(payload, { audience, expiresInSec })\n}\n\n/**\n * Verify a JWT that was signed with an audience-scoped secret. Rejects tokens that are missing\n * or carry a mismatched `aud`/`iss` claim, so a staff JWT cannot be replayed against the\n * customer portal (and vice versa) even when the base `JWT_SECRET` is shared.\n */\nexport function verifyAudienceJwt(audience: string, token: string): JwtPayload | null {\n return verifyJwt(token, { audience })\n}\n\n/**\n * True when a verified JWT payload represents an MFA challenge in progress: the second factor\n * has not been completed yet (`mfa_pending: true`, `mfa_verified` not `true`). Such tokens are\n * provisional \u2014 general staff APIs must reject them and only the MFA completion routes may\n * accept them (see `@open-mercato/shared/lib/auth/mfaPendingAccess`).\n *\n * The predicate keys on `mfa_pending` alone by design. `mfa_verified` is NOT a durable session\n * property: routes that legitimately re-sign a staff token (`PUT /api/auth/profile`,\n * `GET|POST /api/auth/session/refresh`) rebuild the claims without it. Tightening this to require\n * `mfa_verified === true` would therefore silently downgrade those sessions.\n */\nexport function isMfaPendingJwtPayload(payload: unknown): boolean {\n if (!payload || typeof payload !== 'object') return false\n const record = payload as Record<string, unknown>\n return record.mfa_pending === true && record.mfa_verified !== true\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,YAAY;AACnB,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AAEvC,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAE9D,SAAS,UAAU,OAAwB;AACzC,UAAQ,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,OACtD,SAAS,QAAQ,EACjB,QAAQ,MAAM,EAAE,EAChB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG;AACvB;AAmBA,MAAM,iBAAiB;AACvB,MAAM,yBAAsC;AAC5C,MAAM,wBAAwB;AAE9B,MAAM,+BAA+B;AACrC,MAAM,kCAAkC;AAWxC,SAAS,wBAAgC;AACvC,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,aAAa,OAAO,QAAQ,WAAW,IAAI,KAAK,EAAE,YAAY,IAAI;AACxE,MAAI,eAAe,WAAW,eAAe,MAAO,QAAO;AAC3D,SAAO,uBAAuB,YAAY,8BAA8B,EAAE,KAAK,GAAG,SAAS,KAAK,CAAC;AACnG;AAQA,SAAS,+BAA8C;AACrD,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,CAAC,OAAO,CAAC,IAAI,KAAK,EAAG,QAAO;AAChC,QAAM,SAAS,KAAK,MAAM,IAAI,KAAK,CAAC;AACpC,MAAI,OAAO,MAAM,MAAM,GAAG;AACxB;AAAA,MACE;AAAA,MACA;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO,KAAK,MAAM,SAAS,GAAI;AACjC;AAEA,MAAM,oBAAoB;AAO1B,MAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAID,MAAM,aAAa,oBAAI,IAAY;AAEnC,SAAS,SAAS,KAAa,SAAuB;AACpD,MAAI,WAAW,IAAI,GAAG,EAAG;AACzB,aAAW,IAAI,GAAG;AAClB,SAAO,KAAK,OAAO;AACrB;AAEA,SAAS,eAAwB;AAC/B,SAAO,QAAQ,IAAI,aAAa;AAClC;AAEA,SAAS,cAAc,QAA8D;AACnF,QAAM,QAAQ,OAAO,WAAW,WAAW,OAAO,KAAK,IAAI;AAC3D,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,oBAAoB,IAAI,MAAM,YAAY,CAAC,EAAG,QAAO;AACzD,MAAI,MAAM,SAAS,kBAAmB,QAAO;AAC7C,SAAO;AACT;AAEA,SAAS,kBAAkB,MAAc,WAAuC;AAC9E,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO,GAAG,IAAI;AAAA,IAChB,KAAK;AACH,aAAO,GAAG,IAAI;AAAA,IAChB,KAAK;AACH,aAAO,GAAG,IAAI,oBAAoB,iBAAiB;AAAA,EACvD;AACF;AAOA,SAAS,oBAAoB,MAAc,QAAyC;AAClF,QAAM,YAAY,cAAc,MAAM;AACtC,MAAI,CAAC,UAAW;AAChB,QAAM,UAAU,kBAAkB,MAAM,SAAS;AACjD,MAAI,aAAa,GAAG;AAClB,UAAM,IAAI,MAAM,2EAA2E,OAAO,EAAE;AAAA,EACtG;AACA,WAAS,iBAAiB,IAAI,IAAI,SAAS,IAAI,GAAG,OAAO,6CAA6C;AACxG;AAOO,SAAS,wBAA8B;AAC5C,sBAAoB,cAAc,QAAQ,IAAI,UAAU;AACxD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG,GAAG;AACtD,QAAI,CAAC,wCAAwC,KAAK,GAAG,EAAG;AACxD,wBAAoB,KAAK,KAAK;AAAA,EAChC;AACF;AAEA,SAAS,eAAe,UAA2B;AACjD,QAAM,SAAS,YAAY,QAAQ,IAAI;AACvC,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,MAAI,aAAa,OAAW,qBAAoB,cAAc,MAAM;AACpE,SAAO;AACT;AAEA,SAAS,kBAAkB,UAA0B;AACnD,SAAO,SAAS,KAAK,EAAE,YAAY,EAAE,QAAQ,eAAe,GAAG;AACjE;AAEA,MAAM,qBAAqB,oBAAI,IAAoB;AAEnD,SAAS,6BAA6B,YAAoB,MAAsB;AAC9E,QAAM,WAAW,GAAG,UAAU,KAAK,IAAI;AACvC,QAAM,SAAS,mBAAmB,IAAI,QAAQ;AAC9C,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,QAAQ,GAAG,qBAAqB,IAAI,UAAU;AACpD,QAAM,UAAU,OAAO,WAAW,UAAU,IAAI,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAC5E,qBAAmB,IAAI,UAAU,OAAO;AACxC,SAAO;AACT;AAYO,SAAS,wBAAwB,UAAkB,YAA6B;AACrF,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,MAAI,CAAC,WAAY,OAAM,IAAI,MAAM,6CAA6C;AAC9E,QAAM,eAAe,OAAO,WAAW,YAAY,CAAC;AACpD,QAAM,WAAW,QAAQ,IAAI,YAAY;AACzC,MAAI,YAAY,SAAS,KAAK,EAAE,SAAS,GAAG;AAC1C,wBAAoB,cAAc,QAAQ;AAC1C,WAAO;AAAA,EACT;AACA,QAAM,OAAO,eAAe,UAAU;AACtC,SAAO,6BAA6B,YAAY,IAAI;AACtD;AAEA,SAAS,cAAc,OAAqE;AAC1F,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAEA,SAAS,gBAAgB,OAAyE;AAChG,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAEA,SAAS,cAAc,iBAA2C,cAAqG;AACrK,MAAI,cAAc,eAAe,GAAG;AAClC,UAAM,WAAW,gBAAgB,YAAY;AAC7C,UAAM,SAAS,gBAAgB,UAAU,wBAAwB,QAAQ;AACzE,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,WAAO;AAAA,MACL;AAAA,MACA,cAAc,gBAAgB,gBAAgB,KAAK,KAAK;AAAA,MACxD;AAAA,MACA,QAAQ,gBAAgB,UAAU;AAAA,IACpC;AAAA,EACF;AACA,MAAI,OAAO,oBAAoB,UAAU;AAGvC,QAAI,CAAC,gBAAiB,OAAM,IAAI,MAAM,uBAAuB;AAC7D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,gBAAgB,KAAK,KAAK;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,wBAAwB,sBAAsB;AAAA,IACtD,cAAc,gBAAgB,KAAK,KAAK;AAAA,IACxC,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AACF;AAEA,SAAS,gBAAgB,iBAAqG;AAC5H,MAAI,gBAAgB,eAAe,GAAG;AACpC,UAAM,WAAW,gBAAgB,YAAY;AAC7C,UAAM,SAAS,gBAAgB,UAAU,wBAAwB,QAAQ;AACzE,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ,gBAAgB,UAAU;AAAA,IACpC;AAAA,EACF;AACA,MAAI,OAAO,oBAAoB,UAAU;AACvC,QAAI,CAAC,gBAAiB,OAAM,IAAI,MAAM,uBAAuB;AAE7D,WAAO,EAAE,QAAQ,gBAAgB;AAAA,EACnC;AACA,SAAO;AAAA,IACL,QAAQ,wBAAwB,sBAAsB;AAAA,IACtD,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AACF;AAEO,SAAS,QACd,SACA,iBACA,cACA;AACA,QAAM,UAAU,cAAc,iBAAiB,YAAY;AAC3D,QAAM,SAAS,EAAE,KAAK,SAAS,KAAK,MAAM;AAC1C,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,OAAmB,EAAE,KAAK,KAAK,KAAK,MAAM,QAAQ,cAAc,GAAG,QAAQ;AACjF,MAAI,QAAQ,UAAU,KAAK,QAAQ,OAAW,MAAK,MAAM,QAAQ;AACjE,MAAI,QAAQ,YAAY,KAAK,QAAQ,OAAW,MAAK,MAAM,QAAQ;AACnE,QAAM,YAAY,UAAU,KAAK,UAAU,MAAM,CAAC;AAClD,QAAM,UAAU,UAAU,KAAK,UAAU,IAAI,CAAC;AAC9C,QAAM,OAAO,GAAG,SAAS,IAAI,OAAO;AACpC,QAAM,MAAM,OAAO,WAAW,UAAU,QAAQ,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO;AAC5E,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO,GAAG,IAAI,IAAI,MAAM;AAC1B;AAEA,SAAS,kBAAkB,OAAe,SAAoF;AAC5H,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,QAAM,OAAO,GAAG,CAAC,IAAI,CAAC;AACtB,QAAM,WAAW,UAAU,OAAO,WAAW,UAAU,QAAQ,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO,CAAC;AAC5F,QAAM,oBAAoB,OAAO,KAAK,CAAC;AACvC,QAAM,oBAAoB,OAAO,KAAK,QAAQ;AAC9C,MAAI,kBAAkB,WAAW,kBAAkB,OAAQ,QAAO;AAClE,MAAI,CAAC,OAAO,gBAAgB,mBAAmB,iBAAiB,EAAG,QAAO;AAC1E,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,OAAO,KAAK,GAAG,QAAQ,EAAE,SAAS,MAAM,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,MAAI,QAAQ,OAAO,MAAM,QAAQ,IAAK,QAAO;AAC7C,MAAI,QAAQ,aAAa,QAAW;AAClC,QAAI,QAAQ,QAAQ,QAAQ,SAAU,QAAO;AAAA,EAC/C;AACA,MAAI,QAAQ,WAAW,QAAW;AAChC,QAAI,QAAQ,QAAQ,QAAQ,OAAQ,QAAO;AAAA,EAC7C;AACA,SAAO;AACT;AAQA,SAAS,qBAAqB,SAAqB,cAA+B;AAChF,QAAM,YAAY,6BAA6B;AAC/C,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,MAAI,cAAc,QAAQ,OAAO,UAAW,QAAO;AACnD,QAAM,WAAW,QAAQ;AACzB,MAAI,OAAO,aAAa,YAAY,CAAC,OAAO,SAAS,QAAQ,EAAG,QAAO;AACvE,MAAI,WAAW,MAAM,gCAAiC,QAAO;AAC7D,SAAO,MAAM,YAAY,eAAe;AAC1C;AAEO,SAAS,UAAU,OAAe,iBAA6C;AACpF,QAAM,UAAU,gBAAgB,eAAe;AAC/C,QAAM,SAAS,kBAAkB,OAAO,OAAO;AAC/C,MAAI,QAAQ;AAIV,QAAI,OAAO,iBAAiB,OAAW,QAAO,OAAO;AACrD,WAAO;AAAA,EACT;AAMA,MAAI,oBAAoB,QAAW;AACjC,UAAM,eAAe,sBAAsB;AAC3C,UAAM,YAAY,QAAQ,IAAI;AAC9B,QAAI,eAAe,KAAK,WAAW;AACjC,YAAM,eAAe,kBAAkB,OAAO,EAAE,QAAQ,UAAU,CAAC;AACnE,UAAI,gBAAgB,qBAAqB,cAAc,YAAY,GAAG;AACpE,qBAAa,eAAe;AAC5B,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAOO,SAAS,gBACd,UACA,SACA,eAAuB,KAAK,KAAK,GACzB;AACR,SAAO,QAAQ,SAAS,EAAE,UAAU,aAAa,CAAC;AACpD;AAOO,SAAS,kBAAkB,UAAkB,OAAkC;AACpF,SAAO,UAAU,OAAO,EAAE,SAAS,CAAC;AACtC;AAaO,SAAS,uBAAuB,SAA2B;AAChE,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,SAAS;AACf,SAAO,OAAO,gBAAgB,QAAQ,OAAO,iBAAiB;AAChE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const registeredMethodsByPath = /* @__PURE__ */ new Map();
|
|
2
|
+
function normalizePath(path) {
|
|
3
|
+
if (typeof path !== "string") return null;
|
|
4
|
+
const trimmed = path.trim();
|
|
5
|
+
if (!trimmed || !trimmed.startsWith("/")) return null;
|
|
6
|
+
return (trimmed.length > 1 ? trimmed.replace(/\/+$/, "") : trimmed).toLowerCase() || "/";
|
|
7
|
+
}
|
|
8
|
+
function normalizeMethods(methods) {
|
|
9
|
+
if (!Array.isArray(methods)) return [];
|
|
10
|
+
const normalized = methods.filter((method) => typeof method === "string" && method.trim().length > 0).map((method) => method.trim().toUpperCase());
|
|
11
|
+
return Array.from(new Set(normalized)).sort((first, second) => first < second ? -1 : first > second ? 1 : 0);
|
|
12
|
+
}
|
|
13
|
+
function registerMfaPendingAccessRoutes(routes) {
|
|
14
|
+
for (const route of Array.isArray(routes) ? routes : []) {
|
|
15
|
+
const path = normalizePath(route?.path ?? "");
|
|
16
|
+
const methods = normalizeMethods(route?.methods);
|
|
17
|
+
if (!path || !methods.length) continue;
|
|
18
|
+
const existing = registeredMethodsByPath.get(path);
|
|
19
|
+
if (!existing) {
|
|
20
|
+
registeredMethodsByPath.set(path, new Set(methods));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
for (const method of methods) existing.add(method);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isMfaPendingAccessAllowed(method, pathname) {
|
|
27
|
+
const path = normalizePath(typeof pathname === "string" ? pathname : "");
|
|
28
|
+
if (!path) return false;
|
|
29
|
+
if (typeof method !== "string" || method.trim().length === 0) return false;
|
|
30
|
+
const methods = registeredMethodsByPath.get(path);
|
|
31
|
+
if (!methods) return false;
|
|
32
|
+
return methods.has(method.trim().toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
function listMfaPendingAccessRoutes() {
|
|
35
|
+
return Array.from(registeredMethodsByPath.entries()).map(([path, methods]) => ({ path, methods: Array.from(methods).sort((first, second) => first < second ? -1 : first > second ? 1 : 0) })).sort((first, second) => first.path < second.path ? -1 : first.path > second.path ? 1 : 0);
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
isMfaPendingAccessAllowed,
|
|
39
|
+
listMfaPendingAccessRoutes,
|
|
40
|
+
registerMfaPendingAccessRoutes
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=mfaPendingAccess.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/auth/mfaPendingAccess.ts"],
|
|
4
|
+
"sourcesContent": ["export type MfaPendingAccessRoute = {\n path: string\n methods: string[]\n}\n\nconst registeredMethodsByPath = new Map<string, Set<string>>()\n\nfunction normalizePath(path: string): string | null {\n if (typeof path !== 'string') return null\n const trimmed = path.trim()\n if (!trimmed || !trimmed.startsWith('/')) return null\n return (trimmed.length > 1 ? trimmed.replace(/\\/+$/, '') : trimmed).toLowerCase() || '/'\n}\n\nfunction normalizeMethods(methods: unknown): string[] {\n if (!Array.isArray(methods)) return []\n const normalized = methods\n .filter((method): method is string => typeof method === 'string' && method.trim().length > 0)\n .map((method) => method.trim().toUpperCase())\n return Array.from(new Set(normalized)).sort((first, second) => (first < second ? -1 : first > second ? 1 : 0))\n}\n\n/**\n * Register routes that may accept an MFA-pending staff token. The registry starts empty and\n * stays generic: every module that completes an MFA challenge on its own endpoints registers\n * them here during its bootstrap \u2014 the canonical `/api/security/mfa/*` completion routes are\n * registered by the enterprise `security` module (`lib/mfaCompletionRoutes.ts`), third-party\n * MFA implementations register theirs the same way. Registration is additive: methods merge\n * into any existing entry for the same path and are never removed.\n *\n * Contract defined in `.ai/specs/enterprise/implemented/SPEC-ENT-007-2026-03-06-auth-login-interceptors-extension.md`\n * (\u00A7 Amendment 2026-08-21 \u2014 central MFA-pending token gate).\n */\nexport function registerMfaPendingAccessRoutes(routes: MfaPendingAccessRoute[]): void {\n for (const route of Array.isArray(routes) ? routes : []) {\n const path = normalizePath(route?.path ?? '')\n const methods = normalizeMethods(route?.methods)\n if (!path || !methods.length) continue\n const existing = registeredMethodsByPath.get(path)\n if (!existing) {\n registeredMethodsByPath.set(path, new Set(methods))\n continue\n }\n for (const method of methods) existing.add(method)\n }\n}\n\n/**\n * Fail-closed check deciding whether an MFA-pending staff token may be resolved for this exact\n * method + path pair. Only explicitly registered completion routes pass; every unregistered\n * request is denied.\n */\nexport function isMfaPendingAccessAllowed(\n method: string | null | undefined,\n pathname: string | null | undefined,\n): boolean {\n const path = normalizePath(typeof pathname === 'string' ? pathname : '')\n if (!path) return false\n if (typeof method !== 'string' || method.trim().length === 0) return false\n const methods = registeredMethodsByPath.get(path)\n if (!methods) return false\n return methods.has(method.trim().toUpperCase())\n}\n\n/** Test/ops helper: current snapshot of the pending-access registry. */\nexport function listMfaPendingAccessRoutes(): ReadonlyArray<Readonly<MfaPendingAccessRoute>> {\n return Array.from(registeredMethodsByPath.entries())\n .map(([path, methods]) => ({ path, methods: Array.from(methods).sort((first, second) => (first < second ? -1 : first > second ? 1 : 0)) }))\n .sort((first, second) => (first.path < second.path ? -1 : first.path > second.path ? 1 : 0))\n}\n"],
|
|
5
|
+
"mappings": "AAKA,MAAM,0BAA0B,oBAAI,IAAyB;AAE7D,SAAS,cAAc,MAA6B;AAClD,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,CAAC,WAAW,CAAC,QAAQ,WAAW,GAAG,EAAG,QAAO;AACjD,UAAQ,QAAQ,SAAS,IAAI,QAAQ,QAAQ,QAAQ,EAAE,IAAI,SAAS,YAAY,KAAK;AACvF;AAEA,SAAS,iBAAiB,SAA4B;AACpD,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO,CAAC;AACrC,QAAM,aAAa,QAChB,OAAO,CAAC,WAA6B,OAAO,WAAW,YAAY,OAAO,KAAK,EAAE,SAAS,CAAC,EAC3F,IAAI,CAAC,WAAW,OAAO,KAAK,EAAE,YAAY,CAAC;AAC9C,SAAO,MAAM,KAAK,IAAI,IAAI,UAAU,CAAC,EAAE,KAAK,CAAC,OAAO,WAAY,QAAQ,SAAS,KAAK,QAAQ,SAAS,IAAI,CAAE;AAC/G;AAaO,SAAS,+BAA+B,QAAuC;AACpF,aAAW,SAAS,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,GAAG;AACvD,UAAM,OAAO,cAAc,OAAO,QAAQ,EAAE;AAC5C,UAAM,UAAU,iBAAiB,OAAO,OAAO;AAC/C,QAAI,CAAC,QAAQ,CAAC,QAAQ,OAAQ;AAC9B,UAAM,WAAW,wBAAwB,IAAI,IAAI;AACjD,QAAI,CAAC,UAAU;AACb,8BAAwB,IAAI,MAAM,IAAI,IAAI,OAAO,CAAC;AAClD;AAAA,IACF;AACA,eAAW,UAAU,QAAS,UAAS,IAAI,MAAM;AAAA,EACnD;AACF;AAOO,SAAS,0BACd,QACA,UACS;AACT,QAAM,OAAO,cAAc,OAAO,aAAa,WAAW,WAAW,EAAE;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,WAAW,YAAY,OAAO,KAAK,EAAE,WAAW,EAAG,QAAO;AACrE,QAAM,UAAU,wBAAwB,IAAI,IAAI;AAChD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,IAAI,OAAO,KAAK,EAAE,YAAY,CAAC;AAChD;AAGO,SAAS,6BAA6E;AAC3F,SAAO,MAAM,KAAK,wBAAwB,QAAQ,CAAC,EAChD,IAAI,CAAC,CAAC,MAAM,OAAO,OAAO,EAAE,MAAM,SAAS,MAAM,KAAK,OAAO,EAAE,KAAK,CAAC,OAAO,WAAY,QAAQ,SAAS,KAAK,QAAQ,SAAS,IAAI,CAAE,EAAE,EAAE,EACzI,KAAK,CAAC,OAAO,WAAY,MAAM,OAAO,OAAO,OAAO,KAAK,MAAM,OAAO,OAAO,OAAO,IAAI,CAAE;AAC/F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
function isUnrestrictedOrganizationScope(input) {
|
|
2
|
+
return input.isSuperAdmin || input.allowedOrganizationIds === null;
|
|
3
|
+
}
|
|
1
4
|
function isOrganizationAccessAllowed(input) {
|
|
2
|
-
if (input
|
|
3
|
-
if (input.allowedOrganizationIds === null) return true;
|
|
5
|
+
if (isUnrestrictedOrganizationScope(input)) return true;
|
|
4
6
|
if (!input.targetOrganizationId) return false;
|
|
5
|
-
return input.allowedOrganizationIds.includes(input.targetOrganizationId);
|
|
7
|
+
return (input.allowedOrganizationIds ?? []).includes(input.targetOrganizationId);
|
|
6
8
|
}
|
|
7
9
|
export {
|
|
8
|
-
isOrganizationAccessAllowed
|
|
10
|
+
isOrganizationAccessAllowed,
|
|
11
|
+
isUnrestrictedOrganizationScope
|
|
9
12
|
};
|
|
10
13
|
//# sourceMappingURL=organizationAccess.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/auth/organizationAccess.ts"],
|
|
4
|
-
"sourcesContent": ["export type OrganizationAccessDecisionInput = {\n isSuperAdmin: boolean\n allowedOrganizationIds: readonly string[] | null\n targetOrganizationId: string | null\n}\n\n/**\n * Fail-closed organization-access predicate. The single source of truth for\n * \"may this principal act on `targetOrganizationId`\".\n *\n * Decision table:\n * - `isSuperAdmin === true` -> allow (global access)\n * - `allowedOrganizationIds === null` -> allow (truly unrestricted)\n * - restricted + no target org -> deny (empty/unknown scope is not a bypass)\n * - restricted + target org -> allow iff the target is a member of the allowed set\n */\nexport function isOrganizationAccessAllowed(input: OrganizationAccessDecisionInput): boolean {\n if (
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["export type OrganizationAccessDecisionInput = {\n isSuperAdmin: boolean\n allowedOrganizationIds: readonly string[] | null\n targetOrganizationId: string | null\n}\n\nexport type UnrestrictedOrganizationScopeInput = {\n isSuperAdmin: boolean\n allowedOrganizationIds: readonly string[] | null | undefined\n}\n\nexport function isUnrestrictedOrganizationScope(input: UnrestrictedOrganizationScopeInput): boolean {\n return input.isSuperAdmin || input.allowedOrganizationIds === null\n}\n\n/**\n * Fail-closed organization-access predicate. The single source of truth for\n * \"may this principal act on `targetOrganizationId`\".\n *\n * Decision table:\n * - `isSuperAdmin === true` -> allow (global access)\n * - `allowedOrganizationIds === null` -> allow (truly unrestricted)\n * - restricted + no target org -> deny (empty/unknown scope is not a bypass)\n * - restricted + target org -> allow iff the target is a member of the allowed set\n */\nexport function isOrganizationAccessAllowed(input: OrganizationAccessDecisionInput): boolean {\n if (isUnrestrictedOrganizationScope(input)) return true\n if (!input.targetOrganizationId) return false\n return (input.allowedOrganizationIds ?? []).includes(input.targetOrganizationId)\n}\n"],
|
|
5
|
+
"mappings": "AAWO,SAAS,gCAAgC,OAAoD;AAClG,SAAO,MAAM,gBAAgB,MAAM,2BAA2B;AAChE;AAYO,SAAS,4BAA4B,OAAiD;AAC3F,MAAI,gCAAgC,KAAK,EAAG,QAAO;AACnD,MAAI,CAAC,MAAM,qBAAsB,QAAO;AACxC,UAAQ,MAAM,0BAA0B,CAAC,GAAG,SAAS,MAAM,oBAAoB;AACjF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=principal-service.js.map
|
package/dist/lib/auth/server.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { cookies } from "next/headers.js";
|
|
2
|
-
import { verifyJwt } from "./jwt.js";
|
|
2
|
+
import { isMfaPendingJwtPayload, verifyJwt } from "./jwt.js";
|
|
3
|
+
import { isMfaPendingAccessAllowed } from "./mfaPendingAccess.js";
|
|
3
4
|
import { getSharedApiKeyAuthCache } from "./apiKeyAuthCache.js";
|
|
4
5
|
import { isTransientDbError } from "@open-mercato/shared/lib/db/pg-errors";
|
|
6
|
+
function readRequestPathname(req) {
|
|
7
|
+
try {
|
|
8
|
+
return new URL(req.url).pathname;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
5
13
|
const TENANT_COOKIE_NAME = "om_selected_tenant";
|
|
6
14
|
const ORGANIZATION_COOKIE_NAME = "om_selected_org";
|
|
7
15
|
const ALL_ORGANIZATIONS_COOKIE_VALUE = "__all__";
|
|
@@ -114,12 +122,21 @@ async function resolveApiKeyAuth(secret) {
|
|
|
114
122
|
const roles = roleIds.length ? await em.find(Role, { id: { $in: roleIds }, deletedAt: null }) : [];
|
|
115
123
|
const roleNames = roles.map((role) => role.name).filter((name) => typeof name === "string" && name.length > 0);
|
|
116
124
|
let keyIsSuperAdmin = false;
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
const keyOrganizationId = typeof record.organizationId === "string" && record.organizationId.trim().length > 0 ? record.organizationId.trim() : null;
|
|
126
|
+
if (roleIds.length && !keyOrganizationId) {
|
|
127
|
+
const superAcls = await em.find(
|
|
119
128
|
RoleAcl,
|
|
120
|
-
{
|
|
129
|
+
{
|
|
130
|
+
role: { $in: roleIds },
|
|
131
|
+
tenantId: record.tenantId ?? null,
|
|
132
|
+
isSuperAdmin: true,
|
|
133
|
+
deletedAt: null
|
|
134
|
+
}
|
|
121
135
|
);
|
|
122
|
-
keyIsSuperAdmin =
|
|
136
|
+
keyIsSuperAdmin = superAcls.some((acl) => {
|
|
137
|
+
const organizations = Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null;
|
|
138
|
+
return !organizations || organizations.length === 0 || organizations.includes("__all__");
|
|
139
|
+
});
|
|
123
140
|
}
|
|
124
141
|
if (cache.shouldWriteLastUsed(record.id)) {
|
|
125
142
|
try {
|
|
@@ -128,7 +145,14 @@ async function resolveApiKeyAuth(secret) {
|
|
|
128
145
|
} catch {
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
|
-
const
|
|
148
|
+
const isSessionBoundKey = Boolean(
|
|
149
|
+
record.sessionToken || record.sessionUserId || record.sessionSecretEncrypted || record.opencodeSessionId
|
|
150
|
+
);
|
|
151
|
+
const actualUserId = isSessionBoundKey ? record.sessionUserId ?? null : record.createdBy ?? null;
|
|
152
|
+
if (isSessionBoundKey && !actualUserId) {
|
|
153
|
+
cache.setMiss(secret);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
132
156
|
if (actualUserId) {
|
|
133
157
|
const user = await em.findOne(User, { id: actualUserId, deletedAt: null });
|
|
134
158
|
if (!user) {
|
|
@@ -139,7 +163,8 @@ async function resolveApiKeyAuth(secret) {
|
|
|
139
163
|
cache.setMiss(secret);
|
|
140
164
|
return null;
|
|
141
165
|
}
|
|
142
|
-
|
|
166
|
+
const requiresExactOrganization = isSessionBoundKey || Boolean(record.organizationId);
|
|
167
|
+
if (requiresExactOrganization && (user.organizationId ?? null) !== (record.organizationId ?? null)) {
|
|
143
168
|
cache.setMiss(secret);
|
|
144
169
|
return null;
|
|
145
170
|
}
|
|
@@ -215,6 +240,7 @@ async function resolveAuthFromCookiesDetailed() {
|
|
|
215
240
|
const payload = verifyJwt(token);
|
|
216
241
|
if (!payload) return { auth: null, status: "invalid" };
|
|
217
242
|
if (payload.type === "customer") return { auth: null, status: "invalid" };
|
|
243
|
+
if (isMfaPendingJwtPayload(payload)) return { auth: null, status: "invalid" };
|
|
218
244
|
const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload);
|
|
219
245
|
if (!canonicalAuth) return { auth: null, status: "invalid" };
|
|
220
246
|
const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value;
|
|
@@ -255,6 +281,11 @@ async function resolveAuthFromRequestDetailed(req) {
|
|
|
255
281
|
try {
|
|
256
282
|
const payload = verifyJwt(token);
|
|
257
283
|
if (payload && payload.type === "customer") return { auth: null, status: "invalid" };
|
|
284
|
+
if (payload && isMfaPendingJwtPayload(payload)) {
|
|
285
|
+
if (!isMfaPendingAccessAllowed(req.method, readRequestPathname(req))) {
|
|
286
|
+
return { auth: null, status: "invalid" };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
258
289
|
if (payload) {
|
|
259
290
|
const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload);
|
|
260
291
|
if (canonicalAuth) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/auth/server.ts"],
|
|
4
|
-
"sourcesContent": ["import { cookies } from 'next/headers.js'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\nimport { getSharedApiKeyAuthCache } from './apiKeyAuthCache'\nimport { isTransientDbError } from '@open-mercato/shared/lib/db/pg-errors'\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n sid?: string | null\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n userId?: string\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n// 'error' means auth could not be determined because of a transient/unexpected\n// failure (DB unavailable, connection-pool exhaustion, statement timeout, \u2026) \u2014\n// NOT because the token is genuinely invalid. Callers MUST NOT clear the user's\n// session cookies for 'error'; doing so would log everyone out at once during a\n// shared infrastructure blip (issue #4176).\ntype AuthResolutionStatus = 'authenticated' | 'missing' | 'invalid' | 'error'\ntype AuthResolution = {\n auth: AuthContext\n status: AuthResolutionStatus\n}\n\n// Thrown by `resolveCanonicalInteractiveAuthContext` when the canonical auth\n// lookup itself fails unexpectedly (as opposed to resolving to a genuine\n// \"not authorized\" null). Kept distinct so resolvers can map it to the\n// non-destructive 'error' status instead of the cookie-clearing 'invalid'.\nexport class AuthResolutionUnavailableError extends Error {\n constructor(cause?: unknown) {\n super('Auth resolution unavailable')\n this.name = 'AuthResolutionUnavailableError'\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause\n }\n}\n\n// Symbol-keyed trusted auth context. Set on synthetic Request objects by\n// callers that have already authenticated (e.g. the AI in-process operation\n// runner) so downstream auth resolution short-circuits without re-running\n// cookie/JWT/API-key parsing. The hook is fail-open: if absent the normal\n// resolution path runs unchanged.\nexport const TRUSTED_AUTH_CONTEXT_SYMBOL = Symbol.for('open-mercato.auth.trustedContext')\n\nexport type TrustedAuthContextEnvelope = {\n auth: AuthContext\n status?: AuthResolutionStatus\n}\n\nexport function attachTrustedAuthContext(\n request: Request,\n envelope: TrustedAuthContextEnvelope\n): Request {\n ;(request as unknown as Record<symbol, TrustedAuthContextEnvelope>)[TRUSTED_AUTH_CONTEXT_SYMBOL] = envelope\n return request\n}\n\nfunction readTrustedAuthContext(request: Request): TrustedAuthContextEnvelope | null {\n const carrier = request as unknown as Record<symbol, unknown>\n const envelope = carrier[TRUSTED_AUTH_CONTEXT_SYMBOL]\n if (!envelope || typeof envelope !== 'object') return null\n const candidate = envelope as TrustedAuthContextEnvelope\n if (!('auth' in candidate)) return null\n return candidate\n}\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\n/**\n * A blank `om_selected_tenant` is \"no selection\", not a deliberate \"no tenant\".\n *\n * Unlike the organization cookie there is no all-tenants sentinel to express, and the tenant\n * selector renders with `includeEmptyOption={false}`, so no UI produces a blank value. Treating it\n * as an applied override nulled `tenantId` for the whole super-admin session, which is what turned\n * a cosmetic client bug into `tenantId ?? ''` reaching a NOT NULL uuid column. Reporting the\n * override as not applied instead keeps the tenant carried in the token and remediates an\n * already-poisoned browser on its next request, whatever wrote the cookie.\n */\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: false, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: false, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n return (auth as Record<string, unknown>).isSuperAdmin === true\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n const cache = getSharedApiKeyAuthCache()\n const cached = cache.get(secret)\n if (cached !== undefined) return cached as AuthContext\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role, RoleAcl, User } = await import('@open-mercato/core/modules/auth/data/entities')\n const { Organization, Tenant } = await import('@open-mercato/core/modules/directory/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) {\n cache.setMiss(secret)\n return null\n }\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds }, deletedAt: null })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n let keyIsSuperAdmin = false\n if (roleIds.length) {\n const superAcl = await em.findOne(\n RoleAcl,\n { role: { $in: roleIds } as any, isSuperAdmin: true, deletedAt: null } as any,\n )\n keyIsSuperAdmin = !!(superAcl && (superAcl as { isSuperAdmin?: boolean }).isSuperAdmin)\n }\n\n if (cache.shouldWriteLastUsed(record.id)) {\n try {\n record.lastUsedAt = new Date()\n await em.persist(record).flush()\n } catch {\n // best-effort update; ignore write failures\n }\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n if (actualUserId) {\n const user = await em.findOne(User, { id: actualUserId, deletedAt: null })\n if (!user) {\n cache.setMiss(secret)\n return null\n }\n if ((user.tenantId ?? null) !== (record.tenantId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n if ((user.organizationId ?? null) !== (record.organizationId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n } else {\n if (record.tenantId) {\n const tenant = await em.findOne(Tenant, { id: record.tenantId, deletedAt: null, isActive: true })\n if (!tenant) {\n cache.setMiss(secret)\n return null\n }\n }\n if (record.organizationId) {\n const organization = await em.findOne(Organization, { id: record.organizationId, deletedAt: null, isActive: true })\n if (!organization) {\n cache.setMiss(secret)\n return null\n }\n if (record.tenantId && String(organization.tenant.id) !== record.tenantId) {\n cache.setMiss(secret)\n return null\n }\n }\n }\n\n const auth: Exclude<AuthContext, null> = {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n isSuperAdmin: keyIsSuperAdmin,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\n cache.setSuccess(secret, auth, record.expiresAt ? record.expiresAt.getTime() : null)\n return auth\n } catch (err) {\n // A transient DB failure (pool exhausted, `max_connections` reached, DB\n // restarting) means we could not confirm the key \u2014 NOT that it is invalid.\n // Surface it so callers return a retryable 503 instead of masking it as an\n // auth miss (401). Genuine misses already returned `null` above.\n if (isTransientDbError(err)) {\n throw new AuthResolutionUnavailableError(err)\n }\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nasync function resolveCanonicalInteractiveAuthContext(auth: AuthContext): Promise<AuthContext> {\n if (!auth || auth.isApiKey) return auth\n if (typeof auth.sub !== 'string' || auth.sub.trim().length === 0) return null\n\n try {\n const [{ createRequestContainer }, { resolveCanonicalStaffAuthContext }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/auth/lib/sessionIntegrity'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return await resolveCanonicalStaffAuthContext(em, auth)\n } catch (err) {\n // A thrown error here means we could not check the token against the\n // database (DB down, pool exhausted, timeout). This is NOT the same as the\n // token being invalid \u2014 surface it so callers keep the session intact and\n // fail the request transiently instead of force-logging the user out.\n throw new AuthResolutionUnavailableError(err)\n }\n}\n\nexport async function resolveAuthFromCookiesDetailed(): Promise<AuthResolution> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return { auth: null, status: 'missing' }\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return { auth: null, status: 'invalid' }\n if (payload.type === 'customer') return { auth: null, status: 'invalid' }\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (!canonicalAuth) return { auth: null, status: 'invalid' }\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n } catch (err) {\n if (err instanceof AuthResolutionUnavailableError) return { auth: null, status: 'error' }\n return { auth: null, status: 'invalid' }\n }\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n return (await resolveAuthFromCookiesDetailed()).auth\n}\n\nexport async function resolveAuthFromRequestDetailed(req: Request): Promise<AuthResolution> {\n const trusted = readTrustedAuthContext(req)\n if (trusted) {\n return {\n auth: trusted.auth,\n status: trusted.status ?? (trusted.auth ? 'authenticated' : 'missing'),\n }\n }\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n let hadInvalidInteractiveToken = false\n let hadUnavailableResolution = false\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload && payload.type === 'customer') return { auth: null, status: 'invalid' }\n if (payload) {\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (canonicalAuth) {\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n }\n hadInvalidInteractiveToken = true\n }\n } catch (err) {\n // Transient/unexpected canonical-resolution failures must not invalidate\n // the token \u2014 surface them so we return 'error' (retryable) rather than\n // clearing the session cookies.\n if (err instanceof AuthResolutionUnavailableError) hadUnavailableResolution = true\n else hadInvalidInteractiveToken = true\n }\n }\n\n const resolveUnauthenticatedStatus = (): AuthResolutionStatus =>\n hadUnavailableResolution ? 'error' : hadInvalidInteractiveToken ? 'invalid' : 'missing'\n\n const apiKey = extractApiKey(req)\n if (!apiKey) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n let apiAuth: AuthContext\n try {\n apiAuth = await resolveApiKeyAuth(apiKey)\n } catch (err) {\n // Only a transient canonical-resolution failure maps to 'error' (retryable\n // 503), mirroring the interactive-token path above. Anything else is an\n // unexpected bug \u2014 rethrow so it surfaces as a 500 rather than being masked\n // as an auth failure.\n if (!(err instanceof AuthResolutionUnavailableError)) {\n throw err\n }\n hadUnavailableResolution = true\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n if (!apiAuth) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n return {\n auth: applySuperAdminScope(apiAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n return (await resolveAuthFromRequestDetailed(req)).auth\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB,SAAS,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import { cookies } from 'next/headers.js'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { isMfaPendingJwtPayload, verifyJwt } from './jwt'\nimport { isMfaPendingAccessAllowed } from './mfaPendingAccess'\nimport { getSharedApiKeyAuthCache } from './apiKeyAuthCache'\nimport { isTransientDbError } from '@open-mercato/shared/lib/db/pg-errors'\n\nfunction readRequestPathname(req: Request): string | null {\n try {\n return new URL(req.url).pathname\n } catch {\n return null\n }\n}\n\nconst TENANT_COOKIE_NAME = 'om_selected_tenant'\nconst ORGANIZATION_COOKIE_NAME = 'om_selected_org'\nconst ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'\nconst SUPERADMIN_ROLE = 'superadmin'\n\nexport type AuthContext = {\n sub: string\n sid?: string | null\n tenantId: string | null\n orgId: string | null\n email?: string\n roles?: string[]\n isApiKey?: boolean\n userId?: string\n keyId?: string\n keyName?: string\n [k: string]: unknown\n} | null\n\ntype CookieOverride = { applied: boolean; value: string | null }\n// 'error' means auth could not be determined because of a transient/unexpected\n// failure (DB unavailable, connection-pool exhaustion, statement timeout, \u2026) \u2014\n// NOT because the token is genuinely invalid. Callers MUST NOT clear the user's\n// session cookies for 'error'; doing so would log everyone out at once during a\n// shared infrastructure blip (issue #4176).\ntype AuthResolutionStatus = 'authenticated' | 'missing' | 'invalid' | 'error'\ntype AuthResolution = {\n auth: AuthContext\n status: AuthResolutionStatus\n}\n\n// Thrown by `resolveCanonicalInteractiveAuthContext` when the canonical auth\n// lookup itself fails unexpectedly (as opposed to resolving to a genuine\n// \"not authorized\" null). Kept distinct so resolvers can map it to the\n// non-destructive 'error' status instead of the cookie-clearing 'invalid'.\nexport class AuthResolutionUnavailableError extends Error {\n constructor(cause?: unknown) {\n super('Auth resolution unavailable')\n this.name = 'AuthResolutionUnavailableError'\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause\n }\n}\n\n// Symbol-keyed trusted auth context. Set on synthetic Request objects by\n// callers that have already authenticated (e.g. the AI in-process operation\n// runner) so downstream auth resolution short-circuits without re-running\n// cookie/JWT/API-key parsing. The hook is fail-open: if absent the normal\n// resolution path runs unchanged.\nexport const TRUSTED_AUTH_CONTEXT_SYMBOL = Symbol.for('open-mercato.auth.trustedContext')\n\nexport type TrustedAuthContextEnvelope = {\n auth: AuthContext\n status?: AuthResolutionStatus\n}\n\n/**\n * Attach an already-resolved auth context to a synthetic request.\n *\n * INVARIANT callers MUST uphold: `envelope.auth` has to be the product of a gated resolution \u2014\n * `getAuthFromRequest` / `getAuthFromCookies` (or an equivalently validated principal such as an\n * API-key record). The envelope short-circuits `resolveAuthFromRequestDetailed` ahead of both the\n * MFA-pending gate and `resolveCanonicalStaffAuthContext`, so an envelope built straight from an\n * unvalidated JWT would reinstate the MFA bypass this module closes and skip session-revocation\n * checks as well. Never populate it from raw request credentials.\n */\nexport function attachTrustedAuthContext(\n request: Request,\n envelope: TrustedAuthContextEnvelope\n): Request {\n ;(request as unknown as Record<symbol, TrustedAuthContextEnvelope>)[TRUSTED_AUTH_CONTEXT_SYMBOL] = envelope\n return request\n}\n\nfunction readTrustedAuthContext(request: Request): TrustedAuthContextEnvelope | null {\n const carrier = request as unknown as Record<symbol, unknown>\n const envelope = carrier[TRUSTED_AUTH_CONTEXT_SYMBOL]\n if (!envelope || typeof envelope !== 'object') return null\n const candidate = envelope as TrustedAuthContextEnvelope\n if (!('auth' in candidate)) return null\n return candidate\n}\n\nfunction decodeCookieValue(raw: string | undefined): string | null {\n if (raw === undefined) return null\n try {\n const decoded = decodeURIComponent(raw)\n return decoded ?? null\n } catch {\n return raw ?? null\n }\n}\n\nfunction readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\n/**\n * A blank `om_selected_tenant` is \"no selection\", not a deliberate \"no tenant\".\n *\n * Unlike the organization cookie there is no all-tenants sentinel to express, and the tenant\n * selector renders with `includeEmptyOption={false}`, so no UI produces a blank value. Treating it\n * as an applied override nulled `tenantId` for the whole super-admin session, which is what turned\n * a cosmetic client bug into `tenantId ?? ''` reaching a NOT NULL uuid column. Reporting the\n * override as not applied instead keeps the tenant carried in the token and remediates an\n * already-poisoned browser on its next request, whatever wrote the cookie.\n */\nfunction resolveTenantOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded) return { applied: false, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: false, value: null }\n return { applied: true, value: trimmed }\n}\n\nfunction resolveOrganizationOverride(raw: string | undefined): CookieOverride {\n if (raw === undefined) return { applied: false, value: null }\n const decoded = decodeCookieValue(raw)\n if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n const trimmed = decoded.trim()\n if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {\n return { applied: true, value: null }\n }\n return { applied: true, value: trimmed }\n}\n\nfunction isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {\n if (!auth) return false\n return (auth as Record<string, unknown>).isSuperAdmin === true\n}\n\nfunction applySuperAdminScope(\n auth: AuthContext,\n tenantCookie: string | undefined,\n orgCookie: string | undefined\n): AuthContext {\n if (!auth || !isSuperAdminAuth(auth)) return auth\n\n const tenantOverride = resolveTenantOverride(tenantCookie)\n const orgOverride = resolveOrganizationOverride(orgCookie)\n if (!tenantOverride.applied && !orgOverride.applied) return auth\n\n type MutableAuthContext = Exclude<AuthContext, null> & {\n actorTenantId?: string | null\n actorOrgId?: string | null\n }\n const baseAuth = auth as Exclude<AuthContext, null>\n const next: MutableAuthContext = { ...baseAuth }\n if (tenantOverride.applied) {\n if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null\n next.tenantId = tenantOverride.value\n }\n if (orgOverride.applied) {\n if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null\n next.orgId = orgOverride.value\n }\n next.isSuperAdmin = true\n const existingRoles = Array.isArray(next.roles) ? next.roles : []\n if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {\n next.roles = [...existingRoles, 'superadmin']\n }\n return next\n}\n\nasync function resolveApiKeyAuth(secret: string): Promise<AuthContext> {\n if (!secret) return null\n const cache = getSharedApiKeyAuthCache()\n const cached = cache.get(secret)\n if (cached !== undefined) return cached as AuthContext\n try {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')\n const { Role, RoleAcl, User } = await import('@open-mercato/core/modules/auth/data/entities')\n const { Organization, Tenant } = await import('@open-mercato/core/modules/directory/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) {\n cache.setMiss(secret)\n return null\n }\n\n const roleIds = Array.isArray(record.rolesJson)\n ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n const roles = roleIds.length\n ? await em.find(Role, { id: { $in: roleIds }, deletedAt: null })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n // A role-level super-admin grant is authorization-grade only when it is genuinely\n // unrestricted: the grant itself must not be organization-scoped, it must belong to the\n // key's tenant, and the key must not be bound to a single organization. RbacService applies\n // the same intersection when projecting the scoped ACL, but generic guards\n // (tenantAccess.resolveIsSuperAdmin, the scoped-API helpers) trust this raw bit *before*\n // live RBAC runs \u2014 so an organization-restricted key must never carry it.\n let keyIsSuperAdmin = false\n const keyOrganizationId = typeof record.organizationId === 'string' && record.organizationId.trim().length > 0\n ? record.organizationId.trim()\n : null\n if (roleIds.length && !keyOrganizationId) {\n const superAcls = await em.find(\n RoleAcl,\n {\n role: { $in: roleIds } as any,\n tenantId: record.tenantId ?? null,\n isSuperAdmin: true,\n deletedAt: null,\n } as any,\n )\n keyIsSuperAdmin = superAcls.some((acl) => {\n const organizations = Array.isArray((acl as { organizationsJson?: string[] | null }).organizationsJson)\n ? (acl as { organizationsJson?: string[] | null }).organizationsJson as string[]\n : null\n // An empty list means \"inherit the key's own binding\"; with no binding that is tenant-wide.\n return !organizations || organizations.length === 0 || organizations.includes('__all__')\n })\n }\n\n if (cache.shouldWriteLastUsed(record.id)) {\n try {\n record.lastUsedAt = new Date()\n await em.persist(record).flush()\n } catch {\n // best-effort update; ignore write failures\n }\n }\n\n // Ephemeral session keys are always user-bound. Regular keys retain their\n // legacy creator identity, while tenant-scoped regular keys ignore only the\n // creator's concrete organization when validating the wider key scope.\n // Keep every session marker fail-closed so a malformed session key cannot\n // fall back to the regular key path and escape its user/scope binding.\n const isSessionBoundKey = Boolean(\n record.sessionToken\n || record.sessionUserId\n || record.sessionSecretEncrypted\n || record.opencodeSessionId\n )\n const actualUserId = isSessionBoundKey\n ? record.sessionUserId ?? null\n : record.createdBy ?? null\n\n if (isSessionBoundKey && !actualUserId) {\n cache.setMiss(secret)\n return null\n }\n\n if (actualUserId) {\n const user = await em.findOne(User, { id: actualUserId, deletedAt: null })\n if (!user) {\n cache.setMiss(secret)\n return null\n }\n if ((user.tenantId ?? null) !== (record.tenantId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n const requiresExactOrganization = isSessionBoundKey || Boolean(record.organizationId)\n if (requiresExactOrganization && (user.organizationId ?? null) !== (record.organizationId ?? null)) {\n cache.setMiss(secret)\n return null\n }\n } else {\n if (record.tenantId) {\n const tenant = await em.findOne(Tenant, { id: record.tenantId, deletedAt: null, isActive: true })\n if (!tenant) {\n cache.setMiss(secret)\n return null\n }\n }\n if (record.organizationId) {\n const organization = await em.findOne(Organization, { id: record.organizationId, deletedAt: null, isActive: true })\n if (!organization) {\n cache.setMiss(secret)\n return null\n }\n if (record.tenantId && String(organization.tenant.id) !== record.tenantId) {\n cache.setMiss(secret)\n return null\n }\n }\n }\n\n const auth: Exclude<AuthContext, null> = {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n isSuperAdmin: keyIsSuperAdmin,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\n cache.setSuccess(secret, auth, record.expiresAt ? record.expiresAt.getTime() : null)\n return auth\n } catch (err) {\n // A transient DB failure (pool exhausted, `max_connections` reached, DB\n // restarting) means we could not confirm the key \u2014 NOT that it is invalid.\n // Surface it so callers return a retryable 503 instead of masking it as an\n // auth miss (401). Genuine misses already returned `null` above.\n if (isTransientDbError(err)) {\n throw new AuthResolutionUnavailableError(err)\n }\n return null\n }\n}\n\nfunction extractApiKey(req: Request): string | null {\n const header = (req.headers.get('x-api-key') || '').trim()\n if (header) return header\n const authHeader = (req.headers.get('authorization') || '').trim()\n if (authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n return null\n}\n\nasync function resolveCanonicalInteractiveAuthContext(auth: AuthContext): Promise<AuthContext> {\n if (!auth || auth.isApiKey) return auth\n if (typeof auth.sub !== 'string' || auth.sub.trim().length === 0) return null\n\n try {\n const [{ createRequestContainer }, { resolveCanonicalStaffAuthContext }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/auth/lib/sessionIntegrity'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n return await resolveCanonicalStaffAuthContext(em, auth)\n } catch (err) {\n // A thrown error here means we could not check the token against the\n // database (DB down, pool exhausted, timeout). This is NOT the same as the\n // token being invalid \u2014 surface it so callers keep the session intact and\n // fail the request transiently instead of force-logging the user out.\n throw new AuthResolutionUnavailableError(err)\n }\n}\n\nexport async function resolveAuthFromCookiesDetailed(): Promise<AuthResolution> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return { auth: null, status: 'missing' }\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return { auth: null, status: 'invalid' }\n if (payload.type === 'customer') return { auth: null, status: 'invalid' }\n // MFA-pending tokens are provisional: pages never complete the second factor, so they\n // must not resolve to an authenticated context anywhere in the RSC/page flow.\n if (isMfaPendingJwtPayload(payload)) return { auth: null, status: 'invalid' }\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (!canonicalAuth) return { auth: null, status: 'invalid' }\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n } catch (err) {\n if (err instanceof AuthResolutionUnavailableError) return { auth: null, status: 'error' }\n return { auth: null, status: 'invalid' }\n }\n}\n\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n return (await resolveAuthFromCookiesDetailed()).auth\n}\n\nexport async function resolveAuthFromRequestDetailed(req: Request): Promise<AuthResolution> {\n // Deliberately ahead of the MFA-pending gate below: the envelope carries a context that was\n // already gated by its producer (see `attachTrustedAuthContext`), not raw request credentials.\n const trusted = readTrustedAuthContext(req)\n if (trusted) {\n return {\n auth: trusted.auth,\n status: trusted.status ?? (trusted.auth ? 'authenticated' : 'missing'),\n }\n }\n const cookieHeader = req.headers.get('cookie') || ''\n const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)\n const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)\n const authHeader = (req.headers.get('authorization') || '').trim()\n let token: string | undefined\n let hadInvalidInteractiveToken = false\n let hadUnavailableResolution = false\n if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()\n if (!token) {\n const match = cookieHeader.match(/(?:^|;\\s*)auth_token=([^;]+)/)\n if (match) token = decodeURIComponent(match[1])\n }\n if (token) {\n try {\n const payload = verifyJwt(token) as AuthContext\n if (payload && payload.type === 'customer') return { auth: null, status: 'invalid' }\n if (payload && isMfaPendingJwtPayload(payload)) {\n // MFA-pending tokens authenticate only the registered challenge-completion routes;\n // everywhere else they resolve to `invalid` so the dispatcher answers 401 (clearing\n // staff auth cookies) instead of restoring the account's roles/features.\n if (!isMfaPendingAccessAllowed(req.method, readRequestPathname(req))) {\n return { auth: null, status: 'invalid' }\n }\n }\n if (payload) {\n const canonicalAuth = await resolveCanonicalInteractiveAuthContext(payload)\n if (canonicalAuth) {\n return {\n auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n }\n hadInvalidInteractiveToken = true\n }\n } catch (err) {\n // Transient/unexpected canonical-resolution failures must not invalidate\n // the token \u2014 surface them so we return 'error' (retryable) rather than\n // clearing the session cookies.\n if (err instanceof AuthResolutionUnavailableError) hadUnavailableResolution = true\n else hadInvalidInteractiveToken = true\n }\n }\n\n const resolveUnauthenticatedStatus = (): AuthResolutionStatus =>\n hadUnavailableResolution ? 'error' : hadInvalidInteractiveToken ? 'invalid' : 'missing'\n\n const apiKey = extractApiKey(req)\n if (!apiKey) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n let apiAuth: AuthContext\n try {\n apiAuth = await resolveApiKeyAuth(apiKey)\n } catch (err) {\n // Only a transient canonical-resolution failure maps to 'error' (retryable\n // 503), mirroring the interactive-token path above. Anything else is an\n // unexpected bug \u2014 rethrow so it surfaces as a 500 rather than being masked\n // as an auth failure.\n if (!(err instanceof AuthResolutionUnavailableError)) {\n throw err\n }\n hadUnavailableResolution = true\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n if (!apiAuth) {\n return { auth: null, status: resolveUnauthenticatedStatus() }\n }\n return {\n auth: applySuperAdminScope(apiAuth, tenantCookie, orgCookie),\n status: 'authenticated',\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\n return (await resolveAuthFromRequestDetailed(req)).auth\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -471,6 +471,7 @@ class CommandBus {
|
|
|
471
471
|
const tenantId = metadata.tenantId ?? options.ctx.auth?.tenantId ?? null;
|
|
472
472
|
const organizationId = metadata.organizationId ?? options.ctx.selectedOrganizationId ?? options.ctx.auth?.orgId ?? null;
|
|
473
473
|
const actorUserId = metadata.actorUserId ?? options.ctx.auth?.sub ?? null;
|
|
474
|
+
const systemActorContext = !actorUserId && options.ctx.systemActor === true ? { systemActor: "system:command" } : null;
|
|
474
475
|
const payload = {
|
|
475
476
|
tenantId: tenantId ?? void 0,
|
|
476
477
|
organizationId: organizationId ?? void 0,
|
|
@@ -490,7 +491,11 @@ class CommandBus {
|
|
|
490
491
|
if ("snapshotBefore" in metadata && metadata.snapshotBefore !== void 0) payload.snapshotBefore = metadata.snapshotBefore;
|
|
491
492
|
if ("snapshotAfter" in metadata && metadata.snapshotAfter !== void 0) payload.snapshotAfter = metadata.snapshotAfter;
|
|
492
493
|
if ("changes" in metadata && metadata.changes !== void 0 && metadata.changes !== null) payload.changes = metadata.changes;
|
|
493
|
-
if ("context" in metadata && metadata.context !== void 0 && metadata.context !== null)
|
|
494
|
+
if ("context" in metadata && metadata.context !== void 0 && metadata.context !== null) {
|
|
495
|
+
payload.context = { ...systemActorContext ?? {}, ...metadata.context };
|
|
496
|
+
} else if (systemActorContext) {
|
|
497
|
+
payload.context = systemActorContext;
|
|
498
|
+
}
|
|
494
499
|
}
|
|
495
500
|
const redoEnvelope = wrapRedoPayload("commandPayload" in payload ? payload.commandPayload : void 0, options.input);
|
|
496
501
|
payload.commandPayload = redoEnvelope;
|