@zitadel/testing 0.1.0-alpha.18 → 1.0.0-alpha.20

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 CHANGED
@@ -70,6 +70,24 @@ apps; the console maps the same fields to `VITE_*`/`CONSOLE_*` names instead.
70
70
  The fixtures find the instance through `ZITADEL_TESTING_HANDSHAKE`, which
71
71
  `withZitadel()` points at its handshake file.
72
72
 
73
+ `app` is optional. Omit it when the instance itself serves the app — the
74
+ Zitadel binary embeds the console and hosted sign-in at `/ui/console/` and
75
+ `/ui/login/`, so a suite testing those surfaces has no second server to boot.
76
+ Only the instance entry is generated, and `appOrigin` must then be the
77
+ instance's own local origin:
78
+
79
+ ```ts
80
+ export default defineConfig({
81
+ testDir: "./e2e",
82
+ use: { baseURL: "http://localhost:8092" },
83
+ ...withZitadel({
84
+ configDir: import.meta.dirname,
85
+ port: 8092,
86
+ appOrigin: "http://localhost:8092", // the instance is the app server
87
+ }),
88
+ });
89
+ ```
90
+
73
91
  ### Start tests authenticated
74
92
 
75
93
  Most app tests don't want to re-test login. `authenticatedPage` seeds a user,
@@ -181,11 +199,11 @@ const z = await startLocalZitadel({
181
199
  appOrigins, // registered as the project's preview_origins
182
200
  useCase, // "minimal" (default) | "consumer" | "business"
183
201
  preset, // "password-first" (default)
184
- serverBinary, // ZITADEL_SERVER_BINARY override (in-repo: dist/server/nextgen)
202
+ serverBinary, // ZITADEL_SERVER_BINARY override (path to a built server binary)
185
203
  keep, // keep the temp dir for debugging
186
204
  });
187
205
 
188
- z.handle; // serializable: { baseUrl, projectId, projectSecret, schemaId, previewSecret? }
206
+ z.handle; // serializable: { baseUrl, projectId, projectSecret, schemaId, previewSecret?, platform? }
189
207
  z.api; // authenticated @zitadel/api client (bearer = projectSecret)
190
208
  z.appEnv; // { ZITADEL_URL, NEXT_PUBLIC_ZITADEL_PROJECT_ID, ZITADEL_PROJECT_SECRET }
191
209
  await z.seedUser({ email?, password?, attributes? }); // → { id, email, password }
@@ -219,9 +237,48 @@ above.
219
237
  bearer for everything else) → `POST /schemas` (server assigns the schema id)
220
238
  → `POST /flow_definitions` (default login flow pinned to that schema id).
221
239
  Templates come from `@zitadel/config/defaults`.
222
- - **Seeding** is `POST /users` (the body carries `$schema: <schema id>`) +
240
+ - **Seeding** is `POST /users` (the body names the schema in `schema` and puts
241
+ the schema-defined content under `attributes`) +
223
242
  `PUT /users/{id}/password` with `is_change_required: false`.
224
243
 
244
+ ## Credentials: the boot contract
245
+
246
+ The kit's boot contract is the sanctioned way tests and dev loops obtain
247
+ credentials: they come from the boot contract, never from a seed default —
248
+ the rule root ADR 053 §9 sets. The kit owns the server process and its
249
+ database, captures each credential from provisioning
250
+ output at the moment the server mints it, and exposes it predictably on
251
+ `handle`:
252
+
253
+ - `handle.projectSecret` — the seeded customer project's operator credential,
254
+ captured from `POST /projects` (the server returns it exactly once, at
255
+ creation). Bearer behind `z.api` and every seed op.
256
+ - `handle.previewSecret` — the same project's browser-plane credential (the
257
+ publishable-key predecessor from root ADR 036).
258
+ - `handle.platform` — the platform-plane slot (`PlatformCredentials`): the
259
+ reserved platform project's id and publishable key. **Stub today**: the
260
+ server's platform-project provisioner (Console ADR 0004 §2) has not
261
+ landed, so `startLocalZitadel` never populates it yet. Only the fields the
262
+ contract already guarantees are declared; credentials whose design is
263
+ still open — the platform automation principal (deferred to a future PAT /
264
+ service-user decision) and a boot-minted operator session — join as
265
+ optional fields once they exist, which is additive and churn-free.
266
+
267
+ What this rules out, deliberately: there is no server `--test-mode`, no
268
+ seed-document credential flag, and no other server-side door that mints
269
+ deterministic credentials — and none may be added. The production seed
270
+ contract stays credential-free; predictable test access is this kit's job,
271
+ done entirely with what provisioning already returns. If a credential the
272
+ kit needs is not capturable at provisioning time, the fix is to extend the
273
+ provisioning contract the boot path drives — never a server flag, and never
274
+ direct database writes, which would bypass the provisioner and break seed
275
+ ops for `connectZitadel` targets.
276
+
277
+ The in-repo dev loop follows the same contract: `moon run console:dev-real`
278
+ boots, seeds, and threads `handle.projectSecret` into the console dev proxy's
279
+ `CONSOLE_PROJECT_SECRET` itself; `--seed-only` prints the same variables for
280
+ a separately-started dev server. Nothing to remember or export.
281
+
225
282
  ## Parallelism model
226
283
 
227
284
  **One instance per suite, one fresh user per test.** Emails are unique per
@@ -229,7 +286,7 @@ project, so per-test `seed.user()` calls are isolation enough for login-flow
229
286
  tests, and tests run fully parallel against the shared instance
230
287
  (demonstrated by `demo-next-e2e:e2e-real`, 2 workers).
231
288
 
232
- Typical timings with the SQLite local default (dev build, July 2026 — estimates, not a fresh remeasure):
289
+ Typical timings with the SQLite local default (dated estimates from a July 2026 dev build orders of magnitude, not a benchmark; remeasure locally before relying on them):
233
290
 
234
291
  | Operation | Time |
235
292
  | --- | --- |
@@ -259,10 +316,12 @@ Customer installs get the published server binary through `@zitadel/server`'s
259
316
  platform packages; the in-repo workspace carries no such binary, so the repo's
260
317
  own suites run `moon run server:build` and point the kit at the result via
261
318
  `ZITADEL_SERVER_BINARY` (the `withZitadel` option `zitadel.serverBinary` /
262
- `serverBinaryHint` exists for this). The in-repo moon tasks set
319
+ `serverBinaryHint` exists for this). Most in-repo moon tasks set
263
320
  `NEXTGEN_SERVER_LOGIN_ENABLED=false` / `NEXTGEN_SERVER_CONSOLE_ENABLED=false`
264
- the suites drive the app-embedded login, not the server-hosted `/ui/*`.
265
- Customer installs need none of this.
321
+ because they drive the app-embedded login, not the server-hosted `/ui/*`; the
322
+ exception is `console-e2e:e2e-embedded`, which keeps both surfaces on and
323
+ omits `app` — the binary-served `/ui/*` pages are its subject. Customer
324
+ installs need none of this.
266
325
 
267
326
  ## Known limitations
268
327
 
@@ -1,13 +1,48 @@
1
1
  import { ZitadelClient } from "@zitadel/api/client";
2
2
 
3
3
  //#region src/types.d.ts
4
+ /**
5
+ * Credentials of the reserved platform project, captured by the kit at
6
+ * provisioning time. The shape follows root ADR 053 §9 and Console ADR 0004
7
+ * §2 (both Proposed): bootstrap mints **no platform project secret** — the
8
+ * publishable key is the only default credential, and test infrastructure
9
+ * gets its credentials from the testkit's boot contract, not from seed
10
+ * defaults.
11
+ *
12
+ * Stub today: the server's platform-project provisioner does not exist yet,
13
+ * so `startLocalZitadel` never populates this field. Only the fields §9
14
+ * itself guarantees are declared. Credentials whose design is still open —
15
+ * the platform automation principal (deferred to a future PAT /
16
+ * service-user decision) and a boot-minted operator session — join as
17
+ * optional fields once they exist: additions are churn-free, removals are
18
+ * not, so nothing is pre-declared here.
19
+ */
20
+ interface PlatformCredentials {
21
+ /** Reserved platform project id — the Console sign-in target. */
22
+ projectId: string;
23
+ /**
24
+ * Browser-safe publishable key. Required: it is the one credential
25
+ * bootstrap always provisions (sign-in needs it), so a platform block
26
+ * without it is malformed.
27
+ */
28
+ publishableKey: string;
29
+ }
4
30
  /**
5
31
  * Serializable description of a bootstrapped instance + project. This is the
6
- * contract that crosses process boundaries (boot script -> Playwright workers).
32
+ * contract that crosses process boundaries (boot script -> Playwright workers)
33
+ * — and the credential surface of the kit: every credential the server mints
34
+ * during provisioning is captured here, so tests and dev loops read them from
35
+ * the handle instead of hand-set environment or a server-side test door (a
36
+ * server flag that mints deterministic credentials must not exist).
7
37
  */
8
38
  interface InstanceHandle {
9
39
  baseUrl: string;
10
40
  projectId: string;
41
+ /**
42
+ * The seeded customer project's operator credential, captured from
43
+ * `POST /projects` — the server returns it exactly once, at creation.
44
+ * Bearer for the management API (`api`) and every seed op.
45
+ */
11
46
  projectSecret: string;
12
47
  /**
13
48
  * First registered app origin. Flow submissions enforce the project's
@@ -16,12 +51,23 @@ interface InstanceHandle {
16
51
  */
17
52
  appOrigin?: string;
18
53
  /**
19
- * Server-assigned id of the seeded user schema. User documents must
20
- * reference it via their `$schema` field, so seeding needs it alongside the
21
- * project credential.
54
+ * Server-assigned id of the seeded user schema. A user names it in `schema`
55
+ * and puts the schema-defined content under `attributes`, so seeding needs
56
+ * it alongside the project credential.
22
57
  */
23
58
  schemaId: string;
59
+ /**
60
+ * The same project's browser-plane credential (the publishable-key
61
+ * predecessor from root ADR 036), also captured at creation.
62
+ */
24
63
  previewSecret?: string;
64
+ /**
65
+ * Platform-plane credential slot. Unpopulated until the platform-project
66
+ * provisioner (Console ADR 0004 §2) lands server-side; see
67
+ * `PlatformCredentials` for what it carries, why it stays empty today, and
68
+ * why the still-undecided credentials are left out of it.
69
+ */
70
+ platform?: PlatformCredentials;
25
71
  }
26
72
  interface SeedUserInput {
27
73
  email?: string;
@@ -103,13 +149,19 @@ interface LocalZitadel extends ConnectedZitadel, AsyncDisposable {
103
149
  }
104
150
  //#endregion
105
151
  //#region src/app-env.d.ts
152
+ /**
153
+ * Handle fields a template may reference: the flat string facts. Structured
154
+ * fields (e.g. `platform`) are not env-var material — consumers read those
155
+ * from the handle directly.
156
+ */
157
+ type StringHandleField = { [K in keyof InstanceHandle]-?: InstanceHandle[K] extends string | undefined ? K : never }[keyof InstanceHandle];
106
158
  /**
107
159
  * Declarative mapping from an app's env var names to InstanceHandle fields.
108
160
  * A template (not a function) so it can cross process boundaries: the
109
161
  * Playwright config serializes it into the app-runner's environment, where it
110
162
  * is applied to the handle read from the handshake file.
111
163
  */
112
- type AppEnvTemplate = Record<string, keyof InstanceHandle>;
164
+ type AppEnvTemplate = Record<string, StringHandleField>;
113
165
  /**
114
166
  * The env shape `@zitadel/sdk-next` apps read. Other frameworks pass their own
115
167
  * template — the console maps the same handle fields to `VITE_*`/`CONSOLE_*`
@@ -118,5 +170,5 @@ type AppEnvTemplate = Record<string, keyof InstanceHandle>;
118
170
  declare const nextAppEnv: AppEnvTemplate;
119
171
  declare function applyAppEnvTemplate(template: AppEnvTemplate, handle: InstanceHandle): Record<string, string>;
120
172
  //#endregion
121
- export { Identity as a, LocalZitadelRuntime as c, SeedUserInput as d, SeedUsersTemplate as f, ConnectedZitadel as i, MintedSession as l, SessionCookie as m, applyAppEnvTemplate as n, InstanceHandle as o, SeededUser as p, nextAppEnv as r, LocalZitadel as s, AppEnvTemplate as t, SeedSessionInput as u };
122
- //# sourceMappingURL=app-env-D3W0GYhA.d.mts.map
173
+ export { Identity as a, LocalZitadelRuntime as c, SeedSessionInput as d, SeedUserInput as f, SessionCookie as h, ConnectedZitadel as i, MintedSession as l, SeededUser as m, applyAppEnvTemplate as n, InstanceHandle as o, SeedUsersTemplate as p, nextAppEnv as r, LocalZitadel as s, AppEnvTemplate as t, PlatformCredentials as u };
174
+ //# sourceMappingURL=app-env-DlrV_ePR.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-env-DlrV_ePR.d.mts","names":[],"sources":["../src/types.ts","../src/app-env.ts"],"mappings":";;;;;AAkBA;;;;;AAmBA;;;;;;;;;UAnBiB,mBAAA;EAmDf;EAjDA,SAAA;EAiD8B;;AAGhC;;;EA9CE,cAAA;AAAA;;;;;;AAqDF;;;UA1CiB,cAAA;EACf,OAAA;EACA,SAAA;EA2CA;;;AAIF;;EAzCE,aAAA;EA0CA;;AAKF;;;EAzCE,SAAA;EA0CA;;;;;EApCA,QAAA;EAyCI;;AAGN;;EAvCE,aAAA;EA4CqB;;;;;;EArCrB,QAAA,GAAW,mBAAA;AAAA;AAAA,UAGI,aAAA;EACf,KAAA;EACA,QAAA;;EAEA,UAAA,GAAa,MAAA;AAAA;AAAA,UAGE,UAAA;EACf,EAAA;EACA,KAAA;EACA,QAAA;AAAA;;UAIe,QAAA;EACf,KAAA;EACA,QAAA;AAAA;;UAIe,aAAA;EACf,IAAA;EACA,KAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;AAAA;AAAA,UAGe,aAAA;EACf,IAAA,EAAM,UAAA;;EAEN,YAAA;EACA,SAAA;EACA,MAAA,EAAQ,aAAA;AAAA;AAAA,UAGO,gBAAA,SAAyB,aAAA;EAyBP;EAvBjC,IAAA,GAAO,UAAA;EAyByD;EAvBhE,kBAAA;EAyBY;;;;;EAnBZ,MAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA,IAAS,KAAA;EACT,QAAA,IAAY,KAAA;EACZ,UAAA,IAAc,KAAA,aAAkB,MAAA;AAAA;AAAA,UAGjB,gBAAA;EACf,MAAA,EAAQ,cAAA;EAKC;EAHT,GAAA,EAAK,aAAA;EAGoC;EADzC,MAAA,EAAQ,MAAA;EACR,QAAA,CAAS,KAAA,GAAQ,aAAA,GAAgB,OAAA,CAAQ,UAAA;EAEL;EAApC,SAAA,CAAU,KAAA,UAAe,QAAA,GAAW,iBAAA,GAAoB,OAAA,CAAQ,UAAA;EAAR;EAExD,QAAA,IAAY,QAAA;EAAZ;;;;;EAMA,WAAA,CAAY,KAAA,GAAQ,gBAAA,GAAmB,OAAA,CAAQ,aAAA;AAAA;AAAA,UAGhC,mBAAA;EACf,IAAA;EACA,GAAA;EAFkC;EAIlC,GAAA;EACA,OAAA;AAAA;AAAA,UAGe,YAAA,SAAqB,gBAAA,EAAkB,eAAA;EACtD,OAAA,EAAS,mBAAA;EACT,IAAA,IAAQ,OAAA;AAAA;;;;;AA3IV;;;KCXK,iBAAA,iBACS,cAAA,KAAmB,cAAA,CAAe,CAAA,+BAAgC,CAAA,iBACxE,cAAA;;;;;;;KAQI,cAAA,GAAiB,MAAA,SAAe,iBAAA;;;;;;cAO/B,UAAA,EAAY,cAAA;AAAA,iBAMT,mBAAA,CACd,QAAA,EAAU,cAAA,EACV,MAAA,EAAQ,cAAA,GACP,MAAA"}
@@ -1,4 +1,4 @@
1
- const require_handshake = require("./handshake-CRKcgkfN.cjs");
1
+ const require_handshake = require("./handshake-BOsVBPtn.cjs");
2
2
  const require_orchestration = require("./orchestration-D7QBgQ0l.cjs");
3
3
  let node_child_process = require("node:child_process");
4
4
  //#region src/app-runner.ts
@@ -1,4 +1,4 @@
1
- import { i as applyAppEnvTemplate, n as waitForHandshake } from "./handshake-ClzWvG8z.mjs";
1
+ import { i as applyAppEnvTemplate, n as waitForHandshake } from "./handshake-BnAPXC6s.mjs";
2
2
  import { i as parseAppRunnerConfig, o as requireHandshakePath, t as APP_RUNNER_CONFIG_ENV } from "./orchestration-C640_1Lg.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/app-runner.ts
@@ -62,8 +62,21 @@ function validateHandle(value, source) {
62
62
  "schemaId"
63
63
  ]) if (typeof handle[field] !== "string" || handle[field].length === 0) throw new Error(`handshake file ${source} is missing "${field}"`);
64
64
  if (!URL.canParse(handle.baseUrl)) throw new Error(`handshake file ${source} has a malformed "baseUrl": ${handle.baseUrl}`);
65
+ if (handle.platform !== void 0) validatePlatform(handle.platform, source);
65
66
  return handle;
66
67
  }
68
+ /**
69
+ * The handshake is the cross-process contract, so a malformed platform block
70
+ * must fail here with the field name, not later with a less actionable error.
71
+ */
72
+ function validatePlatform(value, source) {
73
+ const fail = (detail) => {
74
+ throw new Error(`handshake file ${source} has a malformed "platform" block: ${detail}`);
75
+ };
76
+ if (typeof value !== "object" || value === null || Array.isArray(value)) fail("not an object");
77
+ const platform = value;
78
+ for (const field of ["projectId", "publishableKey"]) if (typeof platform[field] !== "string" || platform[field].length === 0) fail(`"${field}" is required`);
79
+ }
67
80
  //#endregion
68
81
  Object.defineProperty(exports, "applyAppEnvTemplate", {
69
82
  enumerable: true,
@@ -96,4 +109,4 @@ Object.defineProperty(exports, "writeHandshake", {
96
109
  }
97
110
  });
98
111
 
99
- //# sourceMappingURL=handshake-CRKcgkfN.cjs.map
112
+ //# sourceMappingURL=handshake-BOsVBPtn.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake-BOsVBPtn.cjs","names":[],"sources":["../src/app-env.ts","../src/handshake.ts"],"sourcesContent":["import type { InstanceHandle } from \"./types\";\n\n/**\n * Handle fields a template may reference: the flat string facts. Structured\n * fields (e.g. `platform`) are not env-var material — consumers read those\n * from the handle directly.\n */\ntype StringHandleField = {\n [K in keyof InstanceHandle]-?: InstanceHandle[K] extends string | undefined ? K : never;\n}[keyof InstanceHandle];\n\n/**\n * Declarative mapping from an app's env var names to InstanceHandle fields.\n * A template (not a function) so it can cross process boundaries: the\n * Playwright config serializes it into the app-runner's environment, where it\n * is applied to the handle read from the handshake file.\n */\nexport type AppEnvTemplate = Record<string, StringHandleField>;\n\n/**\n * The env shape `@zitadel/sdk-next` apps read. Other frameworks pass their own\n * template — the console maps the same handle fields to `VITE_*`/`CONSOLE_*`\n * names, for example.\n */\nexport const nextAppEnv: AppEnvTemplate = {\n ZITADEL_URL: \"baseUrl\",\n NEXT_PUBLIC_ZITADEL_PROJECT_ID: \"projectId\",\n ZITADEL_PROJECT_SECRET: \"projectSecret\",\n};\n\nexport function applyAppEnvTemplate(\n template: AppEnvTemplate,\n handle: InstanceHandle,\n): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [name, field] of Object.entries(template)) {\n const value = handle[field];\n if (typeof value !== \"string\" || value.length === 0) {\n // Fail instead of silently dropping the var: an app booted without one\n // of its env vars produces a much harder-to-read failure downstream.\n throw new Error(\n `app env template maps \"${name}\" to handle field \"${field}\", which the instance handle does not carry`,\n );\n }\n env[name] = value;\n }\n return env;\n}\n","import { readFileSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\n\nimport type { InstanceHandle, PlatformCredentials } from \"./types\";\n\n/**\n * The handshake file carries an InstanceHandle across process boundaries:\n * written by the script that boots + bootstraps the instance, read by\n * Playwright workers (fixtures) and the app dev-server wrapper.\n */\nexport async function writeHandshake(path: string, handle: InstanceHandle): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(handle, null, 2)}\\n`, { mode: 0o600 });\n}\n\nexport function readHandshakeSync(path: string): InstanceHandle {\n const contents = readFileSync(path, \"utf8\");\n let value: unknown;\n try {\n value = JSON.parse(contents);\n } catch (error) {\n throw new Error(`handshake file ${path} contains invalid JSON: ${(error as Error).message}`, {\n cause: error,\n });\n }\n return validateHandle(value, path);\n}\n\nexport async function waitForHandshake(path: string, timeoutMs = 60_000): Promise<InstanceHandle> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n try {\n return validateHandle(JSON.parse(await readFile(path, \"utf8\")), path);\n } catch (error) {\n if (Date.now() >= deadline) {\n throw new Error(\n `handshake file not readable within ${timeoutMs}ms: ${path} (${(error as Error).message})`,\n { cause: error },\n );\n }\n await sleep(250);\n }\n }\n}\n\nfunction validateHandle(value: unknown, source: string): InstanceHandle {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(`handshake file ${source} is not an object`);\n }\n const handle = value as Partial<InstanceHandle>;\n for (const field of [\"baseUrl\", \"projectId\", \"projectSecret\", \"schemaId\"] as const) {\n if (typeof handle[field] !== \"string\" || handle[field].length === 0) {\n throw new Error(`handshake file ${source} is missing \"${field}\"`);\n }\n }\n if (!URL.canParse(handle.baseUrl as string)) {\n throw new Error(`handshake file ${source} has a malformed \"baseUrl\": ${handle.baseUrl}`);\n }\n if (handle.platform !== undefined) {\n validatePlatform(handle.platform, source);\n }\n return handle as InstanceHandle;\n}\n\n/**\n * The handshake is the cross-process contract, so a malformed platform block\n * must fail here with the field name, not later with a less actionable error.\n */\nfunction validatePlatform(value: unknown, source: string): void {\n const fail = (detail: string): never => {\n throw new Error(`handshake file ${source} has a malformed \"platform\" block: ${detail}`);\n };\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n fail(\"not an object\");\n }\n const platform = value as Partial<PlatformCredentials>;\n for (const field of [\"projectId\", \"publishableKey\"] as const) {\n if (typeof platform[field] !== \"string\" || platform[field].length === 0) {\n fail(`\"${field}\" is required`);\n }\n }\n // Unknown extra fields pass through on purpose: future platform\n // credentials join additively, and an older reader must not reject a\n // newer producer's handle.\n}\n"],"mappings":";;;;;;;;;;AAwBA,MAAa,aAA6B;CACxC,aAAa;CACb,gCAAgC;CAChC,wBAAwB;CACzB;AAED,SAAgB,oBACd,UACA,QACwB;CACxB,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,EAAE;EACpD,MAAM,QAAQ,OAAO;AACrB,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAGhD,OAAM,IAAI,MACR,0BAA0B,KAAK,qBAAqB,MAAM,6CAC3D;AAEH,MAAI,QAAQ;;AAEd,QAAO;;;;;;;;;AClCT,eAAsB,eAAe,MAAc,QAAuC;AACxF,QAAA,GAAA,iBAAA,QAAA,GAAA,UAAA,SAAoB,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC/C,QAAA,GAAA,iBAAA,WAAgB,MAAM,GAAG,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAO,CAAC;;AAGhF,SAAgB,kBAAkB,MAA8B;CAC9D,MAAM,YAAA,GAAA,QAAA,cAAwB,MAAM,OAAO;CAC3C,IAAI;AACJ,KAAI;AACF,UAAQ,KAAK,MAAM,SAAS;UACrB,OAAO;AACd,QAAM,IAAI,MAAM,kBAAkB,KAAK,0BAA2B,MAAgB,WAAW,EAC3F,OAAO,OACR,CAAC;;AAEJ,QAAO,eAAe,OAAO,KAAK;;AAGpC,eAAsB,iBAAiB,MAAc,YAAY,KAAiC;CAChG,MAAM,WAAW,KAAK,KAAK,GAAG;AAC9B,SACE,KAAI;AACF,SAAO,eAAe,KAAK,MAAM,OAAA,GAAA,iBAAA,UAAe,MAAM,OAAO,CAAC,EAAE,KAAK;UAC9D,OAAO;AACd,MAAI,KAAK,KAAK,IAAI,SAChB,OAAM,IAAI,MACR,sCAAsC,UAAU,MAAM,KAAK,IAAK,MAAgB,QAAQ,IACxF,EAAE,OAAO,OAAO,CACjB;AAEH,SAAA,GAAA,qBAAA,YAAY,IAAI;;;AAKtB,SAAS,eAAe,OAAgB,QAAgC;AACtE,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE,OAAM,IAAI,MAAM,kBAAkB,OAAO,mBAAmB;CAE9D,MAAM,SAAS;AACf,MAAK,MAAM,SAAS;EAAC;EAAW;EAAa;EAAiB;EAAW,CACvE,KAAI,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,WAAW,EAChE,OAAM,IAAI,MAAM,kBAAkB,OAAO,eAAe,MAAM,GAAG;AAGrE,KAAI,CAAC,IAAI,SAAS,OAAO,QAAkB,CACzC,OAAM,IAAI,MAAM,kBAAkB,OAAO,8BAA8B,OAAO,UAAU;AAE1F,KAAI,OAAO,aAAa,KAAA,EACtB,kBAAiB,OAAO,UAAU,OAAO;AAE3C,QAAO;;;;;;AAOT,SAAS,iBAAiB,OAAgB,QAAsB;CAC9D,MAAM,QAAQ,WAA0B;AACtC,QAAM,IAAI,MAAM,kBAAkB,OAAO,qCAAqC,SAAS;;AAEzF,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE,MAAK,gBAAgB;CAEvB,MAAM,WAAW;AACjB,MAAK,MAAM,SAAS,CAAC,aAAa,iBAAiB,CACjD,KAAI,OAAO,SAAS,WAAW,YAAY,SAAS,OAAO,WAAW,EACpE,MAAK,IAAI,MAAM,eAAe"}
@@ -62,9 +62,22 @@ function validateHandle(value, source) {
62
62
  "schemaId"
63
63
  ]) if (typeof handle[field] !== "string" || handle[field].length === 0) throw new Error(`handshake file ${source} is missing "${field}"`);
64
64
  if (!URL.canParse(handle.baseUrl)) throw new Error(`handshake file ${source} has a malformed "baseUrl": ${handle.baseUrl}`);
65
+ if (handle.platform !== void 0) validatePlatform(handle.platform, source);
65
66
  return handle;
66
67
  }
68
+ /**
69
+ * The handshake is the cross-process contract, so a malformed platform block
70
+ * must fail here with the field name, not later with a less actionable error.
71
+ */
72
+ function validatePlatform(value, source) {
73
+ const fail = (detail) => {
74
+ throw new Error(`handshake file ${source} has a malformed "platform" block: ${detail}`);
75
+ };
76
+ if (typeof value !== "object" || value === null || Array.isArray(value)) fail("not an object");
77
+ const platform = value;
78
+ for (const field of ["projectId", "publishableKey"]) if (typeof platform[field] !== "string" || platform[field].length === 0) fail(`"${field}" is required`);
79
+ }
67
80
  //#endregion
68
81
  export { nextAppEnv as a, applyAppEnvTemplate as i, waitForHandshake as n, writeHandshake as r, readHandshakeSync as t };
69
82
 
70
- //# sourceMappingURL=handshake-ClzWvG8z.mjs.map
83
+ //# sourceMappingURL=handshake-BnAPXC6s.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake-BnAPXC6s.mjs","names":["sleep"],"sources":["../src/app-env.ts","../src/handshake.ts"],"sourcesContent":["import type { InstanceHandle } from \"./types\";\n\n/**\n * Handle fields a template may reference: the flat string facts. Structured\n * fields (e.g. `platform`) are not env-var material — consumers read those\n * from the handle directly.\n */\ntype StringHandleField = {\n [K in keyof InstanceHandle]-?: InstanceHandle[K] extends string | undefined ? K : never;\n}[keyof InstanceHandle];\n\n/**\n * Declarative mapping from an app's env var names to InstanceHandle fields.\n * A template (not a function) so it can cross process boundaries: the\n * Playwright config serializes it into the app-runner's environment, where it\n * is applied to the handle read from the handshake file.\n */\nexport type AppEnvTemplate = Record<string, StringHandleField>;\n\n/**\n * The env shape `@zitadel/sdk-next` apps read. Other frameworks pass their own\n * template — the console maps the same handle fields to `VITE_*`/`CONSOLE_*`\n * names, for example.\n */\nexport const nextAppEnv: AppEnvTemplate = {\n ZITADEL_URL: \"baseUrl\",\n NEXT_PUBLIC_ZITADEL_PROJECT_ID: \"projectId\",\n ZITADEL_PROJECT_SECRET: \"projectSecret\",\n};\n\nexport function applyAppEnvTemplate(\n template: AppEnvTemplate,\n handle: InstanceHandle,\n): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [name, field] of Object.entries(template)) {\n const value = handle[field];\n if (typeof value !== \"string\" || value.length === 0) {\n // Fail instead of silently dropping the var: an app booted without one\n // of its env vars produces a much harder-to-read failure downstream.\n throw new Error(\n `app env template maps \"${name}\" to handle field \"${field}\", which the instance handle does not carry`,\n );\n }\n env[name] = value;\n }\n return env;\n}\n","import { readFileSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\n\nimport type { InstanceHandle, PlatformCredentials } from \"./types\";\n\n/**\n * The handshake file carries an InstanceHandle across process boundaries:\n * written by the script that boots + bootstraps the instance, read by\n * Playwright workers (fixtures) and the app dev-server wrapper.\n */\nexport async function writeHandshake(path: string, handle: InstanceHandle): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(handle, null, 2)}\\n`, { mode: 0o600 });\n}\n\nexport function readHandshakeSync(path: string): InstanceHandle {\n const contents = readFileSync(path, \"utf8\");\n let value: unknown;\n try {\n value = JSON.parse(contents);\n } catch (error) {\n throw new Error(`handshake file ${path} contains invalid JSON: ${(error as Error).message}`, {\n cause: error,\n });\n }\n return validateHandle(value, path);\n}\n\nexport async function waitForHandshake(path: string, timeoutMs = 60_000): Promise<InstanceHandle> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n try {\n return validateHandle(JSON.parse(await readFile(path, \"utf8\")), path);\n } catch (error) {\n if (Date.now() >= deadline) {\n throw new Error(\n `handshake file not readable within ${timeoutMs}ms: ${path} (${(error as Error).message})`,\n { cause: error },\n );\n }\n await sleep(250);\n }\n }\n}\n\nfunction validateHandle(value: unknown, source: string): InstanceHandle {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(`handshake file ${source} is not an object`);\n }\n const handle = value as Partial<InstanceHandle>;\n for (const field of [\"baseUrl\", \"projectId\", \"projectSecret\", \"schemaId\"] as const) {\n if (typeof handle[field] !== \"string\" || handle[field].length === 0) {\n throw new Error(`handshake file ${source} is missing \"${field}\"`);\n }\n }\n if (!URL.canParse(handle.baseUrl as string)) {\n throw new Error(`handshake file ${source} has a malformed \"baseUrl\": ${handle.baseUrl}`);\n }\n if (handle.platform !== undefined) {\n validatePlatform(handle.platform, source);\n }\n return handle as InstanceHandle;\n}\n\n/**\n * The handshake is the cross-process contract, so a malformed platform block\n * must fail here with the field name, not later with a less actionable error.\n */\nfunction validatePlatform(value: unknown, source: string): void {\n const fail = (detail: string): never => {\n throw new Error(`handshake file ${source} has a malformed \"platform\" block: ${detail}`);\n };\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n fail(\"not an object\");\n }\n const platform = value as Partial<PlatformCredentials>;\n for (const field of [\"projectId\", \"publishableKey\"] as const) {\n if (typeof platform[field] !== \"string\" || platform[field].length === 0) {\n fail(`\"${field}\" is required`);\n }\n }\n // Unknown extra fields pass through on purpose: future platform\n // credentials join additively, and an older reader must not reject a\n // newer producer's handle.\n}\n"],"mappings":";;;;;;;;;;AAwBA,MAAa,aAA6B;CACxC,aAAa;CACb,gCAAgC;CAChC,wBAAwB;CACzB;AAED,SAAgB,oBACd,UACA,QACwB;CACxB,MAAM,MAA8B,EAAE;AACtC,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,EAAE;EACpD,MAAM,QAAQ,OAAO;AACrB,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAGhD,OAAM,IAAI,MACR,0BAA0B,KAAK,qBAAqB,MAAM,6CAC3D;AAEH,MAAI,QAAQ;;AAEd,QAAO;;;;;;;;;AClCT,eAAsB,eAAe,MAAc,QAAuC;AACxF,OAAM,MAAM,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC/C,OAAM,UAAU,MAAM,GAAG,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAO,CAAC;;AAGhF,SAAgB,kBAAkB,MAA8B;CAC9D,MAAM,WAAW,aAAa,MAAM,OAAO;CAC3C,IAAI;AACJ,KAAI;AACF,UAAQ,KAAK,MAAM,SAAS;UACrB,OAAO;AACd,QAAM,IAAI,MAAM,kBAAkB,KAAK,0BAA2B,MAAgB,WAAW,EAC3F,OAAO,OACR,CAAC;;AAEJ,QAAO,eAAe,OAAO,KAAK;;AAGpC,eAAsB,iBAAiB,MAAc,YAAY,KAAiC;CAChG,MAAM,WAAW,KAAK,KAAK,GAAG;AAC9B,SACE,KAAI;AACF,SAAO,eAAe,KAAK,MAAM,MAAM,SAAS,MAAM,OAAO,CAAC,EAAE,KAAK;UAC9D,OAAO;AACd,MAAI,KAAK,KAAK,IAAI,SAChB,OAAM,IAAI,MACR,sCAAsC,UAAU,MAAM,KAAK,IAAK,MAAgB,QAAQ,IACxF,EAAE,OAAO,OAAO,CACjB;AAEH,QAAMA,WAAM,IAAI;;;AAKtB,SAAS,eAAe,OAAgB,QAAgC;AACtE,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE,OAAM,IAAI,MAAM,kBAAkB,OAAO,mBAAmB;CAE9D,MAAM,SAAS;AACf,MAAK,MAAM,SAAS;EAAC;EAAW;EAAa;EAAiB;EAAW,CACvE,KAAI,OAAO,OAAO,WAAW,YAAY,OAAO,OAAO,WAAW,EAChE,OAAM,IAAI,MAAM,kBAAkB,OAAO,eAAe,MAAM,GAAG;AAGrE,KAAI,CAAC,IAAI,SAAS,OAAO,QAAkB,CACzC,OAAM,IAAI,MAAM,kBAAkB,OAAO,8BAA8B,OAAO,UAAU;AAE1F,KAAI,OAAO,aAAa,KAAA,EACtB,kBAAiB,OAAO,UAAU,OAAO;AAE3C,QAAO;;;;;;AAOT,SAAS,iBAAiB,OAAgB,QAAsB;CAC9D,MAAM,QAAQ,WAA0B;AACtC,QAAM,IAAI,MAAM,kBAAkB,OAAO,qCAAqC,SAAS;;AAEzF,KAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,MAAM,CACrE,MAAK,gBAAgB;CAEvB,MAAM,WAAW;AACjB,MAAK,MAAM,SAAS,CAAC,aAAa,iBAAiB,CACjD,KAAI,OAAO,SAAS,WAAW,YAAY,SAAS,OAAO,WAAW,EACpE,MAAK,IAAI,MAAM,eAAe"}
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_src = require("./src-DirKLqi4.cjs");
3
- const require_handshake = require("./handshake-CRKcgkfN.cjs");
2
+ const require_src = require("./src-DtCJTEId.cjs");
3
+ const require_handshake = require("./handshake-BOsVBPtn.cjs");
4
4
  exports.SESSION_COOKIE_NAME = require_src.SESSION_COOKIE_NAME;
5
5
  exports.applyAppEnvTemplate = require_handshake.applyAppEnvTemplate;
6
6
  exports.bootLocalServer = require_src.bootLocalServer;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Identity, c as LocalZitadelRuntime, d as SeedUserInput, f as SeedUsersTemplate, i as ConnectedZitadel, l as MintedSession, m as SessionCookie, n as applyAppEnvTemplate, o as InstanceHandle, p as SeededUser, r as nextAppEnv, s as LocalZitadel, t as AppEnvTemplate, u as SeedSessionInput } from "./app-env-D3W0GYhA.mjs";
1
+ import { a as Identity, c as LocalZitadelRuntime, d as SeedSessionInput, f as SeedUserInput, h as SessionCookie, i as ConnectedZitadel, l as MintedSession, m as SeededUser, n as applyAppEnvTemplate, o as InstanceHandle, p as SeedUsersTemplate, r as nextAppEnv, s as LocalZitadel, t as AppEnvTemplate, u as PlatformCredentials } from "./app-env-DlrV_ePR.mjs";
2
2
  import { ZitadelClient } from "@zitadel/api/client";
3
3
  import { SetupPreset, SetupUseCase } from "@zitadel/config/defaults";
4
4
 
@@ -88,5 +88,5 @@ declare function connectZitadel(handle: InstanceHandle): ConnectedZitadel;
88
88
  */
89
89
  declare function startLocalZitadel(options?: StartLocalZitadelOptions): Promise<LocalZitadel>;
90
90
  //#endregion
91
- export { type AppEnvTemplate, type BootServerOptions, type BootedServer, type BootstrapProjectOptions, type BootstrappedProject, type ConnectedZitadel, type Identity, type InstanceHandle, type LocalZitadel, type LocalZitadelRuntime, type MintedSession, SESSION_COOKIE_NAME, type SeedSessionInput, type SeedUserInput, type SeedUsersTemplate, type SeededUser, type SessionCookie, StartLocalZitadelOptions, applyAppEnvTemplate, bootLocalServer, bootstrapProject, connectZitadel, nextAppEnv, readHandshakeSync, startLocalZitadel, waitForHandshake, writeHandshake };
91
+ export { type AppEnvTemplate, type BootServerOptions, type BootedServer, type BootstrapProjectOptions, type BootstrappedProject, type ConnectedZitadel, type Identity, type InstanceHandle, type LocalZitadel, type LocalZitadelRuntime, type MintedSession, type PlatformCredentials, SESSION_COOKIE_NAME, type SeedSessionInput, type SeedUserInput, type SeedUsersTemplate, type SeededUser, type SessionCookie, StartLocalZitadelOptions, applyAppEnvTemplate, bootLocalServer, bootstrapProject, connectZitadel, nextAppEnv, readHandshakeSync, startLocalZitadel, waitForHandshake, writeHandshake };
92
92
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as nextAppEnv, i as applyAppEnvTemplate, n as waitForHandshake, r as writeHandshake, t as readHandshakeSync } from "./handshake-ClzWvG8z.mjs";
2
- import { a as bootstrapProject, i as bootLocalServer, n as startLocalZitadel, r as SESSION_COOKIE_NAME, t as connectZitadel } from "./src-eTcdx-ZS.mjs";
1
+ import { a as nextAppEnv, i as applyAppEnvTemplate, n as waitForHandshake, r as writeHandshake, t as readHandshakeSync } from "./handshake-BnAPXC6s.mjs";
2
+ import { a as bootstrapProject, i as bootLocalServer, n as startLocalZitadel, r as SESSION_COOKIE_NAME, t as connectZitadel } from "./src-Dkt0HHu8.mjs";
3
3
  export { SESSION_COOKIE_NAME, applyAppEnvTemplate, bootLocalServer, bootstrapProject, connectZitadel, nextAppEnv, readHandshakeSync, startLocalZitadel, waitForHandshake, writeHandshake };
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_src = require("./src-DirKLqi4.cjs");
3
- const require_handshake = require("./handshake-CRKcgkfN.cjs");
2
+ const require_src = require("./src-DtCJTEId.cjs");
3
+ const require_handshake = require("./handshake-BOsVBPtn.cjs");
4
4
  const require_orchestration = require("./orchestration-D7QBgQ0l.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_url = require("node:url");
@@ -245,6 +245,10 @@ async function fillIfVisible(field, value) {
245
245
  * });
246
246
  * ```
247
247
  *
248
+ * Omit `app` when the instance serves the app itself (the binary's embedded
249
+ * `/ui/console/` and `/ui/login/` surfaces): only the boot entry is
250
+ * generated, and `appOrigin` must be the instance's own origin.
251
+ *
248
252
  * Also points ZITADEL_TESTING_HANDSHAKE at the handshake file so the
249
253
  * `@zitadel/testing/playwright` fixtures resolve the instance — Playwright
250
254
  * workers re-evaluate the config, which re-applies this for every process
@@ -257,9 +261,18 @@ function withZitadel(options, resolveEntry = entryPoint) {
257
261
  if (!Number.isInteger(port) || port <= 0) throw new Error(`withZitadel: port must be a positive integer, got ${port}`);
258
262
  const origin = URL.canParse(appOrigin) ? new URL(appOrigin) : void 0;
259
263
  if (!origin || origin.protocol !== "http:" && origin.protocol !== "https:" || origin.pathname !== "/" || origin.search !== "" || origin.hash !== "") throw new Error(`withZitadel: appOrigin must be an origin like "http://localhost:3002", got "${appOrigin}"`);
260
- if (!app.readyPath.startsWith("/")) throw new Error(`withZitadel: app.readyPath must start with "/", got "${app.readyPath}"`);
261
- if (app.command.length === 0) throw new Error("withZitadel: app.command must not be empty");
262
- if (!(0, node_path.isAbsolute)(app.cwd)) throw new Error(`withZitadel: app.cwd must be absolute, got "${app.cwd}"`);
264
+ if (app === void 0) {
265
+ const loopbackHosts = new Set([
266
+ "localhost",
267
+ "127.0.0.1",
268
+ "[::1]"
269
+ ]);
270
+ if (origin.protocol !== "http:" || !loopbackHosts.has(origin.hostname) || (origin.port || "80") !== String(port)) throw new Error(`withZitadel: with no \`app\`, the instance itself serves the app, so appOrigin must be its local origin (http://localhost:${port}); got "${appOrigin}".`);
271
+ } else {
272
+ if (!app.readyPath.startsWith("/")) throw new Error(`withZitadel: app.readyPath must start with "/", got "${app.readyPath}"`);
273
+ if (app.command.length === 0) throw new Error("withZitadel: app.command must not be empty");
274
+ if (!(0, node_path.isAbsolute)(app.cwd)) throw new Error(`withZitadel: app.cwd must be absolute, got "${app.cwd}"`);
275
+ }
263
276
  for (const [label, value] of [
264
277
  ["zitadel.serverBinary", options.zitadel?.serverBinary],
265
278
  ["zitadel.dir", options.zitadel?.dir],
@@ -278,13 +291,7 @@ function withZitadel(options, resolveEntry = entryPoint) {
278
291
  preset: options.zitadel?.preset,
279
292
  useCase: options.zitadel?.useCase
280
293
  };
281
- const appRunnerConfig = {
282
- command: app.command,
283
- cwd: app.cwd,
284
- env: app.env,
285
- handshakeTimeoutMs: app.readyTimeoutMs ?? 18e4
286
- };
287
- return { webServer: [{
294
+ const supervisorEntry = {
288
295
  command: `node ${JSON.stringify(resolveEntry("supervisor"))}`,
289
296
  url: `http://localhost:${port}/healthz`,
290
297
  reuseExistingServer: false,
@@ -300,7 +307,15 @@ function withZitadel(options, resolveEntry = entryPoint) {
300
307
  signal: "SIGTERM",
301
308
  timeout: 3e4
302
309
  }
303
- }, {
310
+ };
311
+ if (app === void 0) return { webServer: [supervisorEntry] };
312
+ const appRunnerConfig = {
313
+ command: app.command,
314
+ cwd: app.cwd,
315
+ env: app.env,
316
+ handshakeTimeoutMs: app.readyTimeoutMs ?? 18e4
317
+ };
318
+ return { webServer: [supervisorEntry, {
304
319
  command: `node ${JSON.stringify(resolveEntry("app-runner"))}`,
305
320
  url: new URL(app.readyPath, appOrigin).toString(),
306
321
  reuseExistingServer: false,
@@ -337,8 +352,11 @@ const test = _playwright_test.test.extend({
337
352
  zitadel: [async ({}, use) => {
338
353
  const handshakePath = process.env.ZITADEL_TESTING_HANDSHAKE;
339
354
  if (!handshakePath) throw new Error("ZITADEL_TESTING_HANDSHAKE is not set. Point it at the handshake file written by the script that boots the instance (see @zitadel/testing docs).");
340
- await use(require_src.connectZitadel(require_handshake.readHandshakeSync(handshakePath)));
341
- }, { scope: "worker" }],
355
+ await use(require_src.connectZitadel(await require_handshake.waitForHandshake(handshakePath)));
356
+ }, {
357
+ scope: "worker",
358
+ auto: true
359
+ }],
342
360
  seed: async ({ zitadel, baseURL }, use) => {
343
361
  await use({
344
362
  user: (input) => zitadel.seedUser(input),