// Provision (idempotently) the ClaudeDo Zitadel project + two PKCE apps, and resolve the // owner user id. Run: ZITADEL_SERVICE_TOKEN=... bun run provision:zitadel // // Prints the values needed for the app + desktop env. Creates nothing twice: an existing // "ClaudeDo" project and its apps (matched by name) are reused. const ISSUER = (process.env.ZITADEL_ISSUER || "https://auth.kuns.dev").replace(/\/$/, ""); const PAT = process.env.ZITADEL_SERVICE_TOKEN; const WEB_REDIRECT = process.env.WEB_REDIRECT || "https://claudedo.kuns.dev/auth/callback"; const WEB_POST_LOGOUT = process.env.WEB_POST_LOGOUT || "https://claudedo.kuns.dev"; // Desktop is a native/loopback client; register a few common loopback ports. const DESKTOP_REDIRECTS = [ "http://localhost:8765/callback", "http://127.0.0.1:8765/callback", "http://localhost:0/callback", ]; if (!PAT) { console.error("ZITADEL_SERVICE_TOKEN not set"); process.exit(1); } const PROJECT_NAME = "ClaudeDo"; const WEB_APP_NAME = "ClaudeDo Web"; const DESKTOP_APP_NAME = "ClaudeDo Desktop"; async function api(path: string, body?: unknown, method = "POST") { const res = await fetch(`${ISSUER}${path}`, { method, headers: { Authorization: `Bearer ${PAT}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await res.text(); let json: any = null; try { json = text ? JSON.parse(text) : null; } catch { json = text; } if (!res.ok) { throw new Error(`${method} ${path} -> ${res.status}: ${typeof json === "string" ? json : JSON.stringify(json)}`); } return json; } async function findProject(): Promise { const r = await api("/management/v1/projects/_search", { queries: [{ nameQuery: { name: PROJECT_NAME, method: "TEXT_QUERY_METHOD_EQUALS" } }], }); const hit = (r?.result ?? []).find((p: any) => p.name === PROJECT_NAME); return hit?.id ?? null; } async function findApp(projectId: string, name: string): Promise<{ appId: string; clientId: string } | null> { const r = await api(`/management/v1/projects/${projectId}/apps/_search`, { queries: [{ nameQuery: { name, method: "TEXT_QUERY_METHOD_EQUALS" } }], }); const hit = (r?.result ?? []).find((a: any) => a.name === name); if (!hit) return null; return { appId: hit.id, clientId: hit.oidcConfig?.clientId ?? "" }; } async function createProject(): Promise { const existing = await findProject(); if (existing) { console.error(`project "${PROJECT_NAME}" exists: ${existing}`); return existing; } const r = await api("/management/v1/projects", { name: PROJECT_NAME, projectRoleAssertion: false, projectRoleCheck: false, hasProjectCheck: false, privateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED", }); console.error(`created project: ${r.id}`); return r.id; } async function createWebApp(projectId: string): Promise { const existing = await findApp(projectId, WEB_APP_NAME); if (existing?.clientId) { console.error(`web app exists: ${existing.clientId}`); return existing.clientId; } const r = await api(`/management/v1/projects/${projectId}/apps/oidc`, { name: WEB_APP_NAME, redirectUris: [WEB_REDIRECT], postLogoutRedirectUris: [WEB_POST_LOGOUT], responseTypes: ["OIDC_RESPONSE_TYPE_CODE"], grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"], appType: "OIDC_APP_TYPE_USER_AGENT", authMethodType: "OIDC_AUTH_METHOD_TYPE_NONE", // PKCE, public client version: "OIDC_VERSION_1_0", devMode: false, accessTokenType: "OIDC_TOKEN_TYPE_JWT", accessTokenRoleAssertion: true, }); console.error(`created web app: ${r.clientId}`); return r.clientId; } async function createDesktopApp(projectId: string): Promise { const existing = await findApp(projectId, DESKTOP_APP_NAME); if (existing?.clientId) { console.error(`desktop app exists: ${existing.clientId}`); return existing.clientId; } const r = await api(`/management/v1/projects/${projectId}/apps/oidc`, { name: DESKTOP_APP_NAME, redirectUris: DESKTOP_REDIRECTS, responseTypes: ["OIDC_RESPONSE_TYPE_CODE"], grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"], appType: "OIDC_APP_TYPE_NATIVE", authMethodType: "OIDC_AUTH_METHOD_TYPE_NONE", // PKCE (mandatory for native) version: "OIDC_VERSION_1_0", devMode: true, // permit http loopback redirect variations accessTokenType: "OIDC_TOKEN_TYPE_JWT", accessTokenRoleAssertion: true, }); console.error(`created desktop app: ${r.clientId}`); return r.clientId; } async function listHumanUsers(): Promise<{ id: string; userName: string; email: string }[]> { const r = await api("/management/v1/users/_search", { query: { offset: "0", limit: 200, asc: true } }); return (r?.result ?? []) .filter((u: any) => u.human) .map((u: any) => ({ id: u.id, userName: u.userName, email: u.human?.email?.email ?? "", })); } const projectId = await createProject(); const webClientId = await createWebApp(projectId); const desktopClientId = await createDesktopApp(projectId); let owner = { id: "", userName: "", email: "" }; try { const humans = await listHumanUsers(); console.error("\nhuman users:"); for (const u of humans) console.error(` ${u.id} ${u.userName} ${u.email}`); owner = humans.find((u) => u.userName === "mika") ?? humans.find((u) => u.email?.includes("mika")) ?? humans[0] ?? owner; } catch (e) { console.error(`\n(could not list users: ${(e as Error).message}) — set ALLOWED_USER_IDS manually`); } const result = { projectId, webClientId, desktopClientId, ownerUserId: owner.id, ownerUserName: owner.userName, env: { ZITADEL_AUDIENCE: [webClientId, desktopClientId, projectId].filter(Boolean).join(","), ALLOWED_USER_IDS: owner.id, NUXT_PUBLIC_ZITADEL_CLIENT_ID: webClientId, PROJECT_AUDIENCE_SCOPE: `urn:zitadel:iam:org:project:id:${projectId}:aud`, }, }; console.log(JSON.stringify(result, null, 2));