@open-mercato/shared 0.4.8-canary-8502c817af → 0.4.8-develop-4e71d95aba

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.
@@ -1,4 +1,4 @@
1
- import { cookies } from "next/headers.js";
1
+ import { cookies } from "next/headers";
2
2
  import { verifyJwt } from "./jwt.js";
3
3
  const TENANT_COOKIE_NAME = "om_selected_tenant";
4
4
  const ORGANIZATION_COOKIE_NAME = "om_selected_org";
@@ -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'\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 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\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 if ((auth as Record<string, unknown>).isSuperAdmin === true) return true\n const roles = Array.isArray(auth?.roles) ? auth.roles : []\n return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)\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 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 } = await import('@open-mercato/core/modules/auth/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) return null\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 } })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n try {\n record.lastUsedAt = new Date()\n await em.persistAndFlush(record)\n } catch {\n // best-effort update; ignore write failures\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n return {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\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\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return null\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return null\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n return null\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\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 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) return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n // fall back to API key detection\n }\n }\n\n const apiKey = extractApiKey(req)\n if (!apiKey) return null\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) return null\n return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)\n}\n"],
4
+ "sourcesContent": ["import { cookies } from 'next/headers'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { verifyJwt } from './jwt'\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 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\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 if ((auth as Record<string, unknown>).isSuperAdmin === true) return true\n const roles = Array.isArray(auth?.roles) ? auth.roles : []\n return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)\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 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 } = await import('@open-mercato/core/modules/auth/data/entities')\n\n const record = await findApiKeyBySecret(em, secret)\n if (!record) return null\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 } })\n : []\n const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)\n\n try {\n record.lastUsedAt = new Date()\n await em.persistAndFlush(record)\n } catch {\n // best-effort update; ignore write failures\n }\n\n // For session keys, use sessionUserId; for regular keys, use createdBy\n const actualUserId = record.sessionUserId ?? record.createdBy ?? null\n\n return {\n sub: `api_key:${record.id}`,\n tenantId: record.tenantId ?? null,\n orgId: record.organizationId ?? null,\n roles: roleNames,\n isApiKey: true,\n keyId: record.id,\n keyName: record.name,\n ...(actualUserId ? { userId: actualUserId } : {}),\n }\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\nexport async function getAuthFromCookies(): Promise<AuthContext> {\n const cookieStore = await cookies()\n const token = cookieStore.get('auth_token')?.value\n if (!token) return null\n try {\n const payload = verifyJwt(token) as AuthContext\n if (!payload) return null\n const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value\n const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value\n return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n return null\n }\n}\n\nexport async function getAuthFromRequest(req: Request): Promise<AuthContext> {\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 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) return applySuperAdminScope(payload, tenantCookie, orgCookie)\n } catch {\n // fall back to API key detection\n }\n }\n\n const apiKey = extractApiKey(req)\n if (!apiKey) return null\n const apiAuth = await resolveApiKeyAuth(apiKey)\n if (!apiAuth) return null\n return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.4.8-canary-8502c817af";
1
+ const APP_VERSION = "0.4.8-develop-4e71d95aba";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.8-canary-8502c817af'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.8-develop-4e71d95aba'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -1,16 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { hasAllFeatures } from "../../security/features.js";
3
3
  const GLOBAL_COMPONENT_REGISTRY_KEY = "__openMercatoComponentRegistry__";
4
- function isComponentOverride(value) {
5
- if (!value || typeof value !== "object") {
6
- return false;
7
- }
8
- const target = value.target;
9
- if (!target || typeof target !== "object") {
10
- return false;
11
- }
12
- return typeof target.componentId === "string" && target.componentId.length > 0;
13
- }
14
4
  function getState() {
15
5
  const globalValue = globalThis[GLOBAL_COMPONENT_REGISTRY_KEY];
16
6
  if (globalValue && typeof globalValue === "object") {
@@ -32,7 +22,7 @@ function registerComponent(entry) {
32
22
  }
33
23
  function registerComponentOverrides(overrides) {
34
24
  const state = getState();
35
- state.overrides = overrides.filter(isComponentOverride);
25
+ state.overrides = [...overrides];
36
26
  }
37
27
  function getComponentEntry(componentId) {
38
28
  const state = getState();
@@ -41,7 +31,6 @@ function getComponentEntry(componentId) {
41
31
  function getComponentOverrides(componentId, userFeatures) {
42
32
  const state = getState();
43
33
  const relevant = state.overrides.filter((override) => {
44
- if (!isComponentOverride(override)) return false;
45
34
  if (override.target.componentId !== componentId) return false;
46
35
  if (override.features && override.features.length > 0) {
47
36
  if (!hasAllFeatures(userFeatures, override.features)) return false;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/widgets/component-registry.ts"],
4
- "sourcesContent": ["import * as React from 'react'\nimport type { ComponentType, LazyExoticComponent } from 'react'\nimport type { ZodType } from 'zod'\nimport { hasAllFeatures } from '../../security/features'\n\nexport type ComponentRegistryEntry<TProps = unknown> = {\n id: string\n component: ComponentType<TProps>\n metadata: {\n module: string\n description?: string\n propsSchema?: ZodType<TProps>\n }\n}\n\nexport type ComponentOverride<TProps = unknown> = {\n target: { componentId: string }\n priority: number\n features?: string[]\n metadata?: {\n module?: string\n }\n} & (\n | {\n replacement: LazyExoticComponent<ComponentType<TProps>> | ComponentType<TProps>\n propsSchema: ZodType<TProps>\n }\n | {\n wrapper: (Original: ComponentType<TProps>) => ComponentType<TProps>\n }\n | {\n propsTransform: (props: TProps) => TProps\n }\n)\n\ntype RuntimeState = {\n components: Map<string, ComponentRegistryEntry>\n overrides: ComponentOverride[]\n}\n\nconst GLOBAL_COMPONENT_REGISTRY_KEY = '__openMercatoComponentRegistry__'\n\nfunction isComponentOverride(value: unknown): value is ComponentOverride {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n const target = (value as { target?: { componentId?: unknown } }).target\n if (!target || typeof target !== 'object') {\n return false\n }\n\n return typeof target.componentId === 'string' && target.componentId.length > 0\n}\n\nfunction getState(): RuntimeState {\n const globalValue = (globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY]\n if (globalValue && typeof globalValue === 'object') {\n const typed = globalValue as RuntimeState\n if (typed.components instanceof Map && Array.isArray(typed.overrides)) {\n return typed\n }\n }\n const initial: RuntimeState = {\n components: new Map<string, ComponentRegistryEntry>(),\n overrides: [],\n }\n ;(globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY] = initial\n return initial\n}\n\nexport function registerComponent<TProps = unknown>(entry: ComponentRegistryEntry<TProps>) {\n const state = getState()\n state.components.set(entry.id, entry as ComponentRegistryEntry)\n}\n\nexport function registerComponentOverrides(overrides: ComponentOverride[]) {\n const state = getState()\n state.overrides = overrides.filter(isComponentOverride)\n}\n\nexport function getComponentEntry(componentId: string): ComponentRegistryEntry | null {\n const state = getState()\n return state.components.get(componentId) ?? null\n}\n\nexport function getComponentOverrides(componentId: string, userFeatures?: readonly string[]): ComponentOverride[] {\n const state = getState()\n const relevant = state.overrides.filter((override) => {\n if (!isComponentOverride(override)) return false\n if (override.target.componentId !== componentId) return false\n if (override.features && override.features.length > 0) {\n if (!hasAllFeatures(userFeatures, override.features)) return false\n }\n return true\n })\n return relevant.sort((a, b) => a.priority - b.priority)\n}\n\nexport function resolveRegisteredComponent<TProps>(\n componentId: string,\n fallback: ComponentType<TProps>,\n userFeatures?: readonly string[],\n): ComponentType<TProps> {\n const overrides = getComponentOverrides(componentId, userFeatures)\n let resolved: ComponentType<TProps> = fallback\n for (const override of overrides) {\n if ('replacement' in override) {\n resolved = override.replacement as ComponentType<TProps>\n continue\n }\n if ('wrapper' in override) {\n resolved = override.wrapper(resolved as ComponentType<unknown>) as ComponentType<TProps>\n continue\n }\n if ('propsTransform' in override) {\n const transform = override.propsTransform as (props: TProps) => TProps\n const Current = resolved\n resolved = ((props: TProps) => {\n const transformed = transform(props)\n return React.createElement(Current as ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)\n }) as ComponentType<TProps>\n }\n }\n return resolved\n}\n\nexport const ComponentReplacementHandles = {\n page: (path: string) => `page:${path}`,\n dataTable: (tableId: string) => `data-table:${tableId}`,\n crudForm: (entityId: string) => `crud-form:${entityId}`,\n section: (scope: string, sectionId: string) => `section:${scope}.${sectionId}`,\n} as const\n"],
5
- "mappings": "AAAA,YAAY,WAAW;AAGvB,SAAS,sBAAsB;AAqC/B,MAAM,gCAAgC;AAEtC,SAAS,oBAAoB,OAA4C;AACvE,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,SAAU,MAAiD;AACjE,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,SAAS;AAC/E;AAEA,SAAS,WAAyB;AAChC,QAAM,cAAe,WAAuC,6BAA6B;AACzF,MAAI,eAAe,OAAO,gBAAgB,UAAU;AAClD,UAAM,QAAQ;AACd,QAAI,MAAM,sBAAsB,OAAO,MAAM,QAAQ,MAAM,SAAS,GAAG;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAwB;AAAA,IAC5B,YAAY,oBAAI,IAAoC;AAAA,IACpD,WAAW,CAAC;AAAA,EACd;AACC,EAAC,WAAuC,6BAA6B,IAAI;AAC1E,SAAO;AACT;AAEO,SAAS,kBAAoC,OAAuC;AACzF,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,IAAI,MAAM,IAAI,KAA+B;AAChE;AAEO,SAAS,2BAA2B,WAAgC;AACzE,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,UAAU,OAAO,mBAAmB;AACxD;AAEO,SAAS,kBAAkB,aAAoD;AACpF,QAAM,QAAQ,SAAS;AACvB,SAAO,MAAM,WAAW,IAAI,WAAW,KAAK;AAC9C;AAEO,SAAS,sBAAsB,aAAqB,cAAuD;AAChH,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,MAAM,UAAU,OAAO,CAAC,aAAa;AACpD,QAAI,CAAC,oBAAoB,QAAQ,EAAG,QAAO;AAC3C,QAAI,SAAS,OAAO,gBAAgB,YAAa,QAAO;AACxD,QAAI,SAAS,YAAY,SAAS,SAAS,SAAS,GAAG;AACrD,UAAI,CAAC,eAAe,cAAc,SAAS,QAAQ,EAAG,QAAO;AAAA,IAC/D;AACA,WAAO;AAAA,EACT,CAAC;AACD,SAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACxD;AAEO,SAAS,2BACd,aACA,UACA,cACuB;AACvB,QAAM,YAAY,sBAAsB,aAAa,YAAY;AACjE,MAAI,WAAkC;AACtC,aAAW,YAAY,WAAW;AAChC,QAAI,iBAAiB,UAAU;AAC7B,iBAAW,SAAS;AACpB;AAAA,IACF;AACA,QAAI,aAAa,UAAU;AACzB,iBAAW,SAAS,QAAQ,QAAkC;AAC9D;AAAA,IACF;AACA,QAAI,oBAAoB,UAAU;AAChC,YAAM,YAAY,SAAS;AAC3B,YAAM,UAAU;AAChB,kBAAY,CAAC,UAAkB;AAC7B,cAAM,cAAc,UAAU,KAAK;AACnC,eAAO,MAAM,cAAc,SAAmD,WAAsC;AAAA,MACtH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,8BAA8B;AAAA,EACzC,MAAM,CAAC,SAAiB,QAAQ,IAAI;AAAA,EACpC,WAAW,CAAC,YAAoB,cAAc,OAAO;AAAA,EACrD,UAAU,CAAC,aAAqB,aAAa,QAAQ;AAAA,EACrD,SAAS,CAAC,OAAe,cAAsB,WAAW,KAAK,IAAI,SAAS;AAC9E;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { ComponentType, LazyExoticComponent } from 'react'\nimport type { ZodType } from 'zod'\nimport { hasAllFeatures } from '../../security/features'\n\nexport type ComponentRegistryEntry<TProps = unknown> = {\n id: string\n component: ComponentType<TProps>\n metadata: {\n module: string\n description?: string\n propsSchema?: ZodType<TProps>\n }\n}\n\nexport type ComponentOverride<TProps = unknown> = {\n target: { componentId: string }\n priority: number\n features?: string[]\n metadata?: {\n module?: string\n }\n} & (\n | {\n replacement: LazyExoticComponent<ComponentType<TProps>> | ComponentType<TProps>\n propsSchema: ZodType<TProps>\n }\n | {\n wrapper: (Original: ComponentType<TProps>) => ComponentType<TProps>\n }\n | {\n propsTransform: (props: TProps) => TProps\n }\n)\n\ntype RuntimeState = {\n components: Map<string, ComponentRegistryEntry>\n overrides: ComponentOverride[]\n}\n\nconst GLOBAL_COMPONENT_REGISTRY_KEY = '__openMercatoComponentRegistry__'\n\nfunction getState(): RuntimeState {\n const globalValue = (globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY]\n if (globalValue && typeof globalValue === 'object') {\n const typed = globalValue as RuntimeState\n if (typed.components instanceof Map && Array.isArray(typed.overrides)) {\n return typed\n }\n }\n const initial: RuntimeState = {\n components: new Map<string, ComponentRegistryEntry>(),\n overrides: [],\n }\n ;(globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY] = initial\n return initial\n}\n\nexport function registerComponent<TProps = unknown>(entry: ComponentRegistryEntry<TProps>) {\n const state = getState()\n state.components.set(entry.id, entry as ComponentRegistryEntry)\n}\n\nexport function registerComponentOverrides(overrides: ComponentOverride[]) {\n const state = getState()\n state.overrides = [...overrides]\n}\n\nexport function getComponentEntry(componentId: string): ComponentRegistryEntry | null {\n const state = getState()\n return state.components.get(componentId) ?? null\n}\n\nexport function getComponentOverrides(componentId: string, userFeatures?: readonly string[]): ComponentOverride[] {\n const state = getState()\n const relevant = state.overrides.filter((override) => {\n if (override.target.componentId !== componentId) return false\n if (override.features && override.features.length > 0) {\n if (!hasAllFeatures(userFeatures, override.features)) return false\n }\n return true\n })\n return relevant.sort((a, b) => a.priority - b.priority)\n}\n\nexport function resolveRegisteredComponent<TProps>(\n componentId: string,\n fallback: ComponentType<TProps>,\n userFeatures?: readonly string[],\n): ComponentType<TProps> {\n const overrides = getComponentOverrides(componentId, userFeatures)\n let resolved: ComponentType<TProps> = fallback\n for (const override of overrides) {\n if ('replacement' in override) {\n resolved = override.replacement as ComponentType<TProps>\n continue\n }\n if ('wrapper' in override) {\n resolved = override.wrapper(resolved as ComponentType<unknown>) as ComponentType<TProps>\n continue\n }\n if ('propsTransform' in override) {\n const transform = override.propsTransform as (props: TProps) => TProps\n const Current = resolved\n resolved = ((props: TProps) => {\n const transformed = transform(props)\n return React.createElement(Current as ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)\n }) as ComponentType<TProps>\n }\n }\n return resolved\n}\n\nexport const ComponentReplacementHandles = {\n page: (path: string) => `page:${path}`,\n dataTable: (tableId: string) => `data-table:${tableId}`,\n crudForm: (entityId: string) => `crud-form:${entityId}`,\n section: (scope: string, sectionId: string) => `section:${scope}.${sectionId}`,\n} as const\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;AAGvB,SAAS,sBAAsB;AAqC/B,MAAM,gCAAgC;AAEtC,SAAS,WAAyB;AAChC,QAAM,cAAe,WAAuC,6BAA6B;AACzF,MAAI,eAAe,OAAO,gBAAgB,UAAU;AAClD,UAAM,QAAQ;AACd,QAAI,MAAM,sBAAsB,OAAO,MAAM,QAAQ,MAAM,SAAS,GAAG;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAwB;AAAA,IAC5B,YAAY,oBAAI,IAAoC;AAAA,IACpD,WAAW,CAAC;AAAA,EACd;AACC,EAAC,WAAuC,6BAA6B,IAAI;AAC1E,SAAO;AACT;AAEO,SAAS,kBAAoC,OAAuC;AACzF,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,IAAI,MAAM,IAAI,KAA+B;AAChE;AAEO,SAAS,2BAA2B,WAAgC;AACzE,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,CAAC,GAAG,SAAS;AACjC;AAEO,SAAS,kBAAkB,aAAoD;AACpF,QAAM,QAAQ,SAAS;AACvB,SAAO,MAAM,WAAW,IAAI,WAAW,KAAK;AAC9C;AAEO,SAAS,sBAAsB,aAAqB,cAAuD;AAChH,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,MAAM,UAAU,OAAO,CAAC,aAAa;AACpD,QAAI,SAAS,OAAO,gBAAgB,YAAa,QAAO;AACxD,QAAI,SAAS,YAAY,SAAS,SAAS,SAAS,GAAG;AACrD,UAAI,CAAC,eAAe,cAAc,SAAS,QAAQ,EAAG,QAAO;AAAA,IAC/D;AACA,WAAO;AAAA,EACT,CAAC;AACD,SAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACxD;AAEO,SAAS,2BACd,aACA,UACA,cACuB;AACvB,QAAM,YAAY,sBAAsB,aAAa,YAAY;AACjE,MAAI,WAAkC;AACtC,aAAW,YAAY,WAAW;AAChC,QAAI,iBAAiB,UAAU;AAC7B,iBAAW,SAAS;AACpB;AAAA,IACF;AACA,QAAI,aAAa,UAAU;AACzB,iBAAW,SAAS,QAAQ,QAAkC;AAC9D;AAAA,IACF;AACA,QAAI,oBAAoB,UAAU;AAChC,YAAM,YAAY,SAAS;AAC3B,YAAM,UAAU;AAChB,kBAAY,CAAC,UAAkB;AAC7B,cAAM,cAAc,UAAU,KAAK;AACnC,eAAO,MAAM,cAAc,SAAmD,WAAsC;AAAA,MACtH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,8BAA8B;AAAA,EACzC,MAAM,CAAC,SAAiB,QAAQ,IAAI;AAAA,EACpC,WAAW,CAAC,YAAoB,cAAc,OAAO;AAAA,EACrD,UAAU,CAAC,aAAqB,aAAa,QAAQ;AAAA,EACrD,SAAS,CAAC,OAAe,cAAsB,WAAW,KAAK,IAAI,SAAS;AAC9E;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.4.8-canary-8502c817af",
3
+ "version": "0.4.8-develop-4e71d95aba",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import { cookies } from 'next/headers.js'
1
+ import { cookies } from 'next/headers'
2
2
  import type { EntityManager } from '@mikro-orm/postgresql'
3
3
  import { verifyJwt } from './jwt'
4
4
 
@@ -40,19 +40,6 @@ type RuntimeState = {
40
40
 
41
41
  const GLOBAL_COMPONENT_REGISTRY_KEY = '__openMercatoComponentRegistry__'
42
42
 
43
- function isComponentOverride(value: unknown): value is ComponentOverride {
44
- if (!value || typeof value !== 'object') {
45
- return false
46
- }
47
-
48
- const target = (value as { target?: { componentId?: unknown } }).target
49
- if (!target || typeof target !== 'object') {
50
- return false
51
- }
52
-
53
- return typeof target.componentId === 'string' && target.componentId.length > 0
54
- }
55
-
56
43
  function getState(): RuntimeState {
57
44
  const globalValue = (globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY]
58
45
  if (globalValue && typeof globalValue === 'object') {
@@ -76,7 +63,7 @@ export function registerComponent<TProps = unknown>(entry: ComponentRegistryEntr
76
63
 
77
64
  export function registerComponentOverrides(overrides: ComponentOverride[]) {
78
65
  const state = getState()
79
- state.overrides = overrides.filter(isComponentOverride)
66
+ state.overrides = [...overrides]
80
67
  }
81
68
 
82
69
  export function getComponentEntry(componentId: string): ComponentRegistryEntry | null {
@@ -87,7 +74,6 @@ export function getComponentEntry(componentId: string): ComponentRegistryEntry |
87
74
  export function getComponentOverrides(componentId: string, userFeatures?: readonly string[]): ComponentOverride[] {
88
75
  const state = getState()
89
76
  const relevant = state.overrides.filter((override) => {
90
- if (!isComponentOverride(override)) return false
91
77
  if (override.target.componentId !== componentId) return false
92
78
  if (override.features && override.features.length > 0) {
93
79
  if (!hasAllFeatures(userFeatures, override.features)) return false
@@ -1,27 +0,0 @@
1
- import { runApiInterceptorsAfter } from "./interceptor-runner.js";
2
- function normalizeIdentity(value) {
3
- if (!value) return "";
4
- return value;
5
- }
6
- async function runCustomRouteAfterInterceptors(args) {
7
- return runApiInterceptorsAfter({
8
- routePath: args.routePath,
9
- method: args.method,
10
- request: args.request,
11
- response: args.response,
12
- context: {
13
- em: args.context.em,
14
- container: args.context.container,
15
- userId: normalizeIdentity(args.context.userId),
16
- organizationId: normalizeIdentity(args.context.organizationId),
17
- tenantId: normalizeIdentity(args.context.tenantId),
18
- userFeatures: args.context.userFeatures ?? [],
19
- extensionHeaders: args.context.extensionHeaders
20
- },
21
- metadataByInterceptor: args.metadataByInterceptor
22
- });
23
- }
24
- export {
25
- runCustomRouteAfterInterceptors
26
- };
27
- //# sourceMappingURL=custom-route-interceptor.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/crud/custom-route-interceptor.ts"],
4
- "sourcesContent": ["import type { InterceptorContext, InterceptorRequest, InterceptorResponse, ApiInterceptorMethod } from './api-interceptor'\nimport { runApiInterceptorsAfter, type RunInterceptorsAfterResult } from './interceptor-runner'\n\nexport type CustomRouteAfterInterceptorContext = {\n em: InterceptorContext['em']\n container: InterceptorContext['container']\n userId?: string | null\n organizationId?: string | null\n tenantId?: string | null\n userFeatures?: string[]\n extensionHeaders?: InterceptorContext['extensionHeaders']\n}\n\ntype RunCustomRouteAfterInterceptorsArgs = {\n routePath: string\n method: ApiInterceptorMethod\n request: InterceptorRequest\n response: InterceptorResponse\n context: CustomRouteAfterInterceptorContext\n metadataByInterceptor?: Record<string, Record<string, unknown> | undefined>\n}\n\nfunction normalizeIdentity(value: string | null | undefined): string {\n if (!value) return ''\n return value\n}\n\nexport async function runCustomRouteAfterInterceptors(\n args: RunCustomRouteAfterInterceptorsArgs,\n): Promise<RunInterceptorsAfterResult> {\n return runApiInterceptorsAfter({\n routePath: args.routePath,\n method: args.method,\n request: args.request,\n response: args.response,\n context: {\n em: args.context.em,\n container: args.context.container,\n userId: normalizeIdentity(args.context.userId),\n organizationId: normalizeIdentity(args.context.organizationId),\n tenantId: normalizeIdentity(args.context.tenantId),\n userFeatures: args.context.userFeatures ?? [],\n extensionHeaders: args.context.extensionHeaders,\n },\n metadataByInterceptor: args.metadataByInterceptor,\n })\n}\n"],
5
- "mappings": "AACA,SAAS,+BAAgE;AAqBzE,SAAS,kBAAkB,OAA0C;AACnE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO;AACT;AAEA,eAAsB,gCACpB,MACqC;AACrC,SAAO,wBAAwB;AAAA,IAC7B,WAAW,KAAK;AAAA,IAChB,QAAQ,KAAK;AAAA,IACb,SAAS,KAAK;AAAA,IACd,UAAU,KAAK;AAAA,IACf,SAAS;AAAA,MACP,IAAI,KAAK,QAAQ;AAAA,MACjB,WAAW,KAAK,QAAQ;AAAA,MACxB,QAAQ,kBAAkB,KAAK,QAAQ,MAAM;AAAA,MAC7C,gBAAgB,kBAAkB,KAAK,QAAQ,cAAc;AAAA,MAC7D,UAAU,kBAAkB,KAAK,QAAQ,QAAQ;AAAA,MACjD,cAAc,KAAK,QAAQ,gBAAgB,CAAC;AAAA,MAC5C,kBAAkB,KAAK,QAAQ;AAAA,IACjC;AAAA,IACA,uBAAuB,KAAK;AAAA,EAC9B,CAAC;AACH;",
6
- "names": []
7
- }
@@ -1,42 +0,0 @@
1
- import { CONTINUE_PAGE_MIDDLEWARE, matchPageMiddlewareTarget } from "@open-mercato/shared/modules/middleware/page";
2
- const DEFAULT_PRIORITY = 100;
3
- function shouldRunMiddleware(middleware, mode, pathname) {
4
- if (middleware.mode && middleware.mode !== mode) return false;
5
- return matchPageMiddlewareTarget(pathname, middleware.target);
6
- }
7
- function compareMiddleware(a, b) {
8
- const priorityDiff = (a.priority ?? DEFAULT_PRIORITY) - (b.priority ?? DEFAULT_PRIORITY);
9
- if (priorityDiff !== 0) return priorityDiff;
10
- return a.id.localeCompare(b.id);
11
- }
12
- function flattenAndSortMiddleware(entries, mode, pathname) {
13
- return entries.flatMap((entry) => entry.middleware).filter((middleware) => shouldRunMiddleware(middleware, mode, pathname)).sort(compareMiddleware);
14
- }
15
- async function executePageMiddleware(args) {
16
- const { entries, context, onError } = args;
17
- const matchedMiddleware = flattenAndSortMiddleware(entries, context.mode, context.pathname);
18
- for (const middleware of matchedMiddleware) {
19
- try {
20
- const result = await middleware.run(context);
21
- if (result.action === "redirect") return result;
22
- } catch (error) {
23
- if (onError) {
24
- onError(error, { id: middleware.id, priority: middleware.priority });
25
- } else {
26
- console.error("[middleware:page] execution failed", { id: middleware.id, error });
27
- }
28
- throw error;
29
- }
30
- }
31
- return CONTINUE_PAGE_MIDDLEWARE;
32
- }
33
- async function resolvePageMiddlewareRedirect(args) {
34
- const result = await executePageMiddleware(args);
35
- if (result.action !== "redirect") return null;
36
- return result.location;
37
- }
38
- export {
39
- executePageMiddleware,
40
- resolvePageMiddlewareRedirect
41
- };
42
- //# sourceMappingURL=page-executor.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/lib/middleware/page-executor.ts"],
4
- "sourcesContent": ["import type {\n PageMiddlewareContext,\n PageMiddlewareMode,\n PageMiddlewareRegistryEntry,\n PageMiddlewareResult,\n PageRouteMiddleware,\n} from '@open-mercato/shared/modules/middleware/page'\nimport { CONTINUE_PAGE_MIDDLEWARE, matchPageMiddlewareTarget } from '@open-mercato/shared/modules/middleware/page'\n\ntype ExecutePageMiddlewareArgs = {\n entries: PageMiddlewareRegistryEntry[]\n context: PageMiddlewareContext\n onError?: (error: unknown, middleware: Pick<PageRouteMiddleware, 'id' | 'priority'>) => void\n}\n\nconst DEFAULT_PRIORITY = 100\n\nfunction shouldRunMiddleware(middleware: PageRouteMiddleware, mode: PageMiddlewareMode, pathname: string): boolean {\n if (middleware.mode && middleware.mode !== mode) return false\n return matchPageMiddlewareTarget(pathname, middleware.target)\n}\n\nfunction compareMiddleware(a: PageRouteMiddleware, b: PageRouteMiddleware): number {\n const priorityDiff = (a.priority ?? DEFAULT_PRIORITY) - (b.priority ?? DEFAULT_PRIORITY)\n if (priorityDiff !== 0) return priorityDiff\n return a.id.localeCompare(b.id)\n}\n\nfunction flattenAndSortMiddleware(\n entries: PageMiddlewareRegistryEntry[],\n mode: PageMiddlewareMode,\n pathname: string\n): PageRouteMiddleware[] {\n return entries\n .flatMap((entry) => entry.middleware)\n .filter((middleware) => shouldRunMiddleware(middleware, mode, pathname))\n .sort(compareMiddleware)\n}\n\nexport async function executePageMiddleware(args: ExecutePageMiddlewareArgs): Promise<PageMiddlewareResult> {\n const { entries, context, onError } = args\n const matchedMiddleware = flattenAndSortMiddleware(entries, context.mode, context.pathname)\n for (const middleware of matchedMiddleware) {\n try {\n const result = await middleware.run(context)\n if (result.action === 'redirect') return result\n } catch (error) {\n if (onError) {\n onError(error, { id: middleware.id, priority: middleware.priority })\n } else {\n console.error('[middleware:page] execution failed', { id: middleware.id, error })\n }\n throw error\n }\n }\n return CONTINUE_PAGE_MIDDLEWARE\n}\n\nexport async function resolvePageMiddlewareRedirect(args: ExecutePageMiddlewareArgs): Promise<string | null> {\n const result = await executePageMiddleware(args)\n if (result.action !== 'redirect') return null\n return result.location\n}\n"],
5
- "mappings": "AAOA,SAAS,0BAA0B,iCAAiC;AAQpE,MAAM,mBAAmB;AAEzB,SAAS,oBAAoB,YAAiC,MAA0B,UAA2B;AACjH,MAAI,WAAW,QAAQ,WAAW,SAAS,KAAM,QAAO;AACxD,SAAO,0BAA0B,UAAU,WAAW,MAAM;AAC9D;AAEA,SAAS,kBAAkB,GAAwB,GAAgC;AACjF,QAAM,gBAAgB,EAAE,YAAY,qBAAqB,EAAE,YAAY;AACvE,MAAI,iBAAiB,EAAG,QAAO;AAC/B,SAAO,EAAE,GAAG,cAAc,EAAE,EAAE;AAChC;AAEA,SAAS,yBACP,SACA,MACA,UACuB;AACvB,SAAO,QACJ,QAAQ,CAAC,UAAU,MAAM,UAAU,EACnC,OAAO,CAAC,eAAe,oBAAoB,YAAY,MAAM,QAAQ,CAAC,EACtE,KAAK,iBAAiB;AAC3B;AAEA,eAAsB,sBAAsB,MAAgE;AAC1G,QAAM,EAAE,SAAS,SAAS,QAAQ,IAAI;AACtC,QAAM,oBAAoB,yBAAyB,SAAS,QAAQ,MAAM,QAAQ,QAAQ;AAC1F,aAAW,cAAc,mBAAmB;AAC1C,QAAI;AACF,YAAM,SAAS,MAAM,WAAW,IAAI,OAAO;AAC3C,UAAI,OAAO,WAAW,WAAY,QAAO;AAAA,IAC3C,SAAS,OAAO;AACd,UAAI,SAAS;AACX,gBAAQ,OAAO,EAAE,IAAI,WAAW,IAAI,UAAU,WAAW,SAAS,CAAC;AAAA,MACrE,OAAO;AACL,gBAAQ,MAAM,sCAAsC,EAAE,IAAI,WAAW,IAAI,MAAM,CAAC;AAAA,MAClF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAsB,8BAA8B,MAAyD;AAC3G,QAAM,SAAS,MAAM,sBAAsB,IAAI;AAC/C,MAAI,OAAO,WAAW,WAAY,QAAO;AACzC,SAAO,OAAO;AAChB;",
6
- "names": []
7
- }
@@ -1,15 +0,0 @@
1
- function matchPageMiddlewareTarget(pathname, target) {
2
- if (target instanceof RegExp) {
3
- return target.test(pathname);
4
- }
5
- if (target.endsWith("*")) {
6
- return pathname.startsWith(target.slice(0, -1));
7
- }
8
- return pathname === target;
9
- }
10
- const CONTINUE_PAGE_MIDDLEWARE = { action: "continue" };
11
- export {
12
- CONTINUE_PAGE_MIDDLEWARE,
13
- matchPageMiddlewareTarget
14
- };
15
- //# sourceMappingURL=page.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/modules/middleware/page.ts"],
4
- "sourcesContent": ["import type { AuthContext } from '@open-mercato/shared/lib/auth/server'\n\nexport type PageMiddlewareMode = 'frontend' | 'backend'\n\nexport type PageRouteMeta = {\n requireAuth?: boolean\n requireRoles?: string[]\n requireFeatures?: string[]\n}\n\nexport type PageMiddlewareContainer = {\n resolve: (name: string) => unknown\n}\n\nexport type PageMiddlewareContext = {\n pathname: string\n mode: PageMiddlewareMode\n routeMeta: PageRouteMeta\n auth: AuthContext\n ensureContainer: () => Promise<PageMiddlewareContainer>\n}\n\nexport type PageMiddlewareResult =\n | { action: 'continue' }\n | { action: 'redirect'; location: string }\n\nexport type PageMiddlewareTarget = string | RegExp\n\nexport type PageRouteMiddleware = {\n id: string\n mode?: PageMiddlewareMode\n target: PageMiddlewareTarget\n priority?: number\n run: (ctx: PageMiddlewareContext) => Promise<PageMiddlewareResult> | PageMiddlewareResult\n}\n\nexport type PageMiddlewareRegistryEntry = {\n moduleId: string\n middleware: PageRouteMiddleware[]\n}\n\nexport function matchPageMiddlewareTarget(pathname: string, target: PageMiddlewareTarget): boolean {\n if (target instanceof RegExp) {\n return target.test(pathname)\n }\n if (target.endsWith('*')) {\n return pathname.startsWith(target.slice(0, -1))\n }\n return pathname === target\n}\n\nexport const CONTINUE_PAGE_MIDDLEWARE: PageMiddlewareResult = { action: 'continue' }\n"],
5
- "mappings": "AAyCO,SAAS,0BAA0B,UAAkB,QAAuC;AACjG,MAAI,kBAAkB,QAAQ;AAC5B,WAAO,OAAO,KAAK,QAAQ;AAAA,EAC7B;AACA,MAAI,OAAO,SAAS,GAAG,GAAG;AACxB,WAAO,SAAS,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC;AAAA,EAChD;AACA,SAAO,aAAa;AACtB;AAEO,MAAM,2BAAiD,EAAE,QAAQ,WAAW;",
6
- "names": []
7
- }
@@ -1,180 +0,0 @@
1
- import { registerApiInterceptors } from '@open-mercato/shared/lib/crud/interceptor-registry'
2
- import { runCustomRouteAfterInterceptors } from '@open-mercato/shared/lib/crud/custom-route-interceptor'
3
- import type { InterceptorContext } from '@open-mercato/shared/lib/crud/api-interceptor'
4
-
5
- function buildArgs() {
6
- return {
7
- routePath: 'auth/login',
8
- method: 'POST' as const,
9
- request: {
10
- method: 'POST' as const,
11
- url: 'http://localhost/api/auth/login',
12
- headers: {},
13
- body: { email: 'user@example.com' },
14
- },
15
- response: {
16
- statusCode: 200,
17
- body: { ok: true, token: 'token-1', redirect: '/backend' },
18
- headers: { 'x-test': '1' },
19
- },
20
- context: {
21
- em: {} as InterceptorContext['em'],
22
- container: { resolve: jest.fn() } as unknown as InterceptorContext['container'],
23
- },
24
- }
25
- }
26
-
27
- describe('runCustomRouteAfterInterceptors', () => {
28
- beforeEach(() => {
29
- registerApiInterceptors([])
30
- jest.clearAllMocks()
31
- })
32
-
33
- test('returns unchanged response when no interceptor matches', async () => {
34
- const result = await runCustomRouteAfterInterceptors(buildArgs())
35
-
36
- expect(result).toEqual({
37
- ok: true,
38
- statusCode: 200,
39
- body: { ok: true, token: 'token-1', redirect: '/backend' },
40
- headers: { 'x-test': '1' },
41
- })
42
- })
43
-
44
- test('supports merge result from matching after interceptor', async () => {
45
- registerApiInterceptors([
46
- {
47
- moduleId: 'example',
48
- interceptors: [
49
- {
50
- id: 'example.auth.login.merge',
51
- targetRoute: 'auth/login',
52
- methods: ['POST'],
53
- async after() {
54
- return { merge: { mfa_required: true } }
55
- },
56
- },
57
- ],
58
- },
59
- ])
60
-
61
- const result = await runCustomRouteAfterInterceptors(buildArgs())
62
- expect(result.ok).toBe(true)
63
- expect(result.body).toEqual({
64
- ok: true,
65
- token: 'token-1',
66
- redirect: '/backend',
67
- mfa_required: true,
68
- })
69
- })
70
-
71
- test('supports replace result from matching after interceptor', async () => {
72
- registerApiInterceptors([
73
- {
74
- moduleId: 'example',
75
- interceptors: [
76
- {
77
- id: 'example.auth.login.replace',
78
- targetRoute: 'auth/login',
79
- methods: ['POST'],
80
- async after() {
81
- return { replace: { ok: true, mfa_required: true, challenge_id: 'c-1', token: 'pending' } }
82
- },
83
- },
84
- ],
85
- },
86
- ])
87
-
88
- const result = await runCustomRouteAfterInterceptors(buildArgs())
89
- expect(result.ok).toBe(true)
90
- expect(result.body).toEqual({
91
- ok: true,
92
- mfa_required: true,
93
- challenge_id: 'c-1',
94
- token: 'pending',
95
- })
96
- })
97
-
98
- test('propagates timeout failures from interceptor runner', async () => {
99
- registerApiInterceptors([
100
- {
101
- moduleId: 'example',
102
- interceptors: [
103
- {
104
- id: 'example.auth.login.timeout',
105
- targetRoute: 'auth/login',
106
- methods: ['POST'],
107
- timeoutMs: 5,
108
- async after() {
109
- await new Promise((resolve) => setTimeout(resolve, 20))
110
- return {}
111
- },
112
- },
113
- ],
114
- },
115
- ])
116
-
117
- const result = await runCustomRouteAfterInterceptors(buildArgs())
118
- expect(result.ok).toBe(false)
119
- expect(result.statusCode).toBe(504)
120
- })
121
-
122
- test('supports unauthenticated execution context defaults', async () => {
123
- const capturedContexts: Array<{ userId: string; organizationId: string; tenantId: string; userFeatures?: string[] }> = []
124
- registerApiInterceptors([
125
- {
126
- moduleId: 'example',
127
- interceptors: [
128
- {
129
- id: 'example.auth.login.capture-context',
130
- targetRoute: 'auth/login',
131
- methods: ['POST'],
132
- async after(_request, _response, context) {
133
- capturedContexts.push({
134
- userId: context.userId,
135
- organizationId: context.organizationId,
136
- tenantId: context.tenantId,
137
- userFeatures: context.userFeatures,
138
- })
139
- return {}
140
- },
141
- },
142
- ],
143
- },
144
- ])
145
-
146
- const result = await runCustomRouteAfterInterceptors(buildArgs())
147
- expect(result.ok).toBe(true)
148
- expect(capturedContexts).toEqual([
149
- {
150
- userId: '',
151
- organizationId: '',
152
- tenantId: '',
153
- userFeatures: [],
154
- },
155
- ])
156
- })
157
-
158
- test('propagates interceptor exceptions as failed responses', async () => {
159
- registerApiInterceptors([
160
- {
161
- moduleId: 'example',
162
- interceptors: [
163
- {
164
- id: 'example.auth.login.throw',
165
- targetRoute: 'auth/login',
166
- methods: ['POST'],
167
- async after() {
168
- throw new Error('boom')
169
- },
170
- },
171
- ],
172
- },
173
- ])
174
-
175
- const result = await runCustomRouteAfterInterceptors(buildArgs())
176
- expect(result.ok).toBe(false)
177
- expect(result.statusCode).toBe(500)
178
- expect(result.body.error).toBe('Internal interceptor error')
179
- })
180
- })
@@ -1,47 +0,0 @@
1
- import type { InterceptorContext, InterceptorRequest, InterceptorResponse, ApiInterceptorMethod } from './api-interceptor'
2
- import { runApiInterceptorsAfter, type RunInterceptorsAfterResult } from './interceptor-runner'
3
-
4
- export type CustomRouteAfterInterceptorContext = {
5
- em: InterceptorContext['em']
6
- container: InterceptorContext['container']
7
- userId?: string | null
8
- organizationId?: string | null
9
- tenantId?: string | null
10
- userFeatures?: string[]
11
- extensionHeaders?: InterceptorContext['extensionHeaders']
12
- }
13
-
14
- type RunCustomRouteAfterInterceptorsArgs = {
15
- routePath: string
16
- method: ApiInterceptorMethod
17
- request: InterceptorRequest
18
- response: InterceptorResponse
19
- context: CustomRouteAfterInterceptorContext
20
- metadataByInterceptor?: Record<string, Record<string, unknown> | undefined>
21
- }
22
-
23
- function normalizeIdentity(value: string | null | undefined): string {
24
- if (!value) return ''
25
- return value
26
- }
27
-
28
- export async function runCustomRouteAfterInterceptors(
29
- args: RunCustomRouteAfterInterceptorsArgs,
30
- ): Promise<RunInterceptorsAfterResult> {
31
- return runApiInterceptorsAfter({
32
- routePath: args.routePath,
33
- method: args.method,
34
- request: args.request,
35
- response: args.response,
36
- context: {
37
- em: args.context.em,
38
- container: args.context.container,
39
- userId: normalizeIdentity(args.context.userId),
40
- organizationId: normalizeIdentity(args.context.organizationId),
41
- tenantId: normalizeIdentity(args.context.tenantId),
42
- userFeatures: args.context.userFeatures ?? [],
43
- extensionHeaders: args.context.extensionHeaders,
44
- },
45
- metadataByInterceptor: args.metadataByInterceptor,
46
- })
47
- }
@@ -1,161 +0,0 @@
1
- import {
2
- executePageMiddleware,
3
- resolvePageMiddlewareRedirect,
4
- } from '@open-mercato/shared/lib/middleware/page-executor'
5
- import {
6
- CONTINUE_PAGE_MIDDLEWARE,
7
- matchPageMiddlewareTarget,
8
- type PageMiddlewareContext,
9
- type PageMiddlewareRegistryEntry,
10
- } from '@open-mercato/shared/modules/middleware/page'
11
-
12
- function buildContext(overrides?: Partial<PageMiddlewareContext>): PageMiddlewareContext {
13
- return {
14
- pathname: '/backend/customers/people',
15
- mode: 'backend',
16
- routeMeta: { requireAuth: true },
17
- auth: {
18
- sub: 'user-1',
19
- tenantId: 'tenant-1',
20
- orgId: 'org-1',
21
- roles: ['admin'],
22
- },
23
- ensureContainer: async () => ({ resolve: () => null }),
24
- ...overrides,
25
- }
26
- }
27
-
28
- describe('matchPageMiddlewareTarget', () => {
29
- it('matches exact string targets', () => {
30
- expect(matchPageMiddlewareTarget('/backend', '/backend')).toBe(true)
31
- expect(matchPageMiddlewareTarget('/backend/customers', '/backend')).toBe(false)
32
- })
33
-
34
- it('matches wildcard prefix targets', () => {
35
- expect(matchPageMiddlewareTarget('/backend/customers', '/backend/*')).toBe(true)
36
- expect(matchPageMiddlewareTarget('/frontend/home', '/backend/*')).toBe(false)
37
- })
38
-
39
- it('matches regexp targets', () => {
40
- expect(matchPageMiddlewareTarget('/backend/customers', /^\/backend\/.+$/)).toBe(true)
41
- expect(matchPageMiddlewareTarget('/backend', /^\/backend\/.+$/)).toBe(false)
42
- })
43
- })
44
-
45
- describe('executePageMiddleware', () => {
46
- it('returns continue when no middleware matches', async () => {
47
- const entries: PageMiddlewareRegistryEntry[] = [
48
- {
49
- moduleId: 'security',
50
- middleware: [
51
- {
52
- id: 'security.frontend',
53
- mode: 'frontend',
54
- target: '/frontend/*',
55
- run: async () => CONTINUE_PAGE_MIDDLEWARE,
56
- },
57
- ],
58
- },
59
- ]
60
-
61
- await expect(executePageMiddleware({ entries, context: buildContext() })).resolves.toEqual(
62
- CONTINUE_PAGE_MIDDLEWARE,
63
- )
64
- })
65
-
66
- it('applies deterministic priority ordering and short-circuits on redirect', async () => {
67
- const calls: string[] = []
68
- const entries: PageMiddlewareRegistryEntry[] = [
69
- {
70
- moduleId: 'mod-a',
71
- middleware: [
72
- {
73
- id: 'mod-a.first',
74
- mode: 'backend',
75
- target: '/backend/*',
76
- priority: 20,
77
- run: async () => {
78
- calls.push('mod-a.first')
79
- return CONTINUE_PAGE_MIDDLEWARE
80
- },
81
- },
82
- {
83
- id: 'mod-a.third',
84
- mode: 'backend',
85
- target: '/backend/*',
86
- priority: 30,
87
- run: async () => {
88
- calls.push('mod-a.third')
89
- return CONTINUE_PAGE_MIDDLEWARE
90
- },
91
- },
92
- ],
93
- },
94
- {
95
- moduleId: 'mod-b',
96
- middleware: [
97
- {
98
- id: 'mod-b.second',
99
- mode: 'backend',
100
- target: '/backend/*',
101
- priority: 25,
102
- run: async () => {
103
- calls.push('mod-b.second')
104
- return { action: 'redirect', location: '/backend/profile/security/mfa' } as const
105
- },
106
- },
107
- ],
108
- },
109
- ]
110
-
111
- await expect(
112
- executePageMiddleware({ entries, context: buildContext() }),
113
- ).resolves.toEqual({ action: 'redirect', location: '/backend/profile/security/mfa' })
114
- expect(calls).toEqual(['mod-a.first', 'mod-b.second'])
115
- })
116
-
117
- it('throws and emits error callback on middleware failure', async () => {
118
- const onError = jest.fn()
119
- const entries: PageMiddlewareRegistryEntry[] = [
120
- {
121
- moduleId: 'mod-a',
122
- middleware: [
123
- {
124
- id: 'mod-a.fail',
125
- mode: 'backend',
126
- target: '/backend/*',
127
- run: async () => {
128
- throw new Error('boom')
129
- },
130
- },
131
- ],
132
- },
133
- ]
134
-
135
- await expect(
136
- executePageMiddleware({ entries, context: buildContext(), onError }),
137
- ).rejects.toThrow('boom')
138
- expect(onError).toHaveBeenCalledWith(expect.any(Error), { id: 'mod-a.fail', priority: undefined })
139
- })
140
- })
141
-
142
- describe('resolvePageMiddlewareRedirect', () => {
143
- it('returns redirect location for first terminal middleware', async () => {
144
- const entries: PageMiddlewareRegistryEntry[] = [
145
- {
146
- moduleId: 'mod-a',
147
- middleware: [
148
- {
149
- id: 'mod-a.redirect',
150
- target: '/backend/*',
151
- run: async () => ({ action: 'redirect', location: '/backend/profile/security/mfa' }),
152
- },
153
- ],
154
- },
155
- ]
156
-
157
- await expect(resolvePageMiddlewareRedirect({ entries, context: buildContext() })).resolves.toBe(
158
- '/backend/profile/security/mfa',
159
- )
160
- })
161
- })
@@ -1,63 +0,0 @@
1
- import type {
2
- PageMiddlewareContext,
3
- PageMiddlewareMode,
4
- PageMiddlewareRegistryEntry,
5
- PageMiddlewareResult,
6
- PageRouteMiddleware,
7
- } from '@open-mercato/shared/modules/middleware/page'
8
- import { CONTINUE_PAGE_MIDDLEWARE, matchPageMiddlewareTarget } from '@open-mercato/shared/modules/middleware/page'
9
-
10
- type ExecutePageMiddlewareArgs = {
11
- entries: PageMiddlewareRegistryEntry[]
12
- context: PageMiddlewareContext
13
- onError?: (error: unknown, middleware: Pick<PageRouteMiddleware, 'id' | 'priority'>) => void
14
- }
15
-
16
- const DEFAULT_PRIORITY = 100
17
-
18
- function shouldRunMiddleware(middleware: PageRouteMiddleware, mode: PageMiddlewareMode, pathname: string): boolean {
19
- if (middleware.mode && middleware.mode !== mode) return false
20
- return matchPageMiddlewareTarget(pathname, middleware.target)
21
- }
22
-
23
- function compareMiddleware(a: PageRouteMiddleware, b: PageRouteMiddleware): number {
24
- const priorityDiff = (a.priority ?? DEFAULT_PRIORITY) - (b.priority ?? DEFAULT_PRIORITY)
25
- if (priorityDiff !== 0) return priorityDiff
26
- return a.id.localeCompare(b.id)
27
- }
28
-
29
- function flattenAndSortMiddleware(
30
- entries: PageMiddlewareRegistryEntry[],
31
- mode: PageMiddlewareMode,
32
- pathname: string
33
- ): PageRouteMiddleware[] {
34
- return entries
35
- .flatMap((entry) => entry.middleware)
36
- .filter((middleware) => shouldRunMiddleware(middleware, mode, pathname))
37
- .sort(compareMiddleware)
38
- }
39
-
40
- export async function executePageMiddleware(args: ExecutePageMiddlewareArgs): Promise<PageMiddlewareResult> {
41
- const { entries, context, onError } = args
42
- const matchedMiddleware = flattenAndSortMiddleware(entries, context.mode, context.pathname)
43
- for (const middleware of matchedMiddleware) {
44
- try {
45
- const result = await middleware.run(context)
46
- if (result.action === 'redirect') return result
47
- } catch (error) {
48
- if (onError) {
49
- onError(error, { id: middleware.id, priority: middleware.priority })
50
- } else {
51
- console.error('[middleware:page] execution failed', { id: middleware.id, error })
52
- }
53
- throw error
54
- }
55
- }
56
- return CONTINUE_PAGE_MIDDLEWARE
57
- }
58
-
59
- export async function resolvePageMiddlewareRedirect(args: ExecutePageMiddlewareArgs): Promise<string | null> {
60
- const result = await executePageMiddleware(args)
61
- if (result.action !== 'redirect') return null
62
- return result.location
63
- }
@@ -1,52 +0,0 @@
1
- import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
2
-
3
- export type PageMiddlewareMode = 'frontend' | 'backend'
4
-
5
- export type PageRouteMeta = {
6
- requireAuth?: boolean
7
- requireRoles?: string[]
8
- requireFeatures?: string[]
9
- }
10
-
11
- export type PageMiddlewareContainer = {
12
- resolve: (name: string) => unknown
13
- }
14
-
15
- export type PageMiddlewareContext = {
16
- pathname: string
17
- mode: PageMiddlewareMode
18
- routeMeta: PageRouteMeta
19
- auth: AuthContext
20
- ensureContainer: () => Promise<PageMiddlewareContainer>
21
- }
22
-
23
- export type PageMiddlewareResult =
24
- | { action: 'continue' }
25
- | { action: 'redirect'; location: string }
26
-
27
- export type PageMiddlewareTarget = string | RegExp
28
-
29
- export type PageRouteMiddleware = {
30
- id: string
31
- mode?: PageMiddlewareMode
32
- target: PageMiddlewareTarget
33
- priority?: number
34
- run: (ctx: PageMiddlewareContext) => Promise<PageMiddlewareResult> | PageMiddlewareResult
35
- }
36
-
37
- export type PageMiddlewareRegistryEntry = {
38
- moduleId: string
39
- middleware: PageRouteMiddleware[]
40
- }
41
-
42
- export function matchPageMiddlewareTarget(pathname: string, target: PageMiddlewareTarget): boolean {
43
- if (target instanceof RegExp) {
44
- return target.test(pathname)
45
- }
46
- if (target.endsWith('*')) {
47
- return pathname.startsWith(target.slice(0, -1))
48
- }
49
- return pathname === target
50
- }
51
-
52
- export const CONTINUE_PAGE_MIDDLEWARE: PageMiddlewareResult = { action: 'continue' }