@zitadel/testing 0.0.0
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/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/app-env-D3W0GYhA.d.mts +122 -0
- package/dist/app-env-D3W0GYhA.d.mts.map +1 -0
- package/dist/app-runner.cjs +36 -0
- package/dist/app-runner.cjs.map +1 -0
- package/dist/app-runner.d.mts +1 -0
- package/dist/app-runner.mjs +37 -0
- package/dist/app-runner.mjs.map +1 -0
- package/dist/handshake-BPtWruO8.mjs +69 -0
- package/dist/handshake-BPtWruO8.mjs.map +1 -0
- package/dist/handshake-CggSIoxF.cjs +98 -0
- package/dist/handshake-CggSIoxF.cjs.map +1 -0
- package/dist/index.cjs +13 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/orchestration-C640_1Lg.mjs +38 -0
- package/dist/orchestration-C640_1Lg.mjs.map +1 -0
- package/dist/orchestration-D7QBgQ0l.cjs +73 -0
- package/dist/orchestration-D7QBgQ0l.cjs.map +1 -0
- package/dist/playwright.cjs +156 -0
- package/dist/playwright.cjs.map +1 -0
- package/dist/playwright.d.mts +124 -0
- package/dist/playwright.d.mts.map +1 -0
- package/dist/playwright.mjs +146 -0
- package/dist/playwright.mjs.map +1 -0
- package/dist/src-BIL0PdSd.cjs +2356 -0
- package/dist/src-BIL0PdSd.cjs.map +1 -0
- package/dist/src-DkG0V4A6.mjs +515 -0
- package/dist/src-DkG0V4A6.mjs.map +1 -0
- package/dist/supervisor.cjs +76 -0
- package/dist/supervisor.cjs.map +1 -0
- package/dist/supervisor.d.mts +1 -0
- package/dist/supervisor.mjs +77 -0
- package/dist/supervisor.mjs.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZITADEL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# @zitadel/testing
|
|
2
|
+
|
|
3
|
+
Test-kit for **seeded ephemeral local Zitadel instances**: boot the real server
|
|
4
|
+
(binary runtime + embedded Postgres, no Docker) from test code, create a
|
|
5
|
+
project with the default login flow, and mint password users that can complete
|
|
6
|
+
the real login journey immediately.
|
|
7
|
+
|
|
8
|
+
> **Status: alpha.** Published to npm on the shared release train — the kit
|
|
9
|
+
> carries the same version as `@zitadel/cli` and the SDKs, the train publishes
|
|
10
|
+
> under the `alpha` dist-tag (install with `@alpha`), and APIs can still move
|
|
11
|
+
> between alphas. macOS/Linux (see [Known limitations](#known-limitations)).
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
The repo previously had two extremes: the demo e2e suites run against
|
|
16
|
+
`@zitadel/api-mock` (fast, but fake — no user store, no real crypto), and
|
|
17
|
+
`cli-journey-e2e` runs the real packaged product but creates users by clicking
|
|
18
|
+
through the registration UI, serialized per suite. This kit fills the middle:
|
|
19
|
+
**real server, programmatic seeding, parallel-safe per-test users.**
|
|
20
|
+
|
|
21
|
+
## Quick start (Playwright)
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm i -D @zitadel/testing@alpha @playwright/test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The kit drives the `zitadel` CLI, which resolves the published
|
|
28
|
+
`@zitadel/server` platform binary on its own — no binary paths, no env vars.
|
|
29
|
+
|
|
30
|
+
One instance per suite, seeded per test. `withZitadel()` generates the
|
|
31
|
+
`webServer` entries that boot the instance and run your app against it — no
|
|
32
|
+
wrapper scripts:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// playwright.config.ts
|
|
36
|
+
import { defineConfig } from "@playwright/test";
|
|
37
|
+
import { nextAppEnv, withZitadel } from "@zitadel/testing/playwright";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
testDir: "./e2e",
|
|
41
|
+
...withZitadel({
|
|
42
|
+
configDir: import.meta.dirname,
|
|
43
|
+
port: 8092, // fixed, so the readiness URL is known up front
|
|
44
|
+
appOrigin: "http://localhost:3002", // your app's origin (proxy origin check)
|
|
45
|
+
app: {
|
|
46
|
+
command: ["pnpm", "dev"], // your app's dev server
|
|
47
|
+
cwd: import.meta.dirname,
|
|
48
|
+
readyPath: "/login",
|
|
49
|
+
env: nextAppEnv, // or your own AppEnvTemplate
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// my-login.spec.ts
|
|
57
|
+
import { expect, test } from "@zitadel/testing/playwright";
|
|
58
|
+
|
|
59
|
+
test("user signs in with password", async ({ page, seed }) => {
|
|
60
|
+
const user = await seed.user(); // unique email + password, loginable now
|
|
61
|
+
await page.goto("/login");
|
|
62
|
+
await page.getByLabel(/email/i).fill(user.email);
|
|
63
|
+
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
64
|
+
await page.getByLabel(/password/i).fill(user.password);
|
|
65
|
+
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
66
|
+
await expect(page).toHaveURL(/\/admin/);
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`app.env` is an `AppEnvTemplate`: a serializable mapping from your app's env
|
|
71
|
+
var names to `InstanceHandle` fields. `nextAppEnv` covers `@zitadel/sdk-next`
|
|
72
|
+
apps; the console maps the same fields to `VITE_*`/`CONSOLE_*` names instead.
|
|
73
|
+
The fixtures find the instance through `ZITADEL_TESTING_HANDSHAKE`, which
|
|
74
|
+
`withZitadel()` points at its handshake file.
|
|
75
|
+
|
|
76
|
+
### Start tests authenticated
|
|
77
|
+
|
|
78
|
+
Most app tests don't want to re-test login. `authenticatedPage` seeds a user,
|
|
79
|
+
drives the real login flow headlessly (the same Flow API the login UI renders),
|
|
80
|
+
and injects the resulting session cookie into a dedicated browser context:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
test("member sees the dashboard", async ({ authenticatedPage }) => {
|
|
84
|
+
const { page, user } = authenticatedPage;
|
|
85
|
+
await page.goto("/dashboard"); // already signed in as `user`
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Underneath sits `seed.session()`, whose `sessionToken` also drives session
|
|
90
|
+
APIs without any browser — backend tests call the instance directly with the
|
|
91
|
+
cookie header (`cookie: __nextgen_session=<sessionToken>`, the SDK
|
|
92
|
+
middleware's own headless pattern). Scope: password flows (the shipped
|
|
93
|
+
`password-first` presets). A flow step demanding anything beyond the user's
|
|
94
|
+
email and password — a challenge, another factor — fails with the step name;
|
|
95
|
+
log in through the UI for those. `seed.identity()` complements registration
|
|
96
|
+
specs: an unused email+password that creates nothing, so the flow under test
|
|
97
|
+
must create the user.
|
|
98
|
+
|
|
99
|
+
The two in-repo consumers are `apps/demo-next-e2e/playwright.real.config.mts`
|
|
100
|
+
and `apps/console-e2e/playwright.real.config.mts` — run them with
|
|
101
|
+
`moon run demo-next-e2e:e2e-real` / `moon run console-e2e:e2e-real`.
|
|
102
|
+
|
|
103
|
+
### Composable pieces
|
|
104
|
+
|
|
105
|
+
`withZitadel()` is sugar over exported building blocks: a supervisor entry
|
|
106
|
+
that calls `startLocalZitadel()` + `writeHandshake()`, and an app-runner entry
|
|
107
|
+
that `waitForHandshake()`s and spawns the dev server with
|
|
108
|
+
`applyAppEnvTemplate(...)` applied. Suites with unusual topologies (or
|
|
109
|
+
non-Playwright runners) compose those functions directly —
|
|
110
|
+
`apps/console/scripts/dev-real.mts` does, seeding a dev environment rather
|
|
111
|
+
than a test suite.
|
|
112
|
+
|
|
113
|
+
## API
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import {
|
|
117
|
+
startLocalZitadel, // boot + bootstrap an ephemeral instance
|
|
118
|
+
connectZitadel, // attach to an existing instance via its handle
|
|
119
|
+
writeHandshake, readHandshakeSync, waitForHandshake,
|
|
120
|
+
} from "@zitadel/testing";
|
|
121
|
+
|
|
122
|
+
const z = await startLocalZitadel({
|
|
123
|
+
port, // default: free port
|
|
124
|
+
dir, // default: temp dir, removed on stop (caller dirs are kept)
|
|
125
|
+
appOrigins, // registered as the project's previewOrigins
|
|
126
|
+
useCase, // "minimal" (default) | "consumer" | "business"
|
|
127
|
+
preset, // "password-first" (default)
|
|
128
|
+
serverBinary, // ZITADEL_SERVER_BINARY override (in-repo: dist/server/nextgen)
|
|
129
|
+
keep, // keep the temp dir for debugging
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
z.handle; // serializable: { baseUrl, projectId, projectSecret, schemaId, previewSecret? }
|
|
133
|
+
z.api; // authenticated @zitadel/api client (bearer = projectSecret)
|
|
134
|
+
z.appEnv; // { ZITADEL_URL, NEXT_PUBLIC_ZITADEL_PROJECT_ID, ZITADEL_PROJECT_SECRET }
|
|
135
|
+
await z.seedUser({ email?, password?, attributes? }); // → { id, email, password }
|
|
136
|
+
await z.seedUsers(8, { email?, password?, attributes? }); // per-index templates
|
|
137
|
+
z.identity(); // unused { email, password } — creates nothing
|
|
138
|
+
await z.seedSession({ user? }); // → { user, sessionToken, expiresAt, cookie }
|
|
139
|
+
await z.stop(); // stop server, reap embedded Postgres, remove owned temp dir
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`connectZitadel(handle)` returns the same surface minus lifecycle — this is
|
|
143
|
+
what the Playwright fixtures use, and what a future remote-instance mode would
|
|
144
|
+
build on.
|
|
145
|
+
|
|
146
|
+
From `@zitadel/testing/playwright`: the `test`/`expect` fixtures (`seed.user()`
|
|
147
|
+
per test, `zitadel` per worker), plus `withZitadel(options)` returning
|
|
148
|
+
`{ webServer }` for the config, and `nextAppEnv`/`applyAppEnvTemplate` for the
|
|
149
|
+
env-template mechanism described above.
|
|
150
|
+
|
|
151
|
+
## How it works
|
|
152
|
+
|
|
153
|
+
- **Lifecycle** shells out to `zitadel start/stop --json` (the CLI owns port
|
|
154
|
+
preflight, the health wait, process-group stop, and embedded-Postgres
|
|
155
|
+
reaping). Swapping this for direct library calls later will not change the
|
|
156
|
+
public API.
|
|
157
|
+
- **Bootstrap** is the server-side half of `zitadel setup`, no files:
|
|
158
|
+
`POST /projects` (unauthenticated; returns the `projectSecret` used as
|
|
159
|
+
bearer for everything else) → `POST /schemas` (server assigns the schema id)
|
|
160
|
+
→ `POST /flow_definitions` (default login flow pinned to that schema id).
|
|
161
|
+
Templates come from `@zitadel/config/defaults`.
|
|
162
|
+
- **Seeding** is `POST /users` (the body carries `$schema: <schema id>`) +
|
|
163
|
+
`PUT /users/{id}/password` with `isChangeRequired: false`.
|
|
164
|
+
|
|
165
|
+
## Parallelism model
|
|
166
|
+
|
|
167
|
+
**One instance per suite, one fresh user per test.** Emails are unique per
|
|
168
|
+
project, so per-test `seed.user()` calls are isolation enough for login-flow
|
|
169
|
+
tests, and tests run fully parallel against the shared instance
|
|
170
|
+
(demonstrated by `demo-next-e2e:e2e-real`, 2 workers).
|
|
171
|
+
|
|
172
|
+
Measured on an arm64 macBook (dev build, July 2026):
|
|
173
|
+
|
|
174
|
+
| Operation | Time |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| Cold boot (fresh data dir: initdb + migrations + health) | ~20–27s |
|
|
177
|
+
| Warm restart (existing data dir) | ~15s |
|
|
178
|
+
| Stop incl. embedded-Postgres reap | ~12s |
|
|
179
|
+
| Bootstrap (project + schema + flow) | ~100ms |
|
|
180
|
+
| Seed one user (create + password) | ~50ms |
|
|
181
|
+
| Full `e2e-real` suite (boot + Next dev + 2 browser tests) | ~25s |
|
|
182
|
+
|
|
183
|
+
**Instance-per-worker is not worth it for browser e2e**: the app dev server
|
|
184
|
+
boots once with one project's env, and every extra worker would pay the full
|
|
185
|
+
cold boot. It becomes interesting for API-level (no-browser) suites that
|
|
186
|
+
mutate project-wide state; revisit with warm-dir reuse if that need appears.
|
|
187
|
+
|
|
188
|
+
## Debugging
|
|
189
|
+
|
|
190
|
+
- `startLocalZitadel({ keep: true })` keeps the temp dir; the server log is at
|
|
191
|
+
`<dir>/.zitadel/local/server.log`. Boot failures always keep the dir and
|
|
192
|
+
print its path.
|
|
193
|
+
- A crashed run can orphan a server: `zitadel stop --all` sweeps every
|
|
194
|
+
CLI-managed runtime on the machine.
|
|
195
|
+
|
|
196
|
+
## Developing in this repo
|
|
197
|
+
|
|
198
|
+
Customer installs get the published server binary through `@zitadel/server`'s
|
|
199
|
+
platform packages; the in-repo workspace carries no such binary, so the repo's
|
|
200
|
+
own suites run `moon run server:build` and point the kit at the result via
|
|
201
|
+
`ZITADEL_SERVER_BINARY` (the `withZitadel` option `zitadel.serverBinary` /
|
|
202
|
+
`serverBinaryHint` exists for this). The in-repo moon tasks set
|
|
203
|
+
`NEXTGEN_SERVER_LOGIN_ENABLED=false` / `NEXTGEN_SERVER_CONSOLE_ENABLED=false`
|
|
204
|
+
— the suites drive the app-embedded login, not the server-hosted `/ui/*`.
|
|
205
|
+
Customer installs need none of this.
|
|
206
|
+
|
|
207
|
+
## Known limitations
|
|
208
|
+
|
|
209
|
+
- `@zitadel/api`'s client stores auth in module-global state; avoid
|
|
210
|
+
interleaving calls to *different* instances within one process. Separate
|
|
211
|
+
processes (Playwright workers) are unaffected.
|
|
212
|
+
- macOS/Linux only for now. Not because of the port preflight (it degrades
|
|
213
|
+
gracefully where `lsof` is missing) — the untested surface on Windows is the
|
|
214
|
+
process-group stop and embedded-Postgres lifecycle. Revisit once the local
|
|
215
|
+
runtime's SQLite default removes the Postgres component.
|
|
216
|
+
|
|
217
|
+
## Roadmap
|
|
218
|
+
|
|
219
|
+
This package is deliberately the **local runtime core** — "Testcontainers for
|
|
220
|
+
Zitadel" when the app under test and Playwright share one machine. The layers
|
|
221
|
+
on top, in intended order:
|
|
222
|
+
|
|
223
|
+
1. **Registration fixtures.** Landed: `zitadel.identity()` plus the
|
|
224
|
+
demo-next-e2e registration spec driving the real registration UI and
|
|
225
|
+
verifying the created user through the API. `cli-journey-e2e` keeps the
|
|
226
|
+
packaged-product registration coverage.
|
|
227
|
+
2. **Email/OTP capture.** `zitadel.email.waitForCode(address)` for
|
|
228
|
+
verification flows. Blocked on a server-side story (dev SMTP sink or
|
|
229
|
+
API-exposed codes); password-only flows don't hit this.
|
|
230
|
+
3. **Vitest surface.** Landed from this item: `withZitadel(config)`
|
|
231
|
+
orchestration, `AppEnvTemplate`/`nextAppEnv`, and the session-mint seed-op
|
|
232
|
+
(`seed.session()` driving the real flow headlessly, `authenticatedPage` on
|
|
233
|
+
top). Remaining: a dedicated `@zitadel/testing/vitest` entry once a second
|
|
234
|
+
backend consumer exists — `sessionToken` already serves backend tests
|
|
235
|
+
directly.
|
|
236
|
+
4. **Remote mode: ephemeral project on a persistent instance.** For app
|
|
237
|
+
deployments that cannot reach a local process (preview environments).
|
|
238
|
+
`bootstrapProject({ baseUrl })` + `connectZitadel(handle)` already compose
|
|
239
|
+
into this today; formalizing it means cleanup semantics and docs.
|
|
240
|
+
5. **Vercel Sandbox runtime.** A publicly reachable ephemeral instance per
|
|
241
|
+
preview deployment (e.g. `@zitadel/testing/vercel`), returning the same
|
|
242
|
+
`InstanceHandle` so fixtures don't change. Gated on a spike: embedded
|
|
243
|
+
Postgres on the Sandbox image, forwarded proto/host handling, secure
|
|
244
|
+
cookies + issuer + handoff verification through `sandbox.domain()`,
|
|
245
|
+
registering the preview URL as an allowed origin post-deploy
|
|
246
|
+
(`PATCH /projects`), and cleanup that survives failed runs.
|
|
247
|
+
6. **Publishing.** Landed: the consumer journey installs the kit the way a
|
|
248
|
+
customer would in CI, and the kit ships on the release train (release
|
|
249
|
+
manifest + changeset `fixed` group), versioned in lockstep with
|
|
250
|
+
`@zitadel/cli`. Remaining: the Windows decision (deferred until the SQLite
|
|
251
|
+
local default removes the embedded-Postgres lifecycle) and the stability
|
|
252
|
+
commitment when the train leaves alpha.
|
|
253
|
+
|
|
254
|
+
Parked until server support exists: passkey seeding (pre-registered WebAuthn
|
|
255
|
+
credentials). Independent refactor: extract `apps/cli/src/lib/local-server`
|
|
256
|
+
into a shared package and swap the lifecycle shell-out for imports.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ZitadelClient } from "@zitadel/api/client";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Serializable description of a bootstrapped instance + project. This is the
|
|
6
|
+
* contract that crosses process boundaries (boot script -> Playwright workers).
|
|
7
|
+
*/
|
|
8
|
+
interface InstanceHandle {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
projectId: string;
|
|
11
|
+
projectSecret: string;
|
|
12
|
+
/**
|
|
13
|
+
* First registered app origin. Flow submissions enforce the project's
|
|
14
|
+
* origin allowlist, so headless drivers (seedSession) send it as the
|
|
15
|
+
* Origin header the way a browser request through the app would.
|
|
16
|
+
*/
|
|
17
|
+
appOrigin?: string;
|
|
18
|
+
/**
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
schemaId: string;
|
|
24
|
+
previewSecret?: string;
|
|
25
|
+
}
|
|
26
|
+
interface SeedUserInput {
|
|
27
|
+
email?: string;
|
|
28
|
+
password?: string;
|
|
29
|
+
/** Extra schema-defined user properties merged into the create body. */
|
|
30
|
+
attributes?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
interface SeededUser {
|
|
33
|
+
id: string;
|
|
34
|
+
email: string;
|
|
35
|
+
password: string;
|
|
36
|
+
}
|
|
37
|
+
/** An unused identity: seeded nowhere, ready for registration-flow specs. */
|
|
38
|
+
interface Identity {
|
|
39
|
+
email: string;
|
|
40
|
+
password: string;
|
|
41
|
+
}
|
|
42
|
+
/** Injectable mirror of the server's session cookie (session.go). */
|
|
43
|
+
interface SessionCookie {
|
|
44
|
+
name: string;
|
|
45
|
+
value: string;
|
|
46
|
+
httpOnly: boolean;
|
|
47
|
+
secure: boolean;
|
|
48
|
+
sameSite: "Lax";
|
|
49
|
+
path: string;
|
|
50
|
+
}
|
|
51
|
+
interface MintedSession {
|
|
52
|
+
user: SeededUser;
|
|
53
|
+
/** Bearer for session management APIs (e.g. `GET /sessions/me`). */
|
|
54
|
+
sessionToken: string;
|
|
55
|
+
expiresAt: string;
|
|
56
|
+
cookie: SessionCookie;
|
|
57
|
+
}
|
|
58
|
+
interface SeedSessionInput extends SeedUserInput {
|
|
59
|
+
/** Mint for an existing user instead of seeding a fresh one. */
|
|
60
|
+
user?: SeededUser;
|
|
61
|
+
/** Specific flow definition; the project default when omitted. */
|
|
62
|
+
flowDefinitionName?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Origin header for the flow calls (must be on the project's allowlist).
|
|
65
|
+
* Defaults to the handle's registered app origin; the Playwright fixtures
|
|
66
|
+
* pass the suite's baseURL.
|
|
67
|
+
*/
|
|
68
|
+
origin?: string;
|
|
69
|
+
}
|
|
70
|
+
interface SeedUsersTemplate {
|
|
71
|
+
email?: (index: number) => string;
|
|
72
|
+
password?: (index: number) => string;
|
|
73
|
+
attributes?: (index: number) => Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
interface ConnectedZitadel {
|
|
76
|
+
handle: InstanceHandle;
|
|
77
|
+
/** Authenticated platform API client (bearer = project secret). */
|
|
78
|
+
api: ZitadelClient;
|
|
79
|
+
/** Env vars an SDK-based app needs to talk to this instance/project. */
|
|
80
|
+
appEnv: Record<string, string>;
|
|
81
|
+
seedUser(input?: SeedUserInput): Promise<SeededUser>;
|
|
82
|
+
/** Batch-seed users; the template makes fixture data deterministic. */
|
|
83
|
+
seedUsers(count: number, template?: SeedUsersTemplate): Promise<SeededUser[]>;
|
|
84
|
+
/** A unique unused email+password — nothing is created on the instance. */
|
|
85
|
+
identity(): Identity;
|
|
86
|
+
/**
|
|
87
|
+
* Seed (or take) a user and drive the real login flow headlessly to a
|
|
88
|
+
* session: tests inject `cookie` to start authenticated, backend tests use
|
|
89
|
+
* `sessionToken` directly. Password flows only.
|
|
90
|
+
*/
|
|
91
|
+
seedSession(input?: SeedSessionInput): Promise<MintedSession>;
|
|
92
|
+
}
|
|
93
|
+
interface LocalZitadelRuntime {
|
|
94
|
+
port: number;
|
|
95
|
+
pid: number;
|
|
96
|
+
/** State directory holding `.zitadel/local/` (data dir, logs, runtime.json). */
|
|
97
|
+
dir: string;
|
|
98
|
+
logPath: string;
|
|
99
|
+
}
|
|
100
|
+
interface LocalZitadel extends ConnectedZitadel, AsyncDisposable {
|
|
101
|
+
runtime: LocalZitadelRuntime;
|
|
102
|
+
stop(): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/app-env.d.ts
|
|
106
|
+
/**
|
|
107
|
+
* Declarative mapping from an app's env var names to InstanceHandle fields.
|
|
108
|
+
* A template (not a function) so it can cross process boundaries: the
|
|
109
|
+
* Playwright config serializes it into the app-runner's environment, where it
|
|
110
|
+
* is applied to the handle read from the handshake file.
|
|
111
|
+
*/
|
|
112
|
+
type AppEnvTemplate = Record<string, keyof InstanceHandle>;
|
|
113
|
+
/**
|
|
114
|
+
* The env shape `@zitadel/sdk-next` apps read. Other frameworks pass their own
|
|
115
|
+
* template — the console maps the same handle fields to `VITE_*`/`CONSOLE_*`
|
|
116
|
+
* names, for example.
|
|
117
|
+
*/
|
|
118
|
+
declare const nextAppEnv: AppEnvTemplate;
|
|
119
|
+
declare function applyAppEnvTemplate(template: AppEnvTemplate, handle: InstanceHandle): Record<string, string>;
|
|
120
|
+
//#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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-env-D3W0GYhA.d.mts","names":[],"sources":["../src/types.ts","../src/app-env.ts"],"mappings":";;;;;AAMA;;UAAiB,cAAA;EACf,OAAA;EACA,SAAA;EACA,aAAA;EAAA;;;;;EAMA,SAAA;EAUe;;;;;EAJf,QAAA;EACA,aAAA;AAAA;AAAA,UAGe,aAAA;EACf,KAAA;EACA,QAAA;EAKyB;EAHzB,UAAA,GAAa,MAAA;AAAA;AAAA,UAGE,UAAA;EACf,EAAA;EACA,KAAA;EACA,QAAA;AAAA;AAIF;AAAA,UAAiB,QAAA;EACf,KAAA;EACA,QAAA;AAAA;AAIF;AAAA,UAAiB,aAAA;EACf,IAAA;EACA,KAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,IAAA;AAAA;AAAA,UAGe,aAAA;EACf,IAAA,EAAM,UAAA;EAJF;EAMJ,YAAA;EACA,SAAA;EACA,MAAA,EAAQ,aAAA;AAAA;AAAA,UAGO,gBAAA,SAAyB,aAAA;EAPlC;EASN,IAAA,GAAO,UAAA;EANP;EAQA,kBAAA;EAPQ;;;AAGV;;EAUE,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;EAPO;EASf,GAAA,EAAK,aAAA;;EAEL,MAAA,EAAQ,MAAA;EACR,QAAA,CAAS,KAAA,GAAQ,aAAA,GAAgB,OAAA,CAAQ,UAAA;EAXhC;EAaT,SAAA,CAAU,KAAA,UAAe,QAAA,GAAW,iBAAA,GAAoB,OAAA,CAAQ,UAAA;EAZpD;EAcZ,QAAA,IAAY,QAAA;EAbE;;;;AAGhB;EAgBE,WAAA,CAAY,KAAA,GAAQ,gBAAA,GAAmB,OAAA,CAAQ,aAAA;AAAA;AAAA,UAGhC,mBAAA;EACf,IAAA;EACA,GAAA;EAfiB;EAiBjB,GAAA;EACA,OAAA;AAAA;AAAA,UAGe,YAAA,SAAqB,gBAAA,EAAkB,eAAA;EACtD,OAAA,EAAS,mBAAA;EACT,IAAA,IAAQ,OAAA;AAAA;;;;;AAxGV;;;;KCEY,cAAA,GAAiB,MAAA,eAAqB,cAAA;;;;;;cAOrC,UAAA,EAAY,cAAA;AAAA,iBAMT,mBAAA,CACd,QAAA,EAAU,cAAA,EACV,MAAA,EAAQ,cAAA,GACP,MAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const require_handshake = require("./handshake-CggSIoxF.cjs");
|
|
2
|
+
const require_orchestration = require("./orchestration-D7QBgQ0l.cjs");
|
|
3
|
+
let node_child_process = require("node:child_process");
|
|
4
|
+
//#region src/app-runner.ts
|
|
5
|
+
/**
|
|
6
|
+
* Playwright webServer entry generated by `withZitadel()`: waits for the
|
|
7
|
+
* supervisor's handshake, then runs the app's dev server with the instance
|
|
8
|
+
* env applied. Signals forward to the child so Playwright teardown stops the
|
|
9
|
+
* app cleanly. Configuration arrives as JSON in ZITADEL_TESTING_APP_RUNNER.
|
|
10
|
+
*/
|
|
11
|
+
const LOG = "[zitadel-testing-app]";
|
|
12
|
+
async function main() {
|
|
13
|
+
const config = require_orchestration.parseAppRunnerConfig(process.env[require_orchestration.APP_RUNNER_CONFIG_ENV]);
|
|
14
|
+
const handle = await require_handshake.waitForHandshake(require_orchestration.requireHandshakePath(process.env), config.handshakeTimeoutMs ?? 18e4);
|
|
15
|
+
console.log(`${LOG} starting ${config.command.join(" ")} against ${handle.baseUrl}`);
|
|
16
|
+
const [bin, ...args] = config.command;
|
|
17
|
+
const child = (0, node_child_process.spawn)(bin, args, {
|
|
18
|
+
cwd: config.cwd,
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
...require_handshake.applyAppEnvTemplate(config.env ?? {}, handle)
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
child.on("exit", (code) => {
|
|
26
|
+
process.exit(code ?? 1);
|
|
27
|
+
});
|
|
28
|
+
for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => child.kill(signal));
|
|
29
|
+
}
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error(`${LOG} ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=app-runner.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-runner.cjs","names":["parseAppRunnerConfig","APP_RUNNER_CONFIG_ENV","waitForHandshake","requireHandshakePath","applyAppEnvTemplate"],"sources":["../src/app-runner.ts"],"sourcesContent":["/**\n * Playwright webServer entry generated by `withZitadel()`: waits for the\n * supervisor's handshake, then runs the app's dev server with the instance\n * env applied. Signals forward to the child so Playwright teardown stops the\n * app cleanly. Configuration arrives as JSON in ZITADEL_TESTING_APP_RUNNER.\n */\nimport { spawn } from \"node:child_process\";\n\nimport { applyAppEnvTemplate } from \"./app-env\";\nimport { waitForHandshake } from \"./handshake\";\nimport { APP_RUNNER_CONFIG_ENV, parseAppRunnerConfig, requireHandshakePath } from \"./orchestration\";\n\nconst LOG = \"[zitadel-testing-app]\";\n\nasync function main(): Promise<void> {\n const config = parseAppRunnerConfig(process.env[APP_RUNNER_CONFIG_ENV]);\n const handshakePath = requireHandshakePath(process.env);\n\n const handle = await waitForHandshake(handshakePath, config.handshakeTimeoutMs ?? 180_000);\n console.log(`${LOG} starting ${config.command.join(\" \")} against ${handle.baseUrl}`);\n\n const [bin, ...args] = config.command;\n const child = spawn(bin as string, args, {\n cwd: config.cwd,\n stdio: \"inherit\",\n env: {\n ...process.env,\n ...applyAppEnvTemplate(config.env ?? {}, handle),\n },\n });\n child.on(\"exit\", (code) => {\n process.exit(code ?? 1);\n });\n for (const signal of [\"SIGTERM\", \"SIGINT\"] as const) {\n process.on(signal, () => child.kill(signal));\n }\n}\n\nmain().catch((error: unknown) => {\n console.error(`${LOG} ${error instanceof Error ? error.message : String(error)}`);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;AAYA,MAAM,MAAM;AAEZ,eAAe,OAAsB;CACnC,MAAM,SAASA,sBAAAA,qBAAqB,QAAQ,IAAIC,sBAAAA,uBAAuB;CAGvE,MAAM,SAAS,MAAMC,kBAAAA,iBAFCC,sBAAAA,qBAAqB,QAAQ,IAEA,EAAE,OAAO,sBAAsB,KAAQ;AAC1F,SAAQ,IAAI,GAAG,IAAI,YAAY,OAAO,QAAQ,KAAK,IAAI,CAAC,WAAW,OAAO,UAAU;CAEpF,MAAM,CAAC,KAAK,GAAG,QAAQ,OAAO;CAC9B,MAAM,SAAA,GAAA,mBAAA,OAAc,KAAe,MAAM;EACvC,KAAK,OAAO;EACZ,OAAO;EACP,KAAK;GACH,GAAG,QAAQ;GACX,GAAGC,kBAAAA,oBAAoB,OAAO,OAAO,EAAE,EAAE,OAAO;GACjD;EACF,CAAC;AACF,OAAM,GAAG,SAAS,SAAS;AACzB,UAAQ,KAAK,QAAQ,EAAE;GACvB;AACF,MAAK,MAAM,UAAU,CAAC,WAAW,SAAS,CACxC,SAAQ,GAAG,cAAc,MAAM,KAAK,OAAO,CAAC;;AAIhD,MAAM,CAAC,OAAO,UAAmB;AAC/B,SAAQ,MAAM,GAAG,IAAI,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GAAG;AACjF,SAAQ,KAAK,EAAE;EACf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { i as applyAppEnvTemplate, n as waitForHandshake } from "./handshake-BPtWruO8.mjs";
|
|
2
|
+
import { i as parseAppRunnerConfig, o as requireHandshakePath, t as APP_RUNNER_CONFIG_ENV } from "./orchestration-C640_1Lg.mjs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
//#region src/app-runner.ts
|
|
5
|
+
/**
|
|
6
|
+
* Playwright webServer entry generated by `withZitadel()`: waits for the
|
|
7
|
+
* supervisor's handshake, then runs the app's dev server with the instance
|
|
8
|
+
* env applied. Signals forward to the child so Playwright teardown stops the
|
|
9
|
+
* app cleanly. Configuration arrives as JSON in ZITADEL_TESTING_APP_RUNNER.
|
|
10
|
+
*/
|
|
11
|
+
const LOG = "[zitadel-testing-app]";
|
|
12
|
+
async function main() {
|
|
13
|
+
const config = parseAppRunnerConfig(process.env[APP_RUNNER_CONFIG_ENV]);
|
|
14
|
+
const handle = await waitForHandshake(requireHandshakePath(process.env), config.handshakeTimeoutMs ?? 18e4);
|
|
15
|
+
console.log(`${LOG} starting ${config.command.join(" ")} against ${handle.baseUrl}`);
|
|
16
|
+
const [bin, ...args] = config.command;
|
|
17
|
+
const child = spawn(bin, args, {
|
|
18
|
+
cwd: config.cwd,
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
...applyAppEnvTemplate(config.env ?? {}, handle)
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
child.on("exit", (code) => {
|
|
26
|
+
process.exit(code ?? 1);
|
|
27
|
+
});
|
|
28
|
+
for (const signal of ["SIGTERM", "SIGINT"]) process.on(signal, () => child.kill(signal));
|
|
29
|
+
}
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error(`${LOG} ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export {};
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=app-runner.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-runner.mjs","names":[],"sources":["../src/app-runner.ts"],"sourcesContent":["/**\n * Playwright webServer entry generated by `withZitadel()`: waits for the\n * supervisor's handshake, then runs the app's dev server with the instance\n * env applied. Signals forward to the child so Playwright teardown stops the\n * app cleanly. Configuration arrives as JSON in ZITADEL_TESTING_APP_RUNNER.\n */\nimport { spawn } from \"node:child_process\";\n\nimport { applyAppEnvTemplate } from \"./app-env\";\nimport { waitForHandshake } from \"./handshake\";\nimport { APP_RUNNER_CONFIG_ENV, parseAppRunnerConfig, requireHandshakePath } from \"./orchestration\";\n\nconst LOG = \"[zitadel-testing-app]\";\n\nasync function main(): Promise<void> {\n const config = parseAppRunnerConfig(process.env[APP_RUNNER_CONFIG_ENV]);\n const handshakePath = requireHandshakePath(process.env);\n\n const handle = await waitForHandshake(handshakePath, config.handshakeTimeoutMs ?? 180_000);\n console.log(`${LOG} starting ${config.command.join(\" \")} against ${handle.baseUrl}`);\n\n const [bin, ...args] = config.command;\n const child = spawn(bin as string, args, {\n cwd: config.cwd,\n stdio: \"inherit\",\n env: {\n ...process.env,\n ...applyAppEnvTemplate(config.env ?? {}, handle),\n },\n });\n child.on(\"exit\", (code) => {\n process.exit(code ?? 1);\n });\n for (const signal of [\"SIGTERM\", \"SIGINT\"] as const) {\n process.on(signal, () => child.kill(signal));\n }\n}\n\nmain().catch((error: unknown) => {\n console.error(`${LOG} ${error instanceof Error ? error.message : String(error)}`);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;AAYA,MAAM,MAAM;AAEZ,eAAe,OAAsB;CACnC,MAAM,SAAS,qBAAqB,QAAQ,IAAI,uBAAuB;CAGvE,MAAM,SAAS,MAAM,iBAFC,qBAAqB,QAAQ,IAEA,EAAE,OAAO,sBAAsB,KAAQ;AAC1F,SAAQ,IAAI,GAAG,IAAI,YAAY,OAAO,QAAQ,KAAK,IAAI,CAAC,WAAW,OAAO,UAAU;CAEpF,MAAM,CAAC,KAAK,GAAG,QAAQ,OAAO;CAC9B,MAAM,QAAQ,MAAM,KAAe,MAAM;EACvC,KAAK,OAAO;EACZ,OAAO;EACP,KAAK;GACH,GAAG,QAAQ;GACX,GAAG,oBAAoB,OAAO,OAAO,EAAE,EAAE,OAAO;GACjD;EACF,CAAC;AACF,OAAM,GAAG,SAAS,SAAS;AACzB,UAAQ,KAAK,QAAQ,EAAE;GACvB;AACF,MAAK,MAAM,UAAU,CAAC,WAAW,SAAS,CACxC,SAAQ,GAAG,cAAc,MAAM,KAAK,OAAO,CAAC;;AAIhD,MAAM,CAAC,OAAO,UAAmB;AAC/B,SAAQ,MAAM,GAAG,IAAI,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GAAG;AACjF,SAAQ,KAAK,EAAE;EACf"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
//#region src/app-env.ts
|
|
6
|
+
/**
|
|
7
|
+
* The env shape `@zitadel/sdk-next` apps read. Other frameworks pass their own
|
|
8
|
+
* template — the console maps the same handle fields to `VITE_*`/`CONSOLE_*`
|
|
9
|
+
* names, for example.
|
|
10
|
+
*/
|
|
11
|
+
const nextAppEnv = {
|
|
12
|
+
ZITADEL_URL: "baseUrl",
|
|
13
|
+
NEXT_PUBLIC_ZITADEL_PROJECT_ID: "projectId",
|
|
14
|
+
ZITADEL_PROJECT_SECRET: "projectSecret"
|
|
15
|
+
};
|
|
16
|
+
function applyAppEnvTemplate(template, handle) {
|
|
17
|
+
const env = {};
|
|
18
|
+
for (const [name, field] of Object.entries(template)) {
|
|
19
|
+
const value = handle[field];
|
|
20
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`app env template maps "${name}" to handle field "${field}", which the instance handle does not carry`);
|
|
21
|
+
env[name] = value;
|
|
22
|
+
}
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/handshake.ts
|
|
27
|
+
/**
|
|
28
|
+
* The handshake file carries an InstanceHandle across process boundaries:
|
|
29
|
+
* written by the script that boots + bootstraps the instance, read by
|
|
30
|
+
* Playwright workers (fixtures) and the app dev-server wrapper.
|
|
31
|
+
*/
|
|
32
|
+
async function writeHandshake(path, handle) {
|
|
33
|
+
await mkdir(dirname(path), { recursive: true });
|
|
34
|
+
await writeFile(path, `${JSON.stringify(handle, null, 2)}\n`, { mode: 384 });
|
|
35
|
+
}
|
|
36
|
+
function readHandshakeSync(path) {
|
|
37
|
+
const contents = readFileSync(path, "utf8");
|
|
38
|
+
let value;
|
|
39
|
+
try {
|
|
40
|
+
value = JSON.parse(contents);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`handshake file ${path} contains invalid JSON: ${error.message}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
return validateHandle(value, path);
|
|
45
|
+
}
|
|
46
|
+
async function waitForHandshake(path, timeoutMs = 6e4) {
|
|
47
|
+
const deadline = Date.now() + timeoutMs;
|
|
48
|
+
for (;;) try {
|
|
49
|
+
return validateHandle(JSON.parse(await readFile(path, "utf8")), path);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (Date.now() >= deadline) throw new Error(`handshake file not readable within ${timeoutMs}ms: ${path} (${error.message})`, { cause: error });
|
|
52
|
+
await setTimeout(250);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateHandle(value, source) {
|
|
56
|
+
const handle = value;
|
|
57
|
+
for (const field of [
|
|
58
|
+
"baseUrl",
|
|
59
|
+
"projectId",
|
|
60
|
+
"projectSecret",
|
|
61
|
+
"schemaId"
|
|
62
|
+
]) if (typeof handle?.[field] !== "string" || handle[field].length === 0) throw new Error(`handshake file ${source} is missing "${field}"`);
|
|
63
|
+
if (!URL.canParse(handle.baseUrl)) throw new Error(`handshake file ${source} has a malformed "baseUrl": ${handle.baseUrl}`);
|
|
64
|
+
return handle;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { nextAppEnv as a, applyAppEnvTemplate as i, waitForHandshake as n, writeHandshake as r, readHandshakeSync as t };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=handshake-BPtWruO8.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake-BPtWruO8.mjs","names":["sleep"],"sources":["../src/app-env.ts","../src/handshake.ts"],"sourcesContent":["import type { InstanceHandle } from \"./types\";\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, keyof InstanceHandle>;\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 } 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 const handle = value as Partial<InstanceHandle> | null;\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 return handle as InstanceHandle;\n}\n"],"mappings":";;;;;;;;;;AAeA,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;;;;;;;;;ACzBT,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;CACtE,MAAM,SAAS;AACf,MAAK,MAAM,SAAS;EAAC;EAAW;EAAa;EAAiB;EAAW,CACvE,KAAI,OAAO,SAAS,WAAW,YAAY,OAAO,OAAO,WAAW,EAClE,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,QAAO"}
|