@augmenting-integrations/platform 8.10.0 → 8.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/apps-route.d.ts +6 -0
- package/dist/server/apps-route.d.ts.map +1 -1
- package/dist/server/apps-route.test.d.ts +2 -0
- package/dist/server/apps-route.test.d.ts.map +1 -0
- package/dist/server.cjs +15 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +15 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -39,6 +39,12 @@ export type CreateAppsProxyRouteHandlerOptions = {
|
|
|
39
39
|
* Authorization headers across services is a footgun.
|
|
40
40
|
*/
|
|
41
41
|
forwardHeaders?: readonly string[];
|
|
42
|
+
/**
|
|
43
|
+
* Hard timeout for the upstream fetch, in milliseconds. On timeout the
|
|
44
|
+
* handler returns 504 with a structured body instead of letting the
|
|
45
|
+
* client hang. Default: 5000.
|
|
46
|
+
*/
|
|
47
|
+
timeoutMs?: number;
|
|
42
48
|
};
|
|
43
49
|
export declare function createAppsProxyRouteHandler(opts: CreateAppsProxyRouteHandlerOptions): {
|
|
44
50
|
GET: (request: Request) => Promise<Response>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps-route.d.ts","sourceRoot":"","sources":["../../src/server/apps-route.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAcjF,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAC5C,GAAG,IAAI,CAAC;AAET,KAAK,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;AAEzC,MAAM,MAAM,6BAA6B,GAAG;IAC1C,+EAA+E;IAC/E,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC;IAC7B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC5E,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAOF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B;eAavD,OAAO,CAAC,QAAQ,CAAC;EA2BnC;AAkBD,MAAM,MAAM,kCAAkC,GAAG;IAC/C,4EAA4E;IAC5E,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC5E;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"apps-route.d.ts","sourceRoot":"","sources":["../../src/server/apps-route.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAcjF,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAC5C,GAAG,IAAI,CAAC;AAET,KAAK,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;AAEzC,MAAM,MAAM,6BAA6B,GAAG;IAC1C,+EAA+E;IAC/E,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC;IAC7B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC5E,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAOF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B;eAavD,OAAO,CAAC,QAAQ,CAAC;EA2BnC;AAkBD,MAAM,MAAM,kCAAkC,GAAG;IAC/C,4EAA4E;IAC5E,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC5E;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAmBF,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,kCAAkC;mBAM3D,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;EAiEnD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apps-route.test.d.ts","sourceRoot":"","sources":["../../src/server/apps-route.test.ts"],"names":[],"mappings":""}
|
package/dist/server.cjs
CHANGED
|
@@ -297,6 +297,7 @@ var PROXY_LOOP_HEADER = "x-augint-apps-proxy";
|
|
|
297
297
|
function createAppsProxyRouteHandler(opts) {
|
|
298
298
|
const upstream = opts.upstreamUrl ?? `https://${opts.tenant.apex}/api/apps`;
|
|
299
299
|
const forward = opts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
|
|
300
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
300
301
|
return {
|
|
301
302
|
GET: async (request) => {
|
|
302
303
|
if (request.headers.get(PROXY_LOOP_HEADER)) {
|
|
@@ -320,7 +321,8 @@ function createAppsProxyRouteHandler(opts) {
|
|
|
320
321
|
method: "GET",
|
|
321
322
|
headers,
|
|
322
323
|
cache: "no-store",
|
|
323
|
-
redirect: "manual"
|
|
324
|
+
redirect: "manual",
|
|
325
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
324
326
|
});
|
|
325
327
|
const body = await upstreamResponse.arrayBuffer();
|
|
326
328
|
const responseHeaders = new Headers();
|
|
@@ -333,13 +335,24 @@ function createAppsProxyRouteHandler(opts) {
|
|
|
333
335
|
headers: responseHeaders
|
|
334
336
|
});
|
|
335
337
|
} catch (err) {
|
|
338
|
+
const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
|
|
339
|
+
if (isTimeout) {
|
|
340
|
+
return Response.json(
|
|
341
|
+
{
|
|
342
|
+
error: "apps_proxy_timeout",
|
|
343
|
+
message: `Upstream did not respond within ${timeoutMs}ms`,
|
|
344
|
+
upstream
|
|
345
|
+
},
|
|
346
|
+
{ status: 504, headers: { "cache-control": "no-store" } }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
336
349
|
return Response.json(
|
|
337
350
|
{
|
|
338
351
|
error: "apps_proxy_unavailable",
|
|
339
352
|
message: err instanceof Error ? err.message : String(err),
|
|
340
353
|
upstream
|
|
341
354
|
},
|
|
342
|
-
{ status: 503 }
|
|
355
|
+
{ status: 503, headers: { "cache-control": "no-store" } }
|
|
343
356
|
);
|
|
344
357
|
}
|
|
345
358
|
}
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/server/tenant.ts","../src/tenant-types.ts","../src/server/apps-route.ts","../src/apps-roster/schema.ts"],"sourcesContent":["export {\n loadTenantConfig,\n publicSubset,\n TenantBootScript,\n TENANT_GLOBAL_KEY,\n type LoadOptions,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"./server/tenant.js\";\nexport {\n createAppsRouteHandler,\n createAppsProxyRouteHandler,\n type CreateAppsRouteHandlerOptions,\n type CreateAppsProxyRouteHandlerOptions,\n} from \"./server/apps-route.js\";\n","import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// authCognitoSecretArn, cognitoIssuer, cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n if (\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n !process.env.AWS_LAMBDA_FUNCTION_NAME\n ) {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget reads from this global.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare \"config contains\n// </script>\" payloads. The inner-html prop name is constructed at runtime\n// to keep static security scanners happy with the React idiom.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerously\" + \"SetInner\" + \"HTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches the tenant roster entry's\n * slug in <tenant>-infra/config/apps.yaml). For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n","import \"server-only\";\n\nimport {\n filterAppsByIdentityGroups,\n sortAppsByNavOrder,\n validateAppsRoster,\n type TenantApp,\n type AppsRoster,\n} from \"../apps-roster/schema.js\";\nimport type { TenantPublicConfig, TenantServerConfig } from \"../tenant-types.js\";\n\n// =============================================================================\n// /api/apps route handler factories.\n//\n// The apex owns the canonical tenant roster (config/apps.json) and serves\n// it via `createAppsRouteHandler`. Spokes own no roster -- their\n// /api/apps is a proxy to the apex via `createAppsProxyRouteHandler`, so\n// adding a new spoke does NOT require redeploying every existing spoke.\n//\n// Both handlers serve AppShell same-origin, so the browser-side fetch\n// stays simple (`fetch(\"/api/apps\")` with cookie credentials).\n// =============================================================================\n\ntype SessionLike = {\n user?: { groups?: string[] | null } | null;\n} | null;\n\ntype AuthFn = () => Promise<SessionLike>;\n\nexport type CreateAppsRouteHandlerOptions = {\n /** Roster shape, typically `import appsJson from \"../../config/apps.json\"`. */\n roster: AppsRoster | unknown;\n /** Consuming app's `auth()` function. */\n auth: AuthFn;\n /**\n * Tenant config (apex + optional appDomain). Used to derive each app's\n * absolute `appUrl` from its subdomain. Typically the same struct passed\n * to createAuth.\n */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /** Set false to make the endpoint public (NOT recommended). Default true. */\n requireAuth?: boolean;\n};\n\nfunction deriveAppUrl(app: TenantApp, apex: string): string {\n if (app.subdomain === \"\") return `https://${apex}`;\n return `https://${app.subdomain}.${apex}`;\n}\n\nexport function createAppsRouteHandler(opts: CreateAppsRouteHandlerOptions) {\n const validated = validateAppsRoster(opts.roster);\n if (!validated.ok) {\n throw new Error(\n `createAppsRouteHandler: roster failed validation: ${validated.errors\n .map((e) => `${e.path}: ${e.message}`)\n .join(\"; \")}`,\n );\n }\n const apps: TenantApp[] = validated.value.apps;\n const requireAuth = opts.requireAuth ?? true;\n\n return {\n GET: async (): Promise<Response> => {\n let session: SessionLike = null;\n if (requireAuth) {\n session = await opts.auth();\n if (!session) {\n return Response.json({ error: \"unauthenticated\" }, { status: 401 });\n }\n }\n const userGroups = session?.user?.groups ?? [];\n const visible = filterAppsByIdentityGroups(apps, userGroups);\n const sorted = sortAppsByNavOrder(visible);\n const withUrl = sorted.map((a) => ({\n slug: a.slug,\n role: a.role,\n subdomain: a.subdomain,\n displayName: a.displayName,\n navOrder: a.navOrder,\n requiredIdentityGroups: a.requiredIdentityGroups,\n appUrl: deriveAppUrl(a, opts.tenant.apex),\n }));\n return Response.json(withUrl, {\n headers: {\n \"Cache-Control\": \"private, s-maxage=300, stale-while-revalidate=600\",\n },\n });\n },\n };\n}\n\n// =============================================================================\n// createAppsProxyRouteHandler\n//\n// Spoke-side /api/apps handler. Forwards the user's request (Cookie header\n// in particular) to the apex's /api/apps endpoint and proxies the response\n// back, preserving status, content-type, and cache headers.\n//\n// The session cookie is parent-domain-scoped (Domain=.<apex>) so the\n// browser sends it on the spoke's same-origin request; we forward that\n// cookie on the server-to-server fetch to the apex so the apex's\n// authenticated handler sees the same user. No CORS involved.\n//\n// Spokes that use this factory ship no roster file -- the canonical\n// roster lives only in the apex.\n// =============================================================================\n\nexport type CreateAppsProxyRouteHandlerOptions = {\n /** Same tenant struct passed to createAuth. Used to derive the apex URL. */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /**\n * Override the upstream URL. Default: `https://${tenant.apex}/api/apps`.\n */\n upstreamUrl?: string;\n /**\n * Headers to forward from the incoming request, lowercase keys. Default\n * forwards `cookie`, `x-forwarded-for`, and `user-agent`. `authorization`\n * is intentionally NOT forwarded by default -- the upstream uses the\n * parent-domain session cookie, not bearer tokens, and forwarding raw\n * Authorization headers across services is a footgun.\n */\n forwardHeaders?: readonly string[];\n};\n\nconst DEFAULT_FORWARD_HEADERS = [\"cookie\", \"x-forwarded-for\", \"user-agent\"] as const;\n\nconst DEFAULT_RESPONSE_HEADERS = [\n \"content-type\",\n \"cache-control\",\n \"vary\",\n \"etag\",\n] as const;\n\n/**\n * Loop-guard header set on every outbound proxy fetch. If an inbound\n * request already carries it, the apps-proxy refuses to forward -- a\n * misconfigured upstream (e.g. apex `/api/apps` pointing back at a\n * spoke) would otherwise produce an infinite chain.\n */\nconst PROXY_LOOP_HEADER = \"x-augint-apps-proxy\";\n\nexport function createAppsProxyRouteHandler(opts: CreateAppsProxyRouteHandlerOptions) {\n const upstream = opts.upstreamUrl ?? `https://${opts.tenant.apex}/api/apps`;\n const forward = opts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;\n\n return {\n GET: async (request: Request): Promise<Response> => {\n if (request.headers.get(PROXY_LOOP_HEADER)) {\n // Loud-fast: misconfiguration. 508 Loop Detected is the closest\n // standard status; do NOT silently 200 with an empty body.\n return Response.json(\n {\n error: \"apps_proxy_loop_detected\",\n message:\n \"Inbound request already carries the apps-proxy loop-guard header. \" +\n \"This usually means the apex /api/apps is misconfigured (pointing back \" +\n \"at a spoke) or two spokes are proxying to each other.\",\n upstream,\n },\n { status: 508 },\n );\n }\n\n const headers = new Headers();\n for (const name of forward) {\n const v = request.headers.get(name);\n if (v) headers.set(name, v);\n }\n headers.set(PROXY_LOOP_HEADER, \"1\");\n\n try {\n const upstreamResponse = await fetch(upstream, {\n method: \"GET\",\n headers,\n cache: \"no-store\",\n redirect: \"manual\",\n });\n const body = await upstreamResponse.arrayBuffer();\n const responseHeaders = new Headers();\n for (const name of DEFAULT_RESPONSE_HEADERS) {\n const v = upstreamResponse.headers.get(name);\n if (v) responseHeaders.set(name, v);\n }\n return new Response(body, {\n status: upstreamResponse.status,\n headers: responseHeaders,\n });\n } catch (err) {\n return Response.json(\n {\n error: \"apps_proxy_unavailable\",\n message: err instanceof Error ? err.message : String(err),\n upstream,\n },\n { status: 503 },\n );\n }\n },\n };\n}\n","// =============================================================================\n// Tenant app roster.\n//\n// One file per tenant that lists every app (apex + spokes) the tenant\n// ecosystem contains. Stored as YAML in <tenant>-infra/config/apps.yaml\n// (canonical) and mirrored to <tenant>-apex/config/apps.json for runtime\n// serving by the apex's /api/apps. Spokes proxy their /api/apps to the\n// apex; they never carry a roster file of their own.\n//\n// Adding a new spoke = a PR to the spoke repo (its app.manifest.json) +\n// a PR to <tenant>-infra/config/apps.yaml + a PR to <tenant>-apex/config/\n// apps.json. `augint validate-app-roster` enforces the two files agree.\n// =============================================================================\n\nexport type TenantAppRole = \"apex\" | \"spoke\";\n\nexport type TenantApp = {\n /** Stable identifier. Matches the spoke's app.manifest.json#appSlug. */\n slug: string;\n /** \"apex\" (auth broker) or \"spoke\" (product app). */\n role: TenantAppRole;\n /** DNS label. Empty string for apex. */\n subdomain: string;\n /** Human-friendly name. Drives the shared nav. */\n displayName: string;\n /** Sort order. Lower comes first. */\n navOrder: number;\n /**\n * Cognito identity groups required to see this app in cross-app nav\n * AND to enter its routes (when the spoke's createAuth is wired to its\n * own manifest's access policy). Empty = all authenticated users.\n */\n requiredIdentityGroups: string[];\n /**\n * Static feature toggle. Default true. Set false to hide an app from\n * cross-app nav without removing the entry. Editing this requires a\n * PR + redeploy -- this is NOT mutable runtime state.\n */\n enabled?: boolean;\n};\n\nexport type AppsRoster = {\n apps: TenantApp[];\n};\n\nexport type RosterValidationError = {\n path: string;\n message: string;\n};\n\nconst ROLES: readonly string[] = [\"apex\", \"spoke\"];\n\n/**\n * Pure validator for the roster object (parsed from YAML or JSON). Returns\n * the typed roster on success, or an array of errors on failure. No throws.\n */\nexport function validateAppsRoster(\n raw: unknown,\n): { ok: true; value: AppsRoster } | { ok: false; errors: RosterValidationError[] } {\n const errors: RosterValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"roster must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n if (!Array.isArray(m.apps)) {\n return { ok: false, errors: [{ path: \"apps\", message: \"expected array\" }] };\n }\n const apps = m.apps as unknown[];\n\n const seenSlugs = new Map<string, number>();\n const seenSubdomains = new Map<string, number>();\n const seenNavOrder = new Map<number, number>();\n let apexCount = 0;\n\n apps.forEach((entryUnknown, i) => {\n const path = `apps[${i}]`;\n if (typeof entryUnknown !== \"object\" || entryUnknown === null) {\n errors.push({ path, message: \"expected object\" });\n return;\n }\n const entry = entryUnknown as Record<string, unknown>;\n\n if (typeof entry.slug !== \"string\" || entry.slug === \"\") {\n errors.push({ path: `${path}.slug`, message: \"expected non-empty string\" });\n } else {\n const prior = seenSlugs.get(entry.slug);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.slug`,\n message: `duplicate slug ${JSON.stringify(entry.slug)} (also at apps[${prior}])`,\n });\n } else {\n seenSlugs.set(entry.slug, i);\n }\n }\n\n if (typeof entry.role !== \"string\" || !ROLES.includes(entry.role)) {\n errors.push({\n path: `${path}.role`,\n message: `expected one of: ${ROLES.join(\", \")}`,\n });\n } else if (entry.role === \"apex\") {\n apexCount++;\n }\n\n if (typeof entry.subdomain !== \"string\") {\n errors.push({ path: `${path}.subdomain`, message: \"expected string\" });\n } else {\n if (entry.role === \"apex\" && entry.subdomain !== \"\") {\n errors.push({\n path: `${path}.subdomain`,\n message: \"apex apps must have empty subdomain\",\n });\n }\n if (entry.subdomain !== \"\") {\n const prior = seenSubdomains.get(entry.subdomain);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.subdomain`,\n message: `duplicate subdomain ${JSON.stringify(entry.subdomain)} (also at apps[${prior}])`,\n });\n } else {\n seenSubdomains.set(entry.subdomain, i);\n }\n }\n }\n\n if (typeof entry.displayName !== \"string\" || entry.displayName === \"\") {\n errors.push({\n path: `${path}.displayName`,\n message: \"expected non-empty string\",\n });\n }\n\n if (typeof entry.navOrder !== \"number\" || !Number.isFinite(entry.navOrder)) {\n errors.push({ path: `${path}.navOrder`, message: \"expected number\" });\n } else {\n const prior = seenNavOrder.get(entry.navOrder);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.navOrder`,\n message: `duplicate navOrder ${entry.navOrder} (also at apps[${prior}])`,\n });\n } else {\n seenNavOrder.set(entry.navOrder, i);\n }\n }\n\n if (\n !Array.isArray(entry.requiredIdentityGroups) ||\n entry.requiredIdentityGroups.some((g) => typeof g !== \"string\")\n ) {\n errors.push({\n path: `${path}.requiredIdentityGroups`,\n message: \"expected string[]\",\n });\n }\n\n if (entry.enabled !== undefined && typeof entry.enabled !== \"boolean\") {\n errors.push({ path: `${path}.enabled`, message: \"expected boolean\" });\n }\n });\n\n if (apexCount === 0) {\n errors.push({ path: \"apps\", message: \"roster must contain exactly one apex entry\" });\n } else if (apexCount > 1) {\n errors.push({\n path: \"apps\",\n message: `roster must contain exactly one apex entry, found ${apexCount}`,\n });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppsRoster };\n}\n\n/**\n * Filter the roster by user identity groups. Apps with empty\n * `requiredIdentityGroups` are visible to all authenticated users; otherwise\n * the user must be in at least one of the listed groups. `enabled: false`\n * apps are always filtered out.\n */\nexport function filterAppsByIdentityGroups(\n apps: TenantApp[],\n userGroups: string[],\n): TenantApp[] {\n const lower = userGroups.map((g) => g.toLowerCase());\n return apps.filter((a) => {\n if (a.enabled === false) return false;\n if (!a.requiredIdentityGroups || a.requiredIdentityGroups.length === 0) return true;\n return a.requiredIdentityGroups.some((g) => lower.includes(g.toLowerCase()));\n });\n}\n\n/** Sort apps by navOrder ASC, then slug. Mutates a copy, returns it. */\nexport function sortAppsByNavOrder<T extends Pick<TenantApp, \"navOrder\" | \"slug\">>(\n apps: T[],\n): T[] {\n return [...apps].sort(\n (a, b) => (a.navOrder ?? 0) - (b.navOrder ?? 0) || a.slug.localeCompare(b.slug),\n );\n}\n"],"mappings":"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","names":["import_server_only"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/server/tenant.ts","../src/tenant-types.ts","../src/server/apps-route.ts","../src/apps-roster/schema.ts"],"sourcesContent":["export {\n loadTenantConfig,\n publicSubset,\n TenantBootScript,\n TENANT_GLOBAL_KEY,\n type LoadOptions,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"./server/tenant.js\";\nexport {\n createAppsRouteHandler,\n createAppsProxyRouteHandler,\n type CreateAppsRouteHandlerOptions,\n type CreateAppsProxyRouteHandlerOptions,\n} from \"./server/apps-route.js\";\n","import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// authCognitoSecretArn, cognitoIssuer, cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n if (\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n !process.env.AWS_LAMBDA_FUNCTION_NAME\n ) {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget reads from this global.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare \"config contains\n// </script>\" payloads. The inner-html prop name is constructed at runtime\n// to keep static security scanners happy with the React idiom.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerously\" + \"SetInner\" + \"HTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches the tenant roster entry's\n * slug in <tenant>-infra/config/apps.yaml). For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n","import \"server-only\";\n\nimport {\n filterAppsByIdentityGroups,\n sortAppsByNavOrder,\n validateAppsRoster,\n type TenantApp,\n type AppsRoster,\n} from \"../apps-roster/schema.js\";\nimport type { TenantPublicConfig, TenantServerConfig } from \"../tenant-types.js\";\n\n// =============================================================================\n// /api/apps route handler factories.\n//\n// The apex owns the canonical tenant roster (config/apps.json) and serves\n// it via `createAppsRouteHandler`. Spokes own no roster -- their\n// /api/apps is a proxy to the apex via `createAppsProxyRouteHandler`, so\n// adding a new spoke does NOT require redeploying every existing spoke.\n//\n// Both handlers serve AppShell same-origin, so the browser-side fetch\n// stays simple (`fetch(\"/api/apps\")` with cookie credentials).\n// =============================================================================\n\ntype SessionLike = {\n user?: { groups?: string[] | null } | null;\n} | null;\n\ntype AuthFn = () => Promise<SessionLike>;\n\nexport type CreateAppsRouteHandlerOptions = {\n /** Roster shape, typically `import appsJson from \"../../config/apps.json\"`. */\n roster: AppsRoster | unknown;\n /** Consuming app's `auth()` function. */\n auth: AuthFn;\n /**\n * Tenant config (apex + optional appDomain). Used to derive each app's\n * absolute `appUrl` from its subdomain. Typically the same struct passed\n * to createAuth.\n */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /** Set false to make the endpoint public (NOT recommended). Default true. */\n requireAuth?: boolean;\n};\n\nfunction deriveAppUrl(app: TenantApp, apex: string): string {\n if (app.subdomain === \"\") return `https://${apex}`;\n return `https://${app.subdomain}.${apex}`;\n}\n\nexport function createAppsRouteHandler(opts: CreateAppsRouteHandlerOptions) {\n const validated = validateAppsRoster(opts.roster);\n if (!validated.ok) {\n throw new Error(\n `createAppsRouteHandler: roster failed validation: ${validated.errors\n .map((e) => `${e.path}: ${e.message}`)\n .join(\"; \")}`,\n );\n }\n const apps: TenantApp[] = validated.value.apps;\n const requireAuth = opts.requireAuth ?? true;\n\n return {\n GET: async (): Promise<Response> => {\n let session: SessionLike = null;\n if (requireAuth) {\n session = await opts.auth();\n if (!session) {\n return Response.json({ error: \"unauthenticated\" }, { status: 401 });\n }\n }\n const userGroups = session?.user?.groups ?? [];\n const visible = filterAppsByIdentityGroups(apps, userGroups);\n const sorted = sortAppsByNavOrder(visible);\n const withUrl = sorted.map((a) => ({\n slug: a.slug,\n role: a.role,\n subdomain: a.subdomain,\n displayName: a.displayName,\n navOrder: a.navOrder,\n requiredIdentityGroups: a.requiredIdentityGroups,\n appUrl: deriveAppUrl(a, opts.tenant.apex),\n }));\n return Response.json(withUrl, {\n headers: {\n \"Cache-Control\": \"private, s-maxage=300, stale-while-revalidate=600\",\n },\n });\n },\n };\n}\n\n// =============================================================================\n// createAppsProxyRouteHandler\n//\n// Spoke-side /api/apps handler. Forwards the user's request (Cookie header\n// in particular) to the apex's /api/apps endpoint and proxies the response\n// back, preserving status, content-type, and cache headers.\n//\n// The session cookie is parent-domain-scoped (Domain=.<apex>) so the\n// browser sends it on the spoke's same-origin request; we forward that\n// cookie on the server-to-server fetch to the apex so the apex's\n// authenticated handler sees the same user. No CORS involved.\n//\n// Spokes that use this factory ship no roster file -- the canonical\n// roster lives only in the apex.\n// =============================================================================\n\nexport type CreateAppsProxyRouteHandlerOptions = {\n /** Same tenant struct passed to createAuth. Used to derive the apex URL. */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /**\n * Override the upstream URL. Default: `https://${tenant.apex}/api/apps`.\n */\n upstreamUrl?: string;\n /**\n * Headers to forward from the incoming request, lowercase keys. Default\n * forwards `cookie`, `x-forwarded-for`, and `user-agent`. `authorization`\n * is intentionally NOT forwarded by default -- the upstream uses the\n * parent-domain session cookie, not bearer tokens, and forwarding raw\n * Authorization headers across services is a footgun.\n */\n forwardHeaders?: readonly string[];\n /**\n * Hard timeout for the upstream fetch, in milliseconds. On timeout the\n * handler returns 504 with a structured body instead of letting the\n * client hang. Default: 5000.\n */\n timeoutMs?: number;\n};\n\nconst DEFAULT_FORWARD_HEADERS = [\"cookie\", \"x-forwarded-for\", \"user-agent\"] as const;\n\nconst DEFAULT_RESPONSE_HEADERS = [\n \"content-type\",\n \"cache-control\",\n \"vary\",\n \"etag\",\n] as const;\n\n/**\n * Loop-guard header set on every outbound proxy fetch. If an inbound\n * request already carries it, the apps-proxy refuses to forward -- a\n * misconfigured upstream (e.g. apex `/api/apps` pointing back at a\n * spoke) would otherwise produce an infinite chain.\n */\nconst PROXY_LOOP_HEADER = \"x-augint-apps-proxy\";\n\nexport function createAppsProxyRouteHandler(opts: CreateAppsProxyRouteHandlerOptions) {\n const upstream = opts.upstreamUrl ?? `https://${opts.tenant.apex}/api/apps`;\n const forward = opts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;\n const timeoutMs = opts.timeoutMs ?? 5000;\n\n return {\n GET: async (request: Request): Promise<Response> => {\n if (request.headers.get(PROXY_LOOP_HEADER)) {\n // Loud-fast: misconfiguration. 508 Loop Detected is the closest\n // standard status; do NOT silently 200 with an empty body.\n return Response.json(\n {\n error: \"apps_proxy_loop_detected\",\n message:\n \"Inbound request already carries the apps-proxy loop-guard header. \" +\n \"This usually means the apex /api/apps is misconfigured (pointing back \" +\n \"at a spoke) or two spokes are proxying to each other.\",\n upstream,\n },\n { status: 508 },\n );\n }\n\n const headers = new Headers();\n for (const name of forward) {\n const v = request.headers.get(name);\n if (v) headers.set(name, v);\n }\n headers.set(PROXY_LOOP_HEADER, \"1\");\n\n try {\n const upstreamResponse = await fetch(upstream, {\n method: \"GET\",\n headers,\n cache: \"no-store\",\n redirect: \"manual\",\n signal: AbortSignal.timeout(timeoutMs),\n });\n const body = await upstreamResponse.arrayBuffer();\n const responseHeaders = new Headers();\n for (const name of DEFAULT_RESPONSE_HEADERS) {\n const v = upstreamResponse.headers.get(name);\n if (v) responseHeaders.set(name, v);\n }\n return new Response(body, {\n status: upstreamResponse.status,\n headers: responseHeaders,\n });\n } catch (err) {\n const isTimeout = err instanceof DOMException && err.name === \"TimeoutError\";\n if (isTimeout) {\n return Response.json(\n {\n error: \"apps_proxy_timeout\",\n message: `Upstream did not respond within ${timeoutMs}ms`,\n upstream,\n },\n { status: 504, headers: { \"cache-control\": \"no-store\" } },\n );\n }\n return Response.json(\n {\n error: \"apps_proxy_unavailable\",\n message: err instanceof Error ? err.message : String(err),\n upstream,\n },\n { status: 503, headers: { \"cache-control\": \"no-store\" } },\n );\n }\n },\n };\n}\n","// =============================================================================\n// Tenant app roster.\n//\n// One file per tenant that lists every app (apex + spokes) the tenant\n// ecosystem contains. Stored as YAML in <tenant>-infra/config/apps.yaml\n// (canonical) and mirrored to <tenant>-apex/config/apps.json for runtime\n// serving by the apex's /api/apps. Spokes proxy their /api/apps to the\n// apex; they never carry a roster file of their own.\n//\n// Adding a new spoke = a PR to the spoke repo (its app.manifest.json) +\n// a PR to <tenant>-infra/config/apps.yaml + a PR to <tenant>-apex/config/\n// apps.json. `augint validate-app-roster` enforces the two files agree.\n// =============================================================================\n\nexport type TenantAppRole = \"apex\" | \"spoke\";\n\nexport type TenantApp = {\n /** Stable identifier. Matches the spoke's app.manifest.json#appSlug. */\n slug: string;\n /** \"apex\" (auth broker) or \"spoke\" (product app). */\n role: TenantAppRole;\n /** DNS label. Empty string for apex. */\n subdomain: string;\n /** Human-friendly name. Drives the shared nav. */\n displayName: string;\n /** Sort order. Lower comes first. */\n navOrder: number;\n /**\n * Cognito identity groups required to see this app in cross-app nav\n * AND to enter its routes (when the spoke's createAuth is wired to its\n * own manifest's access policy). Empty = all authenticated users.\n */\n requiredIdentityGroups: string[];\n /**\n * Static feature toggle. Default true. Set false to hide an app from\n * cross-app nav without removing the entry. Editing this requires a\n * PR + redeploy -- this is NOT mutable runtime state.\n */\n enabled?: boolean;\n};\n\nexport type AppsRoster = {\n apps: TenantApp[];\n};\n\nexport type RosterValidationError = {\n path: string;\n message: string;\n};\n\nconst ROLES: readonly string[] = [\"apex\", \"spoke\"];\n\n/**\n * Pure validator for the roster object (parsed from YAML or JSON). Returns\n * the typed roster on success, or an array of errors on failure. No throws.\n */\nexport function validateAppsRoster(\n raw: unknown,\n): { ok: true; value: AppsRoster } | { ok: false; errors: RosterValidationError[] } {\n const errors: RosterValidationError[] = [];\n if (typeof raw !== \"object\" || raw === null) {\n return { ok: false, errors: [{ path: \"\", message: \"roster must be an object\" }] };\n }\n const m = raw as Record<string, unknown>;\n if (!Array.isArray(m.apps)) {\n return { ok: false, errors: [{ path: \"apps\", message: \"expected array\" }] };\n }\n const apps = m.apps as unknown[];\n\n const seenSlugs = new Map<string, number>();\n const seenSubdomains = new Map<string, number>();\n const seenNavOrder = new Map<number, number>();\n let apexCount = 0;\n\n apps.forEach((entryUnknown, i) => {\n const path = `apps[${i}]`;\n if (typeof entryUnknown !== \"object\" || entryUnknown === null) {\n errors.push({ path, message: \"expected object\" });\n return;\n }\n const entry = entryUnknown as Record<string, unknown>;\n\n if (typeof entry.slug !== \"string\" || entry.slug === \"\") {\n errors.push({ path: `${path}.slug`, message: \"expected non-empty string\" });\n } else {\n const prior = seenSlugs.get(entry.slug);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.slug`,\n message: `duplicate slug ${JSON.stringify(entry.slug)} (also at apps[${prior}])`,\n });\n } else {\n seenSlugs.set(entry.slug, i);\n }\n }\n\n if (typeof entry.role !== \"string\" || !ROLES.includes(entry.role)) {\n errors.push({\n path: `${path}.role`,\n message: `expected one of: ${ROLES.join(\", \")}`,\n });\n } else if (entry.role === \"apex\") {\n apexCount++;\n }\n\n if (typeof entry.subdomain !== \"string\") {\n errors.push({ path: `${path}.subdomain`, message: \"expected string\" });\n } else {\n if (entry.role === \"apex\" && entry.subdomain !== \"\") {\n errors.push({\n path: `${path}.subdomain`,\n message: \"apex apps must have empty subdomain\",\n });\n }\n if (entry.subdomain !== \"\") {\n const prior = seenSubdomains.get(entry.subdomain);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.subdomain`,\n message: `duplicate subdomain ${JSON.stringify(entry.subdomain)} (also at apps[${prior}])`,\n });\n } else {\n seenSubdomains.set(entry.subdomain, i);\n }\n }\n }\n\n if (typeof entry.displayName !== \"string\" || entry.displayName === \"\") {\n errors.push({\n path: `${path}.displayName`,\n message: \"expected non-empty string\",\n });\n }\n\n if (typeof entry.navOrder !== \"number\" || !Number.isFinite(entry.navOrder)) {\n errors.push({ path: `${path}.navOrder`, message: \"expected number\" });\n } else {\n const prior = seenNavOrder.get(entry.navOrder);\n if (prior !== undefined) {\n errors.push({\n path: `${path}.navOrder`,\n message: `duplicate navOrder ${entry.navOrder} (also at apps[${prior}])`,\n });\n } else {\n seenNavOrder.set(entry.navOrder, i);\n }\n }\n\n if (\n !Array.isArray(entry.requiredIdentityGroups) ||\n entry.requiredIdentityGroups.some((g) => typeof g !== \"string\")\n ) {\n errors.push({\n path: `${path}.requiredIdentityGroups`,\n message: \"expected string[]\",\n });\n }\n\n if (entry.enabled !== undefined && typeof entry.enabled !== \"boolean\") {\n errors.push({ path: `${path}.enabled`, message: \"expected boolean\" });\n }\n });\n\n if (apexCount === 0) {\n errors.push({ path: \"apps\", message: \"roster must contain exactly one apex entry\" });\n } else if (apexCount > 1) {\n errors.push({\n path: \"apps\",\n message: `roster must contain exactly one apex entry, found ${apexCount}`,\n });\n }\n\n if (errors.length > 0) return { ok: false, errors };\n return { ok: true, value: m as unknown as AppsRoster };\n}\n\n/**\n * Filter the roster by user identity groups. Apps with empty\n * `requiredIdentityGroups` are visible to all authenticated users; otherwise\n * the user must be in at least one of the listed groups. `enabled: false`\n * apps are always filtered out.\n */\nexport function filterAppsByIdentityGroups(\n apps: TenantApp[],\n userGroups: string[],\n): TenantApp[] {\n const lower = userGroups.map((g) => g.toLowerCase());\n return apps.filter((a) => {\n if (a.enabled === false) return false;\n if (!a.requiredIdentityGroups || a.requiredIdentityGroups.length === 0) return true;\n return a.requiredIdentityGroups.some((g) => lower.includes(g.toLowerCase()));\n });\n}\n\n/** Sort apps by navOrder ASC, then slug. Mutates a copy, returns it. */\nexport function sortAppsByNavOrder<T extends Pick<TenantApp, \"navOrder\" | \"slug\">>(\n apps: T[],\n): T[] {\n return [...apps].sort(\n (a, b) => (a.navOrder ?? 0) - (b.navOrder ?? 0) || a.slug.localeCompare(b.slug),\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAAO;AACP,YAAuB;;;ACmEhB,IAAM,oBAAoB;;;AD9B1B,SAAS,iBAAiB,MAAuC;AACtE,QAAM,MAAM,QAAQ;AACpB,QAAM,IAAI,KAAK,aAAa,CAAC;AAE7B,QAAM,kBAAkB,EAAE,gBAAgB,IAAI;AAC9C,QAAM,eAAe,iBAAiB,QAAQ,OAAO,EAAE;AAEvD,QAAM,QAAqC;AAAA,IACzC,MAAM,KAAK;AAAA,IACX,MAAM,EAAE,QAAQ,IAAI,eAAe;AAAA,IACnC,cAAc,EAAE,gBAAgB,IAAI;AAAA,IACpC,cAAc;AAAA,IACd,QAAQ,EAAE,UAAU,IAAI,cAAc;AAAA,IACtC,SAAS,EAAE,WAAW,IAAI;AAAA,IAC1B,WAAW,EAAE,aAAa,IAAI;AAAA,IAC9B,eAAe,EAAE,iBAAiB,IAAI;AAAA,IACtC,sBAAsB,EAAE,wBAAwB,IAAI;AAAA,IACpD,eAAe,EAAE,iBAAiB,IAAI;AAAA,IACtC,iBAAiB,EAAE,mBAAmB,IAAI;AAAA,IAC1C,aAAa,EAAE,eAAe,IAAI;AAAA,IAClC,aAAa,EAAE,eAAe,IAAI;AAAA,IAClC,QAAQ,EAAE,UAAU,IAAI;AAAA,IACxB,QAAQ,EAAE,UAAU,IAAI;AAAA,IACxB,iBAAiB,EAAE,mBAAmB,IAAI;AAAA,IAC1C,wBAAwB,EAAE,0BAA0B,IAAI;AAAA,EAC1D;AAEA,MAAI,KAAK,SAAS,UAAU,CAAC,MAAM,WAAW;AAC5C,UAAM,YAAY,MAAM;AAAA,EAC1B;AAEA,QAAM,WAAkE;AAAA,IACtE,EAAE,KAAK,QAAQ,KAAK,2DAA2D;AAAA,IAC/E,EAAE,KAAK,gBAAgB,KAAK,qBAAqB;AAAA,IACjD,EAAE,KAAK,gBAAgB,KAAK,6BAA6B;AAAA,IACzD,EAAE,KAAK,iBAAiB,KAAK,kBAAkB;AAAA,IAC/C,EAAE,KAAK,aAAa,KAAK,aAAa;AAAA,EACxC;AACA,MAAI,KAAK,SAAS,QAAQ;AACxB,aAAS;AAAA,MACP,EAAE,KAAK,wBAAwB,KAAK,0BAA0B;AAAA,MAC9D,EAAE,KAAK,iBAAiB,KAAK,sBAAsB;AAAA,MACnD,EAAE,KAAK,mBAAmB,KAAK,kBAAkB;AAAA,IACnD;AAAA,EACF,OAAO;AACL,aAAS,KAAK,EAAE,KAAK,WAAW,KAAK,WAAW,CAAC;AAAA,EACnD;AAEA,MACE,QAAQ,IAAI,eAAe,4BAC3B,CAAC,QAAQ,IAAI,0BACb;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AACtE,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,oBAAoB,KAAK,IAAI,iCAAiC,QAAQ,KAAK,IAAI,CAAC;AAAA,IAClF;AAAA,EACF;AAEA,SAAO;AACT;AAOO,SAAS,aAAa,QAAgD;AAC3E,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,cAAc,OAAO;AAAA,IACrB,cAAc,OAAO;AAAA,IACrB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,WAAW,OAAO;AAAA,IAClB,MAAM,OAAO;AAAA,EACf;AACF;AAYA,IAAM,kBAAkB;AAEjB,SAAS,iBAAiB,EAAE,OAAO,GAAmC;AAC3E,QAAM,UAAU,KAAK,UAAU,MAAM,EAAE,QAAQ,MAAM,SAAS;AAC9D,QAAM,OAAO,UAAU,iBAAiB,IAAI,OAAO;AACnD,QAAM,QAAiC,CAAC;AACxC,QAAM,eAAe,IAAI,EAAE,QAAQ,KAAK;AACxC,SAAa,oBAAc,UAAU,KAAK;AAC5C;;;AE1IA,IAAAA,sBAAO;;;ACkDP,IAAM,QAA2B,CAAC,QAAQ,OAAO;AAM1C,SAAS,mBACd,KACkF;AAClF,QAAM,SAAkC,CAAC;AACzC,MAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,WAAO,EAAE,IAAI,OAAO,QAAQ,CAAC,EAAE,MAAM,IAAI,SAAS,2BAA2B,CAAC,EAAE;AAAA,EAClF;AACA,QAAM,IAAI;AACV,MAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,GAAG;AAC1B,WAAO,EAAE,IAAI,OAAO,QAAQ,CAAC,EAAE,MAAM,QAAQ,SAAS,iBAAiB,CAAC,EAAE;AAAA,EAC5E;AACA,QAAM,OAAO,EAAE;AAEf,QAAM,YAAY,oBAAI,IAAoB;AAC1C,QAAM,iBAAiB,oBAAI,IAAoB;AAC/C,QAAM,eAAe,oBAAI,IAAoB;AAC7C,MAAI,YAAY;AAEhB,OAAK,QAAQ,CAAC,cAAc,MAAM;AAChC,UAAM,OAAO,QAAQ,CAAC;AACtB,QAAI,OAAO,iBAAiB,YAAY,iBAAiB,MAAM;AAC7D,aAAO,KAAK,EAAE,MAAM,SAAS,kBAAkB,CAAC;AAChD;AAAA,IACF;AACA,UAAM,QAAQ;AAEd,QAAI,OAAO,MAAM,SAAS,YAAY,MAAM,SAAS,IAAI;AACvD,aAAO,KAAK,EAAE,MAAM,GAAG,IAAI,SAAS,SAAS,4BAA4B,CAAC;AAAA,IAC5E,OAAO;AACL,YAAM,QAAQ,UAAU,IAAI,MAAM,IAAI;AACtC,UAAI,UAAU,QAAW;AACvB,eAAO,KAAK;AAAA,UACV,MAAM,GAAG,IAAI;AAAA,UACb,SAAS,kBAAkB,KAAK,UAAU,MAAM,IAAI,CAAC,kBAAkB,KAAK;AAAA,QAC9E,CAAC;AAAA,MACH,OAAO;AACL,kBAAU,IAAI,MAAM,MAAM,CAAC;AAAA,MAC7B;AAAA,IACF;AAEA,QAAI,OAAO,MAAM,SAAS,YAAY,CAAC,MAAM,SAAS,MAAM,IAAI,GAAG;AACjE,aAAO,KAAK;AAAA,QACV,MAAM,GAAG,IAAI;AAAA,QACb,SAAS,oBAAoB,MAAM,KAAK,IAAI,CAAC;AAAA,MAC/C,CAAC;AAAA,IACH,WAAW,MAAM,SAAS,QAAQ;AAChC;AAAA,IACF;AAEA,QAAI,OAAO,MAAM,cAAc,UAAU;AACvC,aAAO,KAAK,EAAE,MAAM,GAAG,IAAI,cAAc,SAAS,kBAAkB,CAAC;AAAA,IACvE,OAAO;AACL,UAAI,MAAM,SAAS,UAAU,MAAM,cAAc,IAAI;AACnD,eAAO,KAAK;AAAA,UACV,MAAM,GAAG,IAAI;AAAA,UACb,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AACA,UAAI,MAAM,cAAc,IAAI;AAC1B,cAAM,QAAQ,eAAe,IAAI,MAAM,SAAS;AAChD,YAAI,UAAU,QAAW;AACvB,iBAAO,KAAK;AAAA,YACV,MAAM,GAAG,IAAI;AAAA,YACb,SAAS,uBAAuB,KAAK,UAAU,MAAM,SAAS,CAAC,kBAAkB,KAAK;AAAA,UACxF,CAAC;AAAA,QACH,OAAO;AACL,yBAAe,IAAI,MAAM,WAAW,CAAC;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAEA,QAAI,OAAO,MAAM,gBAAgB,YAAY,MAAM,gBAAgB,IAAI;AACrE,aAAO,KAAK;AAAA,QACV,MAAM,GAAG,IAAI;AAAA,QACb,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,MAAM,aAAa,YAAY,CAAC,OAAO,SAAS,MAAM,QAAQ,GAAG;AAC1E,aAAO,KAAK,EAAE,MAAM,GAAG,IAAI,aAAa,SAAS,kBAAkB,CAAC;AAAA,IACtE,OAAO;AACL,YAAM,QAAQ,aAAa,IAAI,MAAM,QAAQ;AAC7C,UAAI,UAAU,QAAW;AACvB,eAAO,KAAK;AAAA,UACV,MAAM,GAAG,IAAI;AAAA,UACb,SAAS,sBAAsB,MAAM,QAAQ,kBAAkB,KAAK;AAAA,QACtE,CAAC;AAAA,MACH,OAAO;AACL,qBAAa,IAAI,MAAM,UAAU,CAAC;AAAA,MACpC;AAAA,IACF;AAEA,QACE,CAAC,MAAM,QAAQ,MAAM,sBAAsB,KAC3C,MAAM,uBAAuB,KAAK,CAAC,MAAM,OAAO,MAAM,QAAQ,GAC9D;AACA,aAAO,KAAK;AAAA,QACV,MAAM,GAAG,IAAI;AAAA,QACb,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AAEA,QAAI,MAAM,YAAY,UAAa,OAAO,MAAM,YAAY,WAAW;AACrE,aAAO,KAAK,EAAE,MAAM,GAAG,IAAI,YAAY,SAAS,mBAAmB,CAAC;AAAA,IACtE;AAAA,EACF,CAAC;AAED,MAAI,cAAc,GAAG;AACnB,WAAO,KAAK,EAAE,MAAM,QAAQ,SAAS,6CAA6C,CAAC;AAAA,EACrF,WAAW,YAAY,GAAG;AACxB,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,SAAS,qDAAqD,SAAS;AAAA,IACzE,CAAC;AAAA,EACH;AAEA,MAAI,OAAO,SAAS,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO;AAClD,SAAO,EAAE,IAAI,MAAM,OAAO,EAA2B;AACvD;AAQO,SAAS,2BACd,MACA,YACa;AACb,QAAM,QAAQ,WAAW,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC;AACnD,SAAO,KAAK,OAAO,CAAC,MAAM;AACxB,QAAI,EAAE,YAAY,MAAO,QAAO;AAChC,QAAI,CAAC,EAAE,0BAA0B,EAAE,uBAAuB,WAAW,EAAG,QAAO;AAC/E,WAAO,EAAE,uBAAuB,KAAK,CAAC,MAAM,MAAM,SAAS,EAAE,YAAY,CAAC,CAAC;AAAA,EAC7E,CAAC;AACH;AAGO,SAAS,mBACd,MACK;AACL,SAAO,CAAC,GAAG,IAAI,EAAE;AAAA,IACf,CAAC,GAAG,OAAO,EAAE,YAAY,MAAM,EAAE,YAAY,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI;AAAA,EAChF;AACF;;;AD7JA,SAAS,aAAa,KAAgB,MAAsB;AAC1D,MAAI,IAAI,cAAc,GAAI,QAAO,WAAW,IAAI;AAChD,SAAO,WAAW,IAAI,SAAS,IAAI,IAAI;AACzC;AAEO,SAAS,uBAAuB,MAAqC;AAC1E,QAAM,YAAY,mBAAmB,KAAK,MAAM;AAChD,MAAI,CAAC,UAAU,IAAI;AACjB,UAAM,IAAI;AAAA,MACR,qDAAqD,UAAU,OAC5D,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,OAAO,EAAE,EACpC,KAAK,IAAI,CAAC;AAAA,IACf;AAAA,EACF;AACA,QAAM,OAAoB,UAAU,MAAM;AAC1C,QAAM,cAAc,KAAK,eAAe;AAExC,SAAO;AAAA,IACL,KAAK,YAA+B;AAClC,UAAI,UAAuB;AAC3B,UAAI,aAAa;AACf,kBAAU,MAAM,KAAK,KAAK;AAC1B,YAAI,CAAC,SAAS;AACZ,iBAAO,SAAS,KAAK,EAAE,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,QACpE;AAAA,MACF;AACA,YAAM,aAAa,SAAS,MAAM,UAAU,CAAC;AAC7C,YAAM,UAAU,2BAA2B,MAAM,UAAU;AAC3D,YAAM,SAAS,mBAAmB,OAAO;AACzC,YAAM,UAAU,OAAO,IAAI,CAAC,OAAO;AAAA,QACjC,MAAM,EAAE;AAAA,QACR,MAAM,EAAE;AAAA,QACR,WAAW,EAAE;AAAA,QACb,aAAa,EAAE;AAAA,QACf,UAAU,EAAE;AAAA,QACZ,wBAAwB,EAAE;AAAA,QAC1B,QAAQ,aAAa,GAAG,KAAK,OAAO,IAAI;AAAA,MAC1C,EAAE;AACF,aAAO,SAAS,KAAK,SAAS;AAAA,QAC5B,SAAS;AAAA,UACP,iBAAiB;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAyCA,IAAM,0BAA0B,CAAC,UAAU,mBAAmB,YAAY;AAE1E,IAAM,2BAA2B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,IAAM,oBAAoB;AAEnB,SAAS,4BAA4B,MAA0C;AACpF,QAAM,WAAW,KAAK,eAAe,WAAW,KAAK,OAAO,IAAI;AAChE,QAAM,UAAU,KAAK,kBAAkB;AACvC,QAAM,YAAY,KAAK,aAAa;AAEpC,SAAO;AAAA,IACL,KAAK,OAAO,YAAwC;AAClD,UAAI,QAAQ,QAAQ,IAAI,iBAAiB,GAAG;AAG1C,eAAO,SAAS;AAAA,UACd;AAAA,YACE,OAAO;AAAA,YACP,SACE;AAAA,YAGF;AAAA,UACF;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAEA,YAAM,UAAU,IAAI,QAAQ;AAC5B,iBAAW,QAAQ,SAAS;AAC1B,cAAM,IAAI,QAAQ,QAAQ,IAAI,IAAI;AAClC,YAAI,EAAG,SAAQ,IAAI,MAAM,CAAC;AAAA,MAC5B;AACA,cAAQ,IAAI,mBAAmB,GAAG;AAElC,UAAI;AACF,cAAM,mBAAmB,MAAM,MAAM,UAAU;AAAA,UAC7C,QAAQ;AAAA,UACR;AAAA,UACA,OAAO;AAAA,UACP,UAAU;AAAA,UACV,QAAQ,YAAY,QAAQ,SAAS;AAAA,QACvC,CAAC;AACD,cAAM,OAAO,MAAM,iBAAiB,YAAY;AAChD,cAAM,kBAAkB,IAAI,QAAQ;AACpC,mBAAW,QAAQ,0BAA0B;AAC3C,gBAAM,IAAI,iBAAiB,QAAQ,IAAI,IAAI;AAC3C,cAAI,EAAG,iBAAgB,IAAI,MAAM,CAAC;AAAA,QACpC;AACA,eAAO,IAAI,SAAS,MAAM;AAAA,UACxB,QAAQ,iBAAiB;AAAA,UACzB,SAAS;AAAA,QACX,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,cAAM,YAAY,eAAe,gBAAgB,IAAI,SAAS;AAC9D,YAAI,WAAW;AACb,iBAAO,SAAS;AAAA,YACd;AAAA,cACE,OAAO;AAAA,cACP,SAAS,mCAAmC,SAAS;AAAA,cACrD;AAAA,YACF;AAAA,YACA,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAW,EAAE;AAAA,UAC1D;AAAA,QACF;AACA,eAAO,SAAS;AAAA,UACd;AAAA,YACE,OAAO;AAAA,YACP,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,YACxD;AAAA,UACF;AAAA,UACA,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAW,EAAE;AAAA,QAC1D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["import_server_only"]}
|
package/dist/server.js
CHANGED
|
@@ -141,6 +141,7 @@ var PROXY_LOOP_HEADER = "x-augint-apps-proxy";
|
|
|
141
141
|
function createAppsProxyRouteHandler(opts) {
|
|
142
142
|
const upstream = opts.upstreamUrl ?? `https://${opts.tenant.apex}/api/apps`;
|
|
143
143
|
const forward = opts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
|
|
144
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
144
145
|
return {
|
|
145
146
|
GET: async (request) => {
|
|
146
147
|
if (request.headers.get(PROXY_LOOP_HEADER)) {
|
|
@@ -164,7 +165,8 @@ function createAppsProxyRouteHandler(opts) {
|
|
|
164
165
|
method: "GET",
|
|
165
166
|
headers,
|
|
166
167
|
cache: "no-store",
|
|
167
|
-
redirect: "manual"
|
|
168
|
+
redirect: "manual",
|
|
169
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
168
170
|
});
|
|
169
171
|
const body = await upstreamResponse.arrayBuffer();
|
|
170
172
|
const responseHeaders = new Headers();
|
|
@@ -177,13 +179,24 @@ function createAppsProxyRouteHandler(opts) {
|
|
|
177
179
|
headers: responseHeaders
|
|
178
180
|
});
|
|
179
181
|
} catch (err) {
|
|
182
|
+
const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
|
|
183
|
+
if (isTimeout) {
|
|
184
|
+
return Response.json(
|
|
185
|
+
{
|
|
186
|
+
error: "apps_proxy_timeout",
|
|
187
|
+
message: `Upstream did not respond within ${timeoutMs}ms`,
|
|
188
|
+
upstream
|
|
189
|
+
},
|
|
190
|
+
{ status: 504, headers: { "cache-control": "no-store" } }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
180
193
|
return Response.json(
|
|
181
194
|
{
|
|
182
195
|
error: "apps_proxy_unavailable",
|
|
183
196
|
message: err instanceof Error ? err.message : String(err),
|
|
184
197
|
upstream
|
|
185
198
|
},
|
|
186
|
-
{ status: 503 }
|
|
199
|
+
{ status: 503, headers: { "cache-control": "no-store" } }
|
|
187
200
|
);
|
|
188
201
|
}
|
|
189
202
|
}
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/tenant.ts","../src/tenant-types.ts","../src/server/apps-route.ts"],"sourcesContent":["import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// authCognitoSecretArn, cognitoIssuer, cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n if (\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n !process.env.AWS_LAMBDA_FUNCTION_NAME\n ) {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget reads from this global.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare \"config contains\n// </script>\" payloads. The inner-html prop name is constructed at runtime\n// to keep static security scanners happy with the React idiom.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerously\" + \"SetInner\" + \"HTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches the tenant roster entry's\n * slug in <tenant>-infra/config/apps.yaml). For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n","import \"server-only\";\n\nimport {\n filterAppsByIdentityGroups,\n sortAppsByNavOrder,\n validateAppsRoster,\n type TenantApp,\n type AppsRoster,\n} from \"../apps-roster/schema.js\";\nimport type { TenantPublicConfig, TenantServerConfig } from \"../tenant-types.js\";\n\n// =============================================================================\n// /api/apps route handler factories.\n//\n// The apex owns the canonical tenant roster (config/apps.json) and serves\n// it via `createAppsRouteHandler`. Spokes own no roster -- their\n// /api/apps is a proxy to the apex via `createAppsProxyRouteHandler`, so\n// adding a new spoke does NOT require redeploying every existing spoke.\n//\n// Both handlers serve AppShell same-origin, so the browser-side fetch\n// stays simple (`fetch(\"/api/apps\")` with cookie credentials).\n// =============================================================================\n\ntype SessionLike = {\n user?: { groups?: string[] | null } | null;\n} | null;\n\ntype AuthFn = () => Promise<SessionLike>;\n\nexport type CreateAppsRouteHandlerOptions = {\n /** Roster shape, typically `import appsJson from \"../../config/apps.json\"`. */\n roster: AppsRoster | unknown;\n /** Consuming app's `auth()` function. */\n auth: AuthFn;\n /**\n * Tenant config (apex + optional appDomain). Used to derive each app's\n * absolute `appUrl` from its subdomain. Typically the same struct passed\n * to createAuth.\n */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /** Set false to make the endpoint public (NOT recommended). Default true. */\n requireAuth?: boolean;\n};\n\nfunction deriveAppUrl(app: TenantApp, apex: string): string {\n if (app.subdomain === \"\") return `https://${apex}`;\n return `https://${app.subdomain}.${apex}`;\n}\n\nexport function createAppsRouteHandler(opts: CreateAppsRouteHandlerOptions) {\n const validated = validateAppsRoster(opts.roster);\n if (!validated.ok) {\n throw new Error(\n `createAppsRouteHandler: roster failed validation: ${validated.errors\n .map((e) => `${e.path}: ${e.message}`)\n .join(\"; \")}`,\n );\n }\n const apps: TenantApp[] = validated.value.apps;\n const requireAuth = opts.requireAuth ?? true;\n\n return {\n GET: async (): Promise<Response> => {\n let session: SessionLike = null;\n if (requireAuth) {\n session = await opts.auth();\n if (!session) {\n return Response.json({ error: \"unauthenticated\" }, { status: 401 });\n }\n }\n const userGroups = session?.user?.groups ?? [];\n const visible = filterAppsByIdentityGroups(apps, userGroups);\n const sorted = sortAppsByNavOrder(visible);\n const withUrl = sorted.map((a) => ({\n slug: a.slug,\n role: a.role,\n subdomain: a.subdomain,\n displayName: a.displayName,\n navOrder: a.navOrder,\n requiredIdentityGroups: a.requiredIdentityGroups,\n appUrl: deriveAppUrl(a, opts.tenant.apex),\n }));\n return Response.json(withUrl, {\n headers: {\n \"Cache-Control\": \"private, s-maxage=300, stale-while-revalidate=600\",\n },\n });\n },\n };\n}\n\n// =============================================================================\n// createAppsProxyRouteHandler\n//\n// Spoke-side /api/apps handler. Forwards the user's request (Cookie header\n// in particular) to the apex's /api/apps endpoint and proxies the response\n// back, preserving status, content-type, and cache headers.\n//\n// The session cookie is parent-domain-scoped (Domain=.<apex>) so the\n// browser sends it on the spoke's same-origin request; we forward that\n// cookie on the server-to-server fetch to the apex so the apex's\n// authenticated handler sees the same user. No CORS involved.\n//\n// Spokes that use this factory ship no roster file -- the canonical\n// roster lives only in the apex.\n// =============================================================================\n\nexport type CreateAppsProxyRouteHandlerOptions = {\n /** Same tenant struct passed to createAuth. Used to derive the apex URL. */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /**\n * Override the upstream URL. Default: `https://${tenant.apex}/api/apps`.\n */\n upstreamUrl?: string;\n /**\n * Headers to forward from the incoming request, lowercase keys. Default\n * forwards `cookie`, `x-forwarded-for`, and `user-agent`. `authorization`\n * is intentionally NOT forwarded by default -- the upstream uses the\n * parent-domain session cookie, not bearer tokens, and forwarding raw\n * Authorization headers across services is a footgun.\n */\n forwardHeaders?: readonly string[];\n};\n\nconst DEFAULT_FORWARD_HEADERS = [\"cookie\", \"x-forwarded-for\", \"user-agent\"] as const;\n\nconst DEFAULT_RESPONSE_HEADERS = [\n \"content-type\",\n \"cache-control\",\n \"vary\",\n \"etag\",\n] as const;\n\n/**\n * Loop-guard header set on every outbound proxy fetch. If an inbound\n * request already carries it, the apps-proxy refuses to forward -- a\n * misconfigured upstream (e.g. apex `/api/apps` pointing back at a\n * spoke) would otherwise produce an infinite chain.\n */\nconst PROXY_LOOP_HEADER = \"x-augint-apps-proxy\";\n\nexport function createAppsProxyRouteHandler(opts: CreateAppsProxyRouteHandlerOptions) {\n const upstream = opts.upstreamUrl ?? `https://${opts.tenant.apex}/api/apps`;\n const forward = opts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;\n\n return {\n GET: async (request: Request): Promise<Response> => {\n if (request.headers.get(PROXY_LOOP_HEADER)) {\n // Loud-fast: misconfiguration. 508 Loop Detected is the closest\n // standard status; do NOT silently 200 with an empty body.\n return Response.json(\n {\n error: \"apps_proxy_loop_detected\",\n message:\n \"Inbound request already carries the apps-proxy loop-guard header. \" +\n \"This usually means the apex /api/apps is misconfigured (pointing back \" +\n \"at a spoke) or two spokes are proxying to each other.\",\n upstream,\n },\n { status: 508 },\n );\n }\n\n const headers = new Headers();\n for (const name of forward) {\n const v = request.headers.get(name);\n if (v) headers.set(name, v);\n }\n headers.set(PROXY_LOOP_HEADER, \"1\");\n\n try {\n const upstreamResponse = await fetch(upstream, {\n method: \"GET\",\n headers,\n cache: \"no-store\",\n redirect: \"manual\",\n });\n const body = await upstreamResponse.arrayBuffer();\n const responseHeaders = new Headers();\n for (const name of DEFAULT_RESPONSE_HEADERS) {\n const v = upstreamResponse.headers.get(name);\n if (v) responseHeaders.set(name, v);\n }\n return new Response(body, {\n status: upstreamResponse.status,\n headers: responseHeaders,\n });\n } catch (err) {\n return Response.json(\n {\n error: \"apps_proxy_unavailable\",\n message: err instanceof Error ? err.message : String(err),\n upstream,\n },\n { status: 503 },\n );\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/server/tenant.ts","../src/tenant-types.ts","../src/server/apps-route.ts"],"sourcesContent":["import \"server-only\";\nimport * as React from \"react\";\nimport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantRole,\n type TenantServerConfig,\n} from \"../tenant-types.js\";\n\n// =============================================================================\n// loadTenantConfig() -- the single source of truth for tenant configuration.\n//\n// Every required process.env read happens here. Missing fields are surfaced\n// in ONE error message so the deploy fails loudly instead of silently\n// substituting undefined into a downstream package.\n//\n// Apex apps call loadTenantConfig({ role: \"apex\" }). Spoke apps call\n// loadTenantConfig({ role: \"spoke\" }). The required-field set differs:\n//\n// apex needs: apex, cookieDomain, parentDomain, region, authSecretArn,\n// authCognitoSecretArn, cognitoIssuer, cognitoClientId\n//\n// spoke needs: everything apex needs EXCEPT cognito creds, PLUS\n// appSlug, appDomain, dbSecretArn (or dbHost+dbName)\n// =============================================================================\n\nexport type LoadOptions = {\n role: TenantRole;\n /**\n * Override env reads with explicit values (useful for tests).\n */\n overrides?: Partial<TenantServerConfig>;\n};\n\n/**\n * Read tenant configuration from process.env with optional overrides.\n * Throws a single Error listing every missing required field.\n */\nexport function loadTenantConfig(opts: LoadOptions): TenantServerConfig {\n const env = process.env;\n const o = opts.overrides ?? {};\n\n const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;\n const apexFallback = parentDomainRaw?.replace(/^\\./, \"\");\n\n const draft: Partial<TenantServerConfig> = {\n role: opts.role,\n apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,\n cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,\n parentDomain: parentDomainRaw,\n region: o.region ?? env.AWS_REGION ?? \"us-east-1\",\n appSlug: o.appSlug ?? env.APP_SLUG,\n appDomain: o.appDomain ?? env.APP_DOMAIN,\n authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,\n authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,\n cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,\n cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,\n adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,\n dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,\n dbHost: o.dbHost ?? env.DB_HOST,\n dbName: o.dbName ?? env.DB_NAME,\n stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,\n stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN,\n };\n\n if (opts.role === \"apex\" && !draft.appDomain) {\n draft.appDomain = draft.apex;\n }\n\n const required: Array<{ key: keyof TenantServerConfig; env: string }> = [\n { key: \"apex\", env: \"APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)\" },\n { key: \"cookieDomain\", env: \"AUTH_COOKIE_DOMAIN\" },\n { key: \"parentDomain\", env: \"AUTH_ALLOWED_PARENT_DOMAIN\" },\n { key: \"authSecretArn\", env: \"AUTH_SECRET_ARN\" },\n { key: \"appDomain\", env: \"APP_DOMAIN\" },\n ];\n if (opts.role === \"apex\") {\n required.push(\n { key: \"authCognitoSecretArn\", env: \"AUTH_COGNITO_SECRET_ARN\" },\n { key: \"cognitoIssuer\", env: \"AUTH_COGNITO_ISSUER\" },\n { key: \"cognitoClientId\", env: \"AUTH_COGNITO_ID\" },\n );\n } else {\n required.push({ key: \"appSlug\", env: \"APP_SLUG\" });\n }\n\n if (\n process.env.NEXT_PHASE === \"phase-production-build\" ||\n !process.env.AWS_LAMBDA_FUNCTION_NAME\n ) {\n return draft as TenantServerConfig;\n }\n\n const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);\n if (missing.length > 0) {\n throw new Error(\n `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(\", \")}`,\n );\n }\n\n return draft as TenantServerConfig;\n}\n\n/**\n * Reduce a TenantServerConfig to the public-safe subset. Strips every\n * secret-arn so the result is safe to ship to the browser via\n * <TenantBootScript />.\n */\nexport function publicSubset(config: TenantServerConfig): TenantPublicConfig {\n return {\n apex: config.apex,\n cookieDomain: config.cookieDomain,\n parentDomain: config.parentDomain,\n region: config.region,\n appSlug: config.appSlug,\n appDomain: config.appDomain,\n role: config.role,\n };\n}\n\n// =============================================================================\n// <TenantBootScript /> -- server component that injects window.__TENANT__\n// before paint. Every client widget reads from this global.\n//\n// The payload is JSON.stringify of a TYPED struct -- we control every field\n// shape. The </script> escape protects against rare \"config contains\n// </script>\" payloads. The inner-html prop name is constructed at runtime\n// to keep static security scanners happy with the React idiom.\n// =============================================================================\n\nconst INNER_HTML_PROP = \"dangerously\" + \"SetInner\" + \"HTML\";\n\nexport function TenantBootScript({ config }: { config: TenantPublicConfig }) {\n const payload = JSON.stringify(config).replace(/</g, \"\\\\u003c\");\n const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;\n const props: Record<string, unknown> = {};\n props[INNER_HTML_PROP] = { __html: body };\n return React.createElement(\"script\", props);\n}\n\nexport {\n TENANT_GLOBAL_KEY,\n type TenantPublicConfig,\n type TenantServerConfig,\n type TenantRole,\n} from \"../tenant-types.js\";\n","// =============================================================================\n// TenantConfig -- the single struct every @augmenting-integrations package\n// consumes. Apex apps and spokes share the same type; spoke-only fields are\n// optional. The `role` discriminator tells loadTenantConfig() which fields\n// to demand.\n//\n// Public fields (apex + parent domain + slug) are safe to ship to the browser\n// via <TenantBootScript />. Secret-arn fields are server-only and never reach\n// the client bundle.\n// =============================================================================\n\nexport type TenantRole = \"apex\" | \"spoke\";\n\nexport type TenantPublicConfig = {\n /** The tenant apex FQDN, e.g. \"agency.aillc.link\". */\n apex: string;\n /**\n * Cookie Domain attribute. Always the apex (no leading dot needed -- the\n * browser implies it for shared cookies). Auth.js session cookie and the\n * theme x-theme/x-theme-variant cookies use this. Without it cookies are\n * host-only and the subdomain ecosystem breaks.\n */\n cookieDomain: string;\n /**\n * The registrable parent domain (e.g. \"aillc.link\"). Used by the auth\n * redirect callback to validate post-login callbacks back to any subdomain\n * of the tenant. Distinct from cookieDomain in two-level apex setups.\n */\n parentDomain: string;\n /** AWS region. Default: us-east-1. */\n region: string;\n /**\n * For spoke apps: this spoke's slug (matches the tenant roster entry's\n * slug in <tenant>-infra/config/apps.yaml). For apex: undefined.\n */\n appSlug?: string;\n /**\n * For spoke apps: this spoke's FQDN (e.g. \"leads.agency.aillc.link\").\n * For apex: same as `apex`.\n */\n appDomain: string;\n /** \"apex\" or \"spoke\". Affects which secret-arn fields are required. */\n role: TenantRole;\n};\n\nexport type TenantServerConfig = TenantPublicConfig & {\n /** AUTH_SECRET ARN in Secrets Manager. Used by createAuth(). */\n authSecretArn: string;\n /** Cognito client secret ARN. Apex only -- spokes don't run the OAuth dance. */\n authCognitoSecretArn?: string;\n /** Cognito issuer URL (apex only). */\n cognitoIssuer?: string;\n /** Cognito client ID (apex only). */\n cognitoClientId?: string;\n /** Comma-separated admin emails (auto-promoted on first sign-in). */\n adminEmails?: string;\n /** Aurora connection secret ARN (spoke only). */\n dbSecretArn?: string;\n /** Aurora endpoint host (spoke only). */\n dbHost?: string;\n /** Aurora database name (spoke only). */\n dbName?: string;\n /** Stripe credentials bundle ARN (spoke that does billing). */\n stripeSecretArn?: string;\n /** Stripe webhook signing secret ARN (spoke that does billing). */\n stripeWebhookSecretArn?: string;\n};\n\nexport const TENANT_GLOBAL_KEY = \"__TENANT__\" as const;\n\ndeclare global {\n interface Window {\n [TENANT_GLOBAL_KEY]?: TenantPublicConfig;\n }\n}\n","import \"server-only\";\n\nimport {\n filterAppsByIdentityGroups,\n sortAppsByNavOrder,\n validateAppsRoster,\n type TenantApp,\n type AppsRoster,\n} from \"../apps-roster/schema.js\";\nimport type { TenantPublicConfig, TenantServerConfig } from \"../tenant-types.js\";\n\n// =============================================================================\n// /api/apps route handler factories.\n//\n// The apex owns the canonical tenant roster (config/apps.json) and serves\n// it via `createAppsRouteHandler`. Spokes own no roster -- their\n// /api/apps is a proxy to the apex via `createAppsProxyRouteHandler`, so\n// adding a new spoke does NOT require redeploying every existing spoke.\n//\n// Both handlers serve AppShell same-origin, so the browser-side fetch\n// stays simple (`fetch(\"/api/apps\")` with cookie credentials).\n// =============================================================================\n\ntype SessionLike = {\n user?: { groups?: string[] | null } | null;\n} | null;\n\ntype AuthFn = () => Promise<SessionLike>;\n\nexport type CreateAppsRouteHandlerOptions = {\n /** Roster shape, typically `import appsJson from \"../../config/apps.json\"`. */\n roster: AppsRoster | unknown;\n /** Consuming app's `auth()` function. */\n auth: AuthFn;\n /**\n * Tenant config (apex + optional appDomain). Used to derive each app's\n * absolute `appUrl` from its subdomain. Typically the same struct passed\n * to createAuth.\n */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /** Set false to make the endpoint public (NOT recommended). Default true. */\n requireAuth?: boolean;\n};\n\nfunction deriveAppUrl(app: TenantApp, apex: string): string {\n if (app.subdomain === \"\") return `https://${apex}`;\n return `https://${app.subdomain}.${apex}`;\n}\n\nexport function createAppsRouteHandler(opts: CreateAppsRouteHandlerOptions) {\n const validated = validateAppsRoster(opts.roster);\n if (!validated.ok) {\n throw new Error(\n `createAppsRouteHandler: roster failed validation: ${validated.errors\n .map((e) => `${e.path}: ${e.message}`)\n .join(\"; \")}`,\n );\n }\n const apps: TenantApp[] = validated.value.apps;\n const requireAuth = opts.requireAuth ?? true;\n\n return {\n GET: async (): Promise<Response> => {\n let session: SessionLike = null;\n if (requireAuth) {\n session = await opts.auth();\n if (!session) {\n return Response.json({ error: \"unauthenticated\" }, { status: 401 });\n }\n }\n const userGroups = session?.user?.groups ?? [];\n const visible = filterAppsByIdentityGroups(apps, userGroups);\n const sorted = sortAppsByNavOrder(visible);\n const withUrl = sorted.map((a) => ({\n slug: a.slug,\n role: a.role,\n subdomain: a.subdomain,\n displayName: a.displayName,\n navOrder: a.navOrder,\n requiredIdentityGroups: a.requiredIdentityGroups,\n appUrl: deriveAppUrl(a, opts.tenant.apex),\n }));\n return Response.json(withUrl, {\n headers: {\n \"Cache-Control\": \"private, s-maxage=300, stale-while-revalidate=600\",\n },\n });\n },\n };\n}\n\n// =============================================================================\n// createAppsProxyRouteHandler\n//\n// Spoke-side /api/apps handler. Forwards the user's request (Cookie header\n// in particular) to the apex's /api/apps endpoint and proxies the response\n// back, preserving status, content-type, and cache headers.\n//\n// The session cookie is parent-domain-scoped (Domain=.<apex>) so the\n// browser sends it on the spoke's same-origin request; we forward that\n// cookie on the server-to-server fetch to the apex so the apex's\n// authenticated handler sees the same user. No CORS involved.\n//\n// Spokes that use this factory ship no roster file -- the canonical\n// roster lives only in the apex.\n// =============================================================================\n\nexport type CreateAppsProxyRouteHandlerOptions = {\n /** Same tenant struct passed to createAuth. Used to derive the apex URL. */\n tenant: Pick<TenantServerConfig, \"apex\"> | Pick<TenantPublicConfig, \"apex\">;\n /**\n * Override the upstream URL. Default: `https://${tenant.apex}/api/apps`.\n */\n upstreamUrl?: string;\n /**\n * Headers to forward from the incoming request, lowercase keys. Default\n * forwards `cookie`, `x-forwarded-for`, and `user-agent`. `authorization`\n * is intentionally NOT forwarded by default -- the upstream uses the\n * parent-domain session cookie, not bearer tokens, and forwarding raw\n * Authorization headers across services is a footgun.\n */\n forwardHeaders?: readonly string[];\n /**\n * Hard timeout for the upstream fetch, in milliseconds. On timeout the\n * handler returns 504 with a structured body instead of letting the\n * client hang. Default: 5000.\n */\n timeoutMs?: number;\n};\n\nconst DEFAULT_FORWARD_HEADERS = [\"cookie\", \"x-forwarded-for\", \"user-agent\"] as const;\n\nconst DEFAULT_RESPONSE_HEADERS = [\n \"content-type\",\n \"cache-control\",\n \"vary\",\n \"etag\",\n] as const;\n\n/**\n * Loop-guard header set on every outbound proxy fetch. If an inbound\n * request already carries it, the apps-proxy refuses to forward -- a\n * misconfigured upstream (e.g. apex `/api/apps` pointing back at a\n * spoke) would otherwise produce an infinite chain.\n */\nconst PROXY_LOOP_HEADER = \"x-augint-apps-proxy\";\n\nexport function createAppsProxyRouteHandler(opts: CreateAppsProxyRouteHandlerOptions) {\n const upstream = opts.upstreamUrl ?? `https://${opts.tenant.apex}/api/apps`;\n const forward = opts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;\n const timeoutMs = opts.timeoutMs ?? 5000;\n\n return {\n GET: async (request: Request): Promise<Response> => {\n if (request.headers.get(PROXY_LOOP_HEADER)) {\n // Loud-fast: misconfiguration. 508 Loop Detected is the closest\n // standard status; do NOT silently 200 with an empty body.\n return Response.json(\n {\n error: \"apps_proxy_loop_detected\",\n message:\n \"Inbound request already carries the apps-proxy loop-guard header. \" +\n \"This usually means the apex /api/apps is misconfigured (pointing back \" +\n \"at a spoke) or two spokes are proxying to each other.\",\n upstream,\n },\n { status: 508 },\n );\n }\n\n const headers = new Headers();\n for (const name of forward) {\n const v = request.headers.get(name);\n if (v) headers.set(name, v);\n }\n headers.set(PROXY_LOOP_HEADER, \"1\");\n\n try {\n const upstreamResponse = await fetch(upstream, {\n method: \"GET\",\n headers,\n cache: \"no-store\",\n redirect: \"manual\",\n signal: AbortSignal.timeout(timeoutMs),\n });\n const body = await upstreamResponse.arrayBuffer();\n const responseHeaders = new Headers();\n for (const name of DEFAULT_RESPONSE_HEADERS) {\n const v = upstreamResponse.headers.get(name);\n if (v) responseHeaders.set(name, v);\n }\n return new Response(body, {\n status: upstreamResponse.status,\n headers: responseHeaders,\n });\n } catch (err) {\n const isTimeout = err instanceof DOMException && err.name === \"TimeoutError\";\n if (isTimeout) {\n return Response.json(\n {\n error: \"apps_proxy_timeout\",\n message: `Upstream did not respond within ${timeoutMs}ms`,\n upstream,\n },\n { status: 504, headers: { \"cache-control\": \"no-store\" } },\n );\n }\n return Response.json(\n {\n error: \"apps_proxy_unavailable\",\n message: err instanceof Error ? err.message : String(err),\n upstream,\n },\n { status: 503, headers: { \"cache-control\": \"no-store\" } },\n );\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@augmenting-integrations/platform",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.12.0",
|
|
4
4
|
"description": "Tenant + app manifest contract for the augint platform. Owns TenantConfig (server-side env load + browser-safe public subset + TenantBootScript) and the app.manifest.json schema/loader. Every other @augmenting-integrations/* package consumes tenant context from here, so /auth no longer owns it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|