@jskit-ai/agent-docs 0.1.84 → 0.1.87

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.
@@ -961,39 +961,40 @@ That explains why the login screen and auth guard runtime both care about `/api/
961
961
 
962
962
  It is also why the shell widget can react cleanly to auth state without storing raw session tokens in client state. The browser just asks the app for the current session view, and the app derives that from its cookies plus the selected provider.
963
963
 
964
- ### Authenticated Playwright testing with the dev auth bypass
964
+ ### Authenticated Playwright testing
965
965
 
966
- JSKIT ships a development-only auth bootstrap path specifically so authenticated UI can be verified in Playwright without depending on a real live login flow through an external auth provider.
966
+ JSKIT supports two intentionally separate authenticated-browser paths:
967
967
 
968
- This is the standard path the agent should use for authenticated browser tests:
968
+ - a private localhost exchange for direct local Playwright runs
969
+ - runner-provided Playwright storage state for managed previews
969
970
 
970
- - enable the dev auth bypass in development
971
- - create a session for an existing user through the local app
972
- - let the browser keep the resulting HTTP-only cookies
973
- - navigate to the protected page and verify the feature
974
- - record the Playwright run through `jskit app verify-ui` so `jskit doctor` can verify the receipt later
971
+ Neither path drives a live external login provider. Both put the resulting HTTP-only cookies into the same Playwright browser context that runs the feature assertions.
975
972
 
976
- The feature is intentionally narrow.
973
+ The generated `playwright.config.mjs` is a thin delegate to `@jskit-ai/jskit-cli/test/playwright`. The published helper owns the changing setup behavior:
977
974
 
978
- - It is development-only.
979
- - It must never be enabled in production.
980
- - JSKIT rejects boot if `AUTH_DEV_BYPASS_ENABLED=true` while `NODE_ENV=production`.
981
- - The route only looks up an existing user. It does not create one.
975
+ - without `PLAYWRIGHT_BASE_URL`, it builds the app and starts the local server on `http://127.0.0.1:4173`
976
+ - with `PLAYWRIGHT_BASE_URL`, it uses that managed preview and does not start another server
977
+ - with `JSKIT_PLAYWRIGHT_STORAGE_STATE`, it loads the supplied authenticated state into Playwright contexts
982
978
 
983
- The environment variables are:
979
+ Tests should therefore navigate with relative paths such as `page.goto("/w/acme/admin/contacts")`.
980
+
981
+ #### Direct local dev-auth login
982
+
983
+ The local dev auth bypass remains deliberately narrow:
984
+
985
+ - it is available only outside production
986
+ - the request must arrive locally
987
+ - it selects an existing user and never creates one
988
+ - it requires both CSRF protection and a private exchange secret
989
+
990
+ Enable it for the Playwright process and the local server it starts:
984
991
 
985
992
  ```bash
986
993
  AUTH_DEV_BYPASS_ENABLED=true
987
994
  AUTH_DEV_BYPASS_SECRET=replace-this-with-a-local-dev-secret
988
995
  ```
989
996
 
990
- When enabled outside production, the app exposes:
991
-
992
- ```text
993
- POST /api/dev-auth/login-as
994
- ```
995
-
996
- The request body must contain either:
997
+ When enabled, the app exposes `POST /api/dev-auth/login-as`. The body contains exactly one existing-user identity:
997
998
 
998
999
  ```json
999
1000
  { "userId": "7" }
@@ -1005,70 +1006,45 @@ or:
1005
1006
  { "email": "ada@example.com" }
1006
1007
  ```
1007
1008
 
1008
- The response is intentionally small:
1009
+ Do not call that route from `page.evaluate()`. The route requires the private `x-jskit-dev-auth-secret` header as well as the CSRF token. Putting the secret into browser JavaScript, client-visible environment, query parameters, or page globals would defeat the exchange boundary.
1009
1010
 
1010
- ```json
1011
- {
1012
- "ok": true,
1013
- "userId": "7",
1014
- "username": "Ada Example",
1015
- "email": "ada@example.com"
1016
- }
1017
- ```
1011
+ Use the published Node-side helper instead:
1018
1012
 
1019
- Behind the scenes, JSKIT creates the same HTTP-only auth cookies that the normal login flow would create. That means Playwright should not try to read raw tokens. It should bootstrap the session in the browser context, then navigate normally.
1013
+ ```ts
1014
+ import { expect, test } from "@playwright/test";
1015
+ import { loginAsExistingUser } from "@jskit-ai/auth-web/test/playwright";
1020
1016
 
1021
- One subtle point matters here:
1017
+ test("authenticated contacts filters", async ({ page }) => {
1018
+ await loginAsExistingUser(page, { email: "ada@example.com" });
1022
1019
 
1023
- - `/api/dev-auth/login-as` is still an unsafe `POST`
1024
- - JSKIT still expects a CSRF token
1025
- - the browser can get that token from `/api/session`
1020
+ await page.goto("/w/acme/admin/contacts");
1021
+ await expect(page.getByRole("heading", { name: "Contacts" })).toBeVisible();
1022
+ });
1023
+ ```
1026
1024
 
1027
- So the normal Playwright shape is:
1025
+ `loginAsExistingUser()` runs from the Playwright Node process. It uses the request client attached to `page.context()`, reads `csrfToken` from `GET /api/session`, sends that token plus `x-jskit-dev-auth-secret` to the login route, and leaves the response cookies in the same browser context. It reads `AUTH_DEV_BYPASS_SECRET` from the Node process by default and refuses to send it to a non-local URL.
1028
1026
 
1029
- 1. open a same-origin page first
1030
- 2. call `/api/session` to read `csrfToken`
1031
- 3. call `/api/dev-auth/login-as` with `credentials: "include"` and the `csrf-token` header
1032
- 4. navigate to the protected route and run the assertions
1027
+ That last restriction is important. A managed preview URL is not a direct localhost app connection, even when its backend eventually runs on the same infrastructure. Project code must not receive the managed host's private exchange authority.
1033
1028
 
1034
- For example:
1029
+ #### Managed preview authentication
1035
1030
 
1036
- ```ts
1037
- await page.goto("/");
1031
+ A managed host authenticates outside the project browser context. It performs its trusted identity exchange, writes the resulting cookies and origins to a temporary Playwright storage-state file, and starts the project test with:
1038
1032
 
1039
- await page.evaluate(async ({ email }) => {
1040
- const sessionResponse = await fetch("/api/session", {
1041
- credentials: "include"
1042
- });
1043
- if (!sessionResponse.ok) {
1044
- throw new Error(`Session bootstrap failed: ${sessionResponse.status}`);
1045
- }
1033
+ ```bash
1034
+ PLAYWRIGHT_BASE_URL=https://managed-preview.example.test \
1035
+ JSKIT_PLAYWRIGHT_STORAGE_STATE=/secure/temp/playwright-state.json \
1036
+ playwright test tests/e2e/contacts.spec.ts
1037
+ ```
1046
1038
 
1047
- const sessionPayload = await sessionResponse.json();
1048
- const csrfToken = String(sessionPayload?.csrfToken || "");
1049
- if (!csrfToken) {
1050
- throw new Error("Missing csrfToken from /api/session.");
1051
- }
1039
+ JSKIT only consumes that runner-neutral contract. It does not detect a particular host, call host-specific commands, proxy private headers, or install a browser.
1052
1040
 
1053
- const loginResponse = await fetch("/api/dev-auth/login-as", {
1054
- method: "POST",
1055
- credentials: "include",
1056
- headers: {
1057
- "content-type": "application/json",
1058
- "csrf-token": csrfToken
1059
- },
1060
- body: JSON.stringify({ email })
1061
- });
1041
+ The storage-state file is a secret because it can contain authenticated cookies. The managed runner must create it with appropriately restricted access, must not print or commit it, and must delete it after the run.
1062
1042
 
1063
- if (!loginResponse.ok) {
1064
- throw new Error(`Dev login failed: ${loginResponse.status} ${await loginResponse.text()}`);
1065
- }
1066
- }, { email: "ada@example.com" });
1043
+ Tests using managed state do not call `loginAsExistingUser()`. They begin with the runner-provided identity already present and navigate using relative paths. An ordinary browser or proxy request to `/api/dev-auth/login-as` without the private header must continue to fail with `403 Dev auth exchange is not authorized.`
1067
1044
 
1068
- await page.goto("/w/acme/admin/contacts");
1069
- ```
1045
+ #### Recording the result
1070
1046
 
1071
- In practice, the preferred wrapper is:
1047
+ After the actual Playwright flow succeeds, record it through JSKIT:
1072
1048
 
1073
1049
  ```bash
1074
1050
  npx jskit app verify-ui \
@@ -1077,20 +1053,17 @@ npx jskit app verify-ui \
1077
1053
  --auth-mode dev-auth-login-as
1078
1054
  ```
1079
1055
 
1080
- For local pre-merge review, the next step after recording that Playwright run is usually:
1056
+ Use `--auth-mode session-bootstrap` when a managed runner supplied authenticated storage state.
1057
+
1058
+ `jskit app verify-ui` executes the command and records the command, auth-mode label, feature, and changed UI files. The `--auth-mode` value describes how the Playwright command was authenticated. It does not create a session, inject a secret, or alter the browser context.
1059
+
1060
+ For local pre-merge review, follow the recorded run with:
1081
1061
 
1082
1062
  ```bash
1083
1063
  npx jskit doctor --against origin/main
1084
1064
  ```
1085
1065
 
1086
- Advanced CI pipelines can use the same `--against` contract too, but JSKIT does not scaffold hosted auth/database/browser verification by default.
1087
-
1088
- That flow is preferable to driving the real sign-in form in feature tests because it keeps the test focused on the UI feature being added, not on an external auth dependency. If a chunk changes user-facing UI and the flow requires login, the expected JSKIT review standard is:
1089
-
1090
- - use Playwright
1091
- - record the run with `jskit app verify-ui`
1092
- - use the local dev auth bypass or another local session bootstrap path
1093
- - exercise the actual changed behavior, not only page load
1066
+ This keeps feature tests focused on the changed UI while preserving the security boundary around session creation.
1094
1067
 
1095
1068
  ## When you later switch to Supabase
1096
1069
 
@@ -162,7 +162,6 @@ The most important parts look like this:
162
162
  "server": "node ./bin/server.js",
163
163
  "server:all": "node ./bin/server.js",
164
164
  "server:home": "SERVER_SURFACE=home node ./bin/server.js",
165
- "devlinks": "jskit app link-local-packages",
166
165
  "dev": "vite",
167
166
  "dev:all": "vite",
168
167
  "dev:home": "VITE_SURFACE=home vite",
@@ -206,9 +205,11 @@ The most important parts look like this:
206
205
 
207
206
  Published JSKIT libraries and tooling support Node.js 22 from 22.12.0 onward, Node.js 24, and Node.js 26. Newly generated applications deliberately require Node 26: their app-level `engines` contract, `.nvmrc`, and JSKIT-managed verification workflow all name that runtime. The app-level contract is the runtime boundary for the app and its installed JSKIT runtime packages, while independently consumed JSKIT CLI and tooling packages retain the wider supported range. The dependency on `@local/main` points at `file:packages/main`, which means your app already contains its own local JSKIT package. The maintenance scripts are also useful to notice early, because they show an important ownership boundary in JSKIT.
208
207
 
209
- `verify`, `jskit:update`, `devlinks`, and `release` are intentionally thin wrappers. They stay in `package.json` because they are convenient app-local shortcuts, but the real implementation lives in `jskit app ...`, not in copied scaffold scripts.
208
+ `verify`, `jskit:update`, and `release` are intentionally thin wrappers. They stay in `package.json` because they are convenient app-local shortcuts, but the real implementation lives in `jskit app ...`, not in copied scaffold scripts.
210
209
 
211
- That matters because JSKIT maintenance policy changes over time. If the scaffold copied a large shell script into every app, existing apps would freeze the old behavior forever. By delegating to `jskit app verify`, `jskit app update-packages`, `jskit app link-local-packages`, and `jskit app release`, the app keeps the nice `npm run` shortcuts while the maintained behavior stays in the installed CLI package.
210
+ That matters because JSKIT maintenance policy changes over time. If the scaffold copied a large shell script into every app, existing apps would freeze the old behavior forever. By delegating to `jskit app verify`, `jskit app update-packages`, and `jskit app release`, the app keeps the nice `npm run` shortcuts while the maintained behavior stays in the installed CLI package.
211
+
212
+ The Playwright scaffold follows the same rule. `playwright.config.mjs` delegates to `@jskit-ai/jskit-cli/test/playwright`, and the starter browser specs delegate their shared responsive checks to published JSKIT helpers. The generated files stay small while later JSKIT package updates can change local server startup, managed `PLAYWRIGHT_BASE_URL` handling, and `JSKIT_PLAYWRIGHT_STORAGE_STATE` support without copying that logic into each new app.
212
213
 
213
214
  `jskit app verify` is worth noticing specifically. Linting, tests, and builds check your source code and runtime behavior. The JSKIT part of that flow runs `doctor`, which checks JSKIT-managed app state: installed package visibility, lock-file-backed managed files, and other JSKIT-specific health rules. It is there because a JSKIT app is not only code. It is also a descriptor-driven managed project.
214
215
 
@@ -817,7 +818,7 @@ That is why you saw `@jskit-ai/kernel` and `@jskit-ai/http-runtime` earlier in `
817
818
 
818
819
  ### Other files and options
819
820
 
820
- The remaining files are easier to understand once you know the core pieces above. `vite.config.mjs` configures the frontend build and the `/api` proxy used during development. `index.html` is the HTML shell Vite uses to mount Vue. `tests/` contains basic smoke tests so the app has a verification path from day one. The `scripts/` directory is intentionally small because JSKIT maintenance helpers such as `verify`, `jskit:update`, `devlinks`, and `release` are package-owned CLI commands rather than copied app scripts.
821
+ The remaining files are easier to understand once you know the core pieces above. `vite.config.mjs` configures the frontend build and the `/api` proxy used during development. `index.html` is the HTML shell Vite uses to mount Vue. `tests/` contains basic smoke tests so the app has a verification path from day one. The `scripts/` directory is intentionally small because JSKIT maintenance helpers such as `verify`, `jskit:update`, and `release` are package-owned CLI commands rather than copied app scripts.
821
822
 
822
823
  The `create-app` command also accepts a few other flags that are useful without changing the basic meaning of this chapter's setup. `--title <text>` lets you replace the browser title and other template text with a friendlier app name. `--target <path>` lets you choose a different output directory instead of the default `./exampleapp`. `--tenancy-mode <mode>` can seed `none`, `personal`, or `workspaces`; for this chapter we intentionally use `none` so the first scaffold stays small and non-workspace. `--minimal` selects the bare `minimal-shell` template instead of the default shell-web app template. `--force` allows writing into a non-empty target directory when you know that is what you want. `--dry-run` prints the planned file writes without touching the filesystem, which is useful when you want to inspect what the generator would do. `-h` or `--help` prints the command help.
823
824
 
@@ -53,7 +53,6 @@ The important examples are:
53
53
 
54
54
  - `npm run verify`
55
55
  - `npm run jskit:update`
56
- - `npm run devlinks`
57
56
  - `npm run release`
58
57
 
59
58
  In the current scaffold, those scripts are intentionally thin:
@@ -63,7 +62,6 @@ In the current scaffold, those scripts are intentionally thin:
63
62
  "scripts": {
64
63
  "verify": "jskit app verify && npm run --if-present verify:app",
65
64
  "jskit:update": "jskit app update-packages",
66
- "devlinks": "jskit app link-local-packages",
67
65
  "release": "jskit app release"
68
66
  }
69
67
  }
@@ -71,12 +69,11 @@ In the current scaffold, those scripts are intentionally thin:
71
69
 
72
70
  That is a deliberate design choice.
73
71
 
74
- The app keeps the handy `npm run` names, but the real maintenance policy lives in the installed CLI package instead of copied shell scripts inside the app. That means if JSKIT later changes how package updates, local linking, or baseline verification should work, apps can pick up the new behavior by updating `@jskit-ai/jskit-cli` instead of hand-editing frozen scaffold files.
72
+ The app keeps the handy `npm run` names, but the real maintenance policy lives in the installed CLI package instead of copied shell scripts inside the app. That means if JSKIT later changes how package updates or baseline verification should work, apps can pick up the new behavior by updating `@jskit-ai/jskit-cli` instead of hand-editing frozen scaffold files.
75
73
 
76
74
  This gives you a clean ownership split:
77
75
 
78
76
  - app-owned scripts still describe how *this app* runs, builds, and tests
79
- - `npm run devlinks` re-applies local checkout links after `npm install` when you are testing an app against this monorepo
80
77
  - JSKIT-owned wrapper scripts delegate framework maintenance to `jskit app ...`
81
78
 
82
79
  That split is worth keeping in mind through the rest of the guide. When you see `npm run verify`, read it as "run the app's JSKIT baseline verification policy, then any app-specific extra verification hook".
@@ -116,14 +113,9 @@ npm run jskit:update
116
113
 
117
114
  After it succeeds, npm dependencies and descriptor-managed app state are current together. If a managed app-owned file is missing, a required saved option can no longer be resolved, or another package update cannot be applied safely, the app-wide update fails instead of leaving that package silently stale. `--dry-run` reports which installed packages would be reapplied without invoking their update lifecycle.
118
115
 
119
- There is one unavoidable bootstrap detail for apps whose installed CLI predates managed package reapplication: a CLI process that is already running cannot adopt code npm installs underneath it. Upgrade the CLI once, then run the app-wide update with the new process:
116
+ At the start of a real update, JSKIT resolves and installs the latest `@jskit-ai/jskit-cli`, then hands the rest of the operation to that app-local CLI process. This keeps the one-command contract even when the updater itself has changed: the current code, not the already-running older process, owns package reapplication, migrations, workspace refreshes, and CI synchronization.
120
117
 
121
- ```bash
122
- npm install --save-dev --save-exact @jskit-ai/jskit-cli@latest
123
- npm run jskit:update
124
- ```
125
-
126
- After that bootstrap, normal future upgrades use only `npm run jskit:update`. Do not work around the first upgrade by editing `.jskit/lock.json`; the new updater must reapply the descriptor contract and write managed state itself.
118
+ Do not pre-install the CLI separately and do not edit `.jskit/lock.json`. Run `npm run jskit:update`; the updater owns its bootstrap and writes managed state through the normal package lifecycle.
127
119
 
128
120
  The update reports elapsed progress for registry and install work. It also refreshes JSKIT-managed migrations and CI after root package changes. Preview the complete operation without changing manifests, descriptors, the lockfile, migrations, or CI with:
129
121
 
@@ -571,12 +563,6 @@ npm install
571
563
 
572
564
  `jskit add` changes the app. `npm install` downloads the dependencies that the changed app requires.
573
565
 
574
- If the app is being tested against a local JSKIT checkout with linked packages, run the local-link wrapper again after every `npm install`:
575
-
576
- ```bash
577
- npm run devlinks
578
- ```
579
-
580
566
  ### `add bundle`
581
567
 
582
568
  Bundles are a convenience layer for catalog shortcuts. Prefer package commands in setup docs and normal app work, especially for auth, because provider ownership is clearer when the selected provider package is installed explicitly.
@@ -818,6 +804,8 @@ That command does two things:
818
804
  - runs the targeted Playwright command you give it
819
805
  - writes a receipt describing the verified feature, auth mode, and current dirty UI file set
820
806
 
807
+ The auth mode is receipt metadata, not an authentication implementation. `jskit app verify-ui` does not log a user in, inject a secret, or rewrite the Playwright context. A direct local run uses `loginAsExistingUser()` from `@jskit-ai/auth-web/test/playwright`; a managed runner supplies authenticated state through `JSKIT_PLAYWRIGHT_STORAGE_STATE`. The generated `playwright.config.mjs` consumes that state and honors `PLAYWRIGHT_BASE_URL` without starting a second server.
808
+
821
809
  `doctor` then compares the current changed UI files to that receipt. If you edit the UI again afterwards, the receipt is stale and `doctor` tells you to rerun `jskit app verify-ui`.
822
810
 
823
811
  For local pre-merge review, use `--against <base-ref>` so JSKIT compares against the branch delta instead of only the current dirty worktree. The common shape is:
@@ -314,12 +314,6 @@ So before you build or run the app again, install that new local package:
314
314
  npm install
315
315
  ```
316
316
 
317
- If you are verifying the guide against a local JSKIT checkout and have already been using local package links, rerun:
318
-
319
- ```bash
320
- npm run devlinks
321
- ```
322
-
323
317
  The same rule applies after later server scaffolds such as `addresses` and `comments`. The UI generator can still read the generated resource file directly, but the app runtime needs the local package install boundary to be completed before the CRUD can boot normally.
324
318
 
325
319
  For standard CRUDs, that file is intentionally compact. It uses `defineCrudResource(...)` from `@jskit-ai/resource-crud-core`, authors the canonical `schema` / `searchSchema` / `defaultSort` / `autofilter` shape once, and lets JSKIT derive the standard CRUD operation contracts from it.
@@ -435,12 +429,6 @@ Then install the generated local package:
435
429
  npm install
436
430
  ```
437
431
 
438
- If you are developing against a local JSKIT checkout, relink your app to the local packages before you run the UI:
439
-
440
- ```bash
441
- npm run devlinks
442
- ```
443
-
444
432
  ### Step 3: refine the generated lookup metadata by hand
445
433
 
446
434
  Open `packages/addresses/src/shared/addressResource.js` and add `labelKey: "fullName"` to the generated `contactId` relation:
@@ -617,12 +605,6 @@ Then install the generated local package:
617
605
  npm install
618
606
  ```
619
607
 
620
- If you are developing against a local JSKIT checkout, relink the app before you run the UI:
621
-
622
- ```bash
623
- npm run devlinks
624
- ```
625
-
626
608
  ### Step 3: refine the generated lookup metadata by hand
627
609
 
628
610
  Open `packages/comments/src/shared/commentResource.js` and add `labelKey: "fullName"` to the generated `contactId` relation:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.84",
3
+ "version": "0.1.87",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,80 +11,88 @@ Rules:
11
11
 
12
12
  - Any chunk that adds or changes user-facing UI must include a Playwright flow that exercises the changed behavior before the chunk is done.
13
13
  - Generator or package template UI changes must be checked at compact phone, tablet-ish medium, and expanded desktop widths.
14
- - For generated UI, the browser checks should cover horizontal overflow, clipped text, invisible text, duplicate navigation, broken route placement, and tap targets under 48 px.
15
- - Apps with `shell-web` installed should start from the generated `tests/e2e/adaptive-shell.spec.ts` smoke and extend it for feature-specific assertions.
16
- - Record that Playwright run with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>`.
17
- - `jskit doctor` expects `.jskit/verification/ui.json` to match the current dirty UI file set when UI files are changed.
18
- - For local pre-merge review, follow the recorded Playwright run with `jskit doctor --against <base-ref>` so `doctor` compares against the branch delta instead of only the local dirty worktree.
19
- - Advanced CI setups may also use `--against <base-ref>`, but JSKIT does not scaffold hosted browser/auth/database verification by default.
20
- - Do not rely on a live external auth provider for Playwright verification of normal app features.
21
- - For authenticated UI in the standard JSKIT auth stack, use the development-only dev auth bypass route instead.
22
- - The standard route is `POST /api/dev-auth/login-as`.
23
- - The request body must include either `{ userId }` or `{ email }`.
24
- - The route is available only when `AUTH_DEV_BYPASS_ENABLED=true` and `AUTH_DEV_BYPASS_SECRET` is set outside production.
25
- - This route must never be enabled in production.
26
- - Because the route is an unsafe POST, fetch `csrfToken` from `/api/session` first and send it back as the `csrf-token` header.
27
- - Make the bootstrap request from the same browser context that will run the assertions so the auth cookies land in the page session.
28
- - Use stable seeded users or fixtures for Playwright. Do not depend on whatever account happens to exist in a developer's browser or external auth provider.
29
-
30
- Recommended flow:
14
+ - For generated UI, check horizontal overflow, clipped or invisible text, duplicate navigation, broken route placement, and tap targets under 48 px.
15
+ - Apps with `shell-web` installed should start from `tests/e2e/adaptive-shell.spec.ts` and extend it with feature-specific assertions.
16
+ - Generated `playwright.config.mjs` delegates to `@jskit-ai/jskit-cli/test/playwright`. Do not copy base-URL, web-server, or storage-state logic into app tests.
17
+ - Use relative paths such as `page.goto("/home")`. The shared config owns the browser base URL.
18
+ - A managed runner supplies `PLAYWRIGHT_BASE_URL`. When it is set, JSKIT does not start another app server.
19
+ - A managed runner supplies an authenticated context through `JSKIT_PLAYWRIGHT_STORAGE_STATE`. Treat that file as a temporary secret: do not commit it, print it, or retain it after the run.
20
+ - Do not install a browser when the environment provides a managed browser runner.
21
+
22
+ ## Direct local authentication
23
+
24
+ Use the development-only dev auth bypass when Playwright is talking directly to an app running on localhost.
25
+
26
+ The app must start with:
31
27
 
32
28
  ```bash
33
- npx jskit app verify-ui \
34
- --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
35
- --feature "contacts filters" \
36
- --auth-mode dev-auth-login-as
29
+ AUTH_DEV_BYPASS_ENABLED=true
30
+ AUTH_DEV_BYPASS_SECRET=replace-this-with-a-local-dev-secret
31
+ ```
32
+
33
+ The test uses the published Node-side helper:
34
+
35
+ ```ts
36
+ import { expect, test } from "@playwright/test";
37
+ import { loginAsExistingUser } from "@jskit-ai/auth-web/test/playwright";
38
+
39
+ test("authenticated feature", async ({ page }) => {
40
+ await loginAsExistingUser(page, { email: "ada@example.com" });
41
+ await page.goto("/w/acme/admin/contacts");
42
+ await expect(page.getByRole("heading", { name: "Contacts" })).toBeVisible();
43
+ });
37
44
  ```
38
45
 
39
- Local pre-merge follow-up:
46
+ `loginAsExistingUser()`:
47
+
48
+ - uses the request client belonging to the same Playwright browser context
49
+ - fetches the CSRF token from `GET /api/session`
50
+ - sends the CSRF token and private `x-jskit-dev-auth-secret` header to `POST /api/dev-auth/login-as`
51
+ - leaves the resulting HTTP-only cookies in that browser context
52
+ - refuses to send the secret to a non-local URL
53
+
54
+ The helper reads `AUTH_DEV_BYPASS_SECRET` from the Playwright Node process by default. Never pass that secret through `page.evaluate()`, browser globals, query parameters, or client-visible environment variables.
55
+
56
+ The route only selects an existing user. Seed a stable user or fixture before the test. The bypass must never be enabled in production.
57
+
58
+ ## Managed-host authentication
59
+
60
+ A managed preview must not expose `AUTH_DEV_BYPASS_SECRET` to project code or forward an ordinary browser request to the private login exchange. The host performs its trusted identity exchange outside the application browser context, writes a temporary Playwright storage-state file, and launches the test with:
40
61
 
41
62
  ```bash
42
- npx jskit doctor --against origin/main
63
+ PLAYWRIGHT_BASE_URL=https://managed-preview.example.test \
64
+ JSKIT_PLAYWRIGHT_STORAGE_STATE=/secure/temp/playwright-state.json \
65
+ playwright test tests/e2e/contacts.spec.ts
43
66
  ```
44
67
 
45
- The Playwright command itself should follow this setup shape when login is needed:
68
+ The generated config applies that state to Playwright contexts and omits its local `webServer`. Tests then navigate with relative paths and begin with the runner-provided identity already authenticated.
46
69
 
47
- ```ts
48
- await page.goto("/");
49
-
50
- await page.evaluate(async ({ email }) => {
51
- const sessionResponse = await fetch("/api/session", {
52
- credentials: "include"
53
- });
54
- if (!sessionResponse.ok) {
55
- throw new Error(`Session bootstrap failed: ${sessionResponse.status}`);
56
- }
57
-
58
- const sessionPayload = await sessionResponse.json();
59
- const csrfToken = String(sessionPayload?.csrfToken || "");
60
- if (!csrfToken) {
61
- throw new Error("Missing csrfToken from /api/session.");
62
- }
63
-
64
- const loginResponse = await fetch("/api/dev-auth/login-as", {
65
- method: "POST",
66
- credentials: "include",
67
- headers: {
68
- "content-type": "application/json",
69
- "csrf-token": csrfToken
70
- },
71
- body: JSON.stringify({ email })
72
- });
73
-
74
- if (!loginResponse.ok) {
75
- throw new Error(`Dev login failed: ${loginResponse.status} ${await loginResponse.text()}`);
76
- }
77
- }, { email: "ada@example.com" });
70
+ Do not call `loginAsExistingUser()` against a managed preview. It is deliberately localhost-only. An ordinary request to `/api/dev-auth/login-as` without the private exchange header must return `403`.
71
+
72
+ ## Recording verification
73
+
74
+ After the Playwright command succeeds, record it with:
75
+
76
+ ```bash
77
+ npx jskit app verify-ui \
78
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
79
+ --feature "contacts filters" \
80
+ --auth-mode dev-auth-login-as
78
81
  ```
79
82
 
80
- After that bootstrap:
83
+ Use `--auth-mode session-bootstrap` when a managed runner supplied authenticated storage state.
84
+
85
+ `jskit app verify-ui` runs the command and records its command, auth-mode label, feature, and changed UI files in `.jskit/verification/ui.json`. The auth-mode option describes how the command was authenticated; it does not create a session or modify the Playwright context.
86
+
87
+ For local pre-merge review, follow the recorded run with:
81
88
 
82
- - navigate to the protected page
83
- - exercise the new UI behavior
84
- - assert the actual outcome the chunk introduced
89
+ ```bash
90
+ npx jskit doctor --against origin/main
91
+ ```
85
92
 
86
93
  Do not mark the chunk done if:
87
94
 
88
95
  - the feature changed user-facing UI but no Playwright flow ran
89
96
  - the Playwright flow skipped the changed behavior itself
90
- - authenticated UI work was left untested because no local auth bootstrap path was available and that gap was not called out
97
+ - a managed environment was bypassed by installing another browser
98
+ - an authenticated flow was left untested without clearly reporting the missing session-bootstrap seam
@@ -116,6 +116,10 @@ Exports
116
116
  Exports
117
117
  - None
118
118
 
119
+ ### `templates/base-shell/playwright.config.mjs`
120
+ Exports
121
+ - None
122
+
119
123
  ### `templates/base-shell/server.js`
120
124
  Exports
121
125
  - `createServer()`
@@ -266,6 +270,10 @@ Exports
266
270
  Exports
267
271
  - None
268
272
 
273
+ ### `templates/minimal-shell/playwright.config.mjs`
274
+ Exports
275
+ - None
276
+
269
277
  ### `templates/minimal-shell/server.js`
270
278
  Exports
271
279
  - `createServer()`
@@ -512,15 +512,6 @@ Exports
512
512
  Local functions
513
513
  - `shouldRewriteScript(currentValue = "", scriptName = "", force = false)`
514
514
 
515
- ### `src/server/commandHandlers/appCommands/linkLocalPackages.js`
516
- Exports
517
- - `runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout })`
518
- Local functions
519
- - `collectDeclaredPackageNames(packageJson = {})`
520
- - `verifySymlinkTarget(targetPath = "", sourceDir = "", { packageName = "" } = {})`
521
- - `replaceWithSymlink(targetPath = "", sourceDir = "", { packageName = "" } = {})`
522
- - `maybeLinkCompanionPackages({ appRoot = "", repoRoot = "", stdout, createCliError })`
523
-
524
515
  ### `src/server/commandHandlers/appCommands/migrateSourceMutations.js`
525
516
  Exports
526
517
  - `buildCrudFormFieldPushMigration(sourceText = "")`
@@ -558,11 +549,7 @@ Exports
558
549
  - `formatUtcReleaseTimestamp(date = new Date())`
559
550
  - `resolveLocalJskitBin(appRoot = "")`
560
551
  - `runLocalJskit(appRoot, args = [], { stdout, stderr, createCliError, quiet = false } = {})`
561
- - `runLocalJskitAsync(appRoot, args = [], { stdout, stderr, createCliError, quiet = false } = {})`
562
- - `resolveLocalRepoRoot({ appRoot = "", explicitRepoRoot = "" } = {})`
563
- - `discoverLocalPackageMap(repoRoot = "")`
564
- - `linkPackageBinEntries({ appRoot, packageDirName, sourceDir, stdout } = {})`
565
- - `resolveSymlinkType()`
552
+ - `runLocalJskitAsync(appRoot, args = [], { env = {}, stdout, stderr, createCliError, quiet = false } = {})`
566
553
  Local functions
567
554
  - `ensureCommandSucceeded(result, label, { createCliError, cwd = "", stdout, stderr, quiet = false } = {})`
568
555
 
@@ -752,11 +739,11 @@ Local functions
752
739
  - `collectPlacementComponentTokensFromManagedRecords(installedPackageRecords = [])`
753
740
  - `renderWrappedShellCommand(binaryName, args = [], { maxWidth = 100, continuationIndent = " " } = {})`
754
741
  - `runLocalProjectBinary(binaryName, args = [], { appRoot, io, pathModule = path, createCliError, explanation = "", dryRun = false } = {})`
755
- - `installAppDependenciesForHook({ appRoot, appPackageJson, io, pathModule = path, createCliError, dryRun = false, runDevlinks = false } = {})`
742
+ - `installAppDependenciesForHook({ appRoot, io, pathModule = path, createCliError, dryRun = false } = {})`
756
743
  - `resolvePackageOptionInputForInstall({ packageEntry, existingInstall, packageInlineOptions, appRoot, readFileBufferIfExists })`
757
744
  - `validateHookResult(result = {}, { packageId = "", hookLabel = "" } = {})`
758
745
  - `loadInstallHook({ packageEntry, appRoot, hookSpec, hookLabel = "" } = {})`
759
- - `createInstallHookHelpers({ ctx, appRoot, io, appPackageJson, commandOptions = {} } = {})`
746
+ - `createInstallHookHelpers({ ctx, appRoot, io, appPackageJson } = {})`
760
747
  - `invokeInstallHook({ packageEntry, appRoot, hookSpec, hookLabel, hookContext, createCliError } = {})`
761
748
 
762
749
  ### `src/server/commandHandlers/packageCommands/create.js`