@pablo2410/core-server 0.4.1 → 0.4.3
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 +8 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-BSM4MWQQ.js → chunk-23I5TW53.js} +9 -7
- package/dist/{chunk-BSM4MWQQ.js.map → chunk-23I5TW53.js.map} +1 -1
- package/dist/{chunk-5Y5LLC4Z.js → chunk-OAF3JSO7.js} +23 -6
- package/dist/chunk-OAF3JSO7.js.map +1 -0
- package/dist/hierarchy.d.ts +2 -0
- package/dist/hierarchy.js +1 -1
- package/dist/index.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-5Y5LLC4Z.js.map +0 -1
package/dist/auth.d.ts
CHANGED
|
@@ -7,13 +7,20 @@ interface SessionPayload {
|
|
|
7
7
|
appId: string;
|
|
8
8
|
name: string;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Minimal portal user shape returned by the portal API
|
|
12
|
+
* (GET /users/by-openid/:openId). The portal exposes the enterprise as
|
|
13
|
+
* `enterpriseId`; the legacy `companyId` alias is kept optional only for
|
|
14
|
+
* backwards-compatibility with older portal builds.
|
|
15
|
+
*/
|
|
11
16
|
interface PortalUser {
|
|
12
17
|
id: number;
|
|
13
18
|
openId: string;
|
|
14
19
|
name: string | null;
|
|
15
20
|
email: string | null;
|
|
16
21
|
role: string;
|
|
22
|
+
enterpriseId?: number | null;
|
|
23
|
+
/** @deprecated legacy alias for enterpriseId — older portal builds only */
|
|
17
24
|
companyId?: number;
|
|
18
25
|
siteId?: number;
|
|
19
26
|
}
|
package/dist/auth.js
CHANGED
|
@@ -259,6 +259,7 @@ function createSubdomainAuth(config) {
|
|
|
259
259
|
console.log(
|
|
260
260
|
`${tag} Cross-subdomain SSO: creating user from portal (${portalUser.email || sessionUserId})`
|
|
261
261
|
);
|
|
262
|
+
const portalEnterpriseId = portalUser.enterpriseId ?? portalUser.companyId ?? null;
|
|
262
263
|
await db.upsertUser({
|
|
263
264
|
openId: sessionUserId,
|
|
264
265
|
name: portalUser.name || session.name || null,
|
|
@@ -266,8 +267,8 @@ function createSubdomainAuth(config) {
|
|
|
266
267
|
loginMethod: "portal-sso",
|
|
267
268
|
role: mapRole(portalUser.role),
|
|
268
269
|
lastSignedIn: signedInAt,
|
|
269
|
-
...
|
|
270
|
-
enterpriseId:
|
|
270
|
+
...portalEnterpriseId != null ? {
|
|
271
|
+
enterpriseId: portalEnterpriseId,
|
|
271
272
|
portalUserId: portalUser.id
|
|
272
273
|
} : {}
|
|
273
274
|
});
|
|
@@ -313,13 +314,14 @@ function createSubdomainAuth(config) {
|
|
|
313
314
|
if (user.enterpriseId === null) {
|
|
314
315
|
try {
|
|
315
316
|
const portalUser = await getPortalUser(user.openId);
|
|
316
|
-
|
|
317
|
+
const portalEnterpriseId = portalUser?.enterpriseId ?? portalUser?.companyId ?? null;
|
|
318
|
+
if (portalEnterpriseId != null) {
|
|
317
319
|
console.log(
|
|
318
|
-
`${tag} Backfilling enterpriseId=${
|
|
320
|
+
`${tag} Backfilling enterpriseId=${portalEnterpriseId} for user ${user.id}`
|
|
319
321
|
);
|
|
320
322
|
await db.updateUserFields(user.id, {
|
|
321
|
-
enterpriseId:
|
|
322
|
-
...portalUser
|
|
323
|
+
enterpriseId: portalEnterpriseId,
|
|
324
|
+
...portalUser?.id ? { portalUserId: portalUser.id } : {}
|
|
323
325
|
});
|
|
324
326
|
user = await db.getUserByOpenId(user.openId) ?? user;
|
|
325
327
|
}
|
|
@@ -394,4 +396,4 @@ function createSubdomainAuth(config) {
|
|
|
394
396
|
export {
|
|
395
397
|
createSubdomainAuth
|
|
396
398
|
};
|
|
397
|
-
//# sourceMappingURL=chunk-
|
|
399
|
+
//# sourceMappingURL=chunk-23I5TW53.js.map
|
|
@@ -1 +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 mapRole,\n cookieName = DEFAULT_COOKIE_NAME,\n sessionExpiryMs = ONE_YEAR_MS,\n serviceName = \"Subdomain\",\n } = config;\n\n const tag = `[${serviceName} Auth]`;\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 const { data } = await client.get<PortalUser>(\n `/users/by-openid/${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 // ── 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 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 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 ...(portalUser.companyId\n ? {\n enterpriseId: portalUser.companyId,\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 if (portalUser?.companyId) {\n console.log(\n `${tag} Backfilling enterpriseId=${portalUser.companyId} for user ${user.id}`\n );\n await db.updateUserFields(user.id, {\n enterpriseId: portalUser.companyId,\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,IACA;AAAA,IACA,aAAa;AAAA,IACb,kBAAkB;AAAA,IAClB,cAAc;AAAA,EAChB,IAAI;AAEJ,QAAM,MAAM,IAAI,WAAW;AAM3B,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;AACpB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO;AAAA,QAC5B,oBAAoB,MAAM;AAAA,MAC5B;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;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;AAC5B,QAAI,OAAQ,MAAM,GAAG,gBAAgB,aAAa,KAAM;AAGxD,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;AACA,gBAAM,GAAG,WAAW;AAAA,YAClB,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,WAAW,YACX;AAAA,cACE,cAAc,WAAW;AAAA,cACzB,cAAc,WAAW;AAAA,YAC3B,IACA,CAAC;AAAA,UACP,CAAC;AACD,iBAAQ,MAAM,GAAG,gBAAgB,aAAa,KAAM;AAAA,QACtD,OAAO;AAEL,kBAAQ;AAAA,YACN,GAAG,GAAG,yDAAyD,aAAa;AAAA,UAC9E;AACA,gBAAM,GAAG,WAAW;AAAA,YAClB,QAAQ;AAAA,YACR,MAAM,QAAQ,QAAQ;AAAA,YACtB,aAAa;AAAA,YACb,cAAc;AAAA,UAChB,CAAC;AACD,iBAAQ,MAAM,GAAG,gBAAgB,aAAa,KAAM;AAAA,QACtD;AAAA,MACF,SAAS,OAAO;AAEd,YAAI;AACF,gBAAM,WAAW,MAAM,mBAAmB,iBAAiB,EAAE;AAC7D,gBAAM,GAAG,WAAW;AAAA,YAClB,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,GAAG,gBAAgB,SAAS,MAAM,KAAM;AAAA,QACxD,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;AAClD,YAAI,YAAY,WAAW;AACzB,kBAAQ;AAAA,YACN,GAAG,GAAG,6BAA6B,WAAW,SAAS,aAAa,KAAK,EAAE;AAAA,UAC7E;AACA,gBAAM,GAAG,iBAAiB,KAAK,IAAI;AAAA,YACjC,cAAc,WAAW;AAAA,YACzB,GAAI,WAAW,KAAK,EAAE,cAAc,WAAW,GAAG,IAAI,CAAC;AAAA,UACzD,CAAC;AACD,iBAAQ,MAAM,GAAG,gBAAgB,KAAK,MAAM,KAAM;AAAA,QACpD;AAAA,MACF,SAAS,eAAe;AACtB,gBAAQ;AAAA,UACN,GAAG,GAAG,6CAA6C,KAAK,EAAE;AAAA,UAC1D;AAAA,QACF;AAAA,MAEF;AAAA,IACF;AAGA,UAAM,GAAG,WAAW;AAAA,MAClB,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,GAAG,WAAW;AAAA,YAClB,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":[]}
|
|
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 mapRole,\n cookieName = DEFAULT_COOKIE_NAME,\n sessionExpiryMs = ONE_YEAR_MS,\n serviceName = \"Subdomain\",\n } = config;\n\n const tag = `[${serviceName} Auth]`;\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 const { data } = await client.get<PortalUser>(\n `/users/by-openid/${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 // ── 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 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":"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","names":[]}
|
|
@@ -11,8 +11,12 @@ function createHierarchyService(config) {
|
|
|
11
11
|
} = config;
|
|
12
12
|
const configured = Boolean(portalApiUrl && portalApiKey);
|
|
13
13
|
let cache = null;
|
|
14
|
+
let lastError = null;
|
|
14
15
|
async function fetchRaw() {
|
|
15
|
-
if (!configured)
|
|
16
|
+
if (!configured) {
|
|
17
|
+
lastError = "not configured (PORTAL_API_URL/PORTAL_API_KEY missing)";
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
16
20
|
const url = `${portalApiUrl.replace(/\/$/, "")}/api/service/hierarchy`;
|
|
17
21
|
let res;
|
|
18
22
|
try {
|
|
@@ -22,12 +26,24 @@ function createHierarchyService(config) {
|
|
|
22
26
|
"Content-Type": "application/json"
|
|
23
27
|
}
|
|
24
28
|
});
|
|
25
|
-
} catch {
|
|
29
|
+
} catch (err) {
|
|
30
|
+
lastError = `fetch threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
31
|
+
console.error(`[HierarchyService] ${lastError} (url=${url})`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
lastError = `HTTP ${res.status} ${res.statusText} from ${url}`;
|
|
36
|
+
console.error(`[HierarchyService] ${lastError}`);
|
|
26
37
|
return null;
|
|
27
38
|
}
|
|
28
|
-
if (!res.ok) return null;
|
|
29
39
|
const json = await res.json().catch(() => null);
|
|
30
|
-
|
|
40
|
+
if (!json) {
|
|
41
|
+
lastError = `invalid/empty JSON body from ${url}`;
|
|
42
|
+
console.error(`[HierarchyService] ${lastError}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
lastError = null;
|
|
46
|
+
return normalize(json);
|
|
31
47
|
}
|
|
32
48
|
async function getFullHierarchy() {
|
|
33
49
|
if (cacheTtlMs > 0 && cache && now() - cache.at < cacheTtlMs) {
|
|
@@ -48,7 +64,8 @@ function createHierarchyService(config) {
|
|
|
48
64
|
return {
|
|
49
65
|
configured,
|
|
50
66
|
cached: cache !== null,
|
|
51
|
-
ageMs: cache ? now() - cache.at : null
|
|
67
|
+
ageMs: cache ? now() - cache.at : null,
|
|
68
|
+
lastError
|
|
52
69
|
};
|
|
53
70
|
}
|
|
54
71
|
return { getScopedHierarchy, getFullHierarchy, invalidate, getStatus };
|
|
@@ -95,4 +112,4 @@ function normalize(data) {
|
|
|
95
112
|
export {
|
|
96
113
|
createHierarchyService
|
|
97
114
|
};
|
|
98
|
-
//# sourceMappingURL=chunk-
|
|
115
|
+
//# sourceMappingURL=chunk-OAF3JSO7.js.map
|
|
@@ -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.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":[]}
|
package/dist/hierarchy.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ interface HierarchyService {
|
|
|
54
54
|
configured: boolean;
|
|
55
55
|
cached: boolean;
|
|
56
56
|
ageMs: number | null;
|
|
57
|
+
/** Reason the last fetch failed (null if the last fetch succeeded). */
|
|
58
|
+
lastError: string | null;
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
declare function createHierarchyService(config: HierarchyServiceConfig): HierarchyService;
|
package/dist/hierarchy.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSubdomainAuth
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-23I5TW53.js";
|
|
4
4
|
import {
|
|
5
5
|
createHierarchyService
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-OAF3JSO7.js";
|
|
7
7
|
|
|
8
8
|
// src/constants/sqdcp.ts
|
|
9
9
|
var SQDCP_PILLARS = ["safety", "quality", "delivery", "cost", "people"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablo2410/core-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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.
|
|
37
|
+
"@pablo2410/shared-ui": "^0.9.0",
|
|
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(): { configured: boolean; cached: boolean; ageMs: number | null };\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\n async function fetchRaw(): Promise<OrgHierarchyData | null> {\n if (!configured) return null;\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 {\n return null;\n }\n if (!res.ok) return null;\n const json = await res.json().catch(() => null);\n return json ? normalize(json) : null;\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 };\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":";AAqBA,SAAS,kCAAkC;AAmC3C,IAAM,kBAAkB,KAAK,KAAK;AAE3B,SAAS,uBACd,QACkB;AAClB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,MAAM,KAAK;AAAA,EACb,IAAI;AAEJ,QAAM,aAAa,QAAQ,gBAAgB,YAAY;AACvD,MAAI,QAAuD;AAE3D,iBAAe,WAA6C;AAC1D,QAAI,CAAC,WAAY,QAAO;AACxB,UAAM,MAAM,GAAG,aAAa,QAAQ,OAAO,EAAE,CAAC;AAC9C,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,UAAU,KAAK;AAAA,QACzB,SAAS;AAAA,UACP,iBAAiB;AAAA,UACjB,gBAAgB;AAAA,QAClB;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAC9C,WAAO,OAAO,UAAU,IAAI,IAAI;AAAA,EAClC;AAEA,iBAAe,mBAAqD;AAClE,QAAI,aAAa,KAAK,SAAS,IAAI,IAAI,MAAM,KAAK,YAAY;AAC5D,aAAO,MAAM;AAAA,IACf;AACA,UAAM,OAAO,MAAM,SAAS;AAC5B,QAAI,QAAQ,aAAa,EAAG,SAAQ,EAAE,MAAM,IAAI,IAAI,EAAE;AACtD,WAAO;AAAA,EACT;AAEA,iBAAe,mBACb,MAC2B;AAC3B,UAAM,OAAO,MAAM,iBAAiB;AACpC,WAAO,2BAA2B,MAAM,MAAM,gBAAgB,IAAI;AAAA,EACpE;AAEA,WAAS,aAAmB;AAC1B,YAAQ;AAAA,EACV;AAEA,WAAS,YAAY;AACnB,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,UAAU;AAAA,MAClB,OAAO,QAAQ,IAAI,IAAI,MAAM,KAAK;AAAA,IACpC;AAAA,EACF;AAEA,SAAO,EAAE,oBAAoB,kBAAkB,YAAY,UAAU;AACvE;AAMA,SAAS,UAAU,MAAiC;AAClD,QAAM,IAAK,QAAQ,CAAC;AACpB,SAAO;AAAA,IACL,cAAc,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MAC7C,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,IACV,EAAE;AAAA,IACF,gBAAgB,EAAE,iBAAiB,CAAC,GAAG,IAAI,CAAC,QAAQ;AAAA,MAClD,IAAI,GAAG;AAAA,MACP,MAAM,GAAG;AAAA,MACT,MAAM,GAAG;AAAA,MACT,cAAc,GAAG;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MACjC,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,cAAc,EAAE;AAAA,MAChB,gBAAgB,EAAE,kBAAkB;AAAA,MACpC,UAAU,EAAE;AAAA,IACd,EAAE;AAAA,IACF,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MACjC,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,QAAQ,EAAE;AAAA,IACZ,EAAE;AAAA,IACF,SAAS,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MACnC,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,QAAQ,EAAE;AAAA,MACV,QAAQ,EAAE,UAAU;AAAA,MACpB,WAAW,EAAE;AAAA,IACf,EAAE;AAAA,EACJ;AACF;","names":[]}
|