UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

295 lines (294 loc) • 14.1 kB
import { m as readNonBlankString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { i as getOrCreatePromise, n as createLazyPromise } from "./lazy-promise-DGqyc4Y4.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs"; import { n as runExec } from "./exec-CMAKGVCq.mjs"; import { t as normalizeGitHubLogin } from "./github-login-COsXH2kL.mjs"; import { t as classifyTailscaleLogin } from "./user-profiles-tailscale-login-BtASdRBZ.mjs"; import { m as resolveCanonicalCachedGitHubIdentity } from "./user-profiles-B3UR6UN9.mjs"; import { t as firstHeaderValue } from "./http-header-value-BM79-5V8.mjs"; import { l as syncCanonicalGitHubIdentity, n as ensureCanonicalUserProfileForEmail } from "./user-profile-writes-BHwuU9Y3.mjs"; import { n as gitHubPublicApi, r as githubApiToken } from "./github-public-api-COOMiXSo.mjs"; import fs from "node:fs/promises"; import os from "node:os"; //#region src/infra/host-account-name.ts let cachedName; async function readAccountCommand(command, args) { try { const { stdout } = await runExec(command, args, { timeoutMs: 1e3, maxBuffer: 16384, logOutput: false }); return stdout; } catch { return null; } } async function readHostAccountName() { if (process.platform !== "darwin" && process.platform !== "linux") return null; const { username } = os.userInfo(); const normalizeName = (value) => { const name = value?.trim(); return name && name.toLowerCase() !== username.toLowerCase() ? truncateUtf16Safe(name, 256) : null; }; if (process.platform === "darwin") { const fullName = normalizeName(await readAccountCommand("/usr/bin/id", ["-F"])); if (fullName) return fullName; return normalizeName((await readAccountCommand("/usr/bin/dscl", [ ".", "-read", `/Users/${username}`, "RealName" ]))?.replace(/^RealName:\s*/u, "")); } const fields = (await readAccountCommand("getent", ["passwd", username]) ?? await fs.readFile("/etc/passwd", "utf8")).split("\n").find((line) => line.split(":", 1)[0] === username)?.split(":"); return normalizeName(fields?.[4]?.split(",", 1)[0]); } /** Best-effort human name for the gateway host account; never seeds a login name. */ function resolveHostAccountName() { return cachedName ??= readHostAccountName().catch(() => null); } //#endregion //#region src/gateway/gateway-owner-profile.ts /** Owner attribution never supplies a verified login or changes operator authority. */ function shouldUseGatewayOwnerProfile(params) { return params.role === "operator" && !params.authenticatedUserId && (!params.rolesConfigured || params.authMethod === "token" || params.authMethod === "password"); } //#endregion //#region src/gateway/github-user-identity.ts const CLOUDFLARE_ACCESS_USER_HEADER = "cf-access-authenticated-user-email"; const CLOUDFLARE_ACCESS_ASSERTION_HEADER = "cf-access-jwt-assertion"; const CLOUDFLARE_ACCESS_HOST_SUFFIX = ".cloudflareaccess.com"; const CLOUDFLARE_ACCESS_IDENTITY_PATH = "/cdn-cgi/access/get-identity"; const ACCESS_ASSERTION_MAX_BYTES = 16384; const ACCESS_IDENTITY_MAX_BYTES = 65536; const JWT_SEGMENT_PATTERN = /^[A-Za-z0-9_-]+$/u; const GITHUB_IDENTITY_CACHE_MS = 9e5; const GITHUB_IDENTITY_CACHE_LIMIT = 200; const GITHUB_ETAG_MAX_LENGTH = 1024; const identityMetadataCaches = /* @__PURE__ */ new WeakMap(); function cloudflareAccessIssuer(assertion) { if (Buffer.byteLength(assertion, "utf8") > ACCESS_ASSERTION_MAX_BYTES) throw new Error("Cloudflare Access assertion is invalid"); const segments = assertion.split("."); if (segments.length !== 3 || segments.some((segment) => !JWT_SEGMENT_PATTERN.test(segment))) throw new Error("Cloudflare Access assertion is invalid"); let payload; try { payload = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8")); } catch { throw new Error("Cloudflare Access assertion is invalid"); } if (!isRecord(payload) || typeof payload.iss !== "string") throw new Error("Cloudflare Access assertion issuer is invalid"); const issuer = URL.parse(payload.iss); if (!issuer || issuer.protocol !== "https:" || issuer.username || issuer.password || issuer.port || issuer.pathname !== "/" || issuer.search || issuer.hash || !issuer.hostname.endsWith(CLOUDFLARE_ACCESS_HOST_SUFFIX)) throw new Error("Cloudflare Access assertion issuer is invalid"); return issuer; } async function resolveCloudflareAccessIdentity(assertion, authenticatedPrincipal, oidcConfig) { const issuer = cloudflareAccessIssuer(assertion); let payload; try { const response = await fetch(`${issuer.origin}${CLOUDFLARE_ACCESS_IDENTITY_PATH}`, { headers: { Cookie: `CF_Authorization=${assertion}` }, redirect: "manual", signal: AbortSignal.timeout(gitHubPublicApi.GITHUB_REQUEST_TIMEOUT_MS) }); if (!response.ok) { await response.body?.cancel().catch(() => {}); throw new Error("identity response was not successful"); } const body = await gitHubPublicApi.readBoundedResponse(response, ACCESS_IDENTITY_MAX_BYTES); payload = JSON.parse(body.toString("utf8")); } catch { throw new Error("Cloudflare Access identity lookup failed"); } if (!isRecord(payload)) throw new Error("Cloudflare Access identity response is invalid"); const email = typeof payload.email === "string" ? payload.email.trim() : ""; if (!email || email.toLowerCase() !== authenticatedPrincipal.trim().toLowerCase()) throw new Error("Cloudflare Access identity principal did not match"); if (!isRecord(payload.idp)) throw new Error("Cloudflare Access identity provider is invalid"); if (payload.idp.type === "oidc") { const fields = Object.hasOwn(payload, "oidc_fields") ? payload.oidc_fields : payload.custom; if (!oidcConfig || issuer.origin !== oidcConfig.issuer || payload.idp.id !== oidcConfig.providerId || !isRecord(fields) || !Object.hasOwn(fields, oidcConfig.githubAccountIdClaim)) return { provider: "oidc" }; const claim = fields[oidcConfig.githubAccountIdClaim]; if (typeof claim !== "string" || !/^[1-9][0-9]*$/u.test(claim) || !Number.isSafeInteger(Number(claim))) return { provider: "oidc" }; return { provider: "oidc", accountId: Number(claim) }; } if (payload.idp.type !== "github") throw new Error("Cloudflare Access identity provider is unsupported"); if (typeof payload.id !== "number" || !Number.isSafeInteger(payload.id) || payload.id <= 0) throw new Error("Cloudflare Access GitHub account id is invalid"); const initialDisplayName = typeof payload.name === "string" && payload.name.trim() ? payload.name : void 0; return { provider: "github", accountId: payload.id, ...initialDisplayName ? { initialDisplayName } : {} }; } async function resolveGitHubUserIdentityByLogin(username) { const requestedLogin = normalizeGitHubLogin(username); if (!requestedLogin) throw new TypeError("GitHub username is invalid"); const token = githubApiToken(process.env, void 0, "github.com"); let payload; try { payload = await gitHubPublicApi.fetchGitHubJson(`${gitHubPublicApi.GITHUB_API_ORIGIN}/users/${encodeURIComponent(requestedLogin)}`, fetch, token, void 0, gitHubPublicApi.GITHUB_API_ORIGIN); } catch (error) { if (error instanceof gitHubPublicApi.ControlUiGitHubError) throw error; throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub request failed"); } if (!isRecord(payload)) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub response was not an object"); const accountId = payload.id; if (!Number.isSafeInteger(accountId) || typeof accountId !== "number" || accountId <= 0) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub response omitted a valid account id"); return parseGitHubUserIdentity(accountId, payload); } function parseGitHubUserIdentity(accountId, payload) { if (!isRecord(payload) || payload.id !== accountId) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub account id did not match"); const login = typeof payload.login === "string" ? normalizeGitHubLogin(payload.login) : void 0; if (!login) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub response omitted a valid login"); return { accountId, login, name: readNonBlankString(payload.name) }; } function resolveGitHubUserIdentityById(accountId, token, fetchImpl) { const cache = identityMetadataCaches.get(fetchImpl) ?? { values: /* @__PURE__ */ new Map(), pending: /* @__PURE__ */ new Map() }; identityMetadataCaches.set(fetchImpl, cache); const key = `${accountId}:${gitHubPublicApi.githubApiCredentialCacheScope(token)}`; const cached = cache.values.get(key); if (cached && cached.expiresAt > Date.now()) return Promise.resolve({ identity: cached.identity, refreshed: false }); const promise = getOrCreatePromise(cache.pending, key, async () => { try { const response = await gitHubPublicApi.fetchGitHubApi(`${gitHubPublicApi.GITHUB_API_ORIGIN}/user/${accountId}`, fetchImpl, token, void 0, void 0, cached?.etag, void 0, void 0, gitHubPublicApi.GITHUB_API_ORIGIN); let identity; if (response.status === 304 && cached?.etag) { await gitHubPublicApi.discardResponse(response); identity = cached.identity; } else identity = parseGitHubUserIdentity(accountId, await gitHubPublicApi.readGitHubJsonResponse(response)); const rawEtag = response.headers.get("etag") ?? (response.status === 304 ? cached?.etag : void 0); const etag = rawEtag && rawEtag.length <= GITHUB_ETAG_MAX_LENGTH ? rawEtag : void 0; if (cache.pending.get(key) === promise) { cache.values.set(key, { identity, etag, expiresAt: Date.now() + GITHUB_IDENTITY_CACHE_MS }); pruneMapToMaxSize(cache.values, GITHUB_IDENTITY_CACHE_LIMIT); } return identity; } catch (error) { if (cache.pending.get(key) === promise && error instanceof gitHubPublicApi.ControlUiGitHubError && !error.retryable) cache.values.delete(key); throw error; } }, { evictOnSettled: true }); pruneMapToMaxSize(cache.pending, GITHUB_IDENTITY_CACHE_LIMIT); return promise.then((identity) => ({ identity, refreshed: true })); } function cloudflareAccessAssertion(params) { const trustedProxy = params.authConfig?.trustedProxy; if (!params.authResult.ok || params.authResult.method !== "trusted-proxy" || params.authConfig?.mode !== "trusted-proxy" || normalizeLowercaseStringOrEmpty(trustedProxy?.userHeader) !== CLOUDFLARE_ACCESS_USER_HEADER || !trustedProxy?.requiredHeaders?.some((header) => normalizeLowercaseStringOrEmpty(header) === CLOUDFLARE_ACCESS_ASSERTION_HEADER)) return; const principal = params.authResult.user?.trim(); const assertion = firstHeaderValue(params.requestHeaders?.[CLOUDFLARE_ACCESS_ASSERTION_HEADER])?.trim(); return principal && assertion ? { assertion, principal } : void 0; } function createAuthenticatedGitHubIdentitySync(params) { const options = { assertCurrent: params.assertCurrent }; const tailscaleLogin = params.authResult.tailscaleIdentity ? classifyTailscaleLogin(params.authResult.tailscaleIdentity.login) : void 0; if (tailscaleLogin?.kind === "provider" && tailscaleLogin.provider === "github") return createLazyPromise(async () => { params.assertCurrent?.(); const identity = await resolveGitHubUserIdentityByLogin(tailscaleLogin.subject); params.assertCurrent?.(); const profile = await syncCanonicalGitHubIdentity({ identity, authenticationAlias: { kind: "github-login", login: tailscaleLogin.subject }, initialDisplayName: params.authResult.tailscaleIdentity?.name }, options); params.assertCurrent?.(); return { profileId: profile.id, updatedAt: profile.updatedAt }; }); const access = cloudflareAccessAssertion(params); if (!access) return; return createLazyPromise(async () => { params.assertCurrent?.(); const accessIdentity = await resolveCloudflareAccessIdentity(access.assertion, access.principal, params.authConfig?.trustedProxy?.cloudflareAccessOidc); params.assertCurrent?.(); const accountId = accessIdentity.accountId; if (accountId === void 0) { const profile = await ensureCanonicalUserProfileForEmail(access.principal, options); params.assertCurrent?.(); return { profileId: profile.id, updatedAt: profile.updatedAt }; } const identityBinding = { accountId, email: access.principal }; const token = githubApiToken(process.env, void 0, "github.com"); let lookup; try { lookup = await gitHubPublicApi.withOptionalGitHubAuth(token, (requestToken) => resolveGitHubUserIdentityById(accountId, requestToken, fetch)); } catch (error) { if (error instanceof gitHubPublicApi.ControlUiGitHubError && error.retryable) { params.assertCurrent?.(); const cached = await resolveCanonicalCachedGitHubIdentity(identityBinding); params.assertCurrent?.(); if (cached) return cached; } if (accessIdentity.provider === "oidc") { params.assertCurrent?.(); const profile = await ensureCanonicalUserProfileForEmail(access.principal, { ...options, expectedGitHubAccountId: accountId }); params.assertCurrent?.(); return { profileId: profile.id, updatedAt: profile.updatedAt }; } throw error instanceof gitHubPublicApi.ControlUiGitHubError ? error : new gitHubPublicApi.ControlUiGitHubError(502, "GitHub request failed"); } params.assertCurrent?.(); if (!lookup.refreshed) { const cached = await resolveCanonicalCachedGitHubIdentity(identityBinding); params.assertCurrent?.(); if (cached) return cached; } const profile = await syncCanonicalGitHubIdentity({ identity: lookup.identity, authenticationAlias: { kind: "email", email: access.principal }, initialDisplayName: accessIdentity.provider === "github" ? accessIdentity.initialDisplayName : void 0, preserveEmailProfile: accessIdentity.provider === "oidc" }, options); params.assertCurrent?.(); return { profileId: profile.id, updatedAt: profile.updatedAt }; }); } //#endregion export { shouldUseGatewayOwnerProfile as n, resolveHostAccountName as r, createAuthenticatedGitHubIdentitySync as t };