@zitadel/testing 0.0.0 → 0.1.0-alpha.19
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/README.md +124 -42
- package/dist/{app-env-D3W0GYhA.d.mts → app-env-Btmfcflb.d.mts} +4 -4
- package/dist/{app-env-D3W0GYhA.d.mts.map → app-env-Btmfcflb.d.mts.map} +1 -1
- package/dist/app-runner.cjs +1 -1
- package/dist/app-runner.mjs +1 -1
- package/dist/{handshake-CggSIoxF.cjs → handshake-CRKcgkfN.cjs} +3 -2
- package/dist/{handshake-CggSIoxF.cjs.map → handshake-CRKcgkfN.cjs.map} +1 -1
- package/dist/{handshake-BPtWruO8.mjs → handshake-ClzWvG8z.mjs} +3 -2
- package/dist/{handshake-BPtWruO8.mjs.map → handshake-ClzWvG8z.mjs.map} +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.mts +4 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/playwright.cjs +273 -15
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.mts +173 -6
- package/dist/playwright.d.mts.map +1 -1
- package/dist/playwright.mjs +265 -16
- package/dist/playwright.mjs.map +1 -1
- package/dist/{src-DkG0V4A6.mjs → src-9dFjTIAw.mjs} +19 -17
- package/dist/src-9dFjTIAw.mjs.map +1 -0
- package/dist/{src-BIL0PdSd.cjs → src-I0KAh1zU.cjs} +447 -429
- package/dist/src-I0KAh1zU.cjs.map +1 -0
- package/dist/supervisor.cjs +2 -2
- package/dist/supervisor.mjs +2 -2
- package/package.json +5 -5
- package/dist/src-BIL0PdSd.cjs.map +0 -1
- package/dist/src-DkG0V4A6.mjs.map +0 -1
package/dist/playwright.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright.mjs","names":["base"],"sources":["../src/playwright-config.ts","../src/playwright.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { extname, isAbsolute, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { PlaywrightTestConfig } from \"@playwright/test\";\n\nimport type { AppEnvTemplate } from \"./app-env\";\nimport {\n APP_RUNNER_CONFIG_ENV,\n HANDSHAKE_ENV,\n SUPERVISOR_CONFIG_ENV,\n type AppRunnerConfig,\n type SupervisorConfig,\n} from \"./orchestration\";\n\ntype WebServerEntry = Extract<\n NonNullable<PlaywrightTestConfig[\"webServer\"]>,\n readonly unknown[]\n>[number];\n\nexport interface WithZitadelOptions {\n /**\n * The Playwright config's directory (`import.meta.dirname`). Anchors the\n * default handshake location and the working directory of the generated\n * webServer entries.\n */\n configDir: string;\n /**\n * Fixed TCP port for the instance. Required (unlike `startLocalZitadel`,\n * which defaults to a free port) because Playwright's readiness URL must be\n * known while the config is evaluated, before anything boots.\n */\n port: number;\n /**\n * Origin the browser will use for the app under test. Registered as the\n * project's preview origin (the backend's origin check rejects forwarded\n * requests from unregistered origins) and the base of `app.readyPath`.\n */\n appOrigin: string;\n /** Boot/bootstrap options forwarded to the instance supervisor. */\n zitadel?: {\n /** Absolute path to the server binary. */\n serverBinary?: string;\n /** Appended to the missing-binary error, e.g. \"run `moon run server:build` first.\" */\n serverBinaryHint?: string;\n projectName?: string;\n preset?: SupervisorConfig[\"preset\"];\n useCase?: SupervisorConfig[\"useCase\"];\n /** Absolute state directory; defaults to a fresh temp dir removed on stop. */\n dir?: string;\n /** Keep the owned temp dir after stop (debugging). */\n keep?: boolean;\n /** webServer readiness timeout for the boot; cold boot dominates it. */\n bootTimeoutMs?: number;\n };\n /** The app dev server to run against the instance. */\n app: {\n /** Spawn argv (no shell), e.g. [\"corepack\", \"pnpm\", \"--filter\", \"my-app\", \"dev\"]. */\n command: string[];\n /** Working directory for the app command. */\n cwd: string;\n /** Path on `appOrigin` Playwright polls for readiness, e.g. \"/login\". */\n readyPath: string;\n /**\n * Env vars the app needs, as a template mapping env names to\n * InstanceHandle fields — see `nextAppEnv` for the `@zitadel/sdk-next`\n * shape. A template (not a callback) because it crosses into the app\n * runner process.\n */\n env: AppEnvTemplate;\n readyTimeoutMs?: number;\n /** How long SIGTERM gets before the app is killed on teardown. */\n gracefulShutdownMs?: number;\n };\n /** Absolute path; defaults to `<configDir>/.zitadel-testing/handshake.json`. */\n handshakePath?: string;\n}\n\n/**\n * Generate the Playwright `webServer` entries that boot an ephemeral seeded\n * Zitadel and run the app against it, replacing the per-suite wrapper\n * scripts. Spread the result into `defineConfig`:\n *\n * ```ts\n * export default defineConfig({\n * ...withZitadel({ configDir: import.meta.dirname, port: 8092, ... }),\n * testDir: \"./src-real\",\n * });\n * ```\n *\n * Also points ZITADEL_TESTING_HANDSHAKE at the handshake file so the\n * `@zitadel/testing/playwright` fixtures resolve the instance — Playwright\n * workers re-evaluate the config, which re-applies this for every process\n * that needs it. The returned value is plain data; append your own entries\n * to `webServer` if the suite needs additional servers.\n */\nexport function withZitadel(\n options: WithZitadelOptions,\n /** Test seam: alternative executable resolution. */\n resolveEntry: (name: \"supervisor\" | \"app-runner\") => string = entryPoint,\n): { webServer: WebServerEntry[] } {\n const { configDir, port, appOrigin, app } = options;\n if (!isAbsolute(configDir)) {\n throw new Error(`withZitadel: configDir must be absolute, got \"${configDir}\"`);\n }\n if (!Number.isInteger(port) || port <= 0) {\n throw new Error(`withZitadel: port must be a positive integer, got ${port}`);\n }\n const origin = URL.canParse(appOrigin) ? new URL(appOrigin) : undefined;\n if (\n !origin ||\n (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") ||\n origin.pathname !== \"/\" ||\n origin.search !== \"\" ||\n origin.hash !== \"\"\n ) {\n throw new Error(\n `withZitadel: appOrigin must be an origin like \"http://localhost:3002\", got \"${appOrigin}\"`,\n );\n }\n if (!app.readyPath.startsWith(\"/\")) {\n throw new Error(`withZitadel: app.readyPath must start with \"/\", got \"${app.readyPath}\"`);\n }\n if (app.command.length === 0) {\n throw new Error(\"withZitadel: app.command must not be empty\");\n }\n if (!isAbsolute(app.cwd)) {\n throw new Error(`withZitadel: app.cwd must be absolute, got \"${app.cwd}\"`);\n }\n // Path options are consumed by the executables, whose cwd is configDir —\n // a relative path would silently resolve against that, not the project.\n for (const [label, value] of [\n [\"zitadel.serverBinary\", options.zitadel?.serverBinary],\n [\"zitadel.dir\", options.zitadel?.dir],\n [\"handshakePath\", options.handshakePath],\n ] as const) {\n if (value !== undefined && !isAbsolute(value)) {\n throw new Error(`withZitadel: ${label} must be an absolute path, got \"${value}\"`);\n }\n }\n\n const handshakePath =\n options.handshakePath ?? join(configDir, \".zitadel-testing\", \"handshake.json\");\n // Workers inherit the runner's env; the fixtures resolve the instance from it.\n process.env[HANDSHAKE_ENV] = handshakePath;\n\n const supervisorConfig: SupervisorConfig = {\n port,\n appOrigins: [appOrigin],\n serverBinary: options.zitadel?.serverBinary,\n serverBinaryHint: options.zitadel?.serverBinaryHint,\n dir: options.zitadel?.dir,\n keep: options.zitadel?.keep,\n projectName: options.zitadel?.projectName,\n preset: options.zitadel?.preset,\n useCase: options.zitadel?.useCase,\n };\n const appRunnerConfig: AppRunnerConfig = {\n command: app.command,\n cwd: app.cwd,\n env: app.env,\n handshakeTimeoutMs: app.readyTimeoutMs ?? 180_000,\n };\n\n return {\n webServer: [\n {\n command: `node ${JSON.stringify(resolveEntry(\"supervisor\"))}`,\n url: `http://localhost:${port}/healthz`,\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n // Cold boot (fresh data dir: migrations + health wait) dominates.\n timeout: options.zitadel?.bootTimeoutMs ?? 120_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [SUPERVISOR_CONFIG_ENV]: JSON.stringify(supervisorConfig),\n },\n // SIGTERM first so the supervisor can stop the instance; the default\n // hard kill would orphan the server process group.\n gracefulShutdown: { signal: \"SIGTERM\", timeout: 30_000 },\n },\n {\n command: `node ${JSON.stringify(resolveEntry(\"app-runner\"))}`,\n url: new URL(app.readyPath, appOrigin).toString(),\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n timeout: app.readyTimeoutMs ?? 180_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [APP_RUNNER_CONFIG_ENV]: JSON.stringify(appRunnerConfig),\n },\n gracefulShutdown: {\n signal: \"SIGTERM\",\n timeout: app.gracefulShutdownMs ?? 15_000,\n },\n },\n ],\n };\n}\n\n/**\n * Resolve a sibling dist entry in the same module format this file was loaded\n * as (dist/supervisor.mjs next to dist/playwright.mjs, .cjs next to .cjs), so\n * the spawned process needs no package-manager bin plumbing.\n */\nfunction entryPoint(name: \"supervisor\" | \"app-runner\"): string {\n const self = fileURLToPath(import.meta.url);\n const ext = extname(self);\n if (ext !== \".mjs\" && ext !== \".cjs\") {\n throw new Error(\n `withZitadel: expected to run from the built package (got ${self}); ` +\n \"build @zitadel/testing first (in-repo: `moon run testing:build`).\",\n );\n }\n const path = fileURLToPath(new URL(`./${name}${ext}`, import.meta.url));\n if (!existsSync(path)) {\n throw new Error(\n `withZitadel: missing ${path}; rebuild @zitadel/testing (in-repo: \\`moon run testing:build\\`).`,\n );\n }\n return path;\n}\n","import { test as base, type Page } from \"@playwright/test\";\n\nimport { readHandshakeSync } from \"./handshake\";\nimport { connectZitadel } from \"./index\";\nimport type {\n ConnectedZitadel,\n Identity,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n\nexport interface AuthenticatedPage {\n /** A page in its own context, already carrying the session cookie. */\n page: Page;\n user: SeededUser;\n session: MintedSession;\n}\n\nexport interface ZitadelTestFixtures {\n /** Per-test seeding; each call mints unique data on the shared instance. */\n seed: {\n user(input?: SeedUserInput): Promise<SeededUser>;\n users(count: number, template?: SeedUsersTemplate): Promise<SeededUser[]>;\n /** Unused email+password for registration flows — creates nothing. */\n identity(): Identity;\n /** Seeded user + headless real-flow login; password flows only. */\n session(input?: SeedSessionInput): Promise<MintedSession>;\n };\n /**\n * Start the test authenticated: a fresh user, a real session minted through\n * the flow API, and the cookie injected into a dedicated browser context —\n * the default `page` stays signed out for login-flow tests. Requires\n * `use.baseURL` (every withZitadel consumer sets it).\n */\n authenticatedPage: AuthenticatedPage;\n}\n\nexport interface ZitadelWorkerFixtures {\n /** Connection to the suite's instance, resolved once per worker. */\n zitadel: ConnectedZitadel;\n}\n\nexport const test = base.extend<ZitadelTestFixtures, ZitadelWorkerFixtures>({\n zitadel: [\n // Playwright derives fixture dependencies from the destructuring pattern,\n // so the empty pattern is required here.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n const handshakePath = process.env.ZITADEL_TESTING_HANDSHAKE;\n if (!handshakePath) {\n throw new Error(\n \"ZITADEL_TESTING_HANDSHAKE is not set. Point it at the handshake file \" +\n \"written by the script that boots the instance (see @zitadel/testing docs).\",\n );\n }\n await use(connectZitadel(readHandshakeSync(handshakePath)));\n },\n { scope: \"worker\" },\n ],\n seed: async ({ zitadel, baseURL }, use) => {\n await use({\n user: (input) => zitadel.seedUser(input),\n users: (count, template) => zitadel.seedUsers(count, template),\n identity: () => zitadel.identity(),\n // The suite's baseURL is the app origin the project allowlists.\n session: (input) => zitadel.seedSession({ origin: baseURL, ...input }),\n });\n },\n authenticatedPage: async ({ browser, zitadel, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n \"authenticatedPage requires `use.baseURL` so the session cookie can be \" +\n \"scoped to the app under test.\",\n );\n }\n const session = await zitadel.seedSession({ origin: baseURL });\n const context = await browser.newContext({ baseURL });\n // `addCookies` takes either url or domain/path; url derives the rest.\n const { path: _path, ...cookie } = session.cookie;\n await context.addCookies([{ ...cookie, url: baseURL }]);\n const page = await context.newPage();\n await use({ page, user: session.user, session });\n await context.close();\n },\n});\n\nexport { expect } from \"@playwright/test\";\nexport { applyAppEnvTemplate, nextAppEnv } from \"./app-env\";\nexport type { AppEnvTemplate } from \"./app-env\";\nexport { withZitadel } from \"./playwright-config\";\nexport type { WithZitadelOptions } from \"./playwright-config\";\nexport type {\n ConnectedZitadel,\n Identity,\n InstanceHandle,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playwright.mjs","names":["base"],"sources":["../src/passkey.ts","../src/flows.ts","../src/playwright-config.ts","../src/playwright.ts"],"sourcesContent":["import type { CDPSession, Page } from \"@playwright/test\";\n\n/**\n * A virtual WebAuthn authenticator attached to one page. Ceremonies started\n * from that page (passkey registration, passkey login) complete automatically\n * — no OS authenticator dialog, no touch.\n */\nexport interface VirtualPasskey {\n /** CDP id of the virtual authenticator, for advanced raw-protocol use. */\n authenticatorId: string;\n /** Number of credentials the authenticator currently stores. */\n credentialCount(): Promise<number>;\n /** Remove the authenticator and detach the CDP session. */\n dispose(): Promise<void>;\n}\n\n/**\n * Attach a virtual passkey authenticator to the page via the Chrome DevTools\n * Protocol. The options mirror a platform authenticator with discoverable\n * credentials and automatic user presence — the profile the consumer journey\n * has run in CI since passkey coverage became mandatory there.\n *\n * Chromium only: the CDP WebAuthn domain does not exist in WebKit or Firefox.\n * The authenticator is bound to this page — drive registration and the later\n * login from the same page, or the credential is gone. Serve the app under\n * test on an origin WebAuthn accepts as a relying-party ID: HTTPS on a real\n * domain, or `http://localhost` for local runs — raw IP origins such as\n * `http://127.0.0.1` are invalid RP IDs.\n */\nexport async function enableVirtualPasskey(page: Page): Promise<VirtualPasskey> {\n let client: CDPSession;\n try {\n client = await page.context().newCDPSession(page);\n } catch (error) {\n throw new Error(\n \"enableVirtualPasskey: could not open a CDP session — passkey testing \" +\n \"needs Chromium's virtual authenticator, so run passkey specs in a \" +\n \"Chromium project.\",\n { cause: error },\n );\n }\n let authenticatorId: string;\n try {\n await client.send(\"WebAuthn.enable\");\n ({ authenticatorId } = await client.send(\"WebAuthn.addVirtualAuthenticator\", {\n options: {\n protocol: \"ctap2\",\n transport: \"internal\",\n hasResidentKey: true,\n hasUserVerification: true,\n isUserVerified: true,\n automaticPresenceSimulation: true,\n },\n }));\n } catch (error) {\n // Don't leave a half-initialized CDP session attached behind a throw.\n await client.detach().catch(() => undefined);\n throw error;\n }\n\n return {\n authenticatorId,\n async credentialCount() {\n const { credentials } = await client.send(\"WebAuthn.getCredentials\", {\n authenticatorId,\n });\n return credentials.length;\n },\n async dispose() {\n // Best-effort: the page (and with it the CDP target) may already be\n // gone when teardown runs after a failed test; never mask that failure.\n // Detach even when the removal fails, so the session never outlives it.\n try {\n await client.send(\"WebAuthn.removeVirtualAuthenticator\", { authenticatorId });\n } catch {\n // ignore\n } finally {\n await client.detach().catch(() => undefined);\n }\n },\n };\n}\n","import type { Locator, Page } from \"@playwright/test\";\n\n/**\n * Helpers that drive the `<zitadel-login>` widget through complete auth\n * ceremonies, built on the widget's documented automation hooks\n * (`zitadel-field-*` / `zitadel-input-*` on fields, `zitadel-action-*` on\n * actions — see packages/components/README.md). Field names use the\n * normalised hook token: the flow engine names credential fields\n * `x-auth-methods#password`, but the hook (and this API) says `password`.\n *\n * The ceremony helpers assume the default flow vocabulary — steps that\n * declare `submit` / `passkey` / `passkey_register` actions and `email` /\n * password fields, as `default-login.json` does. They branch only on\n * widget-observable state (which fields and actions the flow renders),\n * because customer flow configurations legitimately vary; they never\n * assert app state. Callers navigate to the widget first and assert their\n * own signed-in surface afterwards. Flows with renamed actions or custom\n * steps drive the widget directly via `flowAction` / `flowField`.\n */\n\nexport interface FlowActionOptions {\n /**\n * Accessible-name fallback for templates that do not emit the documented\n * `data-testid` hooks: adds role-based button/link candidates.\n */\n name?: RegExp;\n}\n\nexport interface FlowFieldOptions {\n /** Label fallback for templates that do not emit the documented hooks. */\n label?: RegExp;\n}\n\n/**\n * One optional registration field, filled only when the flow renders it.\n * The value's type picks the control: a boolean drives a checkbox\n * (`zl-checkbox`), a string first tries a select (`zl-select`) and falls\n * back to filling a text-like input.\n */\nexport interface ProfileEntry {\n /** Normalised field hook token, e.g. `givenName`. */\n field: string;\n value: string | boolean;\n /** Label fallback for text-like inputs on templates without hooks. */\n label?: RegExp;\n}\n\nexport interface LoginCredentials {\n email: string;\n password: string;\n}\n\nexport interface RegistrationDetails {\n email: string;\n /** Extra fields some flows require at registration, filled if rendered. */\n profile?: ProfileEntry[];\n}\n\nexport interface PasswordRegistrationDetails extends RegistrationDetails {\n password: string;\n}\n\n/**\n * A union locator resolves in DOM order across the whole page, so every\n * candidate built from a broad selector — accessible names, labels, the\n * generic `data-action` attribute — is scoped to the `<zitadel-login>`\n * host; otherwise a same-named control in the app's own chrome (header\n * nav, footer forms) could win the union. The host element exists no\n * matter what the tenant's template renders, so these fallbacks keep\n * working for custom templates that emit no automation hooks — the case\n * they exist for. The `zitadel-*` testid hooks and the `zl-button` atom\n * are namespaced and stay page-global.\n */\nfunction widgetRoot(page: Page): Locator {\n return page.locator(\"zitadel-login\");\n}\n\n/**\n * Escape a value for use inside a double-quoted CSS attribute selector,\n * per the CSSOM \"serialize a string\" rules: NUL becomes U+FFFD, control\n * characters become hex code-point escapes, quote and backslash are\n * backslash-escaped. C1 controls (U+0080–U+009F) are escaped too — CSSOM\n * itself leaves them literal, but the escaped form is equivalent and\n * survives stricter-than-spec selector parsers. Anything the flow schema\n * accepts as an action name yields a parseable selector.\n */\nfunction cssAttributeValue(value: string): string {\n let out = \"\";\n for (const ch of value) {\n const code = ch.codePointAt(0) ?? 0;\n if (code === 0) {\n out += \"�\";\n } else if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {\n out += `\\\\${code.toString(16)} `;\n } else if (ch === '\"' || ch === \"\\\\\") {\n out += `\\\\${ch}`;\n } else {\n out += ch;\n }\n }\n return out;\n}\n\n/**\n * Locator for a flow action control by its declared action name. Matches\n * every hook shape the default template emits — host testid, native\n * shadow button/link testids, and the raw `action`/`data-action`\n * attributes (the recover link carries only `data-action`).\n */\nexport function flowAction(page: Page, action: string, options: FlowActionOptions = {}): Locator {\n // Action names are free-form in the flow schema; escape them before\n // interpolating into attribute selectors so an exotic name cannot break\n // the whole union's parsing.\n const attributeSafe = cssAttributeValue(action);\n let candidates = page\n .getByTestId(`zitadel-action-${action}`)\n .or(page.getByTestId(`zitadel-action-${action}-button`))\n .or(page.getByTestId(`zitadel-action-${action}-link`))\n .or(page.locator(`zl-button[action=\"${attributeSafe}\"]`))\n .or(widgetRoot(page).locator(`[data-action=\"${attributeSafe}\"]`));\n if (options.name) {\n candidates = candidates\n .or(widgetRoot(page).getByRole(\"button\", { name: options.name }))\n .or(widgetRoot(page).getByRole(\"link\", { name: options.name }));\n }\n return candidates.first();\n}\n\n/**\n * Locator for a flow field's input by its normalised hook token\n * (`email`, `password`, a user-schema property name).\n */\nexport function flowField(page: Page, field: string, options: FlowFieldOptions = {}): Locator {\n let candidates = page\n .getByTestId(`zitadel-input-${field}`)\n .or(page.getByTestId(`zitadel-field-${field}`).locator(\"input\"));\n if (options.label) {\n candidates = candidates.or(widgetRoot(page).getByLabel(options.label));\n }\n return candidates.first();\n}\n\n/** Click a flow action (auto-waits like any locator click). */\nexport async function clickFlowAction(\n page: Page,\n action: string,\n options?: FlowActionOptions,\n): Promise<void> {\n await flowAction(page, action, options).click();\n}\n\n/** Fill a flow field (auto-waits like any locator fill). */\nexport async function fillFlowField(\n page: Page,\n field: string,\n value: string,\n options?: FlowFieldOptions,\n): Promise<void> {\n await flowField(page, field, options).fill(value);\n}\n\n/**\n * Sign in with email and password from the flow's entry step. Handles both\n * the default split shape (identifier → password) and flows that render\n * the password field on the entry step.\n */\nexport async function loginWithPassword(\n page: Page,\n { email, password }: LoginCredentials,\n): Promise<void> {\n await emailField(page).fill(email);\n const password_ = passwordField(page);\n if (!(await password_.isVisible().catch(() => false))) {\n await flowAction(page, \"submit\").click();\n }\n await password_.fill(password);\n await flowAction(page, \"submit\").click();\n}\n\n/**\n * Sign in with a passkey. With `email`, fills the identifier and takes the\n * step's passkey action; without, taps the entry step's passkey action\n * directly (discoverable-credential one-tap, e.g. the passkey-first\n * preset). Pair with `enableVirtualPasskey` / the `passkey` fixture in\n * headless runs — the ceremony completes automatically once the widget\n * issues the WebAuthn challenge.\n */\nexport async function loginWithPasskey(page: Page, options: { email?: string } = {}): Promise<void> {\n if (options.email !== undefined) {\n await emailField(page).fill(options.email);\n }\n await flowAction(page, \"passkey\").click();\n}\n\n/**\n * Register a new user with a password: enter the (unknown) identifier,\n * advance into the registration step, continue on the password path, and\n * submit the password. Ends when the final submit is clicked — assert your\n * app's signed-in surface afterwards. Flows that route through steps the\n * default flow does not (e.g. a passkey upsell) need caller-side handling\n * after this returns.\n */\nexport async function registerWithPassword(\n page: Page,\n { email, password, profile }: PasswordRegistrationDetails,\n): Promise<void> {\n await advanceToRegistration(page, email, profile);\n // Continue with password unless this flow renders the password field on\n // the registration step itself.\n const password_ = passwordField(page);\n if (!(await password_.isVisible().catch(() => false))) {\n await flowAction(page, \"submit\").click();\n }\n await password_.fill(password);\n await flowAction(page, \"submit\").click();\n}\n\n/**\n * Register a new user with a passkey: enter the (unknown) identifier,\n * advance into the registration step, and take its `passkey_register`\n * action. Requires an authenticator (see `loginWithPasskey`). The default\n * flow completes registration from the ceremony directly.\n */\nexport async function registerWithPasskey(\n page: Page,\n { email, profile }: RegistrationDetails,\n): Promise<void> {\n await advanceToRegistration(page, email, profile);\n await flowAction(page, \"passkey_register\").click();\n}\n\nfunction emailField(page: Page): Locator {\n return flowField(page, \"email\", { label: /email/i });\n}\n\nfunction passwordField(page: Page): Locator {\n return flowField(page, \"password\", { label: /password/i });\n}\n\n/**\n * From the entry step: submit the unknown identifier (the default flow's\n * `user_not_found` transition routes to registration), or take an explicit\n * `register` navigate action when the entry step is a combined\n * email+password step — there, submitting would attempt a password sign-in\n * instead. Then wait for the registration step and fill what it renders.\n */\nasync function advanceToRegistration(\n page: Page,\n email: string,\n profile: ProfileEntry[] | undefined,\n): Promise<void> {\n await emailField(page).fill(email);\n if (await passwordField(page).isVisible().catch(() => false)) {\n await flowAction(page, \"register\").click();\n } else {\n await flowAction(page, \"submit\").click();\n }\n await expectRegistrationStep(page);\n // The engine echoes the attempted identifier into the registration step's\n // email field; refill defensively for flows that render it empty.\n await fillIfVisible(emailField(page), email);\n for (const entry of profile ?? []) {\n await fillProfileEntry(page, entry);\n }\n}\n\n/**\n * Fill one registration field with the verb its control needs: booleans\n * check the `zl-checkbox` native input, strings prefer the `zl-select`\n * native select (option matched by value, falling back to label) and\n * otherwise fill a text-like input. The select/checkbox natives carry the\n * name-first testids the atoms document (`zitadel-select-*`,\n * `zitadel-checkbox-*`); templates without those hooks drive such fields\n * via their own locators.\n */\nasync function fillProfileEntry(page: Page, entry: ProfileEntry): Promise<void> {\n if (typeof entry.value === \"boolean\") {\n const checkbox = page.getByTestId(`zitadel-checkbox-${entry.field}`).first();\n if (await checkbox.isVisible().catch(() => false)) {\n await checkbox.setChecked(entry.value);\n }\n return;\n }\n const select = page.getByTestId(`zitadel-select-${entry.field}`).first();\n if (await select.isVisible().catch(() => false)) {\n await select.selectOption(entry.value);\n return;\n }\n await fillIfVisible(flowField(page, entry.field, { label: entry.label }), entry.value);\n}\n\n/**\n * Barrier between the entry submit and probing the registration step's\n * fields: without it, optional-field probes race the re-render and read\n * the outgoing step. The default flow's registration step declares\n * `passkey_register` — a structural, locale-independent signal; the\n * heading regex covers password-only flows on the default English\n * template.\n */\nasync function expectRegistrationStep(page: Page): Promise<void> {\n await flowAction(page, \"passkey_register\")\n .or(widgetRoot(page).getByRole(\"heading\", { name: /create|register|sign up|no-account/i }))\n .first()\n .waitFor({ state: \"visible\", timeout: 30_000 });\n}\n\nasync function fillIfVisible(field: Locator, value: string): Promise<void> {\n if (await field.isVisible().catch(() => false)) {\n await field.fill(value);\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { extname, isAbsolute, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { PlaywrightTestConfig } from \"@playwright/test\";\n\nimport type { AppEnvTemplate } from \"./app-env\";\nimport {\n APP_RUNNER_CONFIG_ENV,\n HANDSHAKE_ENV,\n SUPERVISOR_CONFIG_ENV,\n type AppRunnerConfig,\n type SupervisorConfig,\n} from \"./orchestration\";\n\ntype WebServerEntry = Extract<\n NonNullable<PlaywrightTestConfig[\"webServer\"]>,\n readonly unknown[]\n>[number];\n\nexport interface WithZitadelOptions {\n /**\n * The Playwright config's directory (`import.meta.dirname`). Anchors the\n * default handshake location and the working directory of the generated\n * webServer entries.\n */\n configDir: string;\n /**\n * Fixed TCP port for the instance. Required (unlike `startLocalZitadel`,\n * which defaults to a free port) because Playwright's readiness URL must be\n * known while the config is evaluated, before anything boots.\n */\n port: number;\n /**\n * Origin the browser will use for the app under test. Registered as the\n * project's preview origin (the backend's origin check rejects forwarded\n * requests from unregistered origins) and the base of `app.readyPath`.\n */\n appOrigin: string;\n /** Boot/bootstrap options forwarded to the instance supervisor. */\n zitadel?: {\n /** Absolute path to the server binary. */\n serverBinary?: string;\n /** Appended to the missing-binary error, e.g. \"run `moon run server:build` first.\" */\n serverBinaryHint?: string;\n projectName?: string;\n preset?: SupervisorConfig[\"preset\"];\n useCase?: SupervisorConfig[\"useCase\"];\n /** Absolute state directory; defaults to a fresh temp dir removed on stop. */\n dir?: string;\n /** Keep the owned temp dir after stop (debugging). */\n keep?: boolean;\n /** webServer readiness timeout for the boot; cold boot dominates it. */\n bootTimeoutMs?: number;\n };\n /**\n * The app dev server to run against the instance.\n *\n * Omit it when the **instance itself serves the app** — the Zitadel binary\n * embeds the console and hosted login shells at `/ui/console/` and\n * `/ui/login/`, so there is no second server to boot and no env to hand it.\n * Only the supervisor entry is generated, and `appOrigin` must then be the\n * instance's own local origin — `http://localhost:<port>` (or another\n * loopback alias); nothing else is served in this mode. This is the only\n * configuration that exercises the production path — under a dev server,\n * the app's proxy rewrites API requests that the binary would serve\n * directly.\n */\n app?: {\n /** Spawn argv (no shell), e.g. [\"corepack\", \"pnpm\", \"--filter\", \"my-app\", \"dev\"]. */\n command: string[];\n /** Working directory for the app command. */\n cwd: string;\n /** Path on `appOrigin` Playwright polls for readiness, e.g. \"/login\". */\n readyPath: string;\n /**\n * Env vars the app needs, as a template mapping env names to\n * InstanceHandle fields — see `nextAppEnv` for the `@zitadel/sdk-next`\n * shape. A template (not a callback) because it crosses into the app\n * runner process.\n */\n env: AppEnvTemplate;\n readyTimeoutMs?: number;\n /** How long SIGTERM gets before the app is killed on teardown. */\n gracefulShutdownMs?: number;\n };\n /** Absolute path; defaults to `<configDir>/.zitadel-testing/handshake.json`. */\n handshakePath?: string;\n}\n\n/**\n * Generate the Playwright `webServer` entries that boot an ephemeral seeded\n * Zitadel and run the app against it, replacing the per-suite wrapper\n * scripts. Spread the result into `defineConfig`:\n *\n * ```ts\n * export default defineConfig({\n * ...withZitadel({ configDir: import.meta.dirname, port: 8092, ... }),\n * testDir: \"./src-real\",\n * });\n * ```\n *\n * Omit `app` when the instance serves the app itself (the binary's embedded\n * `/ui/console/` and `/ui/login/` surfaces): only the boot entry is\n * generated, and `appOrigin` must be the instance's own origin.\n *\n * Also points ZITADEL_TESTING_HANDSHAKE at the handshake file so the\n * `@zitadel/testing/playwright` fixtures resolve the instance — Playwright\n * workers re-evaluate the config, which re-applies this for every process\n * that needs it. The returned value is plain data; append your own entries\n * to `webServer` if the suite needs additional servers.\n */\nexport function withZitadel(\n options: WithZitadelOptions,\n /** Test seam: alternative executable resolution. */\n resolveEntry: (name: \"supervisor\" | \"app-runner\") => string = entryPoint,\n): { webServer: WebServerEntry[] } {\n const { configDir, port, appOrigin, app } = options;\n if (!isAbsolute(configDir)) {\n throw new Error(`withZitadel: configDir must be absolute, got \"${configDir}\"`);\n }\n if (!Number.isInteger(port) || port <= 0) {\n throw new Error(`withZitadel: port must be a positive integer, got ${port}`);\n }\n const origin = URL.canParse(appOrigin) ? new URL(appOrigin) : undefined;\n if (\n !origin ||\n (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") ||\n origin.pathname !== \"/\" ||\n origin.search !== \"\" ||\n origin.hash !== \"\"\n ) {\n throw new Error(\n `withZitadel: appOrigin must be an origin like \"http://localhost:3002\", got \"${appOrigin}\"`,\n );\n }\n if (app === undefined) {\n // Nothing else will bind appOrigin in this mode: the instance is the app\n // server, and it listens on plain HTTP at localhost:<port>. Anything\n // else — another port, a real hostname, https — is an origin nobody\n // serves, so Playwright's readiness wait would hang (or, worse, pass\n // against an unrelated stale server). Loopback aliases are the only\n // freedom; the scheme and port are the instance's.\n const loopbackHosts = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n if (\n origin.protocol !== \"http:\" ||\n !loopbackHosts.has(origin.hostname) ||\n (origin.port || \"80\") !== String(port)\n ) {\n throw new Error(\n `withZitadel: with no \\`app\\`, the instance itself serves the app, so appOrigin ` +\n `must be its local origin (http://localhost:${port}); got \"${appOrigin}\".`,\n );\n }\n } else {\n if (!app.readyPath.startsWith(\"/\")) {\n throw new Error(`withZitadel: app.readyPath must start with \"/\", got \"${app.readyPath}\"`);\n }\n if (app.command.length === 0) {\n throw new Error(\"withZitadel: app.command must not be empty\");\n }\n if (!isAbsolute(app.cwd)) {\n throw new Error(`withZitadel: app.cwd must be absolute, got \"${app.cwd}\"`);\n }\n }\n // Path options are consumed by the executables, whose cwd is configDir —\n // a relative path would silently resolve against that, not the project.\n for (const [label, value] of [\n [\"zitadel.serverBinary\", options.zitadel?.serverBinary],\n [\"zitadel.dir\", options.zitadel?.dir],\n [\"handshakePath\", options.handshakePath],\n ] as const) {\n if (value !== undefined && !isAbsolute(value)) {\n throw new Error(`withZitadel: ${label} must be an absolute path, got \"${value}\"`);\n }\n }\n\n const handshakePath =\n options.handshakePath ?? join(configDir, \".zitadel-testing\", \"handshake.json\");\n // Workers inherit the runner's env; the fixtures resolve the instance from it.\n process.env[HANDSHAKE_ENV] = handshakePath;\n\n const supervisorConfig: SupervisorConfig = {\n port,\n appOrigins: [appOrigin],\n serverBinary: options.zitadel?.serverBinary,\n serverBinaryHint: options.zitadel?.serverBinaryHint,\n dir: options.zitadel?.dir,\n keep: options.zitadel?.keep,\n projectName: options.zitadel?.projectName,\n preset: options.zitadel?.preset,\n useCase: options.zitadel?.useCase,\n };\n const supervisorEntry: WebServerEntry = {\n command: `node ${JSON.stringify(resolveEntry(\"supervisor\"))}`,\n url: `http://localhost:${port}/healthz`,\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n // Cold boot (fresh data dir: migrations + health wait) dominates.\n timeout: options.zitadel?.bootTimeoutMs ?? 120_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [SUPERVISOR_CONFIG_ENV]: JSON.stringify(supervisorConfig),\n },\n // SIGTERM first so the supervisor can stop the instance; the default\n // hard kill would orphan the server process group.\n gracefulShutdown: { signal: \"SIGTERM\", timeout: 30_000 },\n };\n\n if (app === undefined) {\n return { webServer: [supervisorEntry] };\n }\n\n const appRunnerConfig: AppRunnerConfig = {\n command: app.command,\n cwd: app.cwd,\n env: app.env,\n handshakeTimeoutMs: app.readyTimeoutMs ?? 180_000,\n };\n\n return {\n webServer: [\n supervisorEntry,\n {\n command: `node ${JSON.stringify(resolveEntry(\"app-runner\"))}`,\n url: new URL(app.readyPath, appOrigin).toString(),\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n timeout: app.readyTimeoutMs ?? 180_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [APP_RUNNER_CONFIG_ENV]: JSON.stringify(appRunnerConfig),\n },\n gracefulShutdown: {\n signal: \"SIGTERM\",\n timeout: app.gracefulShutdownMs ?? 15_000,\n },\n },\n ],\n };\n}\n\n/**\n * Resolve a sibling dist entry in the same module format this file was loaded\n * as (dist/supervisor.mjs next to dist/playwright.mjs, .cjs next to .cjs), so\n * the spawned process needs no package-manager bin plumbing.\n */\nfunction entryPoint(name: \"supervisor\" | \"app-runner\"): string {\n const self = fileURLToPath(import.meta.url);\n const ext = extname(self);\n if (ext !== \".mjs\" && ext !== \".cjs\") {\n throw new Error(\n `withZitadel: expected to run from the built package (got ${self}); ` +\n \"build @zitadel/testing first (in-repo: `moon run testing:build`).\",\n );\n }\n const path = fileURLToPath(new URL(`./${name}${ext}`, import.meta.url));\n if (!existsSync(path)) {\n throw new Error(\n `withZitadel: missing ${path}; rebuild @zitadel/testing (in-repo: \\`moon run testing:build\\`).`,\n );\n }\n return path;\n}\n","import { test as base, type Page } from \"@playwright/test\";\n\nimport { waitForHandshake } from \"./handshake\";\nimport { connectZitadel } from \"./index\";\nimport { enableVirtualPasskey, type VirtualPasskey } from \"./passkey\";\nimport type {\n ConnectedZitadel,\n Identity,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n\nexport interface AuthenticatedPage {\n /** A page in its own context, already carrying the session cookie. */\n page: Page;\n user: SeededUser;\n session: MintedSession;\n}\n\nexport interface ZitadelTestFixtures {\n /** Per-test seeding; each call mints unique data on the shared instance. */\n seed: {\n user(input?: SeedUserInput): Promise<SeededUser>;\n users(count: number, template?: SeedUsersTemplate): Promise<SeededUser[]>;\n /** Unused email+password for registration flows — creates nothing. */\n identity(): Identity;\n /** Seeded user + headless real-flow login; password flows only. */\n session(input?: SeedSessionInput): Promise<MintedSession>;\n };\n /**\n * Start the test authenticated: a fresh user, a real session minted through\n * the flow API, and the cookie injected into a dedicated browser context —\n * the default `page` stays signed out for login-flow tests. Requires\n * `use.baseURL` (every withZitadel consumer sets it).\n */\n authenticatedPage: AuthenticatedPage;\n /**\n * Virtual passkey authenticator attached to the default `page`, disposed on\n * teardown. On-demand: tests that don't request it pay nothing. Chromium\n * only — see `enableVirtualPasskey` for the constraints.\n */\n passkey: VirtualPasskey;\n}\n\nexport interface ZitadelWorkerFixtures {\n /** Connection to the suite's instance, resolved once per worker. */\n zitadel: ConnectedZitadel;\n}\n\nexport const test = base.extend<ZitadelTestFixtures, ZitadelWorkerFixtures>({\n zitadel: [\n // Playwright derives fixture dependencies from the destructuring pattern,\n // so the empty pattern is required here.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n const handshakePath = process.env.ZITADEL_TESTING_HANDSHAKE;\n if (!handshakePath) {\n throw new Error(\n \"ZITADEL_TESTING_HANDSHAKE is not set. Point it at the handshake file \" +\n \"written by the script that boots the instance (see @zitadel/testing docs).\",\n );\n }\n // Wait, don't read-once: the supervisor's Playwright readiness URL is\n // the instance's /healthz, which answers as soon as the *server* is up\n // — the handshake lands only after *bootstrap* (project, schema,\n // flows) completes a moment later. Suites with an `app` entry never\n // see the gap (the app runner waits for the handshake before the app\n // reports ready), but in app-less mode the first worker can get here\n // first. Bootstrap after health is seconds at most, so the default\n // wait is generous; a bootstrap failure surfaces as this timeout.\n await use(connectZitadel(await waitForHandshake(handshakePath)));\n },\n // `auto`: every worker waits for the bootstrapped instance before its\n // first test, including tests that use no fixture. Without it, a\n // `page`-only test in an app-less suite can navigate during the\n // bootstrap window and observe a project-less deployment — truthful,\n // rendered as the setup hint, and not what any suite means to test.\n // For app-ful suites this is a one-time handshake file read per worker.\n { scope: \"worker\", auto: true },\n ],\n seed: async ({ zitadel, baseURL }, use) => {\n await use({\n user: (input) => zitadel.seedUser(input),\n users: (count, template) => zitadel.seedUsers(count, template),\n identity: () => zitadel.identity(),\n // The suite's baseURL is the app origin the project allowlists.\n session: (input) => zitadel.seedSession({ origin: baseURL, ...input }),\n });\n },\n passkey: async ({ page }, use) => {\n const passkey = await enableVirtualPasskey(page);\n await use(passkey);\n await passkey.dispose();\n },\n authenticatedPage: async ({ browser, zitadel, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n \"authenticatedPage requires `use.baseURL` so the session cookie can be \" +\n \"scoped to the app under test.\",\n );\n }\n const session = await zitadel.seedSession({ origin: baseURL });\n const context = await browser.newContext({ baseURL });\n // `addCookies` takes either url or domain/path; url derives the rest.\n const { path: _path, ...cookie } = session.cookie;\n await context.addCookies([{ ...cookie, url: baseURL }]);\n const page = await context.newPage();\n await use({ page, user: session.user, session });\n await context.close();\n },\n});\n\nexport { expect } from \"@playwright/test\";\nexport { applyAppEnvTemplate, nextAppEnv } from \"./app-env\";\nexport type { AppEnvTemplate } from \"./app-env\";\nexport {\n clickFlowAction,\n fillFlowField,\n flowAction,\n flowField,\n loginWithPassword,\n loginWithPasskey,\n registerWithPassword,\n registerWithPasskey,\n} from \"./flows\";\nexport type {\n FlowActionOptions,\n FlowFieldOptions,\n LoginCredentials,\n PasswordRegistrationDetails,\n ProfileEntry,\n RegistrationDetails,\n} from \"./flows\";\nexport { enableVirtualPasskey } from \"./passkey\";\nexport type { VirtualPasskey } from \"./passkey\";\nexport { withZitadel } from \"./playwright-config\";\nexport type { WithZitadelOptions } from \"./playwright-config\";\nexport type {\n ConnectedZitadel,\n Identity,\n InstanceHandle,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as nextAppEnv, i as applyAppEnvTemplate } from "./handshake-
|
|
1
|
+
import { a as nextAppEnv, i as applyAppEnvTemplate } from "./handshake-ClzWvG8z.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createZitadelClient } from "@zitadel/api/client";
|
|
4
4
|
import { DEFAULT_FLOW_SCHEMA_URI, getDefaultHumanUserSchema, getDefaultLoginFlow } from "@zitadel/config/defaults";
|
|
@@ -20,12 +20,12 @@ async function bootstrapProject(options) {
|
|
|
20
20
|
const { baseUrl } = options;
|
|
21
21
|
const project = await createZitadelClient({ baseUrl }).createProject({
|
|
22
22
|
name: options.projectName ?? DEFAULT_PROJECT_NAME,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
preview_origins: options.appOrigins ?? [],
|
|
24
|
+
seed_defaults: false
|
|
25
25
|
});
|
|
26
26
|
const projectId = requireString(project.id, "project id");
|
|
27
|
-
const projectSecret = requireString(project.
|
|
28
|
-
const previewSecret = typeof project.
|
|
27
|
+
const projectSecret = requireString(project.project_secret, "project secret");
|
|
28
|
+
const previewSecret = typeof project.preview_secret === "string" ? project.preview_secret : void 0;
|
|
29
29
|
const client = createZitadelClient({
|
|
30
30
|
baseUrl,
|
|
31
31
|
token: projectSecret
|
|
@@ -176,9 +176,8 @@ function getFreePort() {
|
|
|
176
176
|
/**
|
|
177
177
|
* Boot an ephemeral local server by shelling out to `zitadel start` and parse
|
|
178
178
|
* its JSON envelope. The CLI owns the subtle parts (port preflight, health
|
|
179
|
-
* wait, process-group stop
|
|
180
|
-
*
|
|
181
|
-
* shape returned here.
|
|
179
|
+
* wait, process-group stop), so this module stays a thin adapter; swapping it
|
|
180
|
+
* for direct library calls later must not change the shape returned here.
|
|
182
181
|
*/
|
|
183
182
|
async function bootLocalServer(options = {}) {
|
|
184
183
|
const ownsDir = options.dir === void 0;
|
|
@@ -283,8 +282,9 @@ function identity() {
|
|
|
283
282
|
}
|
|
284
283
|
/**
|
|
285
284
|
* Create a user that can immediately complete the password login flow:
|
|
286
|
-
* `POST /users` (the body
|
|
287
|
-
* `PUT /users/{id}/password` with
|
|
285
|
+
* `POST /users` (the body carries `schema: <schema id>` and the schema-defined
|
|
286
|
+
* content under `attributes`) followed by `PUT /users/{id}/password` with
|
|
287
|
+
* `is_change_required: false`.
|
|
288
288
|
*
|
|
289
289
|
* Defaults mint a unique email per call (email is x-unique per project), which
|
|
290
290
|
* is what makes per-test seeding parallel-safe on a shared instance.
|
|
@@ -294,14 +294,16 @@ async function seedUser(client, context, input = {}) {
|
|
|
294
294
|
const email = input.email ?? fresh.email;
|
|
295
295
|
const password = input.password ?? fresh.password;
|
|
296
296
|
const id = requireString((await client.createUser({
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
schema: context.schemaId,
|
|
298
|
+
attributes: {
|
|
299
|
+
...input.attributes,
|
|
300
|
+
email
|
|
301
|
+
}
|
|
300
302
|
}, { project_id: context.projectId })).id, "user id");
|
|
301
303
|
await client.setUserPassword(id, {
|
|
302
304
|
password,
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
+
is_change_required: false
|
|
306
|
+
});
|
|
305
307
|
return {
|
|
306
308
|
id,
|
|
307
309
|
email,
|
|
@@ -472,7 +474,7 @@ function connectZitadel(handle) {
|
|
|
472
474
|
};
|
|
473
475
|
}
|
|
474
476
|
/**
|
|
475
|
-
* Boot an ephemeral local instance (binary runtime +
|
|
477
|
+
* Boot an ephemeral local instance (binary runtime + SQLite by default, no
|
|
476
478
|
* Docker) and bootstrap a project + default schema + login flow on it. The
|
|
477
479
|
* result can seed loginable password users immediately.
|
|
478
480
|
*/
|
|
@@ -512,4 +514,4 @@ async function startLocalZitadel(options = {}) {
|
|
|
512
514
|
//#endregion
|
|
513
515
|
export { bootstrapProject as a, bootLocalServer as i, startLocalZitadel as n, SESSION_COOKIE_NAME as r, connectZitadel as t };
|
|
514
516
|
|
|
515
|
-
//# sourceMappingURL=src-
|
|
517
|
+
//# sourceMappingURL=src-9dFjTIAw.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-9dFjTIAw.mjs","names":[],"sources":["../src/bootstrap.ts","../src/cli.ts","../src/envelope.ts","../src/ports.ts","../src/lifecycle.ts","../src/seed.ts","../src/session.ts","../src/index.ts"],"sourcesContent":["import { createZitadelClient, type ZitadelClient } from \"@zitadel/api/client\";\nimport {\n DEFAULT_FLOW_SCHEMA_URI,\n getDefaultHumanUserSchema,\n getDefaultLoginFlow,\n type SetupPreset,\n type SetupUseCase,\n} from \"@zitadel/config/defaults\";\n\nexport interface BootstrapProjectOptions {\n baseUrl: string;\n projectName?: string;\n /**\n * Origins of the apps that will proxy to this instance. The backend's\n * origin check rejects forwarded requests from unregistered origins.\n */\n appOrigins?: string[];\n preset?: SetupPreset;\n useCase?: SetupUseCase;\n}\n\nexport interface BootstrappedProject {\n projectId: string;\n projectSecret: string;\n previewSecret?: string;\n schemaId: string;\n flowId: string;\n}\n\nconst DEFAULT_PROJECT_NAME = \"zitadel-testing\";\n\n/**\n * Server-side half of `zitadel setup`, without any file scaffolding:\n * `POST /projects` is unauthenticated and mints the projectSecret used as the\n * bearer for everything else; the schema is uploaded without `$id` so the\n * server assigns an opaque id, which the flow must then reference.\n */\nexport async function bootstrapProject(\n options: BootstrapProjectOptions,\n): Promise<BootstrappedProject> {\n const { baseUrl } = options;\n const unauthenticated = createZitadelClient({ baseUrl });\n const project = (await unauthenticated.createProject({\n name: options.projectName ?? DEFAULT_PROJECT_NAME,\n preview_origins: options.appOrigins ?? [],\n seed_defaults: false,\n } as Parameters<ZitadelClient[\"createProject\"]>[0])) as Record<string, unknown>;\n const projectId = requireString(project.id, \"project id\");\n const projectSecret = requireString(project.project_secret, \"project secret\");\n const previewSecret =\n typeof project.preview_secret === \"string\" ? project.preview_secret : undefined;\n\n const client = createZitadelClient({ baseUrl, token: projectSecret });\n\n const { $id: _templateId, ...schemaBody } = getDefaultHumanUserSchema({\n preset: options.preset,\n useCase: options.useCase,\n }) as { $id?: string } & Record<string, unknown>;\n void _templateId;\n const schema = (await client.createSchema(\n schemaBody as Parameters<ZitadelClient[\"createSchema\"]>[0],\n { project_id: projectId },\n )) as Record<string, unknown>;\n const schemaId = requireString(schema.id, \"schema id\");\n\n const flowBody = getDefaultLoginFlow({\n userSchemaUrl: schemaId,\n preset: options.preset,\n useCase: options.useCase,\n });\n const flow = (await client.createFlowDefinition({\n project_id: projectId,\n schema_uri: DEFAULT_FLOW_SCHEMA_URI,\n flow_definition: flowBody,\n } as Parameters<ZitadelClient[\"createFlowDefinition\"]>[0])) as Record<string, unknown>;\n const flowId = requireString(flow.id, \"flow definition id\");\n\n return { projectId, projectSecret, previewSecret, schemaId, flowId };\n}\n\nexport function requireString(value: unknown, label: string): string {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n throw new Error(`Missing ${label} in server response.`);\n}\n","import { spawn } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\nexport interface RunCliOptions {\n args: string[];\n env?: NodeJS.ProcessEnv;\n /** Test seam / escape hatch: alternative CLI entry script. */\n bin?: string;\n timeoutMs?: number;\n}\n\nexport interface RunCliResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nexport function resolveCliBin(): string {\n const require = createRequire(import.meta.url);\n const pkgPath = require.resolve(\"@zitadel/cli/package.json\");\n const pkg = require(pkgPath) as { bin?: Record<string, string> };\n const rel = pkg.bin?.zitadel;\n if (!rel) {\n throw new Error(\"@zitadel/cli does not declare a `zitadel` bin entry\");\n }\n return join(dirname(pkgPath), rel);\n}\n\nexport function runCli(options: RunCliOptions): Promise<RunCliResult> {\n const bin = options.bin ?? resolveCliBin();\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [bin, ...options.args], {\n env: { ...process.env, ...options.env },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk: string) => {\n stdout += chunk;\n });\n child.stderr.setEncoding(\"utf8\");\n child.stderr.on(\"data\", (chunk: string) => {\n stderr += chunk;\n });\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n reject(\n new Error(\n `zitadel ${options.args[0] ?? \"\"} timed out after ${timeoutMs}ms\\n${tail(stderr)}`,\n ),\n );\n }, timeoutMs);\n timer.unref();\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n reject(error);\n });\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n resolve({ exitCode: code ?? -1, stdout, stderr });\n });\n });\n}\n\nexport function tail(text: string, lines = 20): string {\n return text.split(\"\\n\").slice(-lines).join(\"\\n\").trim();\n}\n","export interface CliEnvelope<TData> {\n cli_version?: string;\n command?: string;\n source?: string;\n status: string;\n data: TData;\n warnings?: string[];\n /** Error envelopes (`status: \"error\"`) carry remediation guidance. */\n code?: string;\n message?: string;\n hint?: string;\n next_commands?: string[];\n}\n\n/**\n * Render an error envelope's remediation fields for humans — the CLI's\n * `hint`/`next_commands` are the actionable part of a failure (e.g. \"Reinstall\n * @zitadel/cli so npm can install @zitadel/server\"), so surface them instead\n * of a raw stdout dump. Returns undefined when the envelope has no message.\n */\nexport function describeEnvelopeError(envelope: CliEnvelope<unknown>): string | undefined {\n if (typeof envelope.message !== \"string\" || envelope.message.length === 0) {\n return undefined;\n }\n const lines = [envelope.code ? `${envelope.code}: ${envelope.message}` : envelope.message];\n if (envelope.hint) {\n lines.push(`hint: ${envelope.hint}`);\n }\n if (envelope.next_commands && envelope.next_commands.length > 0) {\n lines.push(`next: ${envelope.next_commands.join(\" | \")}`);\n }\n return lines.join(\"\\n\");\n}\n\nexport interface StartEnvelopeData {\n runtime: {\n backend: string;\n pid: number;\n port: number;\n data_dir: string;\n log_path: string;\n };\n urls: {\n api: string;\n console: string;\n login: string;\n };\n}\n\nexport function parseCliEnvelope<TData>(stdout: string, context: string): CliEnvelope<TData> {\n const start = stdout.indexOf(\"{\");\n const end = stdout.lastIndexOf(\"}\");\n if (start === -1 || end <= start) {\n throw new Error(\n `${context}: expected a JSON envelope on stdout, got:\\n${stdout.trim() || \"(empty)\"}`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(stdout.slice(start, end + 1));\n } catch (error) {\n throw new Error(\n `${context}: failed to parse JSON envelope: ${(error as Error).message}\\n${stdout.trim()}`,\n { cause: error },\n );\n }\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof (parsed as { status?: unknown }).status !== \"string\"\n ) {\n throw new Error(`${context}: stdout JSON is not a CLI envelope:\\n${stdout.trim()}`);\n }\n return parsed as CliEnvelope<TData>;\n}\n","import { createServer } from \"node:net\";\n\n/**\n * Ask the OS for a free TCP port. The port is released before returning, so a\n * racing process could grab it; the CLI's own preflight surfaces that as\n * E_PORT_IN_USE, which is loud rather than corrupting.\n */\nexport function getFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.unref();\n server.on(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n server.close();\n reject(new Error(\"could not determine a free port\"));\n return;\n }\n const { port } = address;\n server.close((err) => {\n if (err) {\n reject(err);\n return;\n }\n resolve(port);\n });\n });\n });\n}\n","import { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { runCli, tail, type RunCliResult } from \"./cli\";\nimport {\n describeEnvelopeError,\n parseCliEnvelope,\n type CliEnvelope,\n type StartEnvelopeData,\n} from \"./envelope\";\nimport { getFreePort } from \"./ports\";\nimport type { LocalZitadelRuntime } from \"./types\";\n\nexport interface BootServerOptions {\n /** TCP port for the instance; defaults to an OS-assigned free port. */\n port?: number;\n /**\n * State directory. Defaults to a fresh temp dir that is removed on stop;\n * a caller-provided dir is never removed.\n */\n dir?: string;\n /** Forwarded as ZITADEL_SERVER_BINARY (in-repo runs use dist/server/nextgen). */\n serverBinary?: string;\n /** Keep the owned temp dir after stop (debugging). */\n keep?: boolean;\n /** Test seam: alternative CLI entry script. */\n cliBin?: string;\n timeoutMs?: number;\n}\n\nexport interface BootedServer {\n baseUrl: string;\n runtime: LocalZitadelRuntime;\n stop(): Promise<void>;\n}\n\n/**\n * Boot an ephemeral local server by shelling out to `zitadel start` and parse\n * its JSON envelope. The CLI owns the subtle parts (port preflight, health\n * wait, process-group stop), so this module stays a thin adapter; swapping it\n * for direct library calls later must not change the shape returned here.\n */\nexport async function bootLocalServer(options: BootServerOptions = {}): Promise<BootedServer> {\n const ownsDir = options.dir === undefined;\n const dir = options.dir ?? (await mkdtemp(join(tmpdir(), \"zitadel-testing-\")));\n const port = options.port ?? (await getFreePort());\n const env: NodeJS.ProcessEnv = {};\n if (options.serverBinary) {\n env.ZITADEL_SERVER_BINARY = options.serverBinary;\n }\n\n const result = await runCli({\n args: [\"start\", \"--port\", String(port), \"--non-interactive\", \"--json\", \"-c\", dir],\n bin: options.cliBin,\n env,\n timeoutMs: options.timeoutMs,\n });\n if (result.exitCode !== 0) {\n // Keep the dir on failure: server.log inside it is the diagnostic.\n throw new Error(\n `zitadel start exited with code ${result.exitCode}.\\n` +\n `${failureDetail(result)}\\n` +\n `state dir kept for inspection: ${dir}`,\n );\n }\n const stopViaCli = async (): Promise<void> => {\n const stopResult = await runCli({\n args: [\"stop\", \"--non-interactive\", \"--json\", \"-c\", dir],\n bin: options.cliBin,\n env,\n timeoutMs: options.timeoutMs,\n });\n if (stopResult.exitCode !== 0) {\n throw new Error(\n `zitadel stop exited with code ${stopResult.exitCode}.\\n` +\n `${failureDetail(stopResult)}\\n` +\n `state dir kept for inspection: ${dir}`,\n );\n }\n };\n\n let envelope: CliEnvelope<StartEnvelopeData>;\n try {\n envelope = parseCliEnvelope<StartEnvelopeData>(result.stdout, \"zitadel start\");\n if (envelope.status !== \"ok\") {\n throw new Error(\n `zitadel start reported status \"${envelope.status}\":\\n` +\n `${describeEnvelopeError(envelope) ?? tail(result.stdout)}`,\n );\n }\n } catch (error) {\n const startError = new Error(\n `zitadel start produced unusable output.\\n` +\n `reason: ${error instanceof Error ? error.message : String(error)}\\n` +\n `stdout: ${tail(result.stdout) || \"(empty)\"}\\n` +\n `stderr: ${tail(result.stderr) || \"(empty)\"}\\n` +\n `state dir kept for inspection: ${dir}`,\n { cause: error },\n );\n // start exited 0, so a server may well be running despite the unusable\n // output — stop it instead of orphaning it.\n try {\n await stopViaCli();\n } catch (stopError) {\n // Both errors are preserved in AggregateError.errors, which the rule\n // below cannot model.\n // oxlint-disable-next-line preserve-caught-error\n throw new AggregateError(\n [startError, stopError],\n `${startError.message}\\nStopping the possibly-running instance also failed: ${\n stopError instanceof Error ? stopError.message : String(stopError)\n }`,\n );\n }\n throw startError;\n }\n\n const { runtime, urls } = envelope.data;\n const runStop = async (): Promise<void> => {\n await stopViaCli();\n if (ownsDir && !options.keep) {\n await rm(dir, { recursive: true, force: true });\n }\n };\n // Memoize the in-flight stop so concurrent callers await the same cleanup,\n // and reset on failure so a failed stop can be retried instead of silently\n // leaving the server behind.\n let stopPromise: Promise<void> | undefined;\n const stop = (): Promise<void> => {\n stopPromise ??= runStop().catch((error: unknown) => {\n stopPromise = undefined;\n throw error;\n });\n return stopPromise;\n };\n\n return {\n baseUrl: urls.api,\n runtime: {\n port: runtime.port,\n pid: runtime.pid,\n dir,\n logPath: runtime.log_path,\n },\n stop,\n };\n}\n\n/**\n * A failed CLI run usually still prints an error envelope; its\n * message/hint/next_commands beat raw output tails (e.g. a fresh install\n * missing @zitadel/server gets \"Reinstall @zitadel/cli\" instead of a stack).\n */\nfunction failureDetail(result: RunCliResult): string {\n try {\n const described = describeEnvelopeError(parseCliEnvelope<unknown>(result.stdout, \"zitadel\"));\n if (described) {\n return described;\n }\n } catch {\n // stdout carried no envelope; fall back to the raw tails.\n }\n return `stdout: ${tail(result.stdout) || \"(empty)\"}\\nstderr: ${tail(result.stderr) || \"(empty)\"}`;\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport type { ZitadelClient } from \"@zitadel/api/client\";\n\nimport { requireString } from \"./bootstrap\";\nimport type { Identity, SeededUser, SeedUserInput, SeedUsersTemplate } from \"./types\";\n\nexport interface SeedContext {\n projectId: string;\n schemaId: string;\n}\n\n/**\n * A unique unused email + password. Nothing is created on the instance —\n * this is the input for registration-flow specs, which must prove the flow\n * creates the user.\n */\nexport function identity(): Identity {\n return {\n email: `e2e-${randomUUID().slice(0, 8)}@example.com`,\n password: `Pw!${randomUUID()}`,\n };\n}\n\n/**\n * Create a user that can immediately complete the password login flow:\n * `POST /users` (the body carries `schema: <schema id>` and the schema-defined\n * content under `attributes`) followed by `PUT /users/{id}/password` with\n * `is_change_required: false`.\n *\n * Defaults mint a unique email per call (email is x-unique per project), which\n * is what makes per-test seeding parallel-safe on a shared instance.\n */\nexport async function seedUser(\n client: ZitadelClient,\n context: SeedContext,\n input: SeedUserInput = {},\n): Promise<SeededUser> {\n const fresh = identity();\n const email = input.email ?? fresh.email;\n const password = input.password ?? fresh.password;\n // `email` wins over the templated attributes: the returned SeededUser must\n // never disagree with what was actually created, since a silently overridden\n // email would yield credentials that cannot log in.\n const user = (await client.createUser(\n {\n schema: context.schemaId,\n attributes: { ...input.attributes, email },\n },\n { project_id: context.projectId },\n )) as Record<string, unknown>;\n const id = requireString(user.id, \"user id\");\n await client.setUserPassword(id, { password, is_change_required: false });\n return { id, email, password };\n}\n\n/**\n * Seed `count` users sequentially. The template makes fixture data\n * deterministic per index (stable emails/names keep screenshot diffs about\n * code, not reshuffled data — the `console:dev-real` pattern); untemplated\n * fields fall back to the unique defaults. Name-like attributes need a\n * schema that declares them (`useCase: \"consumer\"` or wider).\n */\nexport async function seedUsers(\n client: ZitadelClient,\n context: SeedContext,\n count: number,\n template: SeedUsersTemplate = {},\n): Promise<SeededUser[]> {\n const users: SeededUser[] = [];\n for (let index = 0; index < count; index += 1) {\n users.push(\n await seedUser(client, context, {\n email: template.email?.(index),\n password: template.password?.(index),\n attributes: template.attributes?.(index),\n }),\n );\n }\n return users;\n}\n","import type { ZitadelClient } from \"@zitadel/api/client\";\nimport type { CreateFlow201, CreateFlow201StepFieldsItem } from \"@zitadel/api/generated/model\";\n\nimport type { SeedContext } from \"./seed\";\nimport type { InstanceHandle, MintedSession, SeededUser } from \"./types\";\n\n/** Mirrors the server's session cookie (internal/api/session.go). */\nexport const SESSION_COOKIE_NAME = \"__nextgen_session\";\n\nconst MAX_FLOW_STEPS = 6;\n\nexport interface MintSessionOptions {\n /** Forwarded to `POST /flow`; the project's default flow when omitted. */\n flowDefinitionName?: string;\n /** Origin header for flow calls (the project's origin check enforces it). */\n origin?: string;\n}\n\n/**\n * Drive the real login flow headlessly for a seeded password user and\n * exchange the terminal handoff for a session: exactly what `<zitadel-login>`\n * does, minus the rendering. Supports flows whose steps only ask for the\n * user's email and password (the shipped `password-first` presets); any step\n * demanding more — a challenge, an unknown field — fails loudly by design.\n *\n * Flow calls use raw fetch instead of the typed client because the flow is\n * stateless through the sealed `_zflow` cookie (internal/api/flow.go): every\n * response re-seals the flow state into Set-Cookie, and submits are rejected\n * without it. Browsers round-trip it implicitly; here a one-cookie jar does.\n */\nexport async function mintSession(\n client: ZitadelClient,\n handle: Pick<InstanceHandle, \"baseUrl\" | \"projectSecret\">,\n context: SeedContext,\n user: SeededUser,\n options: MintSessionOptions = {},\n): Promise<MintedSession> {\n const values: Record<string, string> = { email: user.email, password: user.password };\n const jar = new FlowCookieJar();\n const origin = options.origin;\n\n let response = await flowFetch(handle, jar, origin, \"/flow\", {\n project_id: context.projectId,\n purpose: \"login\",\n ...(options.flowDefinitionName ? { flow_definition_name: options.flowDefinitionName } : {}),\n });\n\n for (let hop = 0; hop < MAX_FLOW_STEPS; hop += 1) {\n if (response.handoff_token) {\n const exchanged = await client.exchangeHandoff(\n { handoff_token: response.handoff_token },\n { project_id: context.projectId },\n );\n return {\n user,\n sessionToken: exchanged.session_token,\n expiresAt: exchanged.session.expires_at,\n cookie: {\n name: SESSION_COOKIE_NAME,\n value: exchanged.session_token,\n httpOnly: true,\n secure: true,\n sameSite: \"Lax\",\n path: \"/\",\n },\n };\n }\n response = await flowFetch(handle, jar, origin, `/flow/${encodeURIComponent(response.id)}/submit`, {\n session_token: response.session_token,\n action: \"submit\",\n fields: collectFields(response, values),\n });\n }\n\n throw new Error(\n `seed.session: flow did not complete within ${MAX_FLOW_STEPS} steps ` +\n `(last step: ${describeStep(response)}).`,\n );\n}\n\n/** One-cookie jar for the sealed `_zflow` flow-state cookie. */\nclass FlowCookieJar {\n private cookie: string | undefined;\n\n absorb(response: Response): void {\n for (const raw of response.headers.getSetCookie()) {\n const [pair] = raw.split(\";\", 1);\n if (pair?.startsWith(\"_zflow=\")) {\n this.cookie = pair;\n }\n }\n }\n\n header(): Record<string, string> {\n return this.cookie ? { cookie: this.cookie } : {};\n }\n}\n\nasync function flowFetch(\n handle: Pick<InstanceHandle, \"baseUrl\" | \"projectSecret\">,\n jar: FlowCookieJar,\n origin: string | undefined,\n path: string,\n body: Record<string, unknown>,\n): Promise<CreateFlow201> {\n const response = await fetch(`${handle.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${handle.projectSecret}`,\n // The project's origin allowlist applies to flow calls; send the app\n // origin the way a browser request through the app would carry it.\n ...(origin ? { origin } : {}),\n ...jar.header(),\n },\n body: JSON.stringify(body),\n });\n jar.absorb(response);\n const parsed = (await response.json().catch(() => undefined)) as CreateFlow201 | undefined;\n if (!response.ok || !parsed) {\n const detail =\n parsed && typeof parsed === \"object\" ? ` — ${JSON.stringify(parsed)}` : \"\";\n // An origin-allowlist rejection without an Origin header is a\n // configuration gap, not a flow problem — say how to close it. (No eager\n // check: a project with an empty allowlist may accept originless calls.)\n const hint =\n !origin && /origin/i.test(detail)\n ? \"\\nNo Origin header was sent: pass `origin` to seedSession() (the Playwright \" +\n \"fixtures pass the suite's baseURL) or `appOrigins` to startLocalZitadel().\"\n : \"\";\n throw new Error(`seed.session: POST ${path} returned ${response.status}${detail}${hint}`);\n }\n return parsed;\n}\n\n/**\n * Fill exactly the fields the current step declares — the orchestrator's\n * convention — from the known email/password values. An unknown required\n * field means this flow needs more than a password login can provide.\n */\nfunction collectFields(response: CreateFlow201, values: Record<string, string>): Record<string, string> {\n const fields: Record<string, string> = {};\n for (const field of response.step.fields ?? []) {\n const value = values[fieldKey(field)];\n if (value === undefined) {\n throw new Error(\n `seed.session supports password flows only; step ${describeStep(response)} ` +\n `declares field \"${field.name}\", which the kit cannot fill. ` +\n `Log in through the UI for flows with additional factors.`,\n );\n }\n fields[field.name] = value;\n }\n return fields;\n}\n\n/**\n * Steps name credential fields with schema pointers (e.g.\n * `x-auth-methods#password`); match on the trailing segment so the value map\n * stays the plain `{ email, password }` a caller thinks in.\n */\nfunction fieldKey(field: CreateFlow201StepFieldsItem): string {\n const name = field.name;\n const tail = name.split(/[#/.]/).at(-1) ?? name;\n return tail.toLowerCase();\n}\n\nfunction describeStep(response: CreateFlow201): string {\n const name = response.step.name ?? \"(unnamed)\";\n const declared = (response.step.fields ?? []).map((field) => field.name).join(\", \");\n return `\"${name}\"${declared ? ` [fields: ${declared}]` : \"\"}`;\n}\n","import { createZitadelClient } from \"@zitadel/api/client\";\n\nimport { applyAppEnvTemplate, nextAppEnv } from \"./app-env\";\nimport { bootstrapProject, type BootstrapProjectOptions } from \"./bootstrap\";\nimport { bootLocalServer, type BootServerOptions } from \"./lifecycle\";\nimport { identity, seedUser, seedUsers } from \"./seed\";\nimport { mintSession } from \"./session\";\nimport type { ConnectedZitadel, InstanceHandle, LocalZitadel } from \"./types\";\n\nexport type StartLocalZitadelOptions = BootServerOptions &\n Omit<BootstrapProjectOptions, \"baseUrl\">;\n\n/**\n * Attach to an already-bootstrapped instance/project. Lifecycle-free on\n * purpose: this is the entry point for Playwright workers (via the handshake\n * file) and, later, for seeding remote instances.\n */\nexport function connectZitadel(handle: InstanceHandle): ConnectedZitadel {\n const api = createZitadelClient({ baseUrl: handle.baseUrl, token: handle.projectSecret });\n const context = { projectId: handle.projectId, schemaId: handle.schemaId };\n const connected: ConnectedZitadel = {\n handle,\n api,\n // The Next-shaped convenience view; other frameworks apply their own\n // template to `handle` (see AppEnvTemplate).\n appEnv: applyAppEnvTemplate(nextAppEnv, handle),\n seedUser: (input) => seedUser(api, context, input),\n seedUsers: (count, template) => seedUsers(api, context, count, template),\n identity,\n seedSession: async (input = {}) => {\n const { user: existing, flowDefinitionName, origin, ...userInput } = input;\n const user = existing ?? (await seedUser(api, context, userInput));\n return mintSession(api, handle, context, user, {\n flowDefinitionName,\n origin: origin ?? handle.appOrigin,\n });\n },\n };\n return connected;\n}\n\n/**\n * Boot an ephemeral local instance (binary runtime + SQLite by default, no\n * Docker) and bootstrap a project + default schema + login flow on it. The\n * result can seed loginable password users immediately.\n */\nexport async function startLocalZitadel(\n options: StartLocalZitadelOptions = {},\n): Promise<LocalZitadel> {\n const server = await bootLocalServer(options);\n let bootstrapped;\n try {\n bootstrapped = await bootstrapProject({\n baseUrl: server.baseUrl,\n projectName: options.projectName,\n appOrigins: options.appOrigins,\n preset: options.preset,\n useCase: options.useCase,\n });\n } catch (error) {\n try {\n await server.stop();\n } catch (stopError) {\n // Both errors are preserved in AggregateError.errors, which the rule\n // below cannot model.\n // oxlint-disable-next-line preserve-caught-error\n throw new AggregateError(\n [error, stopError],\n \"bootstrap failed, and stopping the booted instance also failed\",\n );\n }\n throw error;\n }\n const handle: InstanceHandle = {\n baseUrl: server.baseUrl,\n projectId: bootstrapped.projectId,\n projectSecret: bootstrapped.projectSecret,\n schemaId: bootstrapped.schemaId,\n previewSecret: bootstrapped.previewSecret,\n appOrigin: options.appOrigins?.[0],\n };\n return {\n ...connectZitadel(handle),\n runtime: server.runtime,\n stop: server.stop,\n [Symbol.asyncDispose]: server.stop,\n };\n}\n\nexport { applyAppEnvTemplate, nextAppEnv } from \"./app-env\";\nexport type { AppEnvTemplate } from \"./app-env\";\nexport { bootstrapProject } from \"./bootstrap\";\nexport type { BootstrapProjectOptions, BootstrappedProject } from \"./bootstrap\";\nexport { readHandshakeSync, waitForHandshake, writeHandshake } from \"./handshake\";\nexport { bootLocalServer } from \"./lifecycle\";\nexport type { BootedServer, BootServerOptions } from \"./lifecycle\";\nexport { SESSION_COOKIE_NAME } from \"./session\";\nexport type {\n ConnectedZitadel,\n Identity,\n InstanceHandle,\n LocalZitadel,\n LocalZitadelRuntime,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n SessionCookie,\n} from \"./types\";\n"],"mappings":"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"}
|