@open-mercato/shared 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a
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 +2 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/crud/factory.js +10 -0
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +37 -15
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/package.json +2 -2
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +12 -0
- package/src/lib/crud/factory.ts +13 -0
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +48 -16
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/encryption.ts +3 -0
- package/src/modules/registry.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 257 entry points
|
|
2
2
|
[build:shared] built successfully
|
package/AGENTS.md
CHANGED
|
@@ -91,6 +91,8 @@ import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/
|
|
|
91
91
|
const results = await findWithDecryption(em, 'Entity', filter, { tenantId, organizationId })
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
Encryption maps default to tenant-scoped keys. Use the additive `keyScope: 'system'` option only for records that must exist before a tenant does; the system scope remains authoritative after a tenant id is later assigned so existing ciphertext stays readable.
|
|
95
|
+
|
|
94
96
|
### Search Tokens — MUST use instead of `$ilike` on encrypted columns
|
|
95
97
|
|
|
96
98
|
```typescript
|
package/dist/lib/auth/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cookies } from "next/headers.js";
|
|
2
2
|
import { verifyJwt } from "./jwt.js";
|
|
3
3
|
import { getSharedApiKeyAuthCache } from "./apiKeyAuthCache.js";
|
|
4
|
+
import { isTransientDbError } from "@open-mercato/shared/lib/db/pg-errors";
|
|
4
5
|
const TENANT_COOKIE_NAME = "om_selected_tenant";
|
|
5
6
|
const ORGANIZATION_COOKIE_NAME = "om_selected_org";
|
|
6
7
|
const ALL_ORGANIZATIONS_COOKIE_VALUE = "__all__";
|
|
@@ -175,7 +176,10 @@ async function resolveApiKeyAuth(secret) {
|
|
|
175
176
|
};
|
|
176
177
|
cache.setSuccess(secret, auth, record.expiresAt ? record.expiresAt.getTime() : null);
|
|
177
178
|
return auth;
|
|
178
|
-
} catch {
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (isTransientDbError(err)) {
|
|
181
|
+
throw new AuthResolutionUnavailableError(err);
|
|
182
|
+
}
|
|
179
183
|
return null;
|
|
180
184
|
}
|
|
181
185
|
}
|
|
@@ -271,7 +275,16 @@ async function resolveAuthFromRequestDetailed(req) {
|
|
|
271
275
|
if (!apiKey) {
|
|
272
276
|
return { auth: null, status: resolveUnauthenticatedStatus() };
|
|
273
277
|
}
|
|
274
|
-
|
|
278
|
+
let apiAuth;
|
|
279
|
+
try {
|
|
280
|
+
apiAuth = await resolveApiKeyAuth(apiKey);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (!(err instanceof AuthResolutionUnavailableError)) {
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
hadUnavailableResolution = true;
|
|
286
|
+
return { auth: null, status: resolveUnauthenticatedStatus() };
|
|
287
|
+
}
|
|
275
288
|
if (!apiAuth) {
|
|
276
289
|
return { auth: null, status: resolveUnauthenticatedStatus() };
|
|
277
290
|
}
|
|
@@ -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'\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\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: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, 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 {\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 const apiAuth = await resolveApiKeyAuth(apiKey)\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;AAC1B,SAAS,gCAAgC;
|
|
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\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: true, value: null }\n const trimmed = decoded.trim()\n if (!trimmed) return { applied: true, 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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/crud/factory.js
CHANGED
|
@@ -52,6 +52,7 @@ import { parseExtensionHeaders } from "../umes/extension-headers.js";
|
|
|
52
52
|
import { createGenericOptimisticLockReader } from "./optimistic-lock.js";
|
|
53
53
|
import { registerOptimisticLockReaderIfAbsent } from "./optimistic-lock-store.js";
|
|
54
54
|
import { createLogger } from "../logger/index.js";
|
|
55
|
+
import { isTransientDbError } from "../db/pg-errors.js";
|
|
55
56
|
const logger = createLogger("shared").child({ component: "crud" });
|
|
56
57
|
function resolveSortParams(queryParams) {
|
|
57
58
|
const rawSortField = queryParams.sortField ?? queryParams.sort ?? "id";
|
|
@@ -297,6 +298,15 @@ function handleError(err) {
|
|
|
297
298
|
if (err instanceof Response) return err;
|
|
298
299
|
if (isCrudHttpError(err)) return json(err.body, { status: err.status });
|
|
299
300
|
if (err instanceof z.ZodError) return json({ error: "Invalid input", details: err.issues }, { status: 400 });
|
|
301
|
+
if (isTransientDbError(err)) {
|
|
302
|
+
logger.warn("Transient DB failure during CRUD handler", {
|
|
303
|
+
message: err instanceof Error ? err.message : void 0
|
|
304
|
+
});
|
|
305
|
+
return json(
|
|
306
|
+
{ error: "Service temporarily unavailable" },
|
|
307
|
+
{ status: 503, headers: { "Retry-After": "2" } }
|
|
308
|
+
);
|
|
309
|
+
}
|
|
300
310
|
const message = err instanceof Error ? err.message : void 0;
|
|
301
311
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
302
312
|
logger.error("Unexpected CRUD error", { message, stack, err });
|