@pablo2410/core-server 0.5.2 → 0.6.1
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/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-OAF3JSO7.js → chunk-FSR7KF23.js} +1 -1
- package/dist/chunk-FSR7KF23.js.map +1 -0
- package/dist/{chunk-VQKD3QJB.js → chunk-LH6QZJM4.js} +2 -2
- package/dist/chunk-LH6QZJM4.js.map +1 -0
- package/dist/hierarchy.d.ts +1 -1
- package/dist/hierarchy.js +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.js +9 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-OAF3JSO7.js.map +0 -1
- package/dist/chunk-VQKD3QJB.js.map +0 -1
package/dist/auth.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ interface AuthEnv {
|
|
|
56
56
|
jwtSecret: string;
|
|
57
57
|
/** Shared portal JWT secret for cross-subdomain SSO (PORTAL_JWT_SECRET) */
|
|
58
58
|
portalJwtSecret: string;
|
|
59
|
-
/** Cookie domain for cross-subdomain sharing, e.g. ".
|
|
59
|
+
/** Cookie domain for cross-subdomain sharing, e.g. ".oplyticsdigital.net" */
|
|
60
60
|
cookieDomain: string;
|
|
61
61
|
/** Manus OAuth server URL */
|
|
62
62
|
oAuthServerUrl: string;
|
package/dist/auth.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hierarchy.ts"],"sourcesContent":["/**\n * Hierarchy Service — the single server-side source for org-hierarchy data.\n *\n * Fetches the canonical org-hierarchy from the Portal Service API\n * (GET /api/service/hierarchy, authenticated with a platform-level service key),\n * caches it with a TTL, and scopes it to a caller's enterprise using the shared\n * isolation rule (scopeHierarchyToEnterprise from @pablo2410/shared-ui).\n *\n * One fetch, one mapper, one cache, one scope policy. Every subdomain constructs\n * this once from its env and calls getScopedHierarchy(ctx.user) in its tRPC\n * `hierarchy` procedure — no per-app fetch/scope/null-policy reinvention.\n *\n * Quick example:\n * const hierarchy = createHierarchyService({\n * portalApiUrl: process.env.PORTAL_API_URL ?? \"\",\n * portalApiKey: process.env.PORTAL_API_KEY ?? \"\",\n * });\n * // in a tRPC router:\n * hierarchy: protectedProcedure.query(({ ctx }) => hierarchy.getScopedHierarchy(ctx.user)),\n */\nimport type { OrgHierarchyData } from \"@pablo2410/shared-ui/hierarchy/core\";\nimport { scopeHierarchyToEnterprise } from \"@pablo2410/shared-ui/hierarchy/core\";\n\nexport interface HierarchyServiceConfig {\n /** Portal Service API base URL, e.g. https://portal.oplyticsdigital.net */\n portalApiUrl: string;\n /** Platform-level X-Service-Key for the Portal Service API. */\n portalApiKey: string;\n /** Cache TTL in milliseconds. Default 15 minutes. Set 0 to disable caching. */\n cacheTtlMs?: number;\n /** Injectable fetch implementation (for tests). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n /** Injectable clock (for tests). Defaults to Date.now. */\n now?: () => number;\n}\n\nexport interface HierarchyService {\n /**\n * The org-hierarchy scoped to the caller's enterprise. A user with no resolved\n * enterprise (null/undefined enterpriseId) receives an empty hierarchy — see\n * scopeHierarchyToEnterprise for the policy. Never returns null.\n */\n getScopedHierarchy(\n user: { enterpriseId?: number | null } | null,\n ): Promise<OrgHierarchyData>;\n /**\n * The full, unscoped hierarchy (every enterprise). For Portal/admin tooling\n * only — subdomains must use getScopedHierarchy. null if unconfigured/unreachable.\n */\n getFullHierarchy(): Promise<OrgHierarchyData | null>;\n /** Clear the cache (e.g. after a portal sync). */\n invalidate(): void;\n /** Config + cache status, for health endpoints. */\n getStatus(): {\n configured: boolean;\n cached: boolean;\n ageMs: number | null;\n /** Reason the last fetch failed (null if the last fetch succeeded). */\n lastError: string | null;\n };\n}\n\nconst FIFTEEN_MINUTES = 15 * 60 * 1000;\n\nexport function createHierarchyService(\n config: HierarchyServiceConfig,\n): HierarchyService {\n const {\n portalApiUrl,\n portalApiKey,\n cacheTtlMs = FIFTEEN_MINUTES,\n fetchImpl = fetch,\n now = Date.now,\n } = config;\n\n const configured = Boolean(portalApiUrl && portalApiKey);\n let cache: { data: OrgHierarchyData; at: number } | null = null;\n // Last fetch failure reason, surfaced via getStatus() for diagnostics. A\n // configured service that returns an empty hierarchy is almost always a fetch\n // failure (wrong URL/key, unreachable Portal, non-200) — without this it was\n // swallowed to a silent null and looked like \"enterprise has no data\".\n let lastError: string | null = null;\n\n async function fetchRaw(): Promise<OrgHierarchyData | null> {\n if (!configured) {\n lastError = \"not configured (PORTAL_API_URL/PORTAL_API_KEY missing)\";\n return null;\n }\n const url = `${portalApiUrl.replace(/\\/$/, \"\")}/api/service/hierarchy`;\n let res: Response;\n try {\n res = await fetchImpl(url, {\n headers: {\n \"X-Service-Key\": portalApiKey,\n \"Content-Type\": \"application/json\",\n },\n });\n } catch (err) {\n lastError = `fetch threw: ${err instanceof Error ? err.message : String(err)}`;\n console.error(`[HierarchyService] ${lastError} (url=${url})`);\n return null;\n }\n if (!res.ok) {\n lastError = `HTTP ${res.status} ${res.statusText} from ${url}`;\n console.error(`[HierarchyService] ${lastError}`);\n return null;\n }\n const json = await res.json().catch(() => null);\n if (!json) {\n lastError = `invalid/empty JSON body from ${url}`;\n console.error(`[HierarchyService] ${lastError}`);\n return null;\n }\n lastError = null;\n return normalize(json);\n }\n\n async function getFullHierarchy(): Promise<OrgHierarchyData | null> {\n if (cacheTtlMs > 0 && cache && now() - cache.at < cacheTtlMs) {\n return cache.data;\n }\n const data = await fetchRaw();\n if (data && cacheTtlMs > 0) cache = { data, at: now() };\n return data;\n }\n\n async function getScopedHierarchy(\n user: { enterpriseId?: number | null } | null,\n ): Promise<OrgHierarchyData> {\n const full = await getFullHierarchy();\n return scopeHierarchyToEnterprise(full, user?.enterpriseId ?? null);\n }\n\n function invalidate(): void {\n cache = null;\n }\n\n function getStatus() {\n return {\n configured,\n cached: cache !== null,\n ageMs: cache ? now() - cache.at : null,\n lastError,\n };\n }\n\n return { getScopedHierarchy, getFullHierarchy, invalidate, getStatus };\n}\n\n/**\n * Map the Portal Service API response to the canonical OrgHierarchyData shape.\n * Tolerant of missing arrays (defaults to []) so a partial payload never throws.\n */\nfunction normalize(data: unknown): OrgHierarchyData {\n const d = (data ?? {}) as Record<string, any[]>;\n return {\n enterprises: (d.enterprises ?? []).map((e) => ({\n id: e.id,\n name: e.name,\n code: e.code,\n })),\n businessUnits: (d.businessUnits ?? []).map((bu) => ({\n id: bu.id,\n name: bu.name,\n code: bu.code,\n enterpriseId: bu.enterpriseId,\n })),\n sites: (d.sites ?? []).map((s) => ({\n id: s.id,\n name: s.name,\n code: s.code,\n enterpriseId: s.enterpriseId,\n businessUnitId: s.businessUnitId ?? 0,\n location: s.location,\n })),\n areas: (d.areas ?? []).map((a) => ({\n id: a.id,\n name: a.name,\n code: a.code,\n siteId: a.siteId,\n })),\n assets: (d.assets ?? []).map((a) => ({\n id: a.id,\n name: a.name,\n code: a.code,\n siteId: a.siteId,\n areaId: a.areaId ?? 0,\n assetType: a.assetType,\n })),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -124,7 +124,7 @@ function createSubdomainAuth(config) {
|
|
|
124
124
|
try {
|
|
125
125
|
const parsed = new URL(url);
|
|
126
126
|
if (parsed.protocol !== "https:") return false;
|
|
127
|
-
return parsed.hostname === "
|
|
127
|
+
return parsed.hostname === "oplyticsdigital.net" || parsed.hostname.endsWith(".oplyticsdigital.net");
|
|
128
128
|
} catch {
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
@@ -449,4 +449,4 @@ function createSubdomainAuth(config) {
|
|
|
449
449
|
export {
|
|
450
450
|
createSubdomainAuth
|
|
451
451
|
};
|
|
452
|
-
//# sourceMappingURL=chunk-
|
|
452
|
+
//# sourceMappingURL=chunk-LH6QZJM4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth/createSubdomainAuth.ts"],"sourcesContent":["/**\n * createSubdomainAuth — shared SSO auth factory for Oplytics.digital subdomains.\n *\n * Encapsulates:\n * - JWT session verification (PORTAL_JWT_SECRET first, fallback to JWT_SECRET)\n * - Cookie domain handling via COOKIE_DOMAIN env var\n * - Portal API user enrichment with X-Service-Key header\n * - Auto-create user from JWT payload if not in local DB\n * - Enterprise backfill from portal API\n * - Manus OAuth code exchange and user info\n * - OAuth callback route registration\n *\n * Based on SQDCP's battle-tested sdk.ts + cookies.ts.\n */\n\nimport type { Request, CookieOptions } from \"express\";\nimport type {\n AuthEnv,\n BaseUser,\n DbAdapter,\n ExchangeTokenResponse,\n GetUserInfoResponse,\n GetUserInfoWithJwtResponse,\n PortalUser,\n RoleMapper,\n SessionPayload,\n SubdomainAuth,\n SubdomainAuthConfig,\n} from \"./types\";\n\n// ─── Constants ──────────────────────────────────────────────────────\n\nconst DEFAULT_COOKIE_NAME = \"app_session_id\";\nconst ONE_YEAR_MS = 1000 * 60 * 60 * 24 * 365;\nconst AXIOS_TIMEOUT_MS = 30_000;\n\nconst EXCHANGE_TOKEN_PATH =\n \"/webdev.v1.WebDevAuthPublicService/ExchangeToken\";\nconst GET_USER_INFO_PATH =\n \"/webdev.v1.WebDevAuthPublicService/GetUserInfo\";\nconst GET_USER_INFO_WITH_JWT_PATH =\n \"/webdev.v1.WebDevAuthPublicService/GetUserInfoWithJwt\";\n\n// ─── Helpers ────────────────────────────────────────────────────────\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0;\n\n/**\n * Derive the login method from the Manus platforms array.\n */\nfunction deriveLoginMethod(\n platforms: unknown,\n fallback: string | null | undefined\n): string | null {\n if (fallback && fallback.length > 0) return fallback;\n if (!Array.isArray(platforms) || platforms.length === 0) return null;\n const set = new Set<string>(\n platforms.filter((p): p is string => typeof p === \"string\")\n );\n if (set.has(\"REGISTERED_PLATFORM_EMAIL\")) return \"email\";\n if (set.has(\"REGISTERED_PLATFORM_GOOGLE\")) return \"google\";\n if (set.has(\"REGISTERED_PLATFORM_APPLE\")) return \"apple\";\n if (\n set.has(\"REGISTERED_PLATFORM_MICROSOFT\") ||\n set.has(\"REGISTERED_PLATFORM_AZURE\")\n )\n return \"microsoft\";\n if (set.has(\"REGISTERED_PLATFORM_GITHUB\")) return \"github\";\n const first = Array.from(set)[0];\n return first ? first.toLowerCase() : null;\n}\n\n// ─── Factory ────────────────────────────────────────────────────────\n\nexport function createSubdomainAuth<TUser extends BaseUser = BaseUser>(\n config: SubdomainAuthConfig<TUser>\n): SubdomainAuth<TUser> {\n const {\n env,\n db,\n // Default to identity (no role translation). Subdomains should adopt the\n // Portal's canonical roles verbatim; a custom mapper is legacy.\n mapRole = (role: string) => role,\n userSource = \"db\",\n cookieName = DEFAULT_COOKIE_NAME,\n sessionExpiryMs = ONE_YEAR_MS,\n serviceName = \"Subdomain\",\n portalUserCacheTtlMs = 30_000,\n } = config;\n\n const tag = `[${serviceName} Auth]`;\n\n if (userSource === \"db\" && !db) {\n throw new Error(\n `${tag} userSource \"db\" requires a db adapter in createSubdomainAuth config`\n );\n }\n\n // Short-TTL cache of Portal users (portal mode), keyed by openId, so we don't\n // hit the Portal on every request.\n const portalUserCache = new Map<string, { user: PortalUser; at: number }>();\n\n // ── Lazy-loaded dependencies (jose, cookie, axios) ──────────────\n // We use dynamic imports so consumers don't need to install these\n // as peer deps if they only use other core-server features.\n\n let _jose: typeof import(\"jose\") | null = null;\n async function getJose() {\n if (!_jose) _jose = await import(\"jose\");\n return _jose;\n }\n\n let _cookie: typeof import(\"cookie\") | null = null;\n async function getCookie() {\n if (!_cookie) _cookie = await import(\"cookie\");\n return _cookie;\n }\n\n let _axios: typeof import(\"axios\").default | null = null;\n async function getAxios() {\n if (!_axios) {\n const mod = await import(\"axios\");\n _axios = mod.default;\n }\n return _axios;\n }\n\n // ── OAuth HTTP client ───────────────────────────────────────────\n\n let oauthClient: import(\"axios\").AxiosInstance | null = null;\n async function getOAuthClient() {\n if (oauthClient) return oauthClient;\n const axios = await getAxios();\n oauthClient = axios.create({\n baseURL: env.oAuthServerUrl,\n timeout: AXIOS_TIMEOUT_MS,\n });\n return oauthClient;\n }\n\n // ── Portal API client ──────────────────────────────────────────\n\n let portalClient: import(\"axios\").AxiosInstance | null = null;\n function getPortalClient() {\n if (portalClient) return portalClient;\n // Synchronous check — we only create if we have the URL\n if (!env.portalApiUrl) return null;\n // We need axios synchronously here, but it should already be loaded\n // by the time portal client is needed (after authenticateRequest calls getAxios)\n return null; // Will be created lazily in async context\n }\n\n async function getPortalClientAsync() {\n if (portalClient) return portalClient;\n if (!env.portalApiUrl) return null;\n const axios = await getAxios();\n portalClient = axios.create({\n baseURL: env.portalApiUrl,\n timeout: 10_000,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(env.portalApiKey ? { \"X-Service-Key\": env.portalApiKey } : {}),\n },\n });\n return portalClient;\n }\n\n // ── JWT Secrets ────────────────────────────────────────────────\n\n function getPortalSecret() {\n const secret = env.portalJwtSecret;\n if (!secret) throw new Error(\"PORTAL_JWT_SECRET is not configured\");\n return new TextEncoder().encode(secret);\n }\n\n function getSessionSecret() {\n // Prefer portalJwtSecret for signing so cookies work cross-subdomain\n const secret = env.portalJwtSecret || env.jwtSecret;\n return new TextEncoder().encode(secret);\n }\n\n // ── Cookie Helpers ─────────────────────────────────────────────\n\n function getCookieDomain(): string | undefined {\n return env.cookieDomain || undefined;\n }\n\n function isSecureRequest(req: Request): boolean {\n if (req.protocol === \"https\") return true;\n const forwardedProto = req.headers[\"x-forwarded-proto\"];\n if (!forwardedProto) return false;\n const protoList = Array.isArray(forwardedProto)\n ? forwardedProto\n : forwardedProto.split(\",\");\n return protoList.some((p) => p.trim().toLowerCase() === \"https\");\n }\n\n function getSessionCookieOptions(\n req: Request\n ): Pick<\n CookieOptions,\n \"domain\" | \"httpOnly\" | \"path\" | \"sameSite\" | \"secure\"\n > {\n const secure = isSecureRequest(req);\n const domain = getCookieDomain();\n return {\n domain,\n httpOnly: true,\n path: \"/\",\n sameSite: \"lax\",\n secure,\n };\n }\n\n function isValidReturnUrl(url: string): boolean {\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"https:\") return false;\n return (\n parsed.hostname === \"oplyticsdigital.net\" ||\n parsed.hostname.endsWith(\".oplyticsdigital.net\")\n );\n } catch {\n return false;\n }\n }\n\n // ── Session Token ──────────────────────────────────────────────\n\n async function signSession(\n payload: SessionPayload,\n options: { expiresInMs?: number } = {}\n ): Promise<string> {\n const jose = await getJose();\n const issuedAt = Date.now();\n const expiresInMs = options.expiresInMs ?? sessionExpiryMs;\n const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000);\n const secretKey = getSessionSecret();\n\n return new jose.SignJWT({\n openId: payload.openId,\n appId: payload.appId,\n name: payload.name,\n })\n .setProtectedHeader({ alg: \"HS256\", typ: \"JWT\" })\n .setExpirationTime(expirationSeconds)\n .sign(secretKey);\n }\n\n async function createSessionToken(\n openId: string,\n options: { expiresInMs?: number; name?: string } = {}\n ): Promise<string> {\n return signSession(\n { openId, appId: env.appId, name: options.name || \"\" },\n options\n );\n }\n\n // ── Session Verification ───────────────────────────────────────\n\n async function verifySession(\n cookieValue: string | undefined | null\n ): Promise<SessionPayload | null> {\n if (!cookieValue) {\n console.warn(`${tag} Missing session cookie`);\n return null;\n }\n\n const jose = await getJose();\n\n // Try PORTAL_JWT_SECRET first (cross-subdomain SSO)\n if (env.portalJwtSecret) {\n try {\n const portalKey = getPortalSecret();\n const { payload } = await jose.jwtVerify(cookieValue, portalKey, {\n algorithms: [\"HS256\"],\n });\n const { openId, appId, name } = payload as Record<string, unknown>;\n if (\n !isNonEmptyString(openId) ||\n !isNonEmptyString(appId) ||\n !isNonEmptyString(name)\n ) {\n console.warn(`${tag} Portal JWT payload missing required fields`);\n return null;\n }\n return { openId, appId, name };\n } catch {\n // Portal secret didn't work — fall through to local secret\n }\n }\n\n // Fallback: try local JWT_SECRET\n try {\n const secretKey = getSessionSecret();\n const { payload } = await jose.jwtVerify(cookieValue, secretKey, {\n algorithms: [\"HS256\"],\n });\n const { openId, appId, name } = payload as Record<string, unknown>;\n if (\n !isNonEmptyString(openId) ||\n !isNonEmptyString(appId) ||\n !isNonEmptyString(name)\n ) {\n console.warn(`${tag} Session payload missing required fields`);\n return null;\n }\n return { openId, appId, name };\n } catch (error) {\n console.warn(\n `${tag} Session verification failed:`,\n (error as Error).message\n );\n return null;\n }\n }\n\n // ── Portal API ─────────────────────────────────────────────────\n\n async function getPortalUser(openId: string): Promise<PortalUser | null> {\n try {\n const client = await getPortalClientAsync();\n if (!client) return null;\n // The Portal Service API is under /api/service (base URL is the origin,\n // e.g. https://portal.oplyticsdigital.net). Must include the /api/service\n // prefix — omitting it 404s and silently strands enterprise resolution.\n const { data } = await client.get<PortalUser>(\n `/api/service/users/by-openid/${encodeURIComponent(openId)}`\n );\n return data ?? null;\n } catch (err: unknown) {\n const status = (err as any)?.response?.status;\n if (status === 401) {\n console.error(\n `${tag} Portal API key revoked (401) — enrichment disabled`\n );\n } else if (status === 404) {\n // User not found in portal — normal for first-time users\n } else {\n console.warn(\n `${tag} Portal user lookup failed:`,\n (err as Error).message\n );\n }\n return null;\n }\n }\n\n /** Portal user lookup with a short-TTL cache (portal mode). */\n async function getPortalUserCached(\n openId: string\n ): Promise<PortalUser | null> {\n if (portalUserCacheTtlMs > 0) {\n const hit = portalUserCache.get(openId);\n if (hit && Date.now() - hit.at < portalUserCacheTtlMs) {\n return hit.user;\n }\n }\n const user = await getPortalUser(openId);\n if (user && portalUserCacheTtlMs > 0) {\n portalUserCache.set(openId, { user, at: Date.now() });\n }\n return user;\n }\n\n /**\n * Build ctx.user entirely from the Portal record — no local persistence.\n * The single source of truth. ctx.user.id is the PORTAL user id. Roles and\n * enterprise come from the Portal verbatim (no mapping).\n *\n * On a genuine Portal outage (the by-openid lookup returns null) we keep the\n * user authenticated using their verified session claims, with role/enterprise\n * unresolved, rather than throwing. Throwing here would make createContext (in\n * every subdomain) swallow the error into `user = null`, which the AuthGate\n * renders as the Sign In screen — i.e. a transient Portal blip would log the\n * user out. Their session cookie is already valid; keep them in, degraded,\n * until the Portal recovers. (Enterprise-scoped procedures will 403 during the\n * outage; client array-guards keep pages from crashing.)\n */\n async function resolvePortalUser(\n openId: string,\n session: SessionPayload\n ): Promise<TUser> {\n const portalUser = await getPortalUserCached(openId);\n\n if (!portalUser) {\n console.warn(\n `${tag} Portal user unavailable for ${openId} — keeping session (role/enterprise unresolved until Portal recovers)`\n );\n return {\n id: 0,\n openId,\n name: session.name ?? null,\n email: null,\n role: \"\",\n enterpriseId: null,\n portalUserId: null,\n } as unknown as TUser;\n }\n\n const enterpriseId =\n portalUser.enterpriseId ?? portalUser.companyId ?? null;\n return {\n id: portalUser.id,\n openId: portalUser.openId ?? openId,\n name: portalUser.name ?? session.name ?? null,\n email: portalUser.email ?? null,\n role: portalUser.role,\n enterpriseId,\n portalUserId: portalUser.id,\n } as unknown as TUser;\n }\n\n // ── OAuth ──────────────────────────────────────────────────────\n\n async function exchangeCodeForToken(\n code: string,\n state: string\n ): Promise<ExchangeTokenResponse> {\n const client = await getOAuthClient();\n const redirectUri = atob(state);\n const { data } = await client.post<ExchangeTokenResponse>(\n EXCHANGE_TOKEN_PATH,\n {\n clientId: env.appId,\n grantType: \"authorization_code\",\n code,\n redirectUri,\n }\n );\n return data;\n }\n\n async function getUserInfo(\n accessToken: string\n ): Promise<GetUserInfoResponse> {\n const client = await getOAuthClient();\n const { data } = await client.post<GetUserInfoResponse>(\n GET_USER_INFO_PATH,\n { accessToken }\n );\n const loginMethod = deriveLoginMethod(\n (data as any)?.platforms,\n (data as any)?.platform ?? data.platform ?? null\n );\n return { ...(data as any), platform: loginMethod, loginMethod };\n }\n\n async function getUserInfoWithJwt(\n jwtToken: string\n ): Promise<GetUserInfoWithJwtResponse> {\n const client = await getOAuthClient();\n const { data } = await client.post<GetUserInfoWithJwtResponse>(\n GET_USER_INFO_WITH_JWT_PATH,\n { jwtToken, projectId: env.appId }\n );\n const loginMethod = deriveLoginMethod(\n (data as any)?.platforms,\n (data as any)?.platform ?? data.platform ?? null\n );\n return { ...(data as any), platform: loginMethod, loginMethod };\n }\n\n // ── Authenticate Request ───────────────────────────────────────\n\n async function authenticateRequest(req: Request): Promise<TUser> {\n const cookie = await getCookie();\n const cookies = req.headers.cookie\n ? new Map(Object.entries(cookie.parse(req.headers.cookie)))\n : new Map<string, string>();\n\n const sessionCookie = cookies.get(cookieName);\n const session = await verifySession(sessionCookie);\n\n if (!session) {\n throw Object.assign(new Error(\"Invalid session cookie\"), {\n statusCode: 403,\n });\n }\n\n const sessionUserId = session.openId;\n const signedInAt = new Date();\n\n // ── Portal mode: single source of truth, no local mirror ────\n if (userSource === \"portal\") {\n return await resolvePortalUser(sessionUserId, session);\n }\n\n let user = (await db!.getUserByOpenId(sessionUserId)) ?? null;\n\n // ── Auto-create user from verified JWT ──────────────────────\n if (!user) {\n try {\n // Try portal API first for richer user info\n const portalUser = await getPortalUser(sessionUserId);\n\n if (portalUser) {\n console.log(\n `${tag} Cross-subdomain SSO: creating user from portal (${portalUser.email || sessionUserId})`\n );\n // Portal returns `enterpriseId`; `companyId` is a legacy alias.\n const portalEnterpriseId =\n portalUser.enterpriseId ?? portalUser.companyId ?? null;\n await db!.upsertUser({\n openId: sessionUserId,\n name: portalUser.name || session.name || null,\n email: portalUser.email ?? null,\n loginMethod: \"portal-sso\",\n role: mapRole(portalUser.role),\n lastSignedIn: signedInAt,\n ...(portalEnterpriseId != null\n ? {\n enterpriseId: portalEnterpriseId,\n portalUserId: portalUser.id,\n }\n : {}),\n });\n user = (await db!.getUserByOpenId(sessionUserId)) ?? null;\n } else {\n // Portal unavailable — create from JWT payload\n console.log(\n `${tag} Cross-subdomain SSO: creating user from JWT payload (${sessionUserId})`\n );\n await db!.upsertUser({\n openId: sessionUserId,\n name: session.name || null,\n loginMethod: \"portal-sso\",\n lastSignedIn: signedInAt,\n });\n user = (await db!.getUserByOpenId(sessionUserId)) ?? null;\n }\n } catch (error) {\n // Last resort: try Manus OAuth JWT\n try {\n const userInfo = await getUserInfoWithJwt(sessionCookie ?? \"\");\n await db!.upsertUser({\n openId: userInfo.openId,\n name: userInfo.name || null,\n email: userInfo.email ?? null,\n loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,\n lastSignedIn: signedInAt,\n });\n user = (await db!.getUserByOpenId(userInfo.openId)) ?? null;\n } catch (oauthError) {\n console.error(\n `${tag} All user sync methods failed:`,\n error,\n oauthError\n );\n throw Object.assign(new Error(\"Failed to sync user info\"), {\n statusCode: 403,\n });\n }\n }\n }\n\n if (!user) {\n throw Object.assign(new Error(\"User not found\"), { statusCode: 403 });\n }\n\n // ── Enterprise backfill ─────────────────────────────────────\n if (user.enterpriseId === null) {\n try {\n const portalUser = await getPortalUser(user.openId);\n // Portal returns `enterpriseId`; `companyId` is a legacy alias.\n const portalEnterpriseId =\n portalUser?.enterpriseId ?? portalUser?.companyId ?? null;\n if (portalEnterpriseId != null) {\n console.log(\n `${tag} Backfilling enterpriseId=${portalEnterpriseId} for user ${user.id}`\n );\n await db!.updateUserFields(user.id, {\n enterpriseId: portalEnterpriseId,\n ...(portalUser?.id ? { portalUserId: portalUser.id } : {}),\n });\n user = (await db!.getUserByOpenId(user.openId)) ?? user;\n }\n } catch (backfillError) {\n console.warn(\n `${tag} Failed to backfill enterpriseId for user ${user.id}:`,\n backfillError\n );\n // Non-fatal — user can still proceed\n }\n }\n\n // Touch lastSignedIn\n await db!.upsertUser({\n openId: user.openId,\n lastSignedIn: signedInAt,\n });\n\n return user;\n }\n\n // ── OAuth Route Registration ───────────────────────────────────\n\n function registerOAuthRoutes(app: import(\"express\").Express) {\n app.get(\n \"/api/oauth/callback\",\n async (req: Request, res: import(\"express\").Response) => {\n const code =\n typeof req.query.code === \"string\" ? req.query.code : undefined;\n const state =\n typeof req.query.state === \"string\" ? req.query.state : undefined;\n\n if (!code || !state) {\n res.status(400).json({ error: \"code and state are required\" });\n return;\n }\n\n try {\n const tokenResponse = await exchangeCodeForToken(code, state);\n const userInfo = await getUserInfo(tokenResponse.accessToken);\n\n if (!userInfo.openId) {\n res.status(400).json({ error: \"openId missing from user info\" });\n return;\n }\n\n await db!.upsertUser({\n openId: userInfo.openId,\n name: userInfo.name || null,\n email: userInfo.email ?? null,\n loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,\n lastSignedIn: new Date(),\n });\n\n const sessionToken = await createSessionToken(userInfo.openId, {\n name: userInfo.name || \"\",\n expiresInMs: sessionExpiryMs,\n });\n\n const cookieOptions = getSessionCookieOptions(req);\n res.cookie(cookieName, sessionToken, {\n ...cookieOptions,\n maxAge: sessionExpiryMs,\n });\n\n res.redirect(302, \"/\");\n } catch (error) {\n console.error(`${tag} OAuth callback failed`, error);\n res.status(500).json({ error: \"OAuth callback failed\" });\n }\n }\n );\n }\n\n // ── Return public API ──────────────────────────────────────────\n\n return {\n verifySession,\n createSessionToken,\n authenticateRequest,\n exchangeCodeForToken,\n getUserInfo,\n getUserInfoWithJwt,\n getPortalUser,\n getSessionCookieOptions,\n isValidReturnUrl,\n registerOAuthRoutes,\n };\n}\n"],"mappings":";AAgCA,IAAM,sBAAsB;AAC5B,IAAM,cAAc,MAAO,KAAK,KAAK,KAAK;AAC1C,IAAM,mBAAmB;AAEzB,IAAM,sBACJ;AACF,IAAM,qBACJ;AACF,IAAM,8BACJ;AAIF,IAAM,mBAAmB,CAAC,UACxB,OAAO,UAAU,YAAY,MAAM,SAAS;AAK9C,SAAS,kBACP,WACA,UACe;AACf,MAAI,YAAY,SAAS,SAAS,EAAG,QAAO;AAC5C,MAAI,CAAC,MAAM,QAAQ,SAAS,KAAK,UAAU,WAAW,EAAG,QAAO;AAChE,QAAM,MAAM,IAAI;AAAA,IACd,UAAU,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AAAA,EAC5D;AACA,MAAI,IAAI,IAAI,2BAA2B,EAAG,QAAO;AACjD,MAAI,IAAI,IAAI,4BAA4B,EAAG,QAAO;AAClD,MAAI,IAAI,IAAI,2BAA2B,EAAG,QAAO;AACjD,MACE,IAAI,IAAI,+BAA+B,KACvC,IAAI,IAAI,2BAA2B;AAEnC,WAAO;AACT,MAAI,IAAI,IAAI,4BAA4B,EAAG,QAAO;AAClD,QAAM,QAAQ,MAAM,KAAK,GAAG,EAAE,CAAC;AAC/B,SAAO,QAAQ,MAAM,YAAY,IAAI;AACvC;AAIO,SAAS,oBACd,QACsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,UAAU,CAAC,SAAiB;AAAA,IAC5B,aAAa;AAAA,IACb,aAAa;AAAA,IACb,kBAAkB;AAAA,IAClB,cAAc;AAAA,IACd,uBAAuB;AAAA,EACzB,IAAI;AAEJ,QAAM,MAAM,IAAI,WAAW;AAE3B,MAAI,eAAe,QAAQ,CAAC,IAAI;AAC9B,UAAM,IAAI;AAAA,MACR,GAAG,GAAG;AAAA,IACR;AAAA,EACF;AAIA,QAAM,kBAAkB,oBAAI,IAA8C;AAM1E,MAAI,QAAsC;AAC1C,iBAAe,UAAU;AACvB,QAAI,CAAC,MAAO,SAAQ,MAAM,OAAO,MAAM;AACvC,WAAO;AAAA,EACT;AAEA,MAAI,UAA0C;AAC9C,iBAAe,YAAY;AACzB,QAAI,CAAC,QAAS,WAAU,MAAM,OAAO,QAAQ;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAgD;AACpD,iBAAe,WAAW;AACxB,QAAI,CAAC,QAAQ;AACX,YAAM,MAAM,MAAM,OAAO,OAAO;AAChC,eAAS,IAAI;AAAA,IACf;AACA,WAAO;AAAA,EACT;AAIA,MAAI,cAAoD;AACxD,iBAAe,iBAAiB;AAC9B,QAAI,YAAa,QAAO;AACxB,UAAM,QAAQ,MAAM,SAAS;AAC7B,kBAAc,MAAM,OAAO;AAAA,MACzB,SAAS,IAAI;AAAA,MACb,SAAS;AAAA,IACX,CAAC;AACD,WAAO;AAAA,EACT;AAIA,MAAI,eAAqD;AACzD,WAAS,kBAAkB;AACzB,QAAI,aAAc,QAAO;AAEzB,QAAI,CAAC,IAAI,aAAc,QAAO;AAG9B,WAAO;AAAA,EACT;AAEA,iBAAe,uBAAuB;AACpC,QAAI,aAAc,QAAO;AACzB,QAAI,CAAC,IAAI,aAAc,QAAO;AAC9B,UAAM,QAAQ,MAAM,SAAS;AAC7B,mBAAe,MAAM,OAAO;AAAA,MAC1B,SAAS,IAAI;AAAA,MACb,SAAS;AAAA,MACT,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAI,IAAI,eAAe,EAAE,iBAAiB,IAAI,aAAa,IAAI,CAAC;AAAA,MAClE;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAIA,WAAS,kBAAkB;AACzB,UAAM,SAAS,IAAI;AACnB,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qCAAqC;AAClE,WAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,EACxC;AAEA,WAAS,mBAAmB;AAE1B,UAAM,SAAS,IAAI,mBAAmB,IAAI;AAC1C,WAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,EACxC;AAIA,WAAS,kBAAsC;AAC7C,WAAO,IAAI,gBAAgB;AAAA,EAC7B;AAEA,WAAS,gBAAgB,KAAuB;AAC9C,QAAI,IAAI,aAAa,QAAS,QAAO;AACrC,UAAM,iBAAiB,IAAI,QAAQ,mBAAmB;AACtD,QAAI,CAAC,eAAgB,QAAO;AAC5B,UAAM,YAAY,MAAM,QAAQ,cAAc,IAC1C,iBACA,eAAe,MAAM,GAAG;AAC5B,WAAO,UAAU,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,MAAM,OAAO;AAAA,EACjE;AAEA,WAAS,wBACP,KAIA;AACA,UAAM,SAAS,gBAAgB,GAAG;AAClC,UAAM,SAAS,gBAAgB;AAC/B,WAAO;AAAA,MACL;AAAA,MACA,UAAU;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEA,WAAS,iBAAiB,KAAsB;AAC9C,QAAI,IAAI,WAAW,GAAG,KAAK,CAAC,IAAI,WAAW,IAAI,EAAG,QAAO;AACzD,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,OAAO,aAAa,SAAU,QAAO;AACzC,aACE,OAAO,aAAa,yBACpB,OAAO,SAAS,SAAS,sBAAsB;AAAA,IAEnD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAIA,iBAAe,YACb,SACA,UAAoC,CAAC,GACpB;AACjB,UAAM,OAAO,MAAM,QAAQ;AAC3B,UAAM,WAAW,KAAK,IAAI;AAC1B,UAAM,cAAc,QAAQ,eAAe;AAC3C,UAAM,oBAAoB,KAAK,OAAO,WAAW,eAAe,GAAI;AACpE,UAAM,YAAY,iBAAiB;AAEnC,WAAO,IAAI,KAAK,QAAQ;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ;AAAA,IAChB,CAAC,EACE,mBAAmB,EAAE,KAAK,SAAS,KAAK,MAAM,CAAC,EAC/C,kBAAkB,iBAAiB,EACnC,KAAK,SAAS;AAAA,EACnB;AAEA,iBAAe,mBACb,QACA,UAAmD,CAAC,GACnC;AACjB,WAAO;AAAA,MACL,EAAE,QAAQ,OAAO,IAAI,OAAO,MAAM,QAAQ,QAAQ,GAAG;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAIA,iBAAe,cACb,aACgC;AAChC,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,GAAG,GAAG,yBAAyB;AAC5C,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,QAAQ;AAG3B,QAAI,IAAI,iBAAiB;AACvB,UAAI;AACF,cAAM,YAAY,gBAAgB;AAClC,cAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,UAAU,aAAa,WAAW;AAAA,UAC/D,YAAY,CAAC,OAAO;AAAA,QACtB,CAAC;AACD,cAAM,EAAE,QAAQ,OAAO,KAAK,IAAI;AAChC,YACE,CAAC,iBAAiB,MAAM,KACxB,CAAC,iBAAiB,KAAK,KACvB,CAAC,iBAAiB,IAAI,GACtB;AACA,kBAAQ,KAAK,GAAG,GAAG,6CAA6C;AAChE,iBAAO;AAAA,QACT;AACA,eAAO,EAAE,QAAQ,OAAO,KAAK;AAAA,MAC/B,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,QAAI;AACF,YAAM,YAAY,iBAAiB;AACnC,YAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,UAAU,aAAa,WAAW;AAAA,QAC/D,YAAY,CAAC,OAAO;AAAA,MACtB,CAAC;AACD,YAAM,EAAE,QAAQ,OAAO,KAAK,IAAI;AAChC,UACE,CAAC,iBAAiB,MAAM,KACxB,CAAC,iBAAiB,KAAK,KACvB,CAAC,iBAAiB,IAAI,GACtB;AACA,gBAAQ,KAAK,GAAG,GAAG,0CAA0C;AAC7D,eAAO;AAAA,MACT;AACA,aAAO,EAAE,QAAQ,OAAO,KAAK;AAAA,IAC/B,SAAS,OAAO;AACd,cAAQ;AAAA,QACN,GAAG,GAAG;AAAA,QACL,MAAgB;AAAA,MACnB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAIA,iBAAe,cAAc,QAA4C;AACvE,QAAI;AACF,YAAM,SAAS,MAAM,qBAAqB;AAC1C,UAAI,CAAC,OAAQ,QAAO;AAIpB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,QAC5B,gCAAgC,mBAAmB,MAAM,CAAC;AAAA,MAC5D;AACA,aAAO,QAAQ;AAAA,IACjB,SAAS,KAAc;AACrB,YAAM,SAAU,KAAa,UAAU;AACvC,UAAI,WAAW,KAAK;AAClB,gBAAQ;AAAA,UACN,GAAG,GAAG;AAAA,QACR;AAAA,MACF,WAAW,WAAW,KAAK;AAAA,MAE3B,OAAO;AACL,gBAAQ;AAAA,UACN,GAAG,GAAG;AAAA,UACL,IAAc;AAAA,QACjB;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAGA,iBAAe,oBACb,QAC4B;AAC5B,QAAI,uBAAuB,GAAG;AAC5B,YAAM,MAAM,gBAAgB,IAAI,MAAM;AACtC,UAAI,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,sBAAsB;AACrD,eAAO,IAAI;AAAA,MACb;AAAA,IACF;AACA,UAAM,OAAO,MAAM,cAAc,MAAM;AACvC,QAAI,QAAQ,uBAAuB,GAAG;AACpC,sBAAgB,IAAI,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,IACtD;AACA,WAAO;AAAA,EACT;AAgBA,iBAAe,kBACb,QACA,SACgB;AAChB,UAAM,aAAa,MAAM,oBAAoB,MAAM;AAEnD,QAAI,CAAC,YAAY;AACf,cAAQ;AAAA,QACN,GAAG,GAAG,gCAAgC,MAAM;AAAA,MAC9C;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ;AAAA,QACA,MAAM,QAAQ,QAAQ;AAAA,QACtB,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc;AAAA,QACd,cAAc;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,eACJ,WAAW,gBAAgB,WAAW,aAAa;AACrD,WAAO;AAAA,MACL,IAAI,WAAW;AAAA,MACf,QAAQ,WAAW,UAAU;AAAA,MAC7B,MAAM,WAAW,QAAQ,QAAQ,QAAQ;AAAA,MACzC,OAAO,WAAW,SAAS;AAAA,MAC3B,MAAM,WAAW;AAAA,MACjB;AAAA,MACA,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF;AAIA,iBAAe,qBACb,MACA,OACgC;AAChC,UAAM,SAAS,MAAM,eAAe;AACpC,UAAM,cAAc,KAAK,KAAK;AAC9B,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,MAC5B;AAAA,MACA;AAAA,QACE,UAAU,IAAI;AAAA,QACd,WAAW;AAAA,QACX;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,iBAAe,YACb,aAC8B;AAC9B,UAAM,SAAS,MAAM,eAAe;AACpC,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,MAC5B;AAAA,MACA,EAAE,YAAY;AAAA,IAChB;AACA,UAAM,cAAc;AAAA,MACjB,MAAc;AAAA,MACd,MAAc,YAAY,KAAK,YAAY;AAAA,IAC9C;AACA,WAAO,EAAE,GAAI,MAAc,UAAU,aAAa,YAAY;AAAA,EAChE;AAEA,iBAAe,mBACb,UACqC;AACrC,UAAM,SAAS,MAAM,eAAe;AACpC,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,MAC5B;AAAA,MACA,EAAE,UAAU,WAAW,IAAI,MAAM;AAAA,IACnC;AACA,UAAM,cAAc;AAAA,MACjB,MAAc;AAAA,MACd,MAAc,YAAY,KAAK,YAAY;AAAA,IAC9C;AACA,WAAO,EAAE,GAAI,MAAc,UAAU,aAAa,YAAY;AAAA,EAChE;AAIA,iBAAe,oBAAoB,KAA8B;AAC/D,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,UAAU,IAAI,QAAQ,SACxB,IAAI,IAAI,OAAO,QAAQ,OAAO,MAAM,IAAI,QAAQ,MAAM,CAAC,CAAC,IACxD,oBAAI,IAAoB;AAE5B,UAAM,gBAAgB,QAAQ,IAAI,UAAU;AAC5C,UAAM,UAAU,MAAM,cAAc,aAAa;AAEjD,QAAI,CAAC,SAAS;AACZ,YAAM,OAAO,OAAO,IAAI,MAAM,wBAAwB,GAAG;AAAA,QACvD,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AAEA,UAAM,gBAAgB,QAAQ;AAC9B,UAAM,aAAa,oBAAI,KAAK;AAG5B,QAAI,eAAe,UAAU;AAC3B,aAAO,MAAM,kBAAkB,eAAe,OAAO;AAAA,IACvD;AAEA,QAAI,OAAQ,MAAM,GAAI,gBAAgB,aAAa,KAAM;AAGzD,QAAI,CAAC,MAAM;AACT,UAAI;AAEF,cAAM,aAAa,MAAM,cAAc,aAAa;AAEpD,YAAI,YAAY;AACd,kBAAQ;AAAA,YACN,GAAG,GAAG,oDAAoD,WAAW,SAAS,aAAa;AAAA,UAC7F;AAEA,gBAAM,qBACJ,WAAW,gBAAgB,WAAW,aAAa;AACrD,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ;AAAA,YACR,MAAM,WAAW,QAAQ,QAAQ,QAAQ;AAAA,YACzC,OAAO,WAAW,SAAS;AAAA,YAC3B,aAAa;AAAA,YACb,MAAM,QAAQ,WAAW,IAAI;AAAA,YAC7B,cAAc;AAAA,YACd,GAAI,sBAAsB,OACtB;AAAA,cACE,cAAc;AAAA,cACd,cAAc,WAAW;AAAA,YAC3B,IACA,CAAC;AAAA,UACP,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,aAAa,KAAM;AAAA,QACvD,OAAO;AAEL,kBAAQ;AAAA,YACN,GAAG,GAAG,yDAAyD,aAAa;AAAA,UAC9E;AACA,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ;AAAA,YACR,MAAM,QAAQ,QAAQ;AAAA,YACtB,aAAa;AAAA,YACb,cAAc;AAAA,UAChB,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,aAAa,KAAM;AAAA,QACvD;AAAA,MACF,SAAS,OAAO;AAEd,YAAI;AACF,gBAAM,WAAW,MAAM,mBAAmB,iBAAiB,EAAE;AAC7D,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ,SAAS;AAAA,YACjB,MAAM,SAAS,QAAQ;AAAA,YACvB,OAAO,SAAS,SAAS;AAAA,YACzB,aAAa,SAAS,eAAe,SAAS,YAAY;AAAA,YAC1D,cAAc;AAAA,UAChB,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,SAAS,MAAM,KAAM;AAAA,QACzD,SAAS,YAAY;AACnB,kBAAQ;AAAA,YACN,GAAG,GAAG;AAAA,YACN;AAAA,YACA;AAAA,UACF;AACA,gBAAM,OAAO,OAAO,IAAI,MAAM,0BAA0B,GAAG;AAAA,YACzD,YAAY;AAAA,UACd,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,MAAM;AACT,YAAM,OAAO,OAAO,IAAI,MAAM,gBAAgB,GAAG,EAAE,YAAY,IAAI,CAAC;AAAA,IACtE;AAGA,QAAI,KAAK,iBAAiB,MAAM;AAC9B,UAAI;AACF,cAAM,aAAa,MAAM,cAAc,KAAK,MAAM;AAElD,cAAM,qBACJ,YAAY,gBAAgB,YAAY,aAAa;AACvD,YAAI,sBAAsB,MAAM;AAC9B,kBAAQ;AAAA,YACN,GAAG,GAAG,6BAA6B,kBAAkB,aAAa,KAAK,EAAE;AAAA,UAC3E;AACA,gBAAM,GAAI,iBAAiB,KAAK,IAAI;AAAA,YAClC,cAAc;AAAA,YACd,GAAI,YAAY,KAAK,EAAE,cAAc,WAAW,GAAG,IAAI,CAAC;AAAA,UAC1D,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,KAAK,MAAM,KAAM;AAAA,QACrD;AAAA,MACF,SAAS,eAAe;AACtB,gBAAQ;AAAA,UACN,GAAG,GAAG,6CAA6C,KAAK,EAAE;AAAA,UAC1D;AAAA,QACF;AAAA,MAEF;AAAA,IACF;AAGA,UAAM,GAAI,WAAW;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,cAAc;AAAA,IAChB,CAAC;AAED,WAAO;AAAA,EACT;AAIA,WAAS,oBAAoB,KAAgC;AAC3D,QAAI;AAAA,MACF;AAAA,MACA,OAAO,KAAc,QAAoC;AACvD,cAAM,OACJ,OAAO,IAAI,MAAM,SAAS,WAAW,IAAI,MAAM,OAAO;AACxD,cAAM,QACJ,OAAO,IAAI,MAAM,UAAU,WAAW,IAAI,MAAM,QAAQ;AAE1D,YAAI,CAAC,QAAQ,CAAC,OAAO;AACnB,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,8BAA8B,CAAC;AAC7D;AAAA,QACF;AAEA,YAAI;AACF,gBAAM,gBAAgB,MAAM,qBAAqB,MAAM,KAAK;AAC5D,gBAAM,WAAW,MAAM,YAAY,cAAc,WAAW;AAE5D,cAAI,CAAC,SAAS,QAAQ;AACpB,gBAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,gCAAgC,CAAC;AAC/D;AAAA,UACF;AAEA,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ,SAAS;AAAA,YACjB,MAAM,SAAS,QAAQ;AAAA,YACvB,OAAO,SAAS,SAAS;AAAA,YACzB,aAAa,SAAS,eAAe,SAAS,YAAY;AAAA,YAC1D,cAAc,oBAAI,KAAK;AAAA,UACzB,CAAC;AAED,gBAAM,eAAe,MAAM,mBAAmB,SAAS,QAAQ;AAAA,YAC7D,MAAM,SAAS,QAAQ;AAAA,YACvB,aAAa;AAAA,UACf,CAAC;AAED,gBAAM,gBAAgB,wBAAwB,GAAG;AACjD,cAAI,OAAO,YAAY,cAAc;AAAA,YACnC,GAAG;AAAA,YACH,QAAQ;AAAA,UACV,CAAC;AAED,cAAI,SAAS,KAAK,GAAG;AAAA,QACvB,SAAS,OAAO;AACd,kBAAQ,MAAM,GAAG,GAAG,0BAA0B,KAAK;AACnD,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,wBAAwB,CAAC;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAIA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
package/dist/hierarchy.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { OrgHierarchyData } from '@pablo2410/shared-ui/hierarchy/core';
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
interface HierarchyServiceConfig {
|
|
25
|
-
/** Portal Service API base URL, e.g. https://portal.
|
|
25
|
+
/** Portal Service API base URL, e.g. https://portal.oplyticsdigital.net */
|
|
26
26
|
portalApiUrl: string;
|
|
27
27
|
/** Platform-level X-Service-Key for the Portal Service API. */
|
|
28
28
|
portalApiKey: string;
|
package/dist/hierarchy.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,18 @@ declare const SQDCP_PILLAR_LABELS: Record<SqdcpPillar, string>;
|
|
|
11
11
|
/**
|
|
12
12
|
* Minimal environment interface required by core-server utilities.
|
|
13
13
|
* Each subdomain provides its own ENV object that satisfies this shape.
|
|
14
|
+
*
|
|
15
|
+
* `forgeApiUrl`/`forgeApiKey` back the Manus "Forge" gateway, used by the
|
|
16
|
+
* data API, map, image generation, notification, and transcription helpers.
|
|
17
|
+
* `geminiApiUrl`/`geminiApiKey` back the LLM chat client (llm.ts) directly
|
|
18
|
+
* against Google's Gemini OpenAI-compatible endpoint — separate credential,
|
|
19
|
+
* separate provider, not a Forge passthrough.
|
|
14
20
|
*/
|
|
15
21
|
interface CoreEnv {
|
|
16
22
|
forgeApiUrl: string;
|
|
17
23
|
forgeApiKey: string;
|
|
24
|
+
geminiApiUrl: string;
|
|
25
|
+
geminiApiKey: string;
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
28
|
* Storage upload function signature.
|
|
@@ -58,8 +66,11 @@ interface TokenCostRate {
|
|
|
58
66
|
outputPer1k: number;
|
|
59
67
|
}
|
|
60
68
|
/**
|
|
61
|
-
* Default rate
|
|
62
|
-
*
|
|
69
|
+
* Default rate (USD per 1k tokens). Was priced for gemini-2.5-flash;
|
|
70
|
+
* llm.ts's MODEL moved to gemini-3.5-flash (2.5-flash was deprecated for new
|
|
71
|
+
* API keys) but this rate was NOT re-verified against 3.5-flash's actual
|
|
72
|
+
* published pricing — treat cost figures as stale until confirmed.
|
|
73
|
+
* Override per app via createLLMClient options once real pricing is known.
|
|
63
74
|
*/
|
|
64
75
|
declare const DEFAULT_COST_RATE: TokenCostRate;
|
|
65
76
|
/** Per-call context that varies request-to-request (set via InvokeParams.meta). */
|
|
@@ -115,7 +126,7 @@ declare function estimateMessageTokens(contents: string[]): number;
|
|
|
115
126
|
declare function computeCostUsd(promptTokens: number, completionTokens: number, rate?: TokenCostRate): number;
|
|
116
127
|
|
|
117
128
|
/**
|
|
118
|
-
* LLM helper — calls
|
|
129
|
+
* LLM helper — calls Google's Gemini API via its OpenAI-compatible endpoint.
|
|
119
130
|
*
|
|
120
131
|
* Example:
|
|
121
132
|
* const invokeLLM = createLLMClient(ENV);
|
|
@@ -204,7 +215,7 @@ type InvokeParams = {
|
|
|
204
215
|
type LLMClientOptions = {
|
|
205
216
|
/** App identifier recorded on every usage event, e.g. "marketing-site". */
|
|
206
217
|
app?: string;
|
|
207
|
-
/** Pricing used to estimate cost from tokens. Defaults to gemini-
|
|
218
|
+
/** Pricing used to estimate cost from tokens. Defaults to gemini-3.5-flash (rate unverified, see metering.ts). */
|
|
208
219
|
costRate?: TokenCostRate;
|
|
209
220
|
/** Metering sink — called after each successful call. Errors are swallowed. */
|
|
210
221
|
onUsage?: (event: UsageEvent) => void | Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSubdomainAuth
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LH6QZJM4.js";
|
|
4
4
|
import {
|
|
5
5
|
createHierarchyService
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-FSR7KF23.js";
|
|
7
7
|
|
|
8
8
|
// src/constants/sqdcp.ts
|
|
9
9
|
var SQDCP_PILLARS = ["safety", "quality", "delivery", "cost", "people"];
|
|
@@ -75,9 +75,9 @@ function createDataApiClient(env) {
|
|
|
75
75
|
// src/metering.ts
|
|
76
76
|
var DEFAULT_COST_RATE = {
|
|
77
77
|
inputPer1k: 75e-6,
|
|
78
|
-
// $0.075 / 1M input tokens
|
|
78
|
+
// $0.075 / 1M input tokens (gemini-2.5-flash rate, UNVERIFIED for 3.5)
|
|
79
79
|
outputPer1k: 3e-4
|
|
80
|
-
// $0.30 / 1M output tokens
|
|
80
|
+
// $0.30 / 1M output tokens (gemini-2.5-flash rate, UNVERIFIED for 3.5)
|
|
81
81
|
};
|
|
82
82
|
var LLMBudgetError = class extends Error {
|
|
83
83
|
code = "LLM_BUDGET_BLOCKED";
|
|
@@ -171,7 +171,7 @@ var normalizeResponseFormat = ({
|
|
|
171
171
|
}
|
|
172
172
|
};
|
|
173
173
|
};
|
|
174
|
-
var MODEL = "gemini-
|
|
174
|
+
var MODEL = "gemini-3.5-flash";
|
|
175
175
|
function messageToText(content) {
|
|
176
176
|
if (typeof content === "string") return content;
|
|
177
177
|
if (Array.isArray(content)) {
|
|
@@ -187,10 +187,10 @@ function messageToText(content) {
|
|
|
187
187
|
}
|
|
188
188
|
function createLLMClient(env, options = {}) {
|
|
189
189
|
const { app = "unknown", costRate = DEFAULT_COST_RATE, onUsage, guard } = options;
|
|
190
|
-
const resolveApiUrl = () => env.
|
|
190
|
+
const resolveApiUrl = () => env.geminiApiUrl && env.geminiApiUrl.trim().length > 0 ? `${env.geminiApiUrl.replace(/\/$/, "")}/chat/completions` : "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
|
|
191
191
|
const assertApiKey = () => {
|
|
192
|
-
if (!env.
|
|
193
|
-
throw new Error("
|
|
192
|
+
if (!env.geminiApiKey) {
|
|
193
|
+
throw new Error("GEMINI_API_KEY is not configured");
|
|
194
194
|
}
|
|
195
195
|
};
|
|
196
196
|
return async function invokeLLM(params) {
|
|
@@ -232,7 +232,6 @@ function createLLMClient(env, options = {}) {
|
|
|
232
232
|
payload.tool_choice = normalizedToolChoice;
|
|
233
233
|
}
|
|
234
234
|
payload.max_tokens = 32768;
|
|
235
|
-
payload.thinking = { budget_tokens: 128 };
|
|
236
235
|
const normalizedResponseFormat = normalizeResponseFormat({
|
|
237
236
|
responseFormat,
|
|
238
237
|
response_format,
|
|
@@ -246,7 +245,7 @@ function createLLMClient(env, options = {}) {
|
|
|
246
245
|
method: "POST",
|
|
247
246
|
headers: {
|
|
248
247
|
"content-type": "application/json",
|
|
249
|
-
authorization: `Bearer ${env.
|
|
248
|
+
authorization: `Bearer ${env.geminiApiKey}`
|
|
250
249
|
},
|
|
251
250
|
body: JSON.stringify(payload)
|
|
252
251
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants/sqdcp.ts","../src/dataApi.ts","../src/metering.ts","../src/llm.ts","../src/imageGeneration.ts","../src/map.ts","../src/notification.ts","../src/voiceTranscription.ts","../src/ai/personas.ts","../src/ai/knowledge.ts","../src/ai/supportEngine.ts"],"sourcesContent":["export const SQDCP_PILLARS = ['safety', 'quality', 'delivery', 'cost', 'people'] as const;\nexport type SqdcpPillar = typeof SQDCP_PILLARS[number];\nexport const SQDCP_PILLAR_COLORS: Record<SqdcpPillar, string> = {\n safety: '#ef4444',\n quality: '#3b82f6',\n delivery: '#f59e0b',\n cost: '#10b981',\n people: '#a855f7',\n};\nexport const SQDCP_PILLAR_LABELS: Record<SqdcpPillar, string> = {\n safety: 'Safety',\n quality: 'Quality',\n delivery: 'Delivery',\n cost: 'Cost',\n people: 'People',\n};\n","/**\n * Data API helper — calls the Manus built-in Data API Hub.\n *\n * Quick example:\n * const callDataApi = createDataApiClient(ENV);\n * await callDataApi(\"Youtube/search\", {\n * query: { gl: \"US\", hl: \"en\", q: \"manus\" },\n * });\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type DataApiCallOptions = {\n query?: Record<string, unknown>;\n body?: Record<string, unknown>;\n pathParams?: Record<string, unknown>;\n formData?: Record<string, unknown>;\n};\n\nexport function createDataApiClient(env: CoreEnv) {\n return async function callDataApi(\n apiId: string,\n options: DataApiCallOptions = {}\n ): Promise<unknown> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\n \"webdevtoken.v1.WebDevService/CallApi\",\n baseUrl\n ).toString();\n\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({\n apiId,\n query: options.query,\n body: options.body,\n path_params: options.pathParams,\n multipart_form_data: options.formData,\n }),\n });\n\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `Data API request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : \"\"}`\n );\n }\n\n const payload = await response.json().catch(() => ({}));\n if (payload && typeof payload === \"object\" && \"jsonData\" in payload) {\n try {\n return JSON.parse(\n (payload as Record<string, string>).jsonData ?? \"{}\"\n );\n } catch {\n return (payload as Record<string, unknown>).jsonData;\n }\n }\n return payload;\n };\n}\n","/**\n * Metering + cost-control primitives for the AI layer.\n *\n * Every LLM call funnels through `createLLMClient` (see llm.ts), which is the one\n * place we capture token usage and enforce budgets. This module holds the\n * storage-agnostic types and pure helpers; core-server only *emits* usage and\n * *asks* a guard — each app decides where usage lands and how budgets are stored\n * (typically the Business Hub ledger).\n */\n\n/** Per-1k-token pricing used to estimate cost from token counts. */\nexport interface TokenCostRate {\n inputPer1k: number;\n outputPer1k: number;\n}\n\n/**\n * Default rate for gemini-2.5-flash (USD per 1k tokens).\n * Override per app via createLLMClient options if Forge pricing changes.\n */\nexport const DEFAULT_COST_RATE: TokenCostRate = {\n inputPer1k: 0.000075, // $0.075 / 1M input tokens\n outputPer1k: 0.0003, // $0.30 / 1M output tokens\n};\n\n/** Per-call context that varies request-to-request (set via InvokeParams.meta). */\nexport interface CallMeta {\n /** Feature/surface label, e.g. \"opi\", \"ai-advisor\", \"insights\". */\n label?: string;\n /** \"marketing\" | \"operational\" | a feature name. */\n mode?: string;\n /** Authenticated user, when known (operational surfaces). */\n user?: { id?: string; role?: string; enterpriseId?: string | number };\n /** Current page/route (page-awareness + per-surface attribution). */\n page?: string;\n}\n\n/** Emitted after every successful call. The metering sink persists this. */\nexport interface UsageEvent extends CallMeta {\n /** App identifier, e.g. \"marketing-site\", \"businesshub\", \"oee\". */\n app: string;\n model: string;\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n estCostUsd: number;\n /** True when token counts were estimated (provider returned no usage block). */\n estimated: boolean;\n /** ISO timestamp. */\n at: string;\n}\n\n/** Asked BEFORE a call, so over-budget / disabled traffic never spends. */\nexport interface GuardRequest extends CallMeta {\n app: string;\n /** Rough prompt-token estimate, for budget math before the call. */\n estPromptTokens: number;\n}\n\nexport interface GuardResult {\n allowed: boolean;\n /** User-facing reason when blocked (kill-switch, budget exceeded, rate limit). */\n reason?: string;\n}\n\n/** Thrown by invokeLLM when a guard blocks the call. */\nexport class LLMBudgetError extends Error {\n readonly code = \"LLM_BUDGET_BLOCKED\";\n constructor(reason: string) {\n super(reason);\n this.name = \"LLMBudgetError\";\n }\n}\n\n/** Rough token estimate (~4 chars/token) — fallback when no usage is returned. */\nexport function estimateTokens(text: string): number {\n if (!text) return 0;\n return Math.ceil(text.length / 4);\n}\n\n/** Estimate prompt tokens across a set of message strings. */\nexport function estimateMessageTokens(contents: string[]): number {\n return contents.reduce((sum, c) => sum + estimateTokens(c), 0);\n}\n\n/** Compute estimated USD cost from token counts. */\nexport function computeCostUsd(\n promptTokens: number,\n completionTokens: number,\n rate: TokenCostRate = DEFAULT_COST_RATE\n): number {\n const cost =\n (promptTokens / 1000) * rate.inputPer1k +\n (completionTokens / 1000) * rate.outputPer1k;\n // Round to 6 dp — sub-cent precision without float noise.\n return Math.round(cost * 1e6) / 1e6;\n}\n","/**\n * LLM helper — calls the Manus built-in LLM API (OpenAI-compatible).\n *\n * Example:\n * const invokeLLM = createLLMClient(ENV);\n * const result = await invokeLLM({\n * messages: [\n * { role: \"system\", content: \"You are a helpful assistant.\" },\n * { role: \"user\", content: \"Hello, world!\" },\n * ],\n * });\n */\nimport type { CoreEnv } from \"./types\";\nimport {\n type CallMeta,\n type TokenCostRate,\n type UsageEvent,\n type GuardRequest,\n type GuardResult,\n DEFAULT_COST_RATE,\n LLMBudgetError,\n computeCostUsd,\n estimateMessageTokens,\n estimateTokens,\n} from \"./metering\";\n\nexport type Role = \"system\" | \"user\" | \"assistant\" | \"tool\" | \"function\";\n\nexport type TextContent = {\n type: \"text\";\n text: string;\n};\n\nexport type ImageContent = {\n type: \"image_url\";\n image_url: {\n url: string;\n detail?: \"auto\" | \"low\" | \"high\";\n };\n};\n\nexport type FileContent = {\n type: \"file_url\";\n file_url: {\n url: string;\n mime_type?:\n | \"audio/mpeg\"\n | \"audio/wav\"\n | \"application/pdf\"\n | \"audio/mp4\"\n | \"video/mp4\";\n };\n};\n\nexport type MessageContent = string | TextContent | ImageContent | FileContent;\n\nexport type Message = {\n role: Role;\n name?: string;\n tool_call_id?: string;\n content: MessageContent | MessageContent[];\n};\n\nexport type Tool = {\n type: \"function\";\n function: {\n name: string;\n description?: string;\n parameters?: Record<string, unknown>;\n };\n};\n\ntype ToolChoiceExplicit = {\n type: \"function\";\n function: { name: string };\n};\n\nexport type ToolChoice =\n | \"none\"\n | \"auto\"\n | \"required\"\n | { name: string }\n | ToolChoiceExplicit;\n\ntype JsonSchema = {\n name: string;\n schema: Record<string, unknown>;\n strict?: boolean;\n};\n\nexport type ResponseFormat =\n | { type: \"text\" }\n | { type: \"json_object\" }\n | { type: \"json_schema\"; json_schema: JsonSchema };\n\nexport type OutputSchema = {\n name: string;\n schema: Record<string, unknown>;\n strict?: boolean;\n};\n\nexport type InvokeParams = {\n messages: Message[];\n tools?: Tool[];\n toolChoice?: ToolChoice;\n tool_choice?: ToolChoice;\n outputSchema?: OutputSchema;\n output_schema?: OutputSchema;\n responseFormat?: ResponseFormat;\n response_format?: ResponseFormat;\n /** Per-call attribution for metering/guarding (label, mode, user, page). */\n meta?: CallMeta;\n};\n\n/** Optional metering + cost-control hooks, set once per app. */\nexport type LLMClientOptions = {\n /** App identifier recorded on every usage event, e.g. \"marketing-site\". */\n app?: string;\n /** Pricing used to estimate cost from tokens. Defaults to gemini-2.5-flash. */\n costRate?: TokenCostRate;\n /** Metering sink — called after each successful call. Errors are swallowed. */\n onUsage?: (event: UsageEvent) => void | Promise<void>;\n /** Budget/kill-switch check — called before each call; block = no spend. */\n guard?: (req: GuardRequest) => GuardResult | Promise<GuardResult>;\n};\n\nexport type InvokeResult = {\n id: string;\n object: string;\n created: number;\n model: string;\n choices: Array<{\n index: number;\n message: {\n role: string;\n content: string | null;\n tool_calls?: Array<{\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n }>;\n };\n finish_reason: string;\n }>;\n usage?: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n };\n};\n\nconst ensureArray = (\n value: MessageContent | MessageContent[]\n): MessageContent[] => (Array.isArray(value) ? value : [value]);\n\nconst normalizeContentPart = (\n part: MessageContent\n): TextContent | ImageContent | FileContent => {\n if (typeof part === \"string\") {\n return { type: \"text\", text: part };\n }\n if (part.type === \"text\") return part;\n if (part.type === \"image_url\") return part;\n if (part.type === \"file_url\") return part;\n throw new Error(\"Unsupported message content part\");\n};\n\nconst normalizeMessage = (message: Message) => {\n const { role, name, tool_call_id } = message;\n if (role === \"tool\" || role === \"function\") {\n const content = ensureArray(message.content)\n .map((part) => (typeof part === \"string\" ? part : JSON.stringify(part)))\n .join(\"\\n\");\n return { role, name, tool_call_id, content };\n }\n const contentParts = ensureArray(message.content).map(normalizeContentPart);\n if (contentParts.length === 1 && contentParts[0].type === \"text\") {\n return { role, name, content: contentParts[0].text };\n }\n return { role, name, content: contentParts };\n};\n\nconst normalizeToolChoice = (\n toolChoice: ToolChoice | undefined,\n tools: Tool[] | undefined\n): \"none\" | \"auto\" | ToolChoiceExplicit | undefined => {\n if (!toolChoice) return undefined;\n if (toolChoice === \"none\" || toolChoice === \"auto\") return toolChoice;\n if (toolChoice === \"required\") {\n if (!tools || tools.length === 0) {\n throw new Error(\n \"tool_choice 'required' was provided but no tools were configured\"\n );\n }\n if (tools.length > 1) {\n throw new Error(\n \"tool_choice 'required' needs a single tool or specify the tool name explicitly\"\n );\n }\n return { type: \"function\", function: { name: tools[0].function.name } };\n }\n if (\"name\" in toolChoice) {\n return { type: \"function\", function: { name: toolChoice.name } };\n }\n return toolChoice;\n};\n\nconst normalizeResponseFormat = ({\n responseFormat,\n response_format,\n outputSchema,\n output_schema,\n}: {\n responseFormat?: ResponseFormat;\n response_format?: ResponseFormat;\n outputSchema?: OutputSchema;\n output_schema?: OutputSchema;\n}):\n | { type: \"json_schema\"; json_schema: JsonSchema }\n | { type: \"text\" }\n | { type: \"json_object\" }\n | undefined => {\n const explicitFormat = responseFormat || response_format;\n if (explicitFormat) {\n if (\n explicitFormat.type === \"json_schema\" &&\n !explicitFormat.json_schema?.schema\n ) {\n throw new Error(\n \"responseFormat json_schema requires a defined schema object\"\n );\n }\n return explicitFormat;\n }\n const schema = outputSchema || output_schema;\n if (!schema) return undefined;\n if (!schema.name || !schema.schema) {\n throw new Error(\"outputSchema requires both name and schema\");\n }\n return {\n type: \"json_schema\",\n json_schema: {\n name: schema.name,\n schema: schema.schema,\n ...(typeof schema.strict === \"boolean\" ? { strict: schema.strict } : {}),\n },\n };\n};\n\nconst MODEL = \"gemini-2.5-flash\";\n\n/** Flatten a message's content to plain text for token estimation. */\nfunction messageToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === \"string\") return part;\n if (part && typeof part === \"object\" && \"text\" in part) {\n return String((part as { text?: unknown }).text ?? \"\");\n }\n return \"\";\n })\n .join(\" \");\n }\n return \"\";\n}\n\nexport function createLLMClient(env: CoreEnv, options: LLMClientOptions = {}) {\n const { app = \"unknown\", costRate = DEFAULT_COST_RATE, onUsage, guard } =\n options;\n\n const resolveApiUrl = () =>\n env.forgeApiUrl && env.forgeApiUrl.trim().length > 0\n ? `${env.forgeApiUrl.replace(/\\/$/, \"\")}/v1/chat/completions`\n : \"https://forge.manus.im/v1/chat/completions\";\n\n const assertApiKey = () => {\n if (!env.forgeApiKey) {\n throw new Error(\"OPENAI_API_KEY is not configured\");\n }\n };\n\n return async function invokeLLM(\n params: InvokeParams\n ): Promise<InvokeResult> {\n assertApiKey();\n\n const {\n messages,\n tools,\n toolChoice,\n tool_choice,\n outputSchema,\n output_schema,\n responseFormat,\n response_format,\n meta = {},\n } = params;\n\n const estPromptTokens = estimateMessageTokens(\n messages.map((m) => messageToText(m.content))\n );\n\n // ── Cost control: ask the guard before spending anything ──\n if (guard) {\n const verdict = await guard({ app, estPromptTokens, ...meta });\n if (!verdict.allowed) {\n throw new LLMBudgetError(\n verdict.reason ?? \"AI is temporarily unavailable.\"\n );\n }\n }\n\n const payload: Record<string, unknown> = {\n model: MODEL,\n messages: messages.map(normalizeMessage),\n };\n\n if (tools && tools.length > 0) {\n payload.tools = tools;\n }\n\n const normalizedToolChoice = normalizeToolChoice(\n toolChoice || tool_choice,\n tools\n );\n if (normalizedToolChoice) {\n payload.tool_choice = normalizedToolChoice;\n }\n\n payload.max_tokens = 32768;\n payload.thinking = { budget_tokens: 128 };\n\n const normalizedResponseFormat = normalizeResponseFormat({\n responseFormat,\n response_format,\n outputSchema,\n output_schema,\n });\n if (normalizedResponseFormat) {\n payload.response_format = normalizedResponseFormat;\n }\n\n const response = await fetch(resolveApiUrl(), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `LLM invoke failed: ${response.status} ${response.statusText} – ${errorText}`\n );\n }\n\n const result = (await response.json()) as InvokeResult;\n\n // ── Metering: capture usage and emit to the sink (never breaks the call) ──\n if (onUsage) {\n try {\n const provided = result.usage;\n const estimated = !provided;\n const promptTokens = provided?.prompt_tokens ?? estPromptTokens;\n const completionTokens =\n provided?.completion_tokens ??\n estimateTokens(messageToText(result.choices?.[0]?.message?.content));\n const totalTokens =\n provided?.total_tokens ?? promptTokens + completionTokens;\n\n const event: UsageEvent = {\n app,\n model: result.model ?? MODEL,\n promptTokens,\n completionTokens,\n totalTokens,\n estCostUsd: computeCostUsd(promptTokens, completionTokens, costRate),\n estimated,\n at: new Date().toISOString(),\n ...meta,\n };\n\n await onUsage(event);\n } catch (err) {\n // Metering must never take down a working chat.\n console.warn(\"[core-server] onUsage sink failed:\", err);\n }\n }\n\n return result;\n };\n}\n","/**\n * Image generation helper using internal ImageService.\n *\n * Example:\n * import { storagePut } from \"../storage\";\n * const generateImage = createImageGenerator(ENV, storagePut);\n * const { url } = await generateImage({ prompt: \"A serene landscape\" });\n */\nimport type { CoreEnv, StoragePutFn } from \"./types\";\n\nexport type GenerateImageOptions = {\n prompt: string;\n originalImages?: Array<{\n url?: string;\n b64Json?: string;\n mimeType?: string;\n }>;\n};\n\nexport type GenerateImageResponse = {\n url?: string;\n};\n\nexport function createImageGenerator(env: CoreEnv, storagePut: StoragePutFn) {\n return async function generateImage(\n options: GenerateImageOptions\n ): Promise<GenerateImageResponse> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\n \"images.v1.ImageService/GenerateImage\",\n baseUrl\n ).toString();\n\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({\n prompt: options.prompt,\n original_images: options.originalImages || [],\n }),\n });\n\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `Image generation request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : \"\"}`\n );\n }\n\n const result = (await response.json()) as {\n image: {\n b64Json: string;\n mimeType: string;\n };\n };\n\n const base64Data = result.image.b64Json;\n const buffer = Buffer.from(base64Data, \"base64\");\n\n const { url } = await storagePut(\n `generated/${Date.now()}.png`,\n buffer,\n result.image.mimeType\n );\n\n return { url };\n };\n}\n","/**\n * Google Maps API Integration for Manus WebDev Templates.\n *\n * Main function: createMapClient(env) returns makeRequest<T>(endpoint, params)\n * All credentials are automatically injected. Array parameters use | as separator.\n *\n * Example:\n * const makeRequest = createMapClient(ENV);\n * const result = await makeRequest<GeocodeResult>(\n * \"/maps/api/geocode/json\",\n * { address: \"1600 Amphitheatre Parkway\" }\n * );\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type MapRequestParams = Record<\n string,\n string | number | boolean | string[] | undefined\n>;\n\nexport function createMapClient(env: CoreEnv) {\n return async function makeRequest<T = unknown>(\n endpoint: string,\n params: MapRequestParams = {}\n ): Promise<T> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const proxyUrl = new URL(\n \"webdevtoken.v1.WebDevService/GoogleMapsProxy\",\n baseUrl\n ).toString();\n\n const processedParams: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n processedParams[key] = value.join(\"|\");\n } else {\n processedParams[key] = String(value);\n }\n }\n\n const response = await fetch(proxyUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({\n endpoint,\n params: processedParams,\n }),\n });\n\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `Maps API request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : \"\"}`\n );\n }\n\n const wrapper = (await response.json()) as { jsonData?: string };\n if (wrapper.jsonData) {\n try {\n return JSON.parse(wrapper.jsonData) as T;\n } catch {\n return wrapper.jsonData as unknown as T;\n }\n }\n return wrapper as unknown as T;\n };\n}\n","/**\n * Owner notification helper — pushes operational updates to the Manus project owner.\n *\n * Example:\n * const notifyOwner = createNotificationClient(ENV);\n * await notifyOwner({ title: \"New submission\", content: \"User X submitted form Y\" });\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type NotificationPayload = {\n title: string;\n content: string;\n};\n\nconst TITLE_MAX_LENGTH = 1200;\nconst CONTENT_MAX_LENGTH = 20000;\n\nexport function createNotificationClient(env: CoreEnv) {\n return async function notifyOwner(\n payload: NotificationPayload\n ): Promise<boolean> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const title =\n payload.title.length > TITLE_MAX_LENGTH\n ? payload.title.slice(0, TITLE_MAX_LENGTH)\n : payload.title;\n const content =\n payload.content.length > CONTENT_MAX_LENGTH\n ? payload.content.slice(0, CONTENT_MAX_LENGTH)\n : payload.content;\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\n \"webdevtoken.v1.WebDevService/NotifyOwner\",\n baseUrl\n ).toString();\n\n try {\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({ title, content }),\n });\n\n if (!response.ok) {\n console.error(\n `Notification failed: ${response.status} ${response.statusText}`\n );\n return false;\n }\n return true;\n } catch (error) {\n console.error(\"Notification error:\", error);\n return false;\n }\n };\n}\n","/**\n * Voice transcription helper using internal Speech-to-Text service (Whisper API).\n *\n * Example:\n * const transcribeAudio = createTranscriptionClient(ENV);\n * const result = await transcribeAudio({\n * audioUrl: \"https://storage.example.com/audio/recording.mp3\",\n * language: \"en\",\n * });\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type TranscribeOptions = {\n audioUrl: string;\n language?: string;\n prompt?: string;\n};\n\nexport type WhisperSegment = {\n id: number;\n seek: number;\n start: number;\n end: number;\n text: string;\n tokens: number[];\n temperature: number;\n avg_logprob: number;\n compression_ratio: number;\n no_speech_prob: number;\n};\n\nexport type WhisperResponse = {\n task: string;\n language: string;\n duration: number;\n text: string;\n segments: WhisperSegment[];\n};\n\nexport type TranscriptionError = {\n error: string;\n code: string;\n details: string;\n};\n\nexport type TranscriptionResult = WhisperResponse | TranscriptionError;\n\nconst SUPPORTED_MIME_TYPES = [\n \"audio/webm\",\n \"audio/mp3\",\n \"audio/mpeg\",\n \"audio/wav\",\n \"audio/wave\",\n \"audio/ogg\",\n \"audio/m4a\",\n \"audio/mp4\",\n] as const;\n\nfunction getFileExtension(mimeType: string): string {\n const mimeToExt: Record<string, string> = {\n \"audio/webm\": \"webm\",\n \"audio/mp3\": \"mp3\",\n \"audio/mpeg\": \"mp3\",\n \"audio/wav\": \"wav\",\n \"audio/wave\": \"wav\",\n \"audio/ogg\": \"ogg\",\n \"audio/m4a\": \"m4a\",\n \"audio/mp4\": \"m4a\",\n };\n return mimeToExt[mimeType] || \"audio\";\n}\n\nfunction getLanguageName(langCode: string): string {\n const langMap: Record<string, string> = {\n en: \"English\",\n es: \"Spanish\",\n fr: \"French\",\n de: \"German\",\n it: \"Italian\",\n pt: \"Portuguese\",\n ru: \"Russian\",\n ja: \"Japanese\",\n ko: \"Korean\",\n zh: \"Chinese\",\n ar: \"Arabic\",\n hi: \"Hindi\",\n nl: \"Dutch\",\n pl: \"Polish\",\n tr: \"Turkish\",\n sv: \"Swedish\",\n da: \"Danish\",\n no: \"Norwegian\",\n fi: \"Finnish\",\n };\n return langMap[langCode] || langCode;\n}\n\nexport function createTranscriptionClient(env: CoreEnv) {\n return async function transcribeAudio(\n options: TranscribeOptions\n ): Promise<TranscriptionResult> {\n try {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n // Step 1: Fetch the audio file to determine MIME type and validate\n const audioResponse = await fetch(options.audioUrl);\n if (!audioResponse.ok) {\n return {\n error: \"Failed to fetch audio file\",\n code: \"FETCH_FAILED\",\n details: `HTTP ${audioResponse.status} ${audioResponse.statusText}`,\n };\n }\n\n const contentType =\n audioResponse.headers.get(\"content-type\") || \"audio/webm\";\n const mimeType = contentType.split(\";\")[0].trim();\n\n if (\n !SUPPORTED_MIME_TYPES.includes(\n mimeType as (typeof SUPPORTED_MIME_TYPES)[number]\n )\n ) {\n return {\n error: \"Unsupported audio format\",\n code: \"UNSUPPORTED_FORMAT\",\n details: `MIME type '${mimeType}' is not supported. Supported: ${SUPPORTED_MIME_TYPES.join(\", \")}`,\n };\n }\n\n // Step 2: Read audio buffer and check size\n const audioBuffer = await audioResponse.arrayBuffer();\n const sizeMB = audioBuffer.byteLength / (1024 * 1024);\n if (sizeMB > 16) {\n return {\n error: \"Audio file exceeds maximum size limit\",\n code: \"FILE_TOO_LARGE\",\n details: `File size is ${sizeMB.toFixed(2)}MB, maximum allowed is 16MB`,\n };\n }\n\n // Step 3: Create FormData for multipart upload to Whisper API\n const formData = new FormData();\n const filename = `audio.${getFileExtension(mimeType)}`;\n const audioBlob = new Blob([new Uint8Array(audioBuffer)], {\n type: mimeType,\n });\n formData.append(\"file\", audioBlob, filename);\n formData.append(\"model\", \"whisper-1\");\n formData.append(\"response_format\", \"verbose_json\");\n\n const prompt =\n options.prompt ||\n (options.language\n ? `Transcribe the user's voice to text, the user's working language is ${getLanguageName(options.language)}`\n : \"Transcribe the user's voice to text\");\n formData.append(\"prompt\", prompt);\n\n // Step 4: Call the transcription service\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\"v1/audio/transcriptions\", baseUrl).toString();\n\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${env.forgeApiKey}`,\n \"Accept-Encoding\": \"identity\",\n },\n body: formData,\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n return {\n error: \"Transcription service request failed\",\n code: \"TRANSCRIPTION_FAILED\",\n details: `${response.status} ${response.statusText}${errorText ? `: ${errorText}` : \"\"}`,\n };\n }\n\n // Step 5: Parse and return the transcription result\n const whisperResponse = (await response.json()) as WhisperResponse;\n\n if (\n !whisperResponse.text ||\n typeof whisperResponse.text !== \"string\"\n ) {\n return {\n error: \"Invalid transcription response\",\n code: \"SERVICE_ERROR\",\n details:\n \"Transcription service returned an invalid response format\",\n };\n }\n\n return whisperResponse;\n } catch (error) {\n return {\n error: \"Voice transcription failed\",\n code: \"SERVICE_ERROR\",\n details:\n error instanceof Error\n ? error.message\n : \"An unexpected error occurred\",\n };\n }\n };\n}\n","/**\n * Personas for the shared Oplytics AI assistant.\n *\n * One brain, two voices: the same knowledge base is wrapped in a customer-facing\n * persona (\"Opi\" on the marketing site) or an operational persona (the in-app\n * \"AI Support Engineer\" in the portal / subdomains). The mode selects the overlay.\n */\n\nexport type AssistantMode = \"marketing\" | \"operational\";\n\n/** Identity + tone shared by every surface. */\nconst BASE_PERSONA = `\nYou are the Oplytics AI assistant — a knowledgeable, friendly guide to the Oplytics operational-excellence platform.\nYour personality is warm, approachable, and genuinely helpful, while staying clear, professional, and competent — especially on operational safety and platform technicals.\n\nShared guidelines:\n- Use a warm, professional tone: clear, concise, and easy to follow. Avoid heavy slang, forced catchphrases, or over-familiarity — be friendly without overdoing it.\n- For anything safety- or incident-related, stay serious and accurate while keeping the warm tone — prioritise accuracy over speed.\n- If you don't know something, be honest and guide the user to the right place rather than inventing an answer.\n- Prefer the supplied Oplytics knowledge below over your own assumptions. If the knowledge doesn't cover it, say so plainly.\n`.trim();\n\n/** Customer-facing overlay — Opi on the public marketing site. */\nconst MARKETING_OVERLAY = `\nYou are \"Opi\", talking to a prospective customer on the public Oplytics marketing site.\n- Focus on features, benefits, outcomes, and helping them book a demo or get in touch.\n- Be persuasive but never pushy or salesy; lead with how Oplytics solves their problem.\n- Never discuss competitor products by name.\n- For specific pricing, direct the user to the contact page at /contact rather than quoting figures.\n- You do not have access to any account or operational data — you are pre-sales only.\n`.trim();\n\n/** Authenticated overlay — the in-app AI Support Engineer. */\nconst OPERATIONAL_OVERLAY = `\nYou are the Oplytics AI Support Engineer, supporting a verified, logged-in user inside the platform.\n- Tailor responses to their role and enterprise context (supplied below when available).\n- Help with deep operational support: platform setup, training, troubleshooting, data interpretation, and specific tasks.\n- For safety-related queries, always prioritise accuracy and cite the correct workflow.\n- You may reference platform features and the user's context, but never expose data the user shouldn't see.\n`.trim();\n\nconst OVERLAYS: Record<AssistantMode, string> = {\n marketing: MARKETING_OVERLAY,\n operational: OPERATIONAL_OVERLAY,\n};\n\nexport interface BuildSystemPromptParams {\n mode: AssistantMode;\n /** Pre-formatted knowledge block (see formatKnowledge) injected into the prompt. */\n knowledge?: string;\n /** Free-form runtime context (current page, user/role, enterprise, etc.). */\n context?: string;\n}\n\n/** Compose the full system prompt: base identity + mode overlay + knowledge + context. */\nexport function buildSystemPrompt({\n mode,\n knowledge,\n context,\n}: BuildSystemPromptParams): string {\n const sections = [BASE_PERSONA, OVERLAYS[mode]];\n\n if (knowledge && knowledge.trim().length > 0) {\n sections.push(`Relevant Oplytics knowledge:\\n${knowledge.trim()}`);\n }\n\n if (context && context.trim().length > 0) {\n sections.push(`Current context:\\n${context.trim()}`);\n }\n\n return sections.join(\"\\n\\n\");\n}\n","/**\n * Shared Oplytics knowledge base + a RAG-ready retrieval interface.\n *\n * Phase 2a ships the knowledge as version-controlled data injected into the\n * system prompt, behind a `Retriever` interface. Phase 2b can swap the static\n * retriever for a vector-store-backed one WITHOUT changing any consumer — the\n * signature is the contract.\n *\n * This is a deliberately small seed. Expand `KNOWLEDGE` over time (it's the one\n * source of truth shared by Opi and the in-app AI Support Engineer).\n */\nimport type { AssistantMode } from \"./personas\";\n\nexport interface KnowledgeItem {\n id: string;\n question: string;\n answer: string;\n /** Lowercase keywords/tags used by the static retriever's scoring. */\n tags: string[];\n /** If set, this item is only surfaced in the listed modes. Default: both. */\n modes?: AssistantMode[];\n}\n\nexport interface RetrieveOptions {\n mode: AssistantMode;\n /** Max items to return. Default 4. */\n limit?: number;\n}\n\n/**\n * A retriever maps a user query to the most relevant knowledge items.\n * Async so a future vector/embedding implementation can drop in unchanged.\n */\nexport type Retriever = (\n query: string,\n options: RetrieveOptions\n) => Promise<KnowledgeItem[]> | KnowledgeItem[];\n\n/** Seed knowledge. Sourced from the Oplytics solution set; extend freely. */\nexport const KNOWLEDGE: KnowledgeItem[] = [\n // ── Platform ──────────────────────────────────────────────────────────────\n {\n id: \"platform-overview\",\n question: \"What does Oplytics do?\",\n answer:\n \"Oplytics is an integrated operational-excellence platform for manufacturers. Its modules share data, context, and AI insights so individual tools compound into one advantage instead of working in silos. Four services are live today (OEE Manager, the SQDCP Dashboard, Policy Deployment, and Action Manager), with more on the way.\",\n tags: [\"platform\", \"overview\", \"what\", \"oplytics\", \"modules\", \"manufacturing\"],\n },\n {\n id: \"who-for\",\n question: \"Who is Oplytics for?\",\n answer:\n \"Manufacturers and factory operations teams running continuous improvement, lean, and daily management — from the shop floor to senior leadership. It suits anyone replacing whiteboards, spreadsheets, and disconnected point tools with one connected system.\",\n tags: [\"who\", \"for\", \"audience\", \"manufacturer\", \"factory\", \"lean\", \"operations\"],\n },\n {\n id: \"live-services\",\n question: \"Which services are available right now?\",\n answer:\n \"Live today: OEE Manager, the SQDCP Dashboard, Policy Deployment, and Action Manager. In development and coming soon: OplyticsConnect, Quality Manager, Safety Manager, and Certification Manager — existing customers get early access and preferential pricing when they launch.\",\n tags: [\"live\", \"available\", \"services\", \"roadmap\", \"coming\", \"soon\", \"status\"],\n },\n {\n id: \"integrated-platform\",\n question: \"Why one platform instead of separate tools?\",\n answer:\n \"Every module shares the same data and context, so an action raised on a tier board, a loss in OEE, and a strategic objective all connect. That shared backbone is what lets the AI surface patterns no single tool could see alone.\",\n tags: [\"integrated\", \"platform\", \"connected\", \"modules\", \"data\", \"why\"],\n },\n {\n id: \"methodologies\",\n question: \"What improvement methodologies does it support?\",\n answer:\n \"Best-practice CI is built into the workflows: Lean, Kaizen, and PDCA throughout, plus Hoshin Kanri (with X-matrix and catchball) for strategy deployment and the SQDCP framework for daily management.\",\n tags: [\"lean\", \"kaizen\", \"pdca\", \"hoshin\", \"methodology\", \"ci\", \"continuous\", \"improvement\"],\n },\n {\n id: \"mobile-desktop\",\n question: \"Can we use it on mobile?\",\n answer:\n \"Yes — Oplytics works on desktop and mobile, so boards, actions, and reports are accessible from any device, on the floor or off-site.\",\n tags: [\"mobile\", \"desktop\", \"device\", \"tablet\", \"phone\", \"access\"],\n },\n // ── OEE Manager (live) ─────────────────────────────────────────────────────\n {\n id: \"oee-manager\",\n question: \"How does OEE Manager work?\",\n answer:\n \"OEE Manager tracks Overall Equipment Effectiveness — availability, performance, and quality — in real time across every machine and line. It categorises downtime and speed losses, builds Pareto/loss analysis, and trends OEE by shift, line, product, and operator so teams target the biggest losses first.\",\n tags: [\"oee\", \"manager\", \"downtime\", \"loss\", \"availability\", \"performance\", \"quality\", \"tracking\", \"equipment\"],\n },\n {\n id: \"oee-data-collection\",\n question: \"Does OEE data have to be entered by hand?\",\n answer:\n \"No — OEE Manager can connect directly to PLCs and sensors via SmartConnect for automated data collection, eliminating manual-entry errors. Manual entry is still available where automation isn't in place.\",\n tags: [\"oee\", \"data\", \"collection\", \"automated\", \"smartconnect\", \"plc\", \"sensor\", \"manual\"],\n },\n {\n id: \"oee-targets-handover\",\n question: \"Can we set OEE targets and run shift handovers?\",\n answer:\n \"Yes. Set OEE targets by line, product, and shift with live visual indicators against target, and generate digital shift-handover reports with OEE summaries, open actions, and key events from the previous shift.\",\n tags: [\"oee\", \"target\", \"shift\", \"handover\", \"report\", \"goal\"],\n },\n // ── SQDCP Dashboard (live) ─────────────────────────────────────────────────\n {\n id: \"sqdcp\",\n question: \"What is the SQDCP Dashboard?\",\n answer:\n \"SQDCP replaces physical whiteboards with real-time digital tier boards covering Safety, Quality, Delivery, Cost, and People. Boards are accessible from any device and keep daily management consistent across shifts and sites.\",\n tags: [\"sqdcp\", \"tier\", \"board\", \"dashboard\", \"daily\", \"management\", \"whiteboard\"],\n },\n {\n id: \"sqdcp-pillars\",\n question: \"What does SQDCP track?\",\n answer:\n \"The five pillars: Safety (incidents, near-misses, observations), Quality (first-pass yield, scrap, complaints), Delivery (on-time delivery, schedule adherence), Cost (operational cost and waste), and People (attendance, training, skills).\",\n tags: [\"sqdcp\", \"safety\", \"quality\", \"delivery\", \"cost\", \"people\", \"pillars\", \"metrics\"],\n },\n // ── Policy Deployment (live) ───────────────────────────────────────────────\n {\n id: \"policy-deployment\",\n question: \"What is Policy Deployment?\",\n answer:\n \"Policy Deployment cascades strategy through every level using Hoshin Kanri: define 3-5 year breakthrough objectives and annual priorities, link them in a visual X-matrix, deploy via the catchball process, and review progress on a monthly/quarterly cadence with bowling charts.\",\n tags: [\"policy\", \"deployment\", \"hoshin\", \"kanri\", \"strategy\", \"xmatrix\", \"catchball\", \"objectives\"],\n },\n {\n id: \"catchball\",\n question: \"What is the catchball process?\",\n answer:\n \"Catchball is how objectives are aligned top-down and bottom-up: leadership proposes goals, teams feed back what's achievable, and the two iterate until aligned. Oplytics facilitates this digitally so strategy doesn't get lost between management layers.\",\n tags: [\"catchball\", \"hoshin\", \"alignment\", \"objectives\", \"policy\", \"deployment\"],\n },\n // ── Action Manager (live) ──────────────────────────────────────────────────\n {\n id: \"action-manager\",\n question: \"What is Action Manager?\",\n answer:\n \"Action Manager is one unified action register: capture actions from any source (audits, incidents, meetings, tier boards), assign them with owners and due dates, auto-escalate overdue items, and track status from open to verified closure. Analytics show completion rates, ageing, and source breakdown.\",\n tags: [\"action\", \"manager\", \"tasks\", \"improvement\", \"ci\", \"register\", \"escalation\", \"closure\"],\n },\n // ── Coming soon ────────────────────────────────────────────────────────────\n {\n id: \"safety-manager\",\n question: \"Do you have a Safety Manager?\",\n answer:\n \"Safety Manager is coming soon. It will cover mobile-first incident reporting (with photos and witness statements), hazard identification and risk assessment, behavioural safety observations, and a compliance dashboard. Existing customers get early access at launch.\",\n tags: [\"safety\", \"manager\", \"incident\", \"hazard\", \"risk\", \"observation\", \"compliance\", \"coming\"],\n },\n {\n id: \"quality-manager\",\n question: \"Do you have a Quality Manager?\",\n answer:\n \"Quality Manager is coming soon. It will handle non-conformance tracking from detection to disposition, a structured CAPA workflow with root-cause tools, internal/external audit scheduling, and quality metrics (first-pass yield, defect rates, cost of quality). Existing customers get early access at launch.\",\n tags: [\"quality\", \"manager\", \"ncr\", \"capa\", \"non-conformance\", \"audit\", \"coming\"],\n },\n {\n id: \"certification-manager\",\n question: \"Can it help with ISO certification?\",\n answer:\n \"Certification Manager is coming soon. It will manage standards like ISO 9001, ISO 14001, and IATF 16949 in one place, with version-controlled document control, a complete audit trail, and compliance/gap tracking. Existing customers get early access at launch.\",\n tags: [\"certification\", \"iso\", \"9001\", \"14001\", \"iatf\", \"16949\", \"standards\", \"document\", \"audit\", \"coming\"],\n },\n {\n id: \"connect-integrations\",\n question: \"Can Oplytics connect to our machines and systems?\",\n answer:\n \"Live OEE data already flows in via SmartConnect (PLCs and sensors). The standalone OplyticsConnect service is coming soon and adds broader machine connectivity — PLCs, SCADA, and industrial sensors over OPC-UA, MQTT, Modbus and more — with zero-code configuration and edge processing.\",\n tags: [\"connect\", \"integration\", \"machine\", \"plc\", \"scada\", \"sensor\", \"opcua\", \"mqtt\", \"modbus\", \"iot\", \"smartconnect\"],\n },\n {\n id: \"erp-integration\",\n question: \"Can it integrate with our ERP or other systems?\",\n answer:\n \"Oplytics focuses on shop-floor connectivity (machines, PLCs, sensors) today. For ERP or other business-system integrations, the best step is to get in touch via /contact so the team can scope your specific setup.\",\n tags: [\"erp\", \"integration\", \"system\", \"api\", \"data\", \"contact\"],\n },\n {\n id: \"protocols\",\n question: \"Which industrial protocols are supported?\",\n answer:\n \"Machine connectivity supports common industrial protocols including OPC-UA, MQTT, and Modbus, connecting to PLCs, SCADA systems, and sensors. Configuration is visual and zero-code, with edge processing and local buffering so no data is lost during connectivity drops.\",\n tags: [\"protocol\", \"opcua\", \"mqtt\", \"modbus\", \"plc\", \"scada\", \"edge\", \"connectivity\"],\n },\n // ── AI ─────────────────────────────────────────────────────────────────────\n {\n id: \"integrated-ai\",\n question: \"How does the AI help?\",\n answer:\n \"AI runs across every module — surfacing patterns, predicting issues, and recommending actions no single tool could spot alone — and the assistant helps users navigate, answers questions, and suggests next steps.\",\n tags: [\"ai\", \"insights\", \"predict\", \"assistant\", \"recommend\", \"intelligence\"],\n },\n {\n id: \"ai-assistant\",\n question: \"Who are you / what can you help with?\",\n answer:\n \"I'm Opi, the Oplytics AI guide. I can explain what the platform and each module do, point you to the right place, and help you book a demo or get in touch.\",\n tags: [\"opi\", \"assistant\", \"help\", \"who\", \"guide\", \"bot\"],\n },\n // ── Pricing, demos, getting started (marketing) ────────────────────────────\n {\n id: \"pricing\",\n question: \"How much does it cost?\",\n answer:\n \"Pricing is tailored to your sites and the modules you use, so figures aren't published. The best next step is to get in touch via the contact page (/contact) and the team will put together a quote.\",\n tags: [\"pricing\", \"price\", \"cost\", \"quote\", \"how\", \"much\", \"plan\"],\n modes: [\"marketing\"],\n },\n {\n id: \"plans\",\n question: \"What plans are available?\",\n answer:\n \"There are tiers to suit different stages — an Early Beta Partner programme, Starter, Professional, and Enterprise — covering different usage limits and capabilities like custom integrations and SLAs. All four live services are included; reach out via /contact to find the right fit.\",\n tags: [\"plan\", \"tier\", \"starter\", \"professional\", \"enterprise\", \"beta\", \"package\"],\n modes: [\"marketing\"],\n },\n {\n id: \"beta\",\n question: \"Is there a beta or early-adopter option?\",\n answer:\n \"Yes — the Early Beta Partner programme is for teams who want to get started early and shape the platform. Get in touch via /contact to learn more.\",\n tags: [\"beta\", \"early\", \"partner\", \"adopter\", \"trial\", \"pilot\"],\n modes: [\"marketing\"],\n },\n {\n id: \"demo\",\n question: \"Can I see a demo?\",\n answer:\n \"Absolutely — you can book a demo or try the live module demos. Head to the contact page (/contact) to get set up.\",\n tags: [\"demo\", \"trial\", \"try\", \"book\", \"see\", \"show\"],\n modes: [\"marketing\"],\n },\n {\n id: \"contact\",\n question: \"How do I get in touch?\",\n answer:\n \"Head to the contact page at /contact — tell the team a little about your operation and they'll take it from there, whether that's a demo, a quote, or answering questions.\",\n tags: [\"contact\", \"touch\", \"reach\", \"sales\", \"talk\", \"email\"],\n modes: [\"marketing\"],\n },\n {\n id: \"getting-started\",\n question: \"How do we get started?\",\n answer:\n \"Start with a conversation: get in touch via /contact for a demo and onboarding. The team will help you stand up the live services for your sites and connect your data.\",\n tags: [\"start\", \"started\", \"onboarding\", \"begin\", \"setup\", \"trial\"],\n },\n];\n\n/** Lowercase word tokens, length >= 3. */\nfunction tokenize(text: string): string[] {\n return (text.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter(\n (w) => w.length >= 3\n );\n}\n\n/** Score an item against the query tokens (tag hits weighted over text hits). */\nfunction scoreItem(item: KnowledgeItem, queryTokens: string[]): number {\n if (queryTokens.length === 0) return 0;\n const haystack = tokenize(`${item.question} ${item.answer}`);\n let score = 0;\n for (const token of queryTokens) {\n if (item.tags.includes(token)) score += 3;\n if (haystack.includes(token)) score += 1;\n }\n return score;\n}\n\n/**\n * Build a static, keyword-scored retriever over the supplied knowledge.\n * Drop-in replaceable by a vector retriever implementing the same `Retriever`.\n */\nexport function createStaticRetriever(\n knowledge: KnowledgeItem[] = KNOWLEDGE\n): Retriever {\n return (query, { mode, limit = 4 }) => {\n const queryTokens = tokenize(query);\n const eligible = knowledge.filter((k) => !k.modes || k.modes.includes(mode));\n\n const scored = eligible\n .map((item) => ({ item, score: scoreItem(item, queryTokens) }))\n .filter((s) => s.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n .map((s) => s.item);\n\n // Cold-start / no keyword hits: fall back to the first few eligible items\n // so the assistant always has some grounding to work from.\n if (scored.length === 0) return eligible.slice(0, limit);\n return scored;\n };\n}\n\n/** Render retrieved items into a compact block for the system prompt. */\nexport function formatKnowledge(items: KnowledgeItem[]): string {\n return items.map((i) => `Q: ${i.question}\\nA: ${i.answer}`).join(\"\\n\\n\");\n}\n","/**\n * createSupportEngine — the shared chat brain for Opi (marketing) and the in-app\n * AI Support Engineer (operational). Composes persona + retrieved knowledge +\n * runtime context, then calls the existing LLM client.\n *\n * const engine = createSupportEngine(ENV);\n * const { content } = await engine.chat({ mode: \"marketing\", messages });\n */\nimport type { CoreEnv } from \"../types\";\nimport { createLLMClient } from \"../llm\";\nimport type { Message, LLMClientOptions } from \"../llm\";\nimport { buildSystemPrompt, type AssistantMode } from \"./personas\";\nimport {\n createStaticRetriever,\n formatKnowledge,\n type Retriever,\n} from \"./knowledge\";\n\n/** Lightweight, mode-agnostic chat message (system prompt is added internally). */\nexport interface ChatMessage {\n role: \"user\" | \"assistant\";\n content: string;\n}\n\n/** Optional runtime context to ground the assistant. */\nexport interface SupportContext {\n /** Current page / route the user is on (page-awareness). */\n page?: string;\n /** Authenticated user details (operational mode). */\n user?: { name?: string; role?: string; enterpriseId?: string | number };\n /** Any extra free-form context to append. */\n extra?: string;\n}\n\nexport interface ChatParams {\n mode: AssistantMode;\n messages: ChatMessage[];\n context?: SupportContext;\n}\n\nexport interface ChatResult {\n content: string;\n}\n\nexport interface SupportEngineOptions {\n /**\n * Knowledge retriever. Defaults to the bundled static keyword retriever.\n * Swap for a vector-store retriever later with zero consumer changes.\n */\n retrieve?: Retriever;\n /** Items to retrieve per turn. Default 4. */\n knowledgeLimit?: number;\n /** Metering + cost-control hooks (app, onUsage sink, budget guard, costRate). */\n metering?: LLMClientOptions;\n}\n\nfunction renderContext(mode: AssistantMode, context?: SupportContext): string {\n const lines: string[] = [\n `Mode: ${mode === \"marketing\" ? \"Public Marketing Site\" : \"Authenticated Portal\"}`,\n ];\n if (context?.page) lines.push(`Page: ${context.page}`);\n if (context?.user?.name) lines.push(`User: ${context.user.name}`);\n if (context?.user?.role) lines.push(`Role: ${context.user.role}`);\n if (context?.user?.enterpriseId != null)\n lines.push(`Enterprise ID: ${context.user.enterpriseId}`);\n if (context?.extra) lines.push(context.extra);\n return lines.join(\"\\n\");\n}\n\nexport function createSupportEngine(\n env: CoreEnv,\n options: SupportEngineOptions = {}\n) {\n const invokeLLM = createLLMClient(env, options.metering);\n const retrieve = options.retrieve ?? createStaticRetriever();\n const knowledgeLimit = options.knowledgeLimit ?? 4;\n\n async function chat({ mode, messages, context }: ChatParams): Promise<ChatResult> {\n const lastUser = [...messages].reverse().find((m) => m.role === \"user\");\n const query = lastUser?.content ?? \"\";\n\n const items = await retrieve(query, { mode, limit: knowledgeLimit });\n const knowledge = formatKnowledge(items);\n\n const systemPrompt = buildSystemPrompt({\n mode,\n knowledge,\n context: renderContext(mode, context),\n });\n\n const llmMessages: Message[] = [\n { role: \"system\", content: systemPrompt },\n ...messages.map((m) => ({ role: m.role, content: m.content })),\n ];\n\n const result = await invokeLLM({\n messages: llmMessages,\n meta: {\n label: \"support-engine\",\n mode,\n page: context?.page,\n user: context?.user\n ? { role: context.user.role, enterpriseId: context.user.enterpriseId }\n : undefined,\n },\n });\n const raw = result.choices[0]?.message?.content;\n const content = typeof raw === \"string\" ? raw : JSON.stringify(raw ?? \"\");\n\n return { content };\n }\n\n return { chat };\n}\n\nexport type SupportEngine = ReturnType<typeof createSupportEngine>;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/constants/sqdcp.ts","../src/dataApi.ts","../src/metering.ts","../src/llm.ts","../src/imageGeneration.ts","../src/map.ts","../src/notification.ts","../src/voiceTranscription.ts","../src/ai/personas.ts","../src/ai/knowledge.ts","../src/ai/supportEngine.ts"],"sourcesContent":["export const SQDCP_PILLARS = ['safety', 'quality', 'delivery', 'cost', 'people'] as const;\nexport type SqdcpPillar = typeof SQDCP_PILLARS[number];\nexport const SQDCP_PILLAR_COLORS: Record<SqdcpPillar, string> = {\n safety: '#ef4444',\n quality: '#3b82f6',\n delivery: '#f59e0b',\n cost: '#10b981',\n people: '#a855f7',\n};\nexport const SQDCP_PILLAR_LABELS: Record<SqdcpPillar, string> = {\n safety: 'Safety',\n quality: 'Quality',\n delivery: 'Delivery',\n cost: 'Cost',\n people: 'People',\n};\n","/**\n * Data API helper — calls the Manus built-in Data API Hub.\n *\n * Quick example:\n * const callDataApi = createDataApiClient(ENV);\n * await callDataApi(\"Youtube/search\", {\n * query: { gl: \"US\", hl: \"en\", q: \"manus\" },\n * });\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type DataApiCallOptions = {\n query?: Record<string, unknown>;\n body?: Record<string, unknown>;\n pathParams?: Record<string, unknown>;\n formData?: Record<string, unknown>;\n};\n\nexport function createDataApiClient(env: CoreEnv) {\n return async function callDataApi(\n apiId: string,\n options: DataApiCallOptions = {}\n ): Promise<unknown> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\n \"webdevtoken.v1.WebDevService/CallApi\",\n baseUrl\n ).toString();\n\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({\n apiId,\n query: options.query,\n body: options.body,\n path_params: options.pathParams,\n multipart_form_data: options.formData,\n }),\n });\n\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `Data API request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : \"\"}`\n );\n }\n\n const payload = await response.json().catch(() => ({}));\n if (payload && typeof payload === \"object\" && \"jsonData\" in payload) {\n try {\n return JSON.parse(\n (payload as Record<string, string>).jsonData ?? \"{}\"\n );\n } catch {\n return (payload as Record<string, unknown>).jsonData;\n }\n }\n return payload;\n };\n}\n","/**\n * Metering + cost-control primitives for the AI layer.\n *\n * Every LLM call funnels through `createLLMClient` (see llm.ts), which is the one\n * place we capture token usage and enforce budgets. This module holds the\n * storage-agnostic types and pure helpers; core-server only *emits* usage and\n * *asks* a guard — each app decides where usage lands and how budgets are stored\n * (typically the Business Hub ledger).\n */\n\n/** Per-1k-token pricing used to estimate cost from token counts. */\nexport interface TokenCostRate {\n inputPer1k: number;\n outputPer1k: number;\n}\n\n/**\n * Default rate (USD per 1k tokens). Was priced for gemini-2.5-flash;\n * llm.ts's MODEL moved to gemini-3.5-flash (2.5-flash was deprecated for new\n * API keys) but this rate was NOT re-verified against 3.5-flash's actual\n * published pricing — treat cost figures as stale until confirmed.\n * Override per app via createLLMClient options once real pricing is known.\n */\nexport const DEFAULT_COST_RATE: TokenCostRate = {\n inputPer1k: 0.000075, // $0.075 / 1M input tokens (gemini-2.5-flash rate, UNVERIFIED for 3.5)\n outputPer1k: 0.0003, // $0.30 / 1M output tokens (gemini-2.5-flash rate, UNVERIFIED for 3.5)\n};\n\n/** Per-call context that varies request-to-request (set via InvokeParams.meta). */\nexport interface CallMeta {\n /** Feature/surface label, e.g. \"opi\", \"ai-advisor\", \"insights\". */\n label?: string;\n /** \"marketing\" | \"operational\" | a feature name. */\n mode?: string;\n /** Authenticated user, when known (operational surfaces). */\n user?: { id?: string; role?: string; enterpriseId?: string | number };\n /** Current page/route (page-awareness + per-surface attribution). */\n page?: string;\n}\n\n/** Emitted after every successful call. The metering sink persists this. */\nexport interface UsageEvent extends CallMeta {\n /** App identifier, e.g. \"marketing-site\", \"businesshub\", \"oee\". */\n app: string;\n model: string;\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n estCostUsd: number;\n /** True when token counts were estimated (provider returned no usage block). */\n estimated: boolean;\n /** ISO timestamp. */\n at: string;\n}\n\n/** Asked BEFORE a call, so over-budget / disabled traffic never spends. */\nexport interface GuardRequest extends CallMeta {\n app: string;\n /** Rough prompt-token estimate, for budget math before the call. */\n estPromptTokens: number;\n}\n\nexport interface GuardResult {\n allowed: boolean;\n /** User-facing reason when blocked (kill-switch, budget exceeded, rate limit). */\n reason?: string;\n}\n\n/** Thrown by invokeLLM when a guard blocks the call. */\nexport class LLMBudgetError extends Error {\n readonly code = \"LLM_BUDGET_BLOCKED\";\n constructor(reason: string) {\n super(reason);\n this.name = \"LLMBudgetError\";\n }\n}\n\n/** Rough token estimate (~4 chars/token) — fallback when no usage is returned. */\nexport function estimateTokens(text: string): number {\n if (!text) return 0;\n return Math.ceil(text.length / 4);\n}\n\n/** Estimate prompt tokens across a set of message strings. */\nexport function estimateMessageTokens(contents: string[]): number {\n return contents.reduce((sum, c) => sum + estimateTokens(c), 0);\n}\n\n/** Compute estimated USD cost from token counts. */\nexport function computeCostUsd(\n promptTokens: number,\n completionTokens: number,\n rate: TokenCostRate = DEFAULT_COST_RATE\n): number {\n const cost =\n (promptTokens / 1000) * rate.inputPer1k +\n (completionTokens / 1000) * rate.outputPer1k;\n // Round to 6 dp — sub-cent precision without float noise.\n return Math.round(cost * 1e6) / 1e6;\n}\n","/**\n * LLM helper — calls Google's Gemini API via its OpenAI-compatible endpoint.\n *\n * Example:\n * const invokeLLM = createLLMClient(ENV);\n * const result = await invokeLLM({\n * messages: [\n * { role: \"system\", content: \"You are a helpful assistant.\" },\n * { role: \"user\", content: \"Hello, world!\" },\n * ],\n * });\n */\nimport type { CoreEnv } from \"./types\";\nimport {\n type CallMeta,\n type TokenCostRate,\n type UsageEvent,\n type GuardRequest,\n type GuardResult,\n DEFAULT_COST_RATE,\n LLMBudgetError,\n computeCostUsd,\n estimateMessageTokens,\n estimateTokens,\n} from \"./metering\";\n\nexport type Role = \"system\" | \"user\" | \"assistant\" | \"tool\" | \"function\";\n\nexport type TextContent = {\n type: \"text\";\n text: string;\n};\n\nexport type ImageContent = {\n type: \"image_url\";\n image_url: {\n url: string;\n detail?: \"auto\" | \"low\" | \"high\";\n };\n};\n\nexport type FileContent = {\n type: \"file_url\";\n file_url: {\n url: string;\n mime_type?:\n | \"audio/mpeg\"\n | \"audio/wav\"\n | \"application/pdf\"\n | \"audio/mp4\"\n | \"video/mp4\";\n };\n};\n\nexport type MessageContent = string | TextContent | ImageContent | FileContent;\n\nexport type Message = {\n role: Role;\n name?: string;\n tool_call_id?: string;\n content: MessageContent | MessageContent[];\n};\n\nexport type Tool = {\n type: \"function\";\n function: {\n name: string;\n description?: string;\n parameters?: Record<string, unknown>;\n };\n};\n\ntype ToolChoiceExplicit = {\n type: \"function\";\n function: { name: string };\n};\n\nexport type ToolChoice =\n | \"none\"\n | \"auto\"\n | \"required\"\n | { name: string }\n | ToolChoiceExplicit;\n\ntype JsonSchema = {\n name: string;\n schema: Record<string, unknown>;\n strict?: boolean;\n};\n\nexport type ResponseFormat =\n | { type: \"text\" }\n | { type: \"json_object\" }\n | { type: \"json_schema\"; json_schema: JsonSchema };\n\nexport type OutputSchema = {\n name: string;\n schema: Record<string, unknown>;\n strict?: boolean;\n};\n\nexport type InvokeParams = {\n messages: Message[];\n tools?: Tool[];\n toolChoice?: ToolChoice;\n tool_choice?: ToolChoice;\n outputSchema?: OutputSchema;\n output_schema?: OutputSchema;\n responseFormat?: ResponseFormat;\n response_format?: ResponseFormat;\n /** Per-call attribution for metering/guarding (label, mode, user, page). */\n meta?: CallMeta;\n};\n\n/** Optional metering + cost-control hooks, set once per app. */\nexport type LLMClientOptions = {\n /** App identifier recorded on every usage event, e.g. \"marketing-site\". */\n app?: string;\n /** Pricing used to estimate cost from tokens. Defaults to gemini-3.5-flash (rate unverified, see metering.ts). */\n costRate?: TokenCostRate;\n /** Metering sink — called after each successful call. Errors are swallowed. */\n onUsage?: (event: UsageEvent) => void | Promise<void>;\n /** Budget/kill-switch check — called before each call; block = no spend. */\n guard?: (req: GuardRequest) => GuardResult | Promise<GuardResult>;\n};\n\nexport type InvokeResult = {\n id: string;\n object: string;\n created: number;\n model: string;\n choices: Array<{\n index: number;\n message: {\n role: string;\n content: string | null;\n tool_calls?: Array<{\n id: string;\n type: \"function\";\n function: { name: string; arguments: string };\n }>;\n };\n finish_reason: string;\n }>;\n usage?: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n };\n};\n\nconst ensureArray = (\n value: MessageContent | MessageContent[]\n): MessageContent[] => (Array.isArray(value) ? value : [value]);\n\nconst normalizeContentPart = (\n part: MessageContent\n): TextContent | ImageContent | FileContent => {\n if (typeof part === \"string\") {\n return { type: \"text\", text: part };\n }\n if (part.type === \"text\") return part;\n if (part.type === \"image_url\") return part;\n if (part.type === \"file_url\") return part;\n throw new Error(\"Unsupported message content part\");\n};\n\nconst normalizeMessage = (message: Message) => {\n const { role, name, tool_call_id } = message;\n if (role === \"tool\" || role === \"function\") {\n const content = ensureArray(message.content)\n .map((part) => (typeof part === \"string\" ? part : JSON.stringify(part)))\n .join(\"\\n\");\n return { role, name, tool_call_id, content };\n }\n const contentParts = ensureArray(message.content).map(normalizeContentPart);\n if (contentParts.length === 1 && contentParts[0].type === \"text\") {\n return { role, name, content: contentParts[0].text };\n }\n return { role, name, content: contentParts };\n};\n\nconst normalizeToolChoice = (\n toolChoice: ToolChoice | undefined,\n tools: Tool[] | undefined\n): \"none\" | \"auto\" | ToolChoiceExplicit | undefined => {\n if (!toolChoice) return undefined;\n if (toolChoice === \"none\" || toolChoice === \"auto\") return toolChoice;\n if (toolChoice === \"required\") {\n if (!tools || tools.length === 0) {\n throw new Error(\n \"tool_choice 'required' was provided but no tools were configured\"\n );\n }\n if (tools.length > 1) {\n throw new Error(\n \"tool_choice 'required' needs a single tool or specify the tool name explicitly\"\n );\n }\n return { type: \"function\", function: { name: tools[0].function.name } };\n }\n if (\"name\" in toolChoice) {\n return { type: \"function\", function: { name: toolChoice.name } };\n }\n return toolChoice;\n};\n\nconst normalizeResponseFormat = ({\n responseFormat,\n response_format,\n outputSchema,\n output_schema,\n}: {\n responseFormat?: ResponseFormat;\n response_format?: ResponseFormat;\n outputSchema?: OutputSchema;\n output_schema?: OutputSchema;\n}):\n | { type: \"json_schema\"; json_schema: JsonSchema }\n | { type: \"text\" }\n | { type: \"json_object\" }\n | undefined => {\n const explicitFormat = responseFormat || response_format;\n if (explicitFormat) {\n if (\n explicitFormat.type === \"json_schema\" &&\n !explicitFormat.json_schema?.schema\n ) {\n throw new Error(\n \"responseFormat json_schema requires a defined schema object\"\n );\n }\n return explicitFormat;\n }\n const schema = outputSchema || output_schema;\n if (!schema) return undefined;\n if (!schema.name || !schema.schema) {\n throw new Error(\"outputSchema requires both name and schema\");\n }\n return {\n type: \"json_schema\",\n json_schema: {\n name: schema.name,\n schema: schema.schema,\n ...(typeof schema.strict === \"boolean\" ? { strict: schema.strict } : {}),\n },\n };\n};\n\nconst MODEL = \"gemini-3.5-flash\";\n\n/** Flatten a message's content to plain text for token estimation. */\nfunction messageToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === \"string\") return part;\n if (part && typeof part === \"object\" && \"text\" in part) {\n return String((part as { text?: unknown }).text ?? \"\");\n }\n return \"\";\n })\n .join(\" \");\n }\n return \"\";\n}\n\nexport function createLLMClient(env: CoreEnv, options: LLMClientOptions = {}) {\n const { app = \"unknown\", costRate = DEFAULT_COST_RATE, onUsage, guard } =\n options;\n\n const resolveApiUrl = () =>\n env.geminiApiUrl && env.geminiApiUrl.trim().length > 0\n ? `${env.geminiApiUrl.replace(/\\/$/, \"\")}/chat/completions`\n : \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\";\n\n const assertApiKey = () => {\n if (!env.geminiApiKey) {\n throw new Error(\"GEMINI_API_KEY is not configured\");\n }\n };\n\n return async function invokeLLM(\n params: InvokeParams\n ): Promise<InvokeResult> {\n assertApiKey();\n\n const {\n messages,\n tools,\n toolChoice,\n tool_choice,\n outputSchema,\n output_schema,\n responseFormat,\n response_format,\n meta = {},\n } = params;\n\n const estPromptTokens = estimateMessageTokens(\n messages.map((m) => messageToText(m.content))\n );\n\n // ── Cost control: ask the guard before spending anything ──\n if (guard) {\n const verdict = await guard({ app, estPromptTokens, ...meta });\n if (!verdict.allowed) {\n throw new LLMBudgetError(\n verdict.reason ?? \"AI is temporarily unavailable.\"\n );\n }\n }\n\n const payload: Record<string, unknown> = {\n model: MODEL,\n messages: messages.map(normalizeMessage),\n };\n\n if (tools && tools.length > 0) {\n payload.tools = tools;\n }\n\n const normalizedToolChoice = normalizeToolChoice(\n toolChoice || tool_choice,\n tools\n );\n if (normalizedToolChoice) {\n payload.tool_choice = normalizedToolChoice;\n }\n\n payload.max_tokens = 32768;\n\n const normalizedResponseFormat = normalizeResponseFormat({\n responseFormat,\n response_format,\n outputSchema,\n output_schema,\n });\n if (normalizedResponseFormat) {\n payload.response_format = normalizedResponseFormat;\n }\n\n const response = await fetch(resolveApiUrl(), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${env.geminiApiKey}`,\n },\n body: JSON.stringify(payload),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `LLM invoke failed: ${response.status} ${response.statusText} – ${errorText}`\n );\n }\n\n const result = (await response.json()) as InvokeResult;\n\n // ── Metering: capture usage and emit to the sink (never breaks the call) ──\n if (onUsage) {\n try {\n const provided = result.usage;\n const estimated = !provided;\n const promptTokens = provided?.prompt_tokens ?? estPromptTokens;\n const completionTokens =\n provided?.completion_tokens ??\n estimateTokens(messageToText(result.choices?.[0]?.message?.content));\n const totalTokens =\n provided?.total_tokens ?? promptTokens + completionTokens;\n\n const event: UsageEvent = {\n app,\n model: result.model ?? MODEL,\n promptTokens,\n completionTokens,\n totalTokens,\n estCostUsd: computeCostUsd(promptTokens, completionTokens, costRate),\n estimated,\n at: new Date().toISOString(),\n ...meta,\n };\n\n await onUsage(event);\n } catch (err) {\n // Metering must never take down a working chat.\n console.warn(\"[core-server] onUsage sink failed:\", err);\n }\n }\n\n return result;\n };\n}\n","/**\n * Image generation helper using internal ImageService.\n *\n * Example:\n * import { storagePut } from \"../storage\";\n * const generateImage = createImageGenerator(ENV, storagePut);\n * const { url } = await generateImage({ prompt: \"A serene landscape\" });\n */\nimport type { CoreEnv, StoragePutFn } from \"./types\";\n\nexport type GenerateImageOptions = {\n prompt: string;\n originalImages?: Array<{\n url?: string;\n b64Json?: string;\n mimeType?: string;\n }>;\n};\n\nexport type GenerateImageResponse = {\n url?: string;\n};\n\nexport function createImageGenerator(env: CoreEnv, storagePut: StoragePutFn) {\n return async function generateImage(\n options: GenerateImageOptions\n ): Promise<GenerateImageResponse> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\n \"images.v1.ImageService/GenerateImage\",\n baseUrl\n ).toString();\n\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({\n prompt: options.prompt,\n original_images: options.originalImages || [],\n }),\n });\n\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `Image generation request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : \"\"}`\n );\n }\n\n const result = (await response.json()) as {\n image: {\n b64Json: string;\n mimeType: string;\n };\n };\n\n const base64Data = result.image.b64Json;\n const buffer = Buffer.from(base64Data, \"base64\");\n\n const { url } = await storagePut(\n `generated/${Date.now()}.png`,\n buffer,\n result.image.mimeType\n );\n\n return { url };\n };\n}\n","/**\n * Google Maps API Integration for Manus WebDev Templates.\n *\n * Main function: createMapClient(env) returns makeRequest<T>(endpoint, params)\n * All credentials are automatically injected. Array parameters use | as separator.\n *\n * Example:\n * const makeRequest = createMapClient(ENV);\n * const result = await makeRequest<GeocodeResult>(\n * \"/maps/api/geocode/json\",\n * { address: \"1600 Amphitheatre Parkway\" }\n * );\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type MapRequestParams = Record<\n string,\n string | number | boolean | string[] | undefined\n>;\n\nexport function createMapClient(env: CoreEnv) {\n return async function makeRequest<T = unknown>(\n endpoint: string,\n params: MapRequestParams = {}\n ): Promise<T> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const proxyUrl = new URL(\n \"webdevtoken.v1.WebDevService/GoogleMapsProxy\",\n baseUrl\n ).toString();\n\n const processedParams: Record<string, string> = {};\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n processedParams[key] = value.join(\"|\");\n } else {\n processedParams[key] = String(value);\n }\n }\n\n const response = await fetch(proxyUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({\n endpoint,\n params: processedParams,\n }),\n });\n\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `Maps API request failed (${response.status} ${response.statusText})${detail ? `: ${detail}` : \"\"}`\n );\n }\n\n const wrapper = (await response.json()) as { jsonData?: string };\n if (wrapper.jsonData) {\n try {\n return JSON.parse(wrapper.jsonData) as T;\n } catch {\n return wrapper.jsonData as unknown as T;\n }\n }\n return wrapper as unknown as T;\n };\n}\n","/**\n * Owner notification helper — pushes operational updates to the Manus project owner.\n *\n * Example:\n * const notifyOwner = createNotificationClient(ENV);\n * await notifyOwner({ title: \"New submission\", content: \"User X submitted form Y\" });\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type NotificationPayload = {\n title: string;\n content: string;\n};\n\nconst TITLE_MAX_LENGTH = 1200;\nconst CONTENT_MAX_LENGTH = 20000;\n\nexport function createNotificationClient(env: CoreEnv) {\n return async function notifyOwner(\n payload: NotificationPayload\n ): Promise<boolean> {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n const title =\n payload.title.length > TITLE_MAX_LENGTH\n ? payload.title.slice(0, TITLE_MAX_LENGTH)\n : payload.title;\n const content =\n payload.content.length > CONTENT_MAX_LENGTH\n ? payload.content.slice(0, CONTENT_MAX_LENGTH)\n : payload.content;\n\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\n \"webdevtoken.v1.WebDevService/NotifyOwner\",\n baseUrl\n ).toString();\n\n try {\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n \"connect-protocol-version\": \"1\",\n authorization: `Bearer ${env.forgeApiKey}`,\n },\n body: JSON.stringify({ title, content }),\n });\n\n if (!response.ok) {\n console.error(\n `Notification failed: ${response.status} ${response.statusText}`\n );\n return false;\n }\n return true;\n } catch (error) {\n console.error(\"Notification error:\", error);\n return false;\n }\n };\n}\n","/**\n * Voice transcription helper using internal Speech-to-Text service (Whisper API).\n *\n * Example:\n * const transcribeAudio = createTranscriptionClient(ENV);\n * const result = await transcribeAudio({\n * audioUrl: \"https://storage.example.com/audio/recording.mp3\",\n * language: \"en\",\n * });\n */\nimport type { CoreEnv } from \"./types\";\n\nexport type TranscribeOptions = {\n audioUrl: string;\n language?: string;\n prompt?: string;\n};\n\nexport type WhisperSegment = {\n id: number;\n seek: number;\n start: number;\n end: number;\n text: string;\n tokens: number[];\n temperature: number;\n avg_logprob: number;\n compression_ratio: number;\n no_speech_prob: number;\n};\n\nexport type WhisperResponse = {\n task: string;\n language: string;\n duration: number;\n text: string;\n segments: WhisperSegment[];\n};\n\nexport type TranscriptionError = {\n error: string;\n code: string;\n details: string;\n};\n\nexport type TranscriptionResult = WhisperResponse | TranscriptionError;\n\nconst SUPPORTED_MIME_TYPES = [\n \"audio/webm\",\n \"audio/mp3\",\n \"audio/mpeg\",\n \"audio/wav\",\n \"audio/wave\",\n \"audio/ogg\",\n \"audio/m4a\",\n \"audio/mp4\",\n] as const;\n\nfunction getFileExtension(mimeType: string): string {\n const mimeToExt: Record<string, string> = {\n \"audio/webm\": \"webm\",\n \"audio/mp3\": \"mp3\",\n \"audio/mpeg\": \"mp3\",\n \"audio/wav\": \"wav\",\n \"audio/wave\": \"wav\",\n \"audio/ogg\": \"ogg\",\n \"audio/m4a\": \"m4a\",\n \"audio/mp4\": \"m4a\",\n };\n return mimeToExt[mimeType] || \"audio\";\n}\n\nfunction getLanguageName(langCode: string): string {\n const langMap: Record<string, string> = {\n en: \"English\",\n es: \"Spanish\",\n fr: \"French\",\n de: \"German\",\n it: \"Italian\",\n pt: \"Portuguese\",\n ru: \"Russian\",\n ja: \"Japanese\",\n ko: \"Korean\",\n zh: \"Chinese\",\n ar: \"Arabic\",\n hi: \"Hindi\",\n nl: \"Dutch\",\n pl: \"Polish\",\n tr: \"Turkish\",\n sv: \"Swedish\",\n da: \"Danish\",\n no: \"Norwegian\",\n fi: \"Finnish\",\n };\n return langMap[langCode] || langCode;\n}\n\nexport function createTranscriptionClient(env: CoreEnv) {\n return async function transcribeAudio(\n options: TranscribeOptions\n ): Promise<TranscriptionResult> {\n try {\n if (!env.forgeApiUrl) {\n throw new Error(\"BUILT_IN_FORGE_API_URL is not configured\");\n }\n if (!env.forgeApiKey) {\n throw new Error(\"BUILT_IN_FORGE_API_KEY is not configured\");\n }\n\n // Step 1: Fetch the audio file to determine MIME type and validate\n const audioResponse = await fetch(options.audioUrl);\n if (!audioResponse.ok) {\n return {\n error: \"Failed to fetch audio file\",\n code: \"FETCH_FAILED\",\n details: `HTTP ${audioResponse.status} ${audioResponse.statusText}`,\n };\n }\n\n const contentType =\n audioResponse.headers.get(\"content-type\") || \"audio/webm\";\n const mimeType = contentType.split(\";\")[0].trim();\n\n if (\n !SUPPORTED_MIME_TYPES.includes(\n mimeType as (typeof SUPPORTED_MIME_TYPES)[number]\n )\n ) {\n return {\n error: \"Unsupported audio format\",\n code: \"UNSUPPORTED_FORMAT\",\n details: `MIME type '${mimeType}' is not supported. Supported: ${SUPPORTED_MIME_TYPES.join(\", \")}`,\n };\n }\n\n // Step 2: Read audio buffer and check size\n const audioBuffer = await audioResponse.arrayBuffer();\n const sizeMB = audioBuffer.byteLength / (1024 * 1024);\n if (sizeMB > 16) {\n return {\n error: \"Audio file exceeds maximum size limit\",\n code: \"FILE_TOO_LARGE\",\n details: `File size is ${sizeMB.toFixed(2)}MB, maximum allowed is 16MB`,\n };\n }\n\n // Step 3: Create FormData for multipart upload to Whisper API\n const formData = new FormData();\n const filename = `audio.${getFileExtension(mimeType)}`;\n const audioBlob = new Blob([new Uint8Array(audioBuffer)], {\n type: mimeType,\n });\n formData.append(\"file\", audioBlob, filename);\n formData.append(\"model\", \"whisper-1\");\n formData.append(\"response_format\", \"verbose_json\");\n\n const prompt =\n options.prompt ||\n (options.language\n ? `Transcribe the user's voice to text, the user's working language is ${getLanguageName(options.language)}`\n : \"Transcribe the user's voice to text\");\n formData.append(\"prompt\", prompt);\n\n // Step 4: Call the transcription service\n const baseUrl = env.forgeApiUrl.endsWith(\"/\")\n ? env.forgeApiUrl\n : `${env.forgeApiUrl}/`;\n const fullUrl = new URL(\"v1/audio/transcriptions\", baseUrl).toString();\n\n const response = await fetch(fullUrl, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${env.forgeApiKey}`,\n \"Accept-Encoding\": \"identity\",\n },\n body: formData,\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n return {\n error: \"Transcription service request failed\",\n code: \"TRANSCRIPTION_FAILED\",\n details: `${response.status} ${response.statusText}${errorText ? `: ${errorText}` : \"\"}`,\n };\n }\n\n // Step 5: Parse and return the transcription result\n const whisperResponse = (await response.json()) as WhisperResponse;\n\n if (\n !whisperResponse.text ||\n typeof whisperResponse.text !== \"string\"\n ) {\n return {\n error: \"Invalid transcription response\",\n code: \"SERVICE_ERROR\",\n details:\n \"Transcription service returned an invalid response format\",\n };\n }\n\n return whisperResponse;\n } catch (error) {\n return {\n error: \"Voice transcription failed\",\n code: \"SERVICE_ERROR\",\n details:\n error instanceof Error\n ? error.message\n : \"An unexpected error occurred\",\n };\n }\n };\n}\n","/**\n * Personas for the shared Oplytics AI assistant.\n *\n * One brain, two voices: the same knowledge base is wrapped in a customer-facing\n * persona (\"Opi\" on the marketing site) or an operational persona (the in-app\n * \"AI Support Engineer\" in the portal / subdomains). The mode selects the overlay.\n */\n\nexport type AssistantMode = \"marketing\" | \"operational\";\n\n/** Identity + tone shared by every surface. */\nconst BASE_PERSONA = `\nYou are the Oplytics AI assistant — a knowledgeable, friendly guide to the Oplytics operational-excellence platform.\nYour personality is warm, approachable, and genuinely helpful, while staying clear, professional, and competent — especially on operational safety and platform technicals.\n\nShared guidelines:\n- Use a warm, professional tone: clear, concise, and easy to follow. Avoid heavy slang, forced catchphrases, or over-familiarity — be friendly without overdoing it.\n- For anything safety- or incident-related, stay serious and accurate while keeping the warm tone — prioritise accuracy over speed.\n- If you don't know something, be honest and guide the user to the right place rather than inventing an answer.\n- Prefer the supplied Oplytics knowledge below over your own assumptions. If the knowledge doesn't cover it, say so plainly.\n`.trim();\n\n/** Customer-facing overlay — Opi on the public marketing site. */\nconst MARKETING_OVERLAY = `\nYou are \"Opi\", talking to a prospective customer on the public Oplytics marketing site.\n- Focus on features, benefits, outcomes, and helping them book a demo or get in touch.\n- Be persuasive but never pushy or salesy; lead with how Oplytics solves their problem.\n- Never discuss competitor products by name.\n- For specific pricing, direct the user to the contact page at /contact rather than quoting figures.\n- You do not have access to any account or operational data — you are pre-sales only.\n`.trim();\n\n/** Authenticated overlay — the in-app AI Support Engineer. */\nconst OPERATIONAL_OVERLAY = `\nYou are the Oplytics AI Support Engineer, supporting a verified, logged-in user inside the platform.\n- Tailor responses to their role and enterprise context (supplied below when available).\n- Help with deep operational support: platform setup, training, troubleshooting, data interpretation, and specific tasks.\n- For safety-related queries, always prioritise accuracy and cite the correct workflow.\n- You may reference platform features and the user's context, but never expose data the user shouldn't see.\n`.trim();\n\nconst OVERLAYS: Record<AssistantMode, string> = {\n marketing: MARKETING_OVERLAY,\n operational: OPERATIONAL_OVERLAY,\n};\n\nexport interface BuildSystemPromptParams {\n mode: AssistantMode;\n /** Pre-formatted knowledge block (see formatKnowledge) injected into the prompt. */\n knowledge?: string;\n /** Free-form runtime context (current page, user/role, enterprise, etc.). */\n context?: string;\n}\n\n/** Compose the full system prompt: base identity + mode overlay + knowledge + context. */\nexport function buildSystemPrompt({\n mode,\n knowledge,\n context,\n}: BuildSystemPromptParams): string {\n const sections = [BASE_PERSONA, OVERLAYS[mode]];\n\n if (knowledge && knowledge.trim().length > 0) {\n sections.push(`Relevant Oplytics knowledge:\\n${knowledge.trim()}`);\n }\n\n if (context && context.trim().length > 0) {\n sections.push(`Current context:\\n${context.trim()}`);\n }\n\n return sections.join(\"\\n\\n\");\n}\n","/**\n * Shared Oplytics knowledge base + a RAG-ready retrieval interface.\n *\n * Phase 2a ships the knowledge as version-controlled data injected into the\n * system prompt, behind a `Retriever` interface. Phase 2b can swap the static\n * retriever for a vector-store-backed one WITHOUT changing any consumer — the\n * signature is the contract.\n *\n * This is a deliberately small seed. Expand `KNOWLEDGE` over time (it's the one\n * source of truth shared by Opi and the in-app AI Support Engineer).\n */\nimport type { AssistantMode } from \"./personas\";\n\nexport interface KnowledgeItem {\n id: string;\n question: string;\n answer: string;\n /** Lowercase keywords/tags used by the static retriever's scoring. */\n tags: string[];\n /** If set, this item is only surfaced in the listed modes. Default: both. */\n modes?: AssistantMode[];\n}\n\nexport interface RetrieveOptions {\n mode: AssistantMode;\n /** Max items to return. Default 4. */\n limit?: number;\n}\n\n/**\n * A retriever maps a user query to the most relevant knowledge items.\n * Async so a future vector/embedding implementation can drop in unchanged.\n */\nexport type Retriever = (\n query: string,\n options: RetrieveOptions\n) => Promise<KnowledgeItem[]> | KnowledgeItem[];\n\n/** Seed knowledge. Sourced from the Oplytics solution set; extend freely. */\nexport const KNOWLEDGE: KnowledgeItem[] = [\n // ── Platform ──────────────────────────────────────────────────────────────\n {\n id: \"platform-overview\",\n question: \"What does Oplytics do?\",\n answer:\n \"Oplytics is an integrated operational-excellence platform for manufacturers. Its modules share data, context, and AI insights so individual tools compound into one advantage instead of working in silos. Four services are live today (OEE Manager, the SQDCP Dashboard, Policy Deployment, and Action Manager), with more on the way.\",\n tags: [\"platform\", \"overview\", \"what\", \"oplytics\", \"modules\", \"manufacturing\"],\n },\n {\n id: \"who-for\",\n question: \"Who is Oplytics for?\",\n answer:\n \"Manufacturers and factory operations teams running continuous improvement, lean, and daily management — from the shop floor to senior leadership. It suits anyone replacing whiteboards, spreadsheets, and disconnected point tools with one connected system.\",\n tags: [\"who\", \"for\", \"audience\", \"manufacturer\", \"factory\", \"lean\", \"operations\"],\n },\n {\n id: \"live-services\",\n question: \"Which services are available right now?\",\n answer:\n \"Live today: OEE Manager, the SQDCP Dashboard, Policy Deployment, and Action Manager. In development and coming soon: OplyticsConnect, Quality Manager, Safety Manager, and Certification Manager — existing customers get early access and preferential pricing when they launch.\",\n tags: [\"live\", \"available\", \"services\", \"roadmap\", \"coming\", \"soon\", \"status\"],\n },\n {\n id: \"integrated-platform\",\n question: \"Why one platform instead of separate tools?\",\n answer:\n \"Every module shares the same data and context, so an action raised on a tier board, a loss in OEE, and a strategic objective all connect. That shared backbone is what lets the AI surface patterns no single tool could see alone.\",\n tags: [\"integrated\", \"platform\", \"connected\", \"modules\", \"data\", \"why\"],\n },\n {\n id: \"methodologies\",\n question: \"What improvement methodologies does it support?\",\n answer:\n \"Best-practice CI is built into the workflows: Lean, Kaizen, and PDCA throughout, plus Hoshin Kanri (with X-matrix and catchball) for strategy deployment and the SQDCP framework for daily management.\",\n tags: [\"lean\", \"kaizen\", \"pdca\", \"hoshin\", \"methodology\", \"ci\", \"continuous\", \"improvement\"],\n },\n {\n id: \"mobile-desktop\",\n question: \"Can we use it on mobile?\",\n answer:\n \"Yes — Oplytics works on desktop and mobile, so boards, actions, and reports are accessible from any device, on the floor or off-site.\",\n tags: [\"mobile\", \"desktop\", \"device\", \"tablet\", \"phone\", \"access\"],\n },\n // ── OEE Manager (live) ─────────────────────────────────────────────────────\n {\n id: \"oee-manager\",\n question: \"How does OEE Manager work?\",\n answer:\n \"OEE Manager tracks Overall Equipment Effectiveness — availability, performance, and quality — in real time across every machine and line. It categorises downtime and speed losses, builds Pareto/loss analysis, and trends OEE by shift, line, product, and operator so teams target the biggest losses first.\",\n tags: [\"oee\", \"manager\", \"downtime\", \"loss\", \"availability\", \"performance\", \"quality\", \"tracking\", \"equipment\"],\n },\n {\n id: \"oee-data-collection\",\n question: \"Does OEE data have to be entered by hand?\",\n answer:\n \"No — OEE Manager can connect directly to PLCs and sensors via SmartConnect for automated data collection, eliminating manual-entry errors. Manual entry is still available where automation isn't in place.\",\n tags: [\"oee\", \"data\", \"collection\", \"automated\", \"smartconnect\", \"plc\", \"sensor\", \"manual\"],\n },\n {\n id: \"oee-targets-handover\",\n question: \"Can we set OEE targets and run shift handovers?\",\n answer:\n \"Yes. Set OEE targets by line, product, and shift with live visual indicators against target, and generate digital shift-handover reports with OEE summaries, open actions, and key events from the previous shift.\",\n tags: [\"oee\", \"target\", \"shift\", \"handover\", \"report\", \"goal\"],\n },\n // ── SQDCP Dashboard (live) ─────────────────────────────────────────────────\n {\n id: \"sqdcp\",\n question: \"What is the SQDCP Dashboard?\",\n answer:\n \"SQDCP replaces physical whiteboards with real-time digital tier boards covering Safety, Quality, Delivery, Cost, and People. Boards are accessible from any device and keep daily management consistent across shifts and sites.\",\n tags: [\"sqdcp\", \"tier\", \"board\", \"dashboard\", \"daily\", \"management\", \"whiteboard\"],\n },\n {\n id: \"sqdcp-pillars\",\n question: \"What does SQDCP track?\",\n answer:\n \"The five pillars: Safety (incidents, near-misses, observations), Quality (first-pass yield, scrap, complaints), Delivery (on-time delivery, schedule adherence), Cost (operational cost and waste), and People (attendance, training, skills).\",\n tags: [\"sqdcp\", \"safety\", \"quality\", \"delivery\", \"cost\", \"people\", \"pillars\", \"metrics\"],\n },\n // ── Policy Deployment (live) ───────────────────────────────────────────────\n {\n id: \"policy-deployment\",\n question: \"What is Policy Deployment?\",\n answer:\n \"Policy Deployment cascades strategy through every level using Hoshin Kanri: define 3-5 year breakthrough objectives and annual priorities, link them in a visual X-matrix, deploy via the catchball process, and review progress on a monthly/quarterly cadence with bowling charts.\",\n tags: [\"policy\", \"deployment\", \"hoshin\", \"kanri\", \"strategy\", \"xmatrix\", \"catchball\", \"objectives\"],\n },\n {\n id: \"catchball\",\n question: \"What is the catchball process?\",\n answer:\n \"Catchball is how objectives are aligned top-down and bottom-up: leadership proposes goals, teams feed back what's achievable, and the two iterate until aligned. Oplytics facilitates this digitally so strategy doesn't get lost between management layers.\",\n tags: [\"catchball\", \"hoshin\", \"alignment\", \"objectives\", \"policy\", \"deployment\"],\n },\n // ── Action Manager (live) ──────────────────────────────────────────────────\n {\n id: \"action-manager\",\n question: \"What is Action Manager?\",\n answer:\n \"Action Manager is one unified action register: capture actions from any source (audits, incidents, meetings, tier boards), assign them with owners and due dates, auto-escalate overdue items, and track status from open to verified closure. Analytics show completion rates, ageing, and source breakdown.\",\n tags: [\"action\", \"manager\", \"tasks\", \"improvement\", \"ci\", \"register\", \"escalation\", \"closure\"],\n },\n // ── Coming soon ────────────────────────────────────────────────────────────\n {\n id: \"safety-manager\",\n question: \"Do you have a Safety Manager?\",\n answer:\n \"Safety Manager is coming soon. It will cover mobile-first incident reporting (with photos and witness statements), hazard identification and risk assessment, behavioural safety observations, and a compliance dashboard. Existing customers get early access at launch.\",\n tags: [\"safety\", \"manager\", \"incident\", \"hazard\", \"risk\", \"observation\", \"compliance\", \"coming\"],\n },\n {\n id: \"quality-manager\",\n question: \"Do you have a Quality Manager?\",\n answer:\n \"Quality Manager is coming soon. It will handle non-conformance tracking from detection to disposition, a structured CAPA workflow with root-cause tools, internal/external audit scheduling, and quality metrics (first-pass yield, defect rates, cost of quality). Existing customers get early access at launch.\",\n tags: [\"quality\", \"manager\", \"ncr\", \"capa\", \"non-conformance\", \"audit\", \"coming\"],\n },\n {\n id: \"certification-manager\",\n question: \"Can it help with ISO certification?\",\n answer:\n \"Certification Manager is coming soon. It will manage standards like ISO 9001, ISO 14001, and IATF 16949 in one place, with version-controlled document control, a complete audit trail, and compliance/gap tracking. Existing customers get early access at launch.\",\n tags: [\"certification\", \"iso\", \"9001\", \"14001\", \"iatf\", \"16949\", \"standards\", \"document\", \"audit\", \"coming\"],\n },\n {\n id: \"connect-integrations\",\n question: \"Can Oplytics connect to our machines and systems?\",\n answer:\n \"Live OEE data already flows in via SmartConnect (PLCs and sensors). The standalone OplyticsConnect service is coming soon and adds broader machine connectivity — PLCs, SCADA, and industrial sensors over OPC-UA, MQTT, Modbus and more — with zero-code configuration and edge processing.\",\n tags: [\"connect\", \"integration\", \"machine\", \"plc\", \"scada\", \"sensor\", \"opcua\", \"mqtt\", \"modbus\", \"iot\", \"smartconnect\"],\n },\n {\n id: \"erp-integration\",\n question: \"Can it integrate with our ERP or other systems?\",\n answer:\n \"Oplytics focuses on shop-floor connectivity (machines, PLCs, sensors) today. For ERP or other business-system integrations, the best step is to get in touch via /contact so the team can scope your specific setup.\",\n tags: [\"erp\", \"integration\", \"system\", \"api\", \"data\", \"contact\"],\n },\n {\n id: \"protocols\",\n question: \"Which industrial protocols are supported?\",\n answer:\n \"Machine connectivity supports common industrial protocols including OPC-UA, MQTT, and Modbus, connecting to PLCs, SCADA systems, and sensors. Configuration is visual and zero-code, with edge processing and local buffering so no data is lost during connectivity drops.\",\n tags: [\"protocol\", \"opcua\", \"mqtt\", \"modbus\", \"plc\", \"scada\", \"edge\", \"connectivity\"],\n },\n // ── AI ─────────────────────────────────────────────────────────────────────\n {\n id: \"integrated-ai\",\n question: \"How does the AI help?\",\n answer:\n \"AI runs across every module — surfacing patterns, predicting issues, and recommending actions no single tool could spot alone — and the assistant helps users navigate, answers questions, and suggests next steps.\",\n tags: [\"ai\", \"insights\", \"predict\", \"assistant\", \"recommend\", \"intelligence\"],\n },\n {\n id: \"ai-assistant\",\n question: \"Who are you / what can you help with?\",\n answer:\n \"I'm Opi, the Oplytics AI guide. I can explain what the platform and each module do, point you to the right place, and help you book a demo or get in touch.\",\n tags: [\"opi\", \"assistant\", \"help\", \"who\", \"guide\", \"bot\"],\n },\n // ── Pricing, demos, getting started (marketing) ────────────────────────────\n {\n id: \"pricing\",\n question: \"How much does it cost?\",\n answer:\n \"Pricing is tailored to your sites and the modules you use, so figures aren't published. The best next step is to get in touch via the contact page (/contact) and the team will put together a quote.\",\n tags: [\"pricing\", \"price\", \"cost\", \"quote\", \"how\", \"much\", \"plan\"],\n modes: [\"marketing\"],\n },\n {\n id: \"plans\",\n question: \"What plans are available?\",\n answer:\n \"There are tiers to suit different stages — an Early Beta Partner programme, Starter, Professional, and Enterprise — covering different usage limits and capabilities like custom integrations and SLAs. All four live services are included; reach out via /contact to find the right fit.\",\n tags: [\"plan\", \"tier\", \"starter\", \"professional\", \"enterprise\", \"beta\", \"package\"],\n modes: [\"marketing\"],\n },\n {\n id: \"beta\",\n question: \"Is there a beta or early-adopter option?\",\n answer:\n \"Yes — the Early Beta Partner programme is for teams who want to get started early and shape the platform. Get in touch via /contact to learn more.\",\n tags: [\"beta\", \"early\", \"partner\", \"adopter\", \"trial\", \"pilot\"],\n modes: [\"marketing\"],\n },\n {\n id: \"demo\",\n question: \"Can I see a demo?\",\n answer:\n \"Absolutely — you can book a demo or try the live module demos. Head to the contact page (/contact) to get set up.\",\n tags: [\"demo\", \"trial\", \"try\", \"book\", \"see\", \"show\"],\n modes: [\"marketing\"],\n },\n {\n id: \"contact\",\n question: \"How do I get in touch?\",\n answer:\n \"Head to the contact page at /contact — tell the team a little about your operation and they'll take it from there, whether that's a demo, a quote, or answering questions.\",\n tags: [\"contact\", \"touch\", \"reach\", \"sales\", \"talk\", \"email\"],\n modes: [\"marketing\"],\n },\n {\n id: \"getting-started\",\n question: \"How do we get started?\",\n answer:\n \"Start with a conversation: get in touch via /contact for a demo and onboarding. The team will help you stand up the live services for your sites and connect your data.\",\n tags: [\"start\", \"started\", \"onboarding\", \"begin\", \"setup\", \"trial\"],\n },\n];\n\n/** Lowercase word tokens, length >= 3. */\nfunction tokenize(text: string): string[] {\n return (text.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter(\n (w) => w.length >= 3\n );\n}\n\n/** Score an item against the query tokens (tag hits weighted over text hits). */\nfunction scoreItem(item: KnowledgeItem, queryTokens: string[]): number {\n if (queryTokens.length === 0) return 0;\n const haystack = tokenize(`${item.question} ${item.answer}`);\n let score = 0;\n for (const token of queryTokens) {\n if (item.tags.includes(token)) score += 3;\n if (haystack.includes(token)) score += 1;\n }\n return score;\n}\n\n/**\n * Build a static, keyword-scored retriever over the supplied knowledge.\n * Drop-in replaceable by a vector retriever implementing the same `Retriever`.\n */\nexport function createStaticRetriever(\n knowledge: KnowledgeItem[] = KNOWLEDGE\n): Retriever {\n return (query, { mode, limit = 4 }) => {\n const queryTokens = tokenize(query);\n const eligible = knowledge.filter((k) => !k.modes || k.modes.includes(mode));\n\n const scored = eligible\n .map((item) => ({ item, score: scoreItem(item, queryTokens) }))\n .filter((s) => s.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n .map((s) => s.item);\n\n // Cold-start / no keyword hits: fall back to the first few eligible items\n // so the assistant always has some grounding to work from.\n if (scored.length === 0) return eligible.slice(0, limit);\n return scored;\n };\n}\n\n/** Render retrieved items into a compact block for the system prompt. */\nexport function formatKnowledge(items: KnowledgeItem[]): string {\n return items.map((i) => `Q: ${i.question}\\nA: ${i.answer}`).join(\"\\n\\n\");\n}\n","/**\n * createSupportEngine — the shared chat brain for Opi (marketing) and the in-app\n * AI Support Engineer (operational). Composes persona + retrieved knowledge +\n * runtime context, then calls the existing LLM client.\n *\n * const engine = createSupportEngine(ENV);\n * const { content } = await engine.chat({ mode: \"marketing\", messages });\n */\nimport type { CoreEnv } from \"../types\";\nimport { createLLMClient } from \"../llm\";\nimport type { Message, LLMClientOptions } from \"../llm\";\nimport { buildSystemPrompt, type AssistantMode } from \"./personas\";\nimport {\n createStaticRetriever,\n formatKnowledge,\n type Retriever,\n} from \"./knowledge\";\n\n/** Lightweight, mode-agnostic chat message (system prompt is added internally). */\nexport interface ChatMessage {\n role: \"user\" | \"assistant\";\n content: string;\n}\n\n/** Optional runtime context to ground the assistant. */\nexport interface SupportContext {\n /** Current page / route the user is on (page-awareness). */\n page?: string;\n /** Authenticated user details (operational mode). */\n user?: { name?: string; role?: string; enterpriseId?: string | number };\n /** Any extra free-form context to append. */\n extra?: string;\n}\n\nexport interface ChatParams {\n mode: AssistantMode;\n messages: ChatMessage[];\n context?: SupportContext;\n}\n\nexport interface ChatResult {\n content: string;\n}\n\nexport interface SupportEngineOptions {\n /**\n * Knowledge retriever. Defaults to the bundled static keyword retriever.\n * Swap for a vector-store retriever later with zero consumer changes.\n */\n retrieve?: Retriever;\n /** Items to retrieve per turn. Default 4. */\n knowledgeLimit?: number;\n /** Metering + cost-control hooks (app, onUsage sink, budget guard, costRate). */\n metering?: LLMClientOptions;\n}\n\nfunction renderContext(mode: AssistantMode, context?: SupportContext): string {\n const lines: string[] = [\n `Mode: ${mode === \"marketing\" ? \"Public Marketing Site\" : \"Authenticated Portal\"}`,\n ];\n if (context?.page) lines.push(`Page: ${context.page}`);\n if (context?.user?.name) lines.push(`User: ${context.user.name}`);\n if (context?.user?.role) lines.push(`Role: ${context.user.role}`);\n if (context?.user?.enterpriseId != null)\n lines.push(`Enterprise ID: ${context.user.enterpriseId}`);\n if (context?.extra) lines.push(context.extra);\n return lines.join(\"\\n\");\n}\n\nexport function createSupportEngine(\n env: CoreEnv,\n options: SupportEngineOptions = {}\n) {\n const invokeLLM = createLLMClient(env, options.metering);\n const retrieve = options.retrieve ?? createStaticRetriever();\n const knowledgeLimit = options.knowledgeLimit ?? 4;\n\n async function chat({ mode, messages, context }: ChatParams): Promise<ChatResult> {\n const lastUser = [...messages].reverse().find((m) => m.role === \"user\");\n const query = lastUser?.content ?? \"\";\n\n const items = await retrieve(query, { mode, limit: knowledgeLimit });\n const knowledge = formatKnowledge(items);\n\n const systemPrompt = buildSystemPrompt({\n mode,\n knowledge,\n context: renderContext(mode, context),\n });\n\n const llmMessages: Message[] = [\n { role: \"system\", content: systemPrompt },\n ...messages.map((m) => ({ role: m.role, content: m.content })),\n ];\n\n const result = await invokeLLM({\n messages: llmMessages,\n meta: {\n label: \"support-engine\",\n mode,\n page: context?.page,\n user: context?.user\n ? { role: context.user.role, enterpriseId: context.user.enterpriseId }\n : undefined,\n },\n });\n const raw = result.choices[0]?.message?.content;\n const content = typeof raw === \"string\" ? raw : JSON.stringify(raw ?? \"\");\n\n return { content };\n }\n\n return { chat };\n}\n\nexport type SupportEngine = ReturnType<typeof createSupportEngine>;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablo2410/core-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Shared server utilities for Oplytics.digital subdomains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@pablo2410/shared-ui": "^0.9.
|
|
37
|
+
"@pablo2410/shared-ui": "^0.9.2",
|
|
38
38
|
"@types/express": "^5.0.0",
|
|
39
39
|
"@types/node": "^25.5.0",
|
|
40
40
|
"axios": "^1.7.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hierarchy.ts"],"sourcesContent":["/**\n * Hierarchy Service — the single server-side source for org-hierarchy data.\n *\n * Fetches the canonical org-hierarchy from the Portal Service API\n * (GET /api/service/hierarchy, authenticated with a platform-level service key),\n * caches it with a TTL, and scopes it to a caller's enterprise using the shared\n * isolation rule (scopeHierarchyToEnterprise from @pablo2410/shared-ui).\n *\n * One fetch, one mapper, one cache, one scope policy. Every subdomain constructs\n * this once from its env and calls getScopedHierarchy(ctx.user) in its tRPC\n * `hierarchy` procedure — no per-app fetch/scope/null-policy reinvention.\n *\n * Quick example:\n * const hierarchy = createHierarchyService({\n * portalApiUrl: process.env.PORTAL_API_URL ?? \"\",\n * portalApiKey: process.env.PORTAL_API_KEY ?? \"\",\n * });\n * // in a tRPC router:\n * hierarchy: protectedProcedure.query(({ ctx }) => hierarchy.getScopedHierarchy(ctx.user)),\n */\nimport type { OrgHierarchyData } from \"@pablo2410/shared-ui/hierarchy/core\";\nimport { scopeHierarchyToEnterprise } from \"@pablo2410/shared-ui/hierarchy/core\";\n\nexport interface HierarchyServiceConfig {\n /** Portal Service API base URL, e.g. https://portal.oplytics.digital */\n portalApiUrl: string;\n /** Platform-level X-Service-Key for the Portal Service API. */\n portalApiKey: string;\n /** Cache TTL in milliseconds. Default 15 minutes. Set 0 to disable caching. */\n cacheTtlMs?: number;\n /** Injectable fetch implementation (for tests). Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n /** Injectable clock (for tests). Defaults to Date.now. */\n now?: () => number;\n}\n\nexport interface HierarchyService {\n /**\n * The org-hierarchy scoped to the caller's enterprise. A user with no resolved\n * enterprise (null/undefined enterpriseId) receives an empty hierarchy — see\n * scopeHierarchyToEnterprise for the policy. Never returns null.\n */\n getScopedHierarchy(\n user: { enterpriseId?: number | null } | null,\n ): Promise<OrgHierarchyData>;\n /**\n * The full, unscoped hierarchy (every enterprise). For Portal/admin tooling\n * only — subdomains must use getScopedHierarchy. null if unconfigured/unreachable.\n */\n getFullHierarchy(): Promise<OrgHierarchyData | null>;\n /** Clear the cache (e.g. after a portal sync). */\n invalidate(): void;\n /** Config + cache status, for health endpoints. */\n getStatus(): {\n configured: boolean;\n cached: boolean;\n ageMs: number | null;\n /** Reason the last fetch failed (null if the last fetch succeeded). */\n lastError: string | null;\n };\n}\n\nconst FIFTEEN_MINUTES = 15 * 60 * 1000;\n\nexport function createHierarchyService(\n config: HierarchyServiceConfig,\n): HierarchyService {\n const {\n portalApiUrl,\n portalApiKey,\n cacheTtlMs = FIFTEEN_MINUTES,\n fetchImpl = fetch,\n now = Date.now,\n } = config;\n\n const configured = Boolean(portalApiUrl && portalApiKey);\n let cache: { data: OrgHierarchyData; at: number } | null = null;\n // Last fetch failure reason, surfaced via getStatus() for diagnostics. A\n // configured service that returns an empty hierarchy is almost always a fetch\n // failure (wrong URL/key, unreachable Portal, non-200) — without this it was\n // swallowed to a silent null and looked like \"enterprise has no data\".\n let lastError: string | null = null;\n\n async function fetchRaw(): Promise<OrgHierarchyData | null> {\n if (!configured) {\n lastError = \"not configured (PORTAL_API_URL/PORTAL_API_KEY missing)\";\n return null;\n }\n const url = `${portalApiUrl.replace(/\\/$/, \"\")}/api/service/hierarchy`;\n let res: Response;\n try {\n res = await fetchImpl(url, {\n headers: {\n \"X-Service-Key\": portalApiKey,\n \"Content-Type\": \"application/json\",\n },\n });\n } catch (err) {\n lastError = `fetch threw: ${err instanceof Error ? err.message : String(err)}`;\n console.error(`[HierarchyService] ${lastError} (url=${url})`);\n return null;\n }\n if (!res.ok) {\n lastError = `HTTP ${res.status} ${res.statusText} from ${url}`;\n console.error(`[HierarchyService] ${lastError}`);\n return null;\n }\n const json = await res.json().catch(() => null);\n if (!json) {\n lastError = `invalid/empty JSON body from ${url}`;\n console.error(`[HierarchyService] ${lastError}`);\n return null;\n }\n lastError = null;\n return normalize(json);\n }\n\n async function getFullHierarchy(): Promise<OrgHierarchyData | null> {\n if (cacheTtlMs > 0 && cache && now() - cache.at < cacheTtlMs) {\n return cache.data;\n }\n const data = await fetchRaw();\n if (data && cacheTtlMs > 0) cache = { data, at: now() };\n return data;\n }\n\n async function getScopedHierarchy(\n user: { enterpriseId?: number | null } | null,\n ): Promise<OrgHierarchyData> {\n const full = await getFullHierarchy();\n return scopeHierarchyToEnterprise(full, user?.enterpriseId ?? null);\n }\n\n function invalidate(): void {\n cache = null;\n }\n\n function getStatus() {\n return {\n configured,\n cached: cache !== null,\n ageMs: cache ? now() - cache.at : null,\n lastError,\n };\n }\n\n return { getScopedHierarchy, getFullHierarchy, invalidate, getStatus };\n}\n\n/**\n * Map the Portal Service API response to the canonical OrgHierarchyData shape.\n * Tolerant of missing arrays (defaults to []) so a partial payload never throws.\n */\nfunction normalize(data: unknown): OrgHierarchyData {\n const d = (data ?? {}) as Record<string, any[]>;\n return {\n enterprises: (d.enterprises ?? []).map((e) => ({\n id: e.id,\n name: e.name,\n code: e.code,\n })),\n businessUnits: (d.businessUnits ?? []).map((bu) => ({\n id: bu.id,\n name: bu.name,\n code: bu.code,\n enterpriseId: bu.enterpriseId,\n })),\n sites: (d.sites ?? []).map((s) => ({\n id: s.id,\n name: s.name,\n code: s.code,\n enterpriseId: s.enterpriseId,\n businessUnitId: s.businessUnitId ?? 0,\n location: s.location,\n })),\n areas: (d.areas ?? []).map((a) => ({\n id: a.id,\n name: a.name,\n code: a.code,\n siteId: a.siteId,\n })),\n assets: (d.assets ?? []).map((a) => ({\n id: a.id,\n name: a.name,\n code: a.code,\n siteId: a.siteId,\n areaId: a.areaId ?? 0,\n assetType: a.assetType,\n })),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/createSubdomainAuth.ts"],"sourcesContent":["/**\n * createSubdomainAuth — shared SSO auth factory for Oplytics.digital subdomains.\n *\n * Encapsulates:\n * - JWT session verification (PORTAL_JWT_SECRET first, fallback to JWT_SECRET)\n * - Cookie domain handling via COOKIE_DOMAIN env var\n * - Portal API user enrichment with X-Service-Key header\n * - Auto-create user from JWT payload if not in local DB\n * - Enterprise backfill from portal API\n * - Manus OAuth code exchange and user info\n * - OAuth callback route registration\n *\n * Based on SQDCP's battle-tested sdk.ts + cookies.ts.\n */\n\nimport type { Request, CookieOptions } from \"express\";\nimport type {\n AuthEnv,\n BaseUser,\n DbAdapter,\n ExchangeTokenResponse,\n GetUserInfoResponse,\n GetUserInfoWithJwtResponse,\n PortalUser,\n RoleMapper,\n SessionPayload,\n SubdomainAuth,\n SubdomainAuthConfig,\n} from \"./types\";\n\n// ─── Constants ──────────────────────────────────────────────────────\n\nconst DEFAULT_COOKIE_NAME = \"app_session_id\";\nconst ONE_YEAR_MS = 1000 * 60 * 60 * 24 * 365;\nconst AXIOS_TIMEOUT_MS = 30_000;\n\nconst EXCHANGE_TOKEN_PATH =\n \"/webdev.v1.WebDevAuthPublicService/ExchangeToken\";\nconst GET_USER_INFO_PATH =\n \"/webdev.v1.WebDevAuthPublicService/GetUserInfo\";\nconst GET_USER_INFO_WITH_JWT_PATH =\n \"/webdev.v1.WebDevAuthPublicService/GetUserInfoWithJwt\";\n\n// ─── Helpers ────────────────────────────────────────────────────────\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0;\n\n/**\n * Derive the login method from the Manus platforms array.\n */\nfunction deriveLoginMethod(\n platforms: unknown,\n fallback: string | null | undefined\n): string | null {\n if (fallback && fallback.length > 0) return fallback;\n if (!Array.isArray(platforms) || platforms.length === 0) return null;\n const set = new Set<string>(\n platforms.filter((p): p is string => typeof p === \"string\")\n );\n if (set.has(\"REGISTERED_PLATFORM_EMAIL\")) return \"email\";\n if (set.has(\"REGISTERED_PLATFORM_GOOGLE\")) return \"google\";\n if (set.has(\"REGISTERED_PLATFORM_APPLE\")) return \"apple\";\n if (\n set.has(\"REGISTERED_PLATFORM_MICROSOFT\") ||\n set.has(\"REGISTERED_PLATFORM_AZURE\")\n )\n return \"microsoft\";\n if (set.has(\"REGISTERED_PLATFORM_GITHUB\")) return \"github\";\n const first = Array.from(set)[0];\n return first ? first.toLowerCase() : null;\n}\n\n// ─── Factory ────────────────────────────────────────────────────────\n\nexport function createSubdomainAuth<TUser extends BaseUser = BaseUser>(\n config: SubdomainAuthConfig<TUser>\n): SubdomainAuth<TUser> {\n const {\n env,\n db,\n // Default to identity (no role translation). Subdomains should adopt the\n // Portal's canonical roles verbatim; a custom mapper is legacy.\n mapRole = (role: string) => role,\n userSource = \"db\",\n cookieName = DEFAULT_COOKIE_NAME,\n sessionExpiryMs = ONE_YEAR_MS,\n serviceName = \"Subdomain\",\n portalUserCacheTtlMs = 30_000,\n } = config;\n\n const tag = `[${serviceName} Auth]`;\n\n if (userSource === \"db\" && !db) {\n throw new Error(\n `${tag} userSource \"db\" requires a db adapter in createSubdomainAuth config`\n );\n }\n\n // Short-TTL cache of Portal users (portal mode), keyed by openId, so we don't\n // hit the Portal on every request.\n const portalUserCache = new Map<string, { user: PortalUser; at: number }>();\n\n // ── Lazy-loaded dependencies (jose, cookie, axios) ──────────────\n // We use dynamic imports so consumers don't need to install these\n // as peer deps if they only use other core-server features.\n\n let _jose: typeof import(\"jose\") | null = null;\n async function getJose() {\n if (!_jose) _jose = await import(\"jose\");\n return _jose;\n }\n\n let _cookie: typeof import(\"cookie\") | null = null;\n async function getCookie() {\n if (!_cookie) _cookie = await import(\"cookie\");\n return _cookie;\n }\n\n let _axios: typeof import(\"axios\").default | null = null;\n async function getAxios() {\n if (!_axios) {\n const mod = await import(\"axios\");\n _axios = mod.default;\n }\n return _axios;\n }\n\n // ── OAuth HTTP client ───────────────────────────────────────────\n\n let oauthClient: import(\"axios\").AxiosInstance | null = null;\n async function getOAuthClient() {\n if (oauthClient) return oauthClient;\n const axios = await getAxios();\n oauthClient = axios.create({\n baseURL: env.oAuthServerUrl,\n timeout: AXIOS_TIMEOUT_MS,\n });\n return oauthClient;\n }\n\n // ── Portal API client ──────────────────────────────────────────\n\n let portalClient: import(\"axios\").AxiosInstance | null = null;\n function getPortalClient() {\n if (portalClient) return portalClient;\n // Synchronous check — we only create if we have the URL\n if (!env.portalApiUrl) return null;\n // We need axios synchronously here, but it should already be loaded\n // by the time portal client is needed (after authenticateRequest calls getAxios)\n return null; // Will be created lazily in async context\n }\n\n async function getPortalClientAsync() {\n if (portalClient) return portalClient;\n if (!env.portalApiUrl) return null;\n const axios = await getAxios();\n portalClient = axios.create({\n baseURL: env.portalApiUrl,\n timeout: 10_000,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(env.portalApiKey ? { \"X-Service-Key\": env.portalApiKey } : {}),\n },\n });\n return portalClient;\n }\n\n // ── JWT Secrets ────────────────────────────────────────────────\n\n function getPortalSecret() {\n const secret = env.portalJwtSecret;\n if (!secret) throw new Error(\"PORTAL_JWT_SECRET is not configured\");\n return new TextEncoder().encode(secret);\n }\n\n function getSessionSecret() {\n // Prefer portalJwtSecret for signing so cookies work cross-subdomain\n const secret = env.portalJwtSecret || env.jwtSecret;\n return new TextEncoder().encode(secret);\n }\n\n // ── Cookie Helpers ─────────────────────────────────────────────\n\n function getCookieDomain(): string | undefined {\n return env.cookieDomain || undefined;\n }\n\n function isSecureRequest(req: Request): boolean {\n if (req.protocol === \"https\") return true;\n const forwardedProto = req.headers[\"x-forwarded-proto\"];\n if (!forwardedProto) return false;\n const protoList = Array.isArray(forwardedProto)\n ? forwardedProto\n : forwardedProto.split(\",\");\n return protoList.some((p) => p.trim().toLowerCase() === \"https\");\n }\n\n function getSessionCookieOptions(\n req: Request\n ): Pick<\n CookieOptions,\n \"domain\" | \"httpOnly\" | \"path\" | \"sameSite\" | \"secure\"\n > {\n const secure = isSecureRequest(req);\n const domain = getCookieDomain();\n return {\n domain,\n httpOnly: true,\n path: \"/\",\n sameSite: \"lax\",\n secure,\n };\n }\n\n function isValidReturnUrl(url: string): boolean {\n if (url.startsWith(\"/\") && !url.startsWith(\"//\")) return true;\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"https:\") return false;\n return (\n parsed.hostname === \"oplytics.digital\" ||\n parsed.hostname.endsWith(\".oplytics.digital\")\n );\n } catch {\n return false;\n }\n }\n\n // ── Session Token ──────────────────────────────────────────────\n\n async function signSession(\n payload: SessionPayload,\n options: { expiresInMs?: number } = {}\n ): Promise<string> {\n const jose = await getJose();\n const issuedAt = Date.now();\n const expiresInMs = options.expiresInMs ?? sessionExpiryMs;\n const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000);\n const secretKey = getSessionSecret();\n\n return new jose.SignJWT({\n openId: payload.openId,\n appId: payload.appId,\n name: payload.name,\n })\n .setProtectedHeader({ alg: \"HS256\", typ: \"JWT\" })\n .setExpirationTime(expirationSeconds)\n .sign(secretKey);\n }\n\n async function createSessionToken(\n openId: string,\n options: { expiresInMs?: number; name?: string } = {}\n ): Promise<string> {\n return signSession(\n { openId, appId: env.appId, name: options.name || \"\" },\n options\n );\n }\n\n // ── Session Verification ───────────────────────────────────────\n\n async function verifySession(\n cookieValue: string | undefined | null\n ): Promise<SessionPayload | null> {\n if (!cookieValue) {\n console.warn(`${tag} Missing session cookie`);\n return null;\n }\n\n const jose = await getJose();\n\n // Try PORTAL_JWT_SECRET first (cross-subdomain SSO)\n if (env.portalJwtSecret) {\n try {\n const portalKey = getPortalSecret();\n const { payload } = await jose.jwtVerify(cookieValue, portalKey, {\n algorithms: [\"HS256\"],\n });\n const { openId, appId, name } = payload as Record<string, unknown>;\n if (\n !isNonEmptyString(openId) ||\n !isNonEmptyString(appId) ||\n !isNonEmptyString(name)\n ) {\n console.warn(`${tag} Portal JWT payload missing required fields`);\n return null;\n }\n return { openId, appId, name };\n } catch {\n // Portal secret didn't work — fall through to local secret\n }\n }\n\n // Fallback: try local JWT_SECRET\n try {\n const secretKey = getSessionSecret();\n const { payload } = await jose.jwtVerify(cookieValue, secretKey, {\n algorithms: [\"HS256\"],\n });\n const { openId, appId, name } = payload as Record<string, unknown>;\n if (\n !isNonEmptyString(openId) ||\n !isNonEmptyString(appId) ||\n !isNonEmptyString(name)\n ) {\n console.warn(`${tag} Session payload missing required fields`);\n return null;\n }\n return { openId, appId, name };\n } catch (error) {\n console.warn(\n `${tag} Session verification failed:`,\n (error as Error).message\n );\n return null;\n }\n }\n\n // ── Portal API ─────────────────────────────────────────────────\n\n async function getPortalUser(openId: string): Promise<PortalUser | null> {\n try {\n const client = await getPortalClientAsync();\n if (!client) return null;\n // The Portal Service API is under /api/service (base URL is the origin,\n // e.g. https://portal.oplytics.digital). Must include the /api/service\n // prefix — omitting it 404s and silently strands enterprise resolution.\n const { data } = await client.get<PortalUser>(\n `/api/service/users/by-openid/${encodeURIComponent(openId)}`\n );\n return data ?? null;\n } catch (err: unknown) {\n const status = (err as any)?.response?.status;\n if (status === 401) {\n console.error(\n `${tag} Portal API key revoked (401) — enrichment disabled`\n );\n } else if (status === 404) {\n // User not found in portal — normal for first-time users\n } else {\n console.warn(\n `${tag} Portal user lookup failed:`,\n (err as Error).message\n );\n }\n return null;\n }\n }\n\n /** Portal user lookup with a short-TTL cache (portal mode). */\n async function getPortalUserCached(\n openId: string\n ): Promise<PortalUser | null> {\n if (portalUserCacheTtlMs > 0) {\n const hit = portalUserCache.get(openId);\n if (hit && Date.now() - hit.at < portalUserCacheTtlMs) {\n return hit.user;\n }\n }\n const user = await getPortalUser(openId);\n if (user && portalUserCacheTtlMs > 0) {\n portalUserCache.set(openId, { user, at: Date.now() });\n }\n return user;\n }\n\n /**\n * Build ctx.user entirely from the Portal record — no local persistence.\n * The single source of truth. ctx.user.id is the PORTAL user id. Roles and\n * enterprise come from the Portal verbatim (no mapping).\n *\n * On a genuine Portal outage (the by-openid lookup returns null) we keep the\n * user authenticated using their verified session claims, with role/enterprise\n * unresolved, rather than throwing. Throwing here would make createContext (in\n * every subdomain) swallow the error into `user = null`, which the AuthGate\n * renders as the Sign In screen — i.e. a transient Portal blip would log the\n * user out. Their session cookie is already valid; keep them in, degraded,\n * until the Portal recovers. (Enterprise-scoped procedures will 403 during the\n * outage; client array-guards keep pages from crashing.)\n */\n async function resolvePortalUser(\n openId: string,\n session: SessionPayload\n ): Promise<TUser> {\n const portalUser = await getPortalUserCached(openId);\n\n if (!portalUser) {\n console.warn(\n `${tag} Portal user unavailable for ${openId} — keeping session (role/enterprise unresolved until Portal recovers)`\n );\n return {\n id: 0,\n openId,\n name: session.name ?? null,\n email: null,\n role: \"\",\n enterpriseId: null,\n portalUserId: null,\n } as unknown as TUser;\n }\n\n const enterpriseId =\n portalUser.enterpriseId ?? portalUser.companyId ?? null;\n return {\n id: portalUser.id,\n openId: portalUser.openId ?? openId,\n name: portalUser.name ?? session.name ?? null,\n email: portalUser.email ?? null,\n role: portalUser.role,\n enterpriseId,\n portalUserId: portalUser.id,\n } as unknown as TUser;\n }\n\n // ── OAuth ──────────────────────────────────────────────────────\n\n async function exchangeCodeForToken(\n code: string,\n state: string\n ): Promise<ExchangeTokenResponse> {\n const client = await getOAuthClient();\n const redirectUri = atob(state);\n const { data } = await client.post<ExchangeTokenResponse>(\n EXCHANGE_TOKEN_PATH,\n {\n clientId: env.appId,\n grantType: \"authorization_code\",\n code,\n redirectUri,\n }\n );\n return data;\n }\n\n async function getUserInfo(\n accessToken: string\n ): Promise<GetUserInfoResponse> {\n const client = await getOAuthClient();\n const { data } = await client.post<GetUserInfoResponse>(\n GET_USER_INFO_PATH,\n { accessToken }\n );\n const loginMethod = deriveLoginMethod(\n (data as any)?.platforms,\n (data as any)?.platform ?? data.platform ?? null\n );\n return { ...(data as any), platform: loginMethod, loginMethod };\n }\n\n async function getUserInfoWithJwt(\n jwtToken: string\n ): Promise<GetUserInfoWithJwtResponse> {\n const client = await getOAuthClient();\n const { data } = await client.post<GetUserInfoWithJwtResponse>(\n GET_USER_INFO_WITH_JWT_PATH,\n { jwtToken, projectId: env.appId }\n );\n const loginMethod = deriveLoginMethod(\n (data as any)?.platforms,\n (data as any)?.platform ?? data.platform ?? null\n );\n return { ...(data as any), platform: loginMethod, loginMethod };\n }\n\n // ── Authenticate Request ───────────────────────────────────────\n\n async function authenticateRequest(req: Request): Promise<TUser> {\n const cookie = await getCookie();\n const cookies = req.headers.cookie\n ? new Map(Object.entries(cookie.parse(req.headers.cookie)))\n : new Map<string, string>();\n\n const sessionCookie = cookies.get(cookieName);\n const session = await verifySession(sessionCookie);\n\n if (!session) {\n throw Object.assign(new Error(\"Invalid session cookie\"), {\n statusCode: 403,\n });\n }\n\n const sessionUserId = session.openId;\n const signedInAt = new Date();\n\n // ── Portal mode: single source of truth, no local mirror ────\n if (userSource === \"portal\") {\n return await resolvePortalUser(sessionUserId, session);\n }\n\n let user = (await db!.getUserByOpenId(sessionUserId)) ?? null;\n\n // ── Auto-create user from verified JWT ──────────────────────\n if (!user) {\n try {\n // Try portal API first for richer user info\n const portalUser = await getPortalUser(sessionUserId);\n\n if (portalUser) {\n console.log(\n `${tag} Cross-subdomain SSO: creating user from portal (${portalUser.email || sessionUserId})`\n );\n // Portal returns `enterpriseId`; `companyId` is a legacy alias.\n const portalEnterpriseId =\n portalUser.enterpriseId ?? portalUser.companyId ?? null;\n await db!.upsertUser({\n openId: sessionUserId,\n name: portalUser.name || session.name || null,\n email: portalUser.email ?? null,\n loginMethod: \"portal-sso\",\n role: mapRole(portalUser.role),\n lastSignedIn: signedInAt,\n ...(portalEnterpriseId != null\n ? {\n enterpriseId: portalEnterpriseId,\n portalUserId: portalUser.id,\n }\n : {}),\n });\n user = (await db!.getUserByOpenId(sessionUserId)) ?? null;\n } else {\n // Portal unavailable — create from JWT payload\n console.log(\n `${tag} Cross-subdomain SSO: creating user from JWT payload (${sessionUserId})`\n );\n await db!.upsertUser({\n openId: sessionUserId,\n name: session.name || null,\n loginMethod: \"portal-sso\",\n lastSignedIn: signedInAt,\n });\n user = (await db!.getUserByOpenId(sessionUserId)) ?? null;\n }\n } catch (error) {\n // Last resort: try Manus OAuth JWT\n try {\n const userInfo = await getUserInfoWithJwt(sessionCookie ?? \"\");\n await db!.upsertUser({\n openId: userInfo.openId,\n name: userInfo.name || null,\n email: userInfo.email ?? null,\n loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,\n lastSignedIn: signedInAt,\n });\n user = (await db!.getUserByOpenId(userInfo.openId)) ?? null;\n } catch (oauthError) {\n console.error(\n `${tag} All user sync methods failed:`,\n error,\n oauthError\n );\n throw Object.assign(new Error(\"Failed to sync user info\"), {\n statusCode: 403,\n });\n }\n }\n }\n\n if (!user) {\n throw Object.assign(new Error(\"User not found\"), { statusCode: 403 });\n }\n\n // ── Enterprise backfill ─────────────────────────────────────\n if (user.enterpriseId === null) {\n try {\n const portalUser = await getPortalUser(user.openId);\n // Portal returns `enterpriseId`; `companyId` is a legacy alias.\n const portalEnterpriseId =\n portalUser?.enterpriseId ?? portalUser?.companyId ?? null;\n if (portalEnterpriseId != null) {\n console.log(\n `${tag} Backfilling enterpriseId=${portalEnterpriseId} for user ${user.id}`\n );\n await db!.updateUserFields(user.id, {\n enterpriseId: portalEnterpriseId,\n ...(portalUser?.id ? { portalUserId: portalUser.id } : {}),\n });\n user = (await db!.getUserByOpenId(user.openId)) ?? user;\n }\n } catch (backfillError) {\n console.warn(\n `${tag} Failed to backfill enterpriseId for user ${user.id}:`,\n backfillError\n );\n // Non-fatal — user can still proceed\n }\n }\n\n // Touch lastSignedIn\n await db!.upsertUser({\n openId: user.openId,\n lastSignedIn: signedInAt,\n });\n\n return user;\n }\n\n // ── OAuth Route Registration ───────────────────────────────────\n\n function registerOAuthRoutes(app: import(\"express\").Express) {\n app.get(\n \"/api/oauth/callback\",\n async (req: Request, res: import(\"express\").Response) => {\n const code =\n typeof req.query.code === \"string\" ? req.query.code : undefined;\n const state =\n typeof req.query.state === \"string\" ? req.query.state : undefined;\n\n if (!code || !state) {\n res.status(400).json({ error: \"code and state are required\" });\n return;\n }\n\n try {\n const tokenResponse = await exchangeCodeForToken(code, state);\n const userInfo = await getUserInfo(tokenResponse.accessToken);\n\n if (!userInfo.openId) {\n res.status(400).json({ error: \"openId missing from user info\" });\n return;\n }\n\n await db!.upsertUser({\n openId: userInfo.openId,\n name: userInfo.name || null,\n email: userInfo.email ?? null,\n loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,\n lastSignedIn: new Date(),\n });\n\n const sessionToken = await createSessionToken(userInfo.openId, {\n name: userInfo.name || \"\",\n expiresInMs: sessionExpiryMs,\n });\n\n const cookieOptions = getSessionCookieOptions(req);\n res.cookie(cookieName, sessionToken, {\n ...cookieOptions,\n maxAge: sessionExpiryMs,\n });\n\n res.redirect(302, \"/\");\n } catch (error) {\n console.error(`${tag} OAuth callback failed`, error);\n res.status(500).json({ error: \"OAuth callback failed\" });\n }\n }\n );\n }\n\n // ── Return public API ──────────────────────────────────────────\n\n return {\n verifySession,\n createSessionToken,\n authenticateRequest,\n exchangeCodeForToken,\n getUserInfo,\n getUserInfoWithJwt,\n getPortalUser,\n getSessionCookieOptions,\n isValidReturnUrl,\n registerOAuthRoutes,\n };\n}\n"],"mappings":";AAgCA,IAAM,sBAAsB;AAC5B,IAAM,cAAc,MAAO,KAAK,KAAK,KAAK;AAC1C,IAAM,mBAAmB;AAEzB,IAAM,sBACJ;AACF,IAAM,qBACJ;AACF,IAAM,8BACJ;AAIF,IAAM,mBAAmB,CAAC,UACxB,OAAO,UAAU,YAAY,MAAM,SAAS;AAK9C,SAAS,kBACP,WACA,UACe;AACf,MAAI,YAAY,SAAS,SAAS,EAAG,QAAO;AAC5C,MAAI,CAAC,MAAM,QAAQ,SAAS,KAAK,UAAU,WAAW,EAAG,QAAO;AAChE,QAAM,MAAM,IAAI;AAAA,IACd,UAAU,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AAAA,EAC5D;AACA,MAAI,IAAI,IAAI,2BAA2B,EAAG,QAAO;AACjD,MAAI,IAAI,IAAI,4BAA4B,EAAG,QAAO;AAClD,MAAI,IAAI,IAAI,2BAA2B,EAAG,QAAO;AACjD,MACE,IAAI,IAAI,+BAA+B,KACvC,IAAI,IAAI,2BAA2B;AAEnC,WAAO;AACT,MAAI,IAAI,IAAI,4BAA4B,EAAG,QAAO;AAClD,QAAM,QAAQ,MAAM,KAAK,GAAG,EAAE,CAAC;AAC/B,SAAO,QAAQ,MAAM,YAAY,IAAI;AACvC;AAIO,SAAS,oBACd,QACsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA;AAAA;AAAA,IAGA,UAAU,CAAC,SAAiB;AAAA,IAC5B,aAAa;AAAA,IACb,aAAa;AAAA,IACb,kBAAkB;AAAA,IAClB,cAAc;AAAA,IACd,uBAAuB;AAAA,EACzB,IAAI;AAEJ,QAAM,MAAM,IAAI,WAAW;AAE3B,MAAI,eAAe,QAAQ,CAAC,IAAI;AAC9B,UAAM,IAAI;AAAA,MACR,GAAG,GAAG;AAAA,IACR;AAAA,EACF;AAIA,QAAM,kBAAkB,oBAAI,IAA8C;AAM1E,MAAI,QAAsC;AAC1C,iBAAe,UAAU;AACvB,QAAI,CAAC,MAAO,SAAQ,MAAM,OAAO,MAAM;AACvC,WAAO;AAAA,EACT;AAEA,MAAI,UAA0C;AAC9C,iBAAe,YAAY;AACzB,QAAI,CAAC,QAAS,WAAU,MAAM,OAAO,QAAQ;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAgD;AACpD,iBAAe,WAAW;AACxB,QAAI,CAAC,QAAQ;AACX,YAAM,MAAM,MAAM,OAAO,OAAO;AAChC,eAAS,IAAI;AAAA,IACf;AACA,WAAO;AAAA,EACT;AAIA,MAAI,cAAoD;AACxD,iBAAe,iBAAiB;AAC9B,QAAI,YAAa,QAAO;AACxB,UAAM,QAAQ,MAAM,SAAS;AAC7B,kBAAc,MAAM,OAAO;AAAA,MACzB,SAAS,IAAI;AAAA,MACb,SAAS;AAAA,IACX,CAAC;AACD,WAAO;AAAA,EACT;AAIA,MAAI,eAAqD;AACzD,WAAS,kBAAkB;AACzB,QAAI,aAAc,QAAO;AAEzB,QAAI,CAAC,IAAI,aAAc,QAAO;AAG9B,WAAO;AAAA,EACT;AAEA,iBAAe,uBAAuB;AACpC,QAAI,aAAc,QAAO;AACzB,QAAI,CAAC,IAAI,aAAc,QAAO;AAC9B,UAAM,QAAQ,MAAM,SAAS;AAC7B,mBAAe,MAAM,OAAO;AAAA,MAC1B,SAAS,IAAI;AAAA,MACb,SAAS;AAAA,MACT,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAI,IAAI,eAAe,EAAE,iBAAiB,IAAI,aAAa,IAAI,CAAC;AAAA,MAClE;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAIA,WAAS,kBAAkB;AACzB,UAAM,SAAS,IAAI;AACnB,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qCAAqC;AAClE,WAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,EACxC;AAEA,WAAS,mBAAmB;AAE1B,UAAM,SAAS,IAAI,mBAAmB,IAAI;AAC1C,WAAO,IAAI,YAAY,EAAE,OAAO,MAAM;AAAA,EACxC;AAIA,WAAS,kBAAsC;AAC7C,WAAO,IAAI,gBAAgB;AAAA,EAC7B;AAEA,WAAS,gBAAgB,KAAuB;AAC9C,QAAI,IAAI,aAAa,QAAS,QAAO;AACrC,UAAM,iBAAiB,IAAI,QAAQ,mBAAmB;AACtD,QAAI,CAAC,eAAgB,QAAO;AAC5B,UAAM,YAAY,MAAM,QAAQ,cAAc,IAC1C,iBACA,eAAe,MAAM,GAAG;AAC5B,WAAO,UAAU,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,MAAM,OAAO;AAAA,EACjE;AAEA,WAAS,wBACP,KAIA;AACA,UAAM,SAAS,gBAAgB,GAAG;AAClC,UAAM,SAAS,gBAAgB;AAC/B,WAAO;AAAA,MACL;AAAA,MACA,UAAU;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEA,WAAS,iBAAiB,KAAsB;AAC9C,QAAI,IAAI,WAAW,GAAG,KAAK,CAAC,IAAI,WAAW,IAAI,EAAG,QAAO;AACzD,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,OAAO,aAAa,SAAU,QAAO;AACzC,aACE,OAAO,aAAa,sBACpB,OAAO,SAAS,SAAS,mBAAmB;AAAA,IAEhD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAIA,iBAAe,YACb,SACA,UAAoC,CAAC,GACpB;AACjB,UAAM,OAAO,MAAM,QAAQ;AAC3B,UAAM,WAAW,KAAK,IAAI;AAC1B,UAAM,cAAc,QAAQ,eAAe;AAC3C,UAAM,oBAAoB,KAAK,OAAO,WAAW,eAAe,GAAI;AACpE,UAAM,YAAY,iBAAiB;AAEnC,WAAO,IAAI,KAAK,QAAQ;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ;AAAA,IAChB,CAAC,EACE,mBAAmB,EAAE,KAAK,SAAS,KAAK,MAAM,CAAC,EAC/C,kBAAkB,iBAAiB,EACnC,KAAK,SAAS;AAAA,EACnB;AAEA,iBAAe,mBACb,QACA,UAAmD,CAAC,GACnC;AACjB,WAAO;AAAA,MACL,EAAE,QAAQ,OAAO,IAAI,OAAO,MAAM,QAAQ,QAAQ,GAAG;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAIA,iBAAe,cACb,aACgC;AAChC,QAAI,CAAC,aAAa;AAChB,cAAQ,KAAK,GAAG,GAAG,yBAAyB;AAC5C,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,QAAQ;AAG3B,QAAI,IAAI,iBAAiB;AACvB,UAAI;AACF,cAAM,YAAY,gBAAgB;AAClC,cAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,UAAU,aAAa,WAAW;AAAA,UAC/D,YAAY,CAAC,OAAO;AAAA,QACtB,CAAC;AACD,cAAM,EAAE,QAAQ,OAAO,KAAK,IAAI;AAChC,YACE,CAAC,iBAAiB,MAAM,KACxB,CAAC,iBAAiB,KAAK,KACvB,CAAC,iBAAiB,IAAI,GACtB;AACA,kBAAQ,KAAK,GAAG,GAAG,6CAA6C;AAChE,iBAAO;AAAA,QACT;AACA,eAAO,EAAE,QAAQ,OAAO,KAAK;AAAA,MAC/B,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,QAAI;AACF,YAAM,YAAY,iBAAiB;AACnC,YAAM,EAAE,QAAQ,IAAI,MAAM,KAAK,UAAU,aAAa,WAAW;AAAA,QAC/D,YAAY,CAAC,OAAO;AAAA,MACtB,CAAC;AACD,YAAM,EAAE,QAAQ,OAAO,KAAK,IAAI;AAChC,UACE,CAAC,iBAAiB,MAAM,KACxB,CAAC,iBAAiB,KAAK,KACvB,CAAC,iBAAiB,IAAI,GACtB;AACA,gBAAQ,KAAK,GAAG,GAAG,0CAA0C;AAC7D,eAAO;AAAA,MACT;AACA,aAAO,EAAE,QAAQ,OAAO,KAAK;AAAA,IAC/B,SAAS,OAAO;AACd,cAAQ;AAAA,QACN,GAAG,GAAG;AAAA,QACL,MAAgB;AAAA,MACnB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAIA,iBAAe,cAAc,QAA4C;AACvE,QAAI;AACF,YAAM,SAAS,MAAM,qBAAqB;AAC1C,UAAI,CAAC,OAAQ,QAAO;AAIpB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,QAC5B,gCAAgC,mBAAmB,MAAM,CAAC;AAAA,MAC5D;AACA,aAAO,QAAQ;AAAA,IACjB,SAAS,KAAc;AACrB,YAAM,SAAU,KAAa,UAAU;AACvC,UAAI,WAAW,KAAK;AAClB,gBAAQ;AAAA,UACN,GAAG,GAAG;AAAA,QACR;AAAA,MACF,WAAW,WAAW,KAAK;AAAA,MAE3B,OAAO;AACL,gBAAQ;AAAA,UACN,GAAG,GAAG;AAAA,UACL,IAAc;AAAA,QACjB;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAGA,iBAAe,oBACb,QAC4B;AAC5B,QAAI,uBAAuB,GAAG;AAC5B,YAAM,MAAM,gBAAgB,IAAI,MAAM;AACtC,UAAI,OAAO,KAAK,IAAI,IAAI,IAAI,KAAK,sBAAsB;AACrD,eAAO,IAAI;AAAA,MACb;AAAA,IACF;AACA,UAAM,OAAO,MAAM,cAAc,MAAM;AACvC,QAAI,QAAQ,uBAAuB,GAAG;AACpC,sBAAgB,IAAI,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,IACtD;AACA,WAAO;AAAA,EACT;AAgBA,iBAAe,kBACb,QACA,SACgB;AAChB,UAAM,aAAa,MAAM,oBAAoB,MAAM;AAEnD,QAAI,CAAC,YAAY;AACf,cAAQ;AAAA,QACN,GAAG,GAAG,gCAAgC,MAAM;AAAA,MAC9C;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ;AAAA,QACA,MAAM,QAAQ,QAAQ;AAAA,QACtB,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc;AAAA,QACd,cAAc;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,eACJ,WAAW,gBAAgB,WAAW,aAAa;AACrD,WAAO;AAAA,MACL,IAAI,WAAW;AAAA,MACf,QAAQ,WAAW,UAAU;AAAA,MAC7B,MAAM,WAAW,QAAQ,QAAQ,QAAQ;AAAA,MACzC,OAAO,WAAW,SAAS;AAAA,MAC3B,MAAM,WAAW;AAAA,MACjB;AAAA,MACA,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF;AAIA,iBAAe,qBACb,MACA,OACgC;AAChC,UAAM,SAAS,MAAM,eAAe;AACpC,UAAM,cAAc,KAAK,KAAK;AAC9B,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,MAC5B;AAAA,MACA;AAAA,QACE,UAAU,IAAI;AAAA,QACd,WAAW;AAAA,QACX;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,iBAAe,YACb,aAC8B;AAC9B,UAAM,SAAS,MAAM,eAAe;AACpC,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,MAC5B;AAAA,MACA,EAAE,YAAY;AAAA,IAChB;AACA,UAAM,cAAc;AAAA,MACjB,MAAc;AAAA,MACd,MAAc,YAAY,KAAK,YAAY;AAAA,IAC9C;AACA,WAAO,EAAE,GAAI,MAAc,UAAU,aAAa,YAAY;AAAA,EAChE;AAEA,iBAAe,mBACb,UACqC;AACrC,UAAM,SAAS,MAAM,eAAe;AACpC,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,MAC5B;AAAA,MACA,EAAE,UAAU,WAAW,IAAI,MAAM;AAAA,IACnC;AACA,UAAM,cAAc;AAAA,MACjB,MAAc;AAAA,MACd,MAAc,YAAY,KAAK,YAAY;AAAA,IAC9C;AACA,WAAO,EAAE,GAAI,MAAc,UAAU,aAAa,YAAY;AAAA,EAChE;AAIA,iBAAe,oBAAoB,KAA8B;AAC/D,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,UAAU,IAAI,QAAQ,SACxB,IAAI,IAAI,OAAO,QAAQ,OAAO,MAAM,IAAI,QAAQ,MAAM,CAAC,CAAC,IACxD,oBAAI,IAAoB;AAE5B,UAAM,gBAAgB,QAAQ,IAAI,UAAU;AAC5C,UAAM,UAAU,MAAM,cAAc,aAAa;AAEjD,QAAI,CAAC,SAAS;AACZ,YAAM,OAAO,OAAO,IAAI,MAAM,wBAAwB,GAAG;AAAA,QACvD,YAAY;AAAA,MACd,CAAC;AAAA,IACH;AAEA,UAAM,gBAAgB,QAAQ;AAC9B,UAAM,aAAa,oBAAI,KAAK;AAG5B,QAAI,eAAe,UAAU;AAC3B,aAAO,MAAM,kBAAkB,eAAe,OAAO;AAAA,IACvD;AAEA,QAAI,OAAQ,MAAM,GAAI,gBAAgB,aAAa,KAAM;AAGzD,QAAI,CAAC,MAAM;AACT,UAAI;AAEF,cAAM,aAAa,MAAM,cAAc,aAAa;AAEpD,YAAI,YAAY;AACd,kBAAQ;AAAA,YACN,GAAG,GAAG,oDAAoD,WAAW,SAAS,aAAa;AAAA,UAC7F;AAEA,gBAAM,qBACJ,WAAW,gBAAgB,WAAW,aAAa;AACrD,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ;AAAA,YACR,MAAM,WAAW,QAAQ,QAAQ,QAAQ;AAAA,YACzC,OAAO,WAAW,SAAS;AAAA,YAC3B,aAAa;AAAA,YACb,MAAM,QAAQ,WAAW,IAAI;AAAA,YAC7B,cAAc;AAAA,YACd,GAAI,sBAAsB,OACtB;AAAA,cACE,cAAc;AAAA,cACd,cAAc,WAAW;AAAA,YAC3B,IACA,CAAC;AAAA,UACP,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,aAAa,KAAM;AAAA,QACvD,OAAO;AAEL,kBAAQ;AAAA,YACN,GAAG,GAAG,yDAAyD,aAAa;AAAA,UAC9E;AACA,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ;AAAA,YACR,MAAM,QAAQ,QAAQ;AAAA,YACtB,aAAa;AAAA,YACb,cAAc;AAAA,UAChB,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,aAAa,KAAM;AAAA,QACvD;AAAA,MACF,SAAS,OAAO;AAEd,YAAI;AACF,gBAAM,WAAW,MAAM,mBAAmB,iBAAiB,EAAE;AAC7D,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ,SAAS;AAAA,YACjB,MAAM,SAAS,QAAQ;AAAA,YACvB,OAAO,SAAS,SAAS;AAAA,YACzB,aAAa,SAAS,eAAe,SAAS,YAAY;AAAA,YAC1D,cAAc;AAAA,UAChB,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,SAAS,MAAM,KAAM;AAAA,QACzD,SAAS,YAAY;AACnB,kBAAQ;AAAA,YACN,GAAG,GAAG;AAAA,YACN;AAAA,YACA;AAAA,UACF;AACA,gBAAM,OAAO,OAAO,IAAI,MAAM,0BAA0B,GAAG;AAAA,YACzD,YAAY;AAAA,UACd,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,MAAM;AACT,YAAM,OAAO,OAAO,IAAI,MAAM,gBAAgB,GAAG,EAAE,YAAY,IAAI,CAAC;AAAA,IACtE;AAGA,QAAI,KAAK,iBAAiB,MAAM;AAC9B,UAAI;AACF,cAAM,aAAa,MAAM,cAAc,KAAK,MAAM;AAElD,cAAM,qBACJ,YAAY,gBAAgB,YAAY,aAAa;AACvD,YAAI,sBAAsB,MAAM;AAC9B,kBAAQ;AAAA,YACN,GAAG,GAAG,6BAA6B,kBAAkB,aAAa,KAAK,EAAE;AAAA,UAC3E;AACA,gBAAM,GAAI,iBAAiB,KAAK,IAAI;AAAA,YAClC,cAAc;AAAA,YACd,GAAI,YAAY,KAAK,EAAE,cAAc,WAAW,GAAG,IAAI,CAAC;AAAA,UAC1D,CAAC;AACD,iBAAQ,MAAM,GAAI,gBAAgB,KAAK,MAAM,KAAM;AAAA,QACrD;AAAA,MACF,SAAS,eAAe;AACtB,gBAAQ;AAAA,UACN,GAAG,GAAG,6CAA6C,KAAK,EAAE;AAAA,UAC1D;AAAA,QACF;AAAA,MAEF;AAAA,IACF;AAGA,UAAM,GAAI,WAAW;AAAA,MACnB,QAAQ,KAAK;AAAA,MACb,cAAc;AAAA,IAChB,CAAC;AAED,WAAO;AAAA,EACT;AAIA,WAAS,oBAAoB,KAAgC;AAC3D,QAAI;AAAA,MACF;AAAA,MACA,OAAO,KAAc,QAAoC;AACvD,cAAM,OACJ,OAAO,IAAI,MAAM,SAAS,WAAW,IAAI,MAAM,OAAO;AACxD,cAAM,QACJ,OAAO,IAAI,MAAM,UAAU,WAAW,IAAI,MAAM,QAAQ;AAE1D,YAAI,CAAC,QAAQ,CAAC,OAAO;AACnB,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,8BAA8B,CAAC;AAC7D;AAAA,QACF;AAEA,YAAI;AACF,gBAAM,gBAAgB,MAAM,qBAAqB,MAAM,KAAK;AAC5D,gBAAM,WAAW,MAAM,YAAY,cAAc,WAAW;AAE5D,cAAI,CAAC,SAAS,QAAQ;AACpB,gBAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,gCAAgC,CAAC;AAC/D;AAAA,UACF;AAEA,gBAAM,GAAI,WAAW;AAAA,YACnB,QAAQ,SAAS;AAAA,YACjB,MAAM,SAAS,QAAQ;AAAA,YACvB,OAAO,SAAS,SAAS;AAAA,YACzB,aAAa,SAAS,eAAe,SAAS,YAAY;AAAA,YAC1D,cAAc,oBAAI,KAAK;AAAA,UACzB,CAAC;AAED,gBAAM,eAAe,MAAM,mBAAmB,SAAS,QAAQ;AAAA,YAC7D,MAAM,SAAS,QAAQ;AAAA,YACvB,aAAa;AAAA,UACf,CAAC;AAED,gBAAM,gBAAgB,wBAAwB,GAAG;AACjD,cAAI,OAAO,YAAY,cAAc;AAAA,YACnC,GAAG;AAAA,YACH,QAAQ;AAAA,UACV,CAAC;AAED,cAAI,SAAS,KAAK,GAAG;AAAA,QACvB,SAAS,OAAO;AACd,kBAAQ,MAAM,GAAG,GAAG,0BAA0B,KAAK;AACnD,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,wBAAwB,CAAC;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAIA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|