@jskit-ai/agent-docs 0.1.6 → 0.1.8

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.
@@ -914,6 +914,129 @@ That explains why the login screen and auth guard runtime both care about `/api/
914
914
 
915
915
  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 Supabase.
916
916
 
917
+ ### Authenticated Playwright testing with the dev auth bypass
918
+
919
+ JSKIT now 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 Supabase.
920
+
921
+ This is the standard path the agent should use for authenticated browser tests:
922
+
923
+ - enable the dev auth bypass in development
924
+ - create a session for an existing user through the local app
925
+ - let the browser keep the resulting HTTP-only cookies
926
+ - navigate to the protected page and verify the feature
927
+ - record the Playwright run through `jskit app verify-ui` so `jskit doctor` can verify the receipt later
928
+
929
+ The feature is intentionally narrow.
930
+
931
+ - It is development-only.
932
+ - It must never be enabled in production.
933
+ - JSKIT rejects boot if `AUTH_DEV_BYPASS_ENABLED=true` while `NODE_ENV=production`.
934
+ - The route only looks up an existing user. It does not create one.
935
+
936
+ The environment variables are:
937
+
938
+ ```bash
939
+ AUTH_DEV_BYPASS_ENABLED=true
940
+ AUTH_DEV_BYPASS_SECRET=replace-this-with-a-local-dev-secret
941
+ ```
942
+
943
+ When enabled outside production, the app exposes:
944
+
945
+ ```text
946
+ POST /api/dev-auth/login-as
947
+ ```
948
+
949
+ The request body must contain either:
950
+
951
+ ```json
952
+ { "userId": "7" }
953
+ ```
954
+
955
+ or:
956
+
957
+ ```json
958
+ { "email": "ada@example.com" }
959
+ ```
960
+
961
+ The response is intentionally small:
962
+
963
+ ```json
964
+ {
965
+ "ok": true,
966
+ "userId": "7",
967
+ "username": "Ada Example",
968
+ "email": "ada@example.com"
969
+ }
970
+ ```
971
+
972
+ 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.
973
+
974
+ One subtle point matters here:
975
+
976
+ - `/api/dev-auth/login-as` is still an unsafe `POST`
977
+ - JSKIT still expects a CSRF token
978
+ - the browser can get that token from `/api/session`
979
+
980
+ So the normal Playwright shape is:
981
+
982
+ 1. open a same-origin page first
983
+ 2. call `/api/session` to read `csrfToken`
984
+ 3. call `/api/dev-auth/login-as` with `credentials: "include"` and the `csrf-token` header
985
+ 4. navigate to the protected route and run the assertions
986
+
987
+ For example:
988
+
989
+ ```ts
990
+ await page.goto("/");
991
+
992
+ await page.evaluate(async ({ email }) => {
993
+ const sessionResponse = await fetch("/api/session", {
994
+ credentials: "include"
995
+ });
996
+ if (!sessionResponse.ok) {
997
+ throw new Error(`Session bootstrap failed: ${sessionResponse.status}`);
998
+ }
999
+
1000
+ const sessionPayload = await sessionResponse.json();
1001
+ const csrfToken = String(sessionPayload?.csrfToken || "");
1002
+ if (!csrfToken) {
1003
+ throw new Error("Missing csrfToken from /api/session.");
1004
+ }
1005
+
1006
+ const loginResponse = await fetch("/api/dev-auth/login-as", {
1007
+ method: "POST",
1008
+ credentials: "include",
1009
+ headers: {
1010
+ "content-type": "application/json",
1011
+ "csrf-token": csrfToken
1012
+ },
1013
+ body: JSON.stringify({ email })
1014
+ });
1015
+
1016
+ if (!loginResponse.ok) {
1017
+ throw new Error(`Dev login failed: ${loginResponse.status} ${await loginResponse.text()}`);
1018
+ }
1019
+ }, { email: "ada@example.com" });
1020
+
1021
+ await page.goto("/w/acme/admin/contacts");
1022
+ ```
1023
+
1024
+ In practice, the preferred wrapper is:
1025
+
1026
+ ```bash
1027
+ npx jskit app verify-ui \
1028
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
1029
+ --feature "contacts filters" \
1030
+ --auth-mode dev-auth-login-as
1031
+ ```
1032
+
1033
+ 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:
1034
+
1035
+ - use Playwright
1036
+ - record the run with `jskit app verify-ui`
1037
+ - use the local dev auth bypass or another local session bootstrap path
1038
+ - exercise the actual changed behavior, not only page load
1039
+
917
1040
  ## What appears in Supabase
918
1041
 
919
1042
  It is important to separate **Supabase auth data** from **JSKIT app-owned data**.
@@ -666,6 +666,7 @@ In the current CLI, that includes checks such as:
666
666
  - whether managed files recorded in the lock still exist
667
667
  - whether installed packages are still visible in the package registry
668
668
  - certain JSKIT-specific app checks, such as invalid raw `mdi-*` icon literals in Vue templates when the app uses Vuetify's `mdi-svg` iconset
669
+ - UI verification receipts for current dirty UI files in git, via `.jskit/verification/ui.json`
669
670
 
670
671
  That last check is narrower than it sounds. `doctor` is looking for the broken case in direct Vue templates, such as:
671
672
 
@@ -726,6 +727,24 @@ Good times to run it manually include:
726
727
 
727
728
  One important nuance: `doctor` is checking for broken JSKIT ownership and visibility, not trying to stop you from editing app-owned files. For example, a managed file that still exists but whose contents changed is normally fine. The problem is when JSKIT expects a managed file to exist and it is gone, or when the installed package state no longer resolves cleanly.
728
729
 
730
+ There is one intentional exception now for user-facing UI work.
731
+
732
+ If the current git working tree has changed UI files under the app's normal UI paths, `doctor` expects a matching `.jskit/verification/ui.json` receipt. The intended way to create that receipt is:
733
+
734
+ ```bash
735
+ npx jskit app verify-ui \
736
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
737
+ --feature "contacts filters" \
738
+ --auth-mode dev-auth-login-as
739
+ ```
740
+
741
+ That command does two things:
742
+
743
+ - runs the targeted Playwright command you give it
744
+ - writes a receipt describing the verified feature, auth mode, and current dirty UI file set
745
+
746
+ `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`.
747
+
729
748
  ### Why `--json` exists
730
749
 
731
750
  If you want machine-readable output, use:
@@ -613,6 +613,57 @@ The safe mental model is:
613
613
  - use command runtimes for actions
614
614
  - keep the server as the source of truth for derived state
615
615
 
616
+ ### Choosing the right client request seam
617
+
618
+ When you need client-side HTTP work in JSKIT, do not start with raw `fetch(...)`.
619
+
620
+ Choose the highest-level runtime that matches the interaction:
621
+
622
+ ```js
623
+ // 1. Button/toggle/small mutation
624
+ const command = useCommand({ ... });
625
+
626
+ // 2. List endpoint
627
+ const list = useList({ ... });
628
+
629
+ // 3. Single-record endpoint
630
+ const view = useView({ ... });
631
+
632
+ // 4. Form save flow
633
+ const form = useAddEdit({ ... });
634
+
635
+ // 5. Truly custom endpoint
636
+ const resource = useEndpointResource({ ... });
637
+ ```
638
+
639
+ Use the CRUD wrappers when they fit:
640
+
641
+ - `useCrudList()` for routed CRUD lists
642
+ - `useCrudView()` for routed CRUD record loading
643
+ - `useCrudAddEdit()` for routed CRUD forms
644
+
645
+ Why this is the standard JSKIT shape:
646
+
647
+ - `useCommand()` resolves the correct scoped API path for the current route and surface.
648
+ - The higher-level list, view, add/edit, and command runtimes send requests through the standard HTTP runtime instead of ad hoc request code.
649
+ - The default client runtime uses `usersWebHttpClient`, which already handles credentials and CSRF token behavior.
650
+ - `useEndpointResource()` gives the shared endpoint primitive for loading, saving, and standard load/save error handling. Higher-level runtimes like `useCommand()` and `useAddEdit()` layer UI feedback and field-error behavior on top of that primitive.
651
+
652
+ If you need a custom scoped endpoint path outside the higher-level runtimes, prefer `usePaths().api(...)` rather than hand-building scoped URLs:
653
+
654
+ ```js
655
+ const paths = usePaths();
656
+ const reportsApiPath = computed(() => paths.api("/reports"));
657
+ ```
658
+
659
+ The safe mental model is:
660
+
661
+ - do not raw `fetch(...)` for normal app work
662
+ - do not invent ad hoc local AJAX helpers
663
+ - use the operation/runtime composable that matches the UI interaction
664
+ - drop to `usersWebHttpClient.request(...)` only for exceptional low-level cases
665
+ - use `usePaths().api(...)` when you need a custom scoped API path and the higher-level runtime does not already resolve it for you
666
+
616
667
  ### `_components/CrudAddEditForm.vue`
617
668
 
618
669
  This is the shared rendering shell for the add/edit form.
@@ -783,6 +834,75 @@ Usually nothing changes. The client can keep sending `q`.
783
834
  - Do not dump every text column into search just because you can.
784
835
  - In this tutorial CRUD, `notes` is a good example of a field you might leave out if you want fast, predictable list search.
785
836
 
837
+ ### Pattern 2B: repository mapping and computed output fields
838
+
839
+ Do not treat the output schema as if it also defined database storage.
840
+
841
+ In JSKIT CRUD:
842
+
843
+ - the schema defines the API contract
844
+ - `resource.fieldMeta` defines storage mapping
845
+ - the repository runtime owns computed SQL projections
846
+
847
+ Use these rules:
848
+
849
+ - for explicit DB column overrides, use `repository.column`
850
+ - for computed output fields, use `repository.storage: "virtual"`
851
+ - do not put computed fields in create/patch write schemas
852
+
853
+ Example field metadata:
854
+
855
+ ```js
856
+ RESOURCE_FIELD_META.push({
857
+ key: "createdAt",
858
+ repository: {
859
+ column: "created_at"
860
+ }
861
+ });
862
+
863
+ RESOURCE_FIELD_META.push({
864
+ key: "remainingBatchWeight",
865
+ repository: {
866
+ storage: "virtual"
867
+ }
868
+ });
869
+ ```
870
+
871
+ Register the computed projection once in the repository runtime:
872
+
873
+ ```js
874
+ const repositoryRuntime = createCrudResourceRuntime(resource, {
875
+ context: "receivals repository",
876
+ list: LIST_CONFIG,
877
+ virtualFields: {
878
+ remainingBatchWeight: {
879
+ applyProjection(dbQuery, { knex, tableName, alias }) {
880
+ const { sql, bindings } = getRemainingBatchWeightSqlParts({ tableName });
881
+ dbQuery.select(knex.raw(`${sql} as ??`, [...bindings, alias]));
882
+ }
883
+ }
884
+ }
885
+ });
886
+ ```
887
+
888
+ Once registered there:
889
+
890
+ - generic CRUD `list`
891
+ - generic CRUD `findById`
892
+ - generic CRUD `listByIds`
893
+ - generic CRUD `listByForeignIds`
894
+
895
+ all pick up the projection automatically, so you should not hand-patch `clearSelect()` / re-select logic into each method.
896
+
897
+ Important limits:
898
+
899
+ - `virtual` fields are output-only in v1
900
+ - fallback search derivation only uses column-backed fields
901
+ - parent-filter fallback derivation only uses column-backed fields
902
+ - `listByIds(..., { valueKey })` requires a column-backed `valueKey`
903
+
904
+ For the agent-facing quick rule, see `patterns/crud-repository-mapping.md`.
905
+
786
906
  ### Pattern 3: structured list filters from one shared definition
787
907
 
788
908
  This is the default pattern once a CRUD list needs real filters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Distributed JSKIT agent workflows, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
package/patterns/INDEX.md CHANGED
@@ -20,8 +20,14 @@ How to use it:
20
20
  - `crud-links.md`
21
21
  - live actions, checkbox, toggle, patch button, inline action, `useCommand()`
22
22
  - `live-actions.md`
23
+ - ajax, fetch, API call, request, endpoint, HTTP client, `useList()`, `useView()`, `useAddEdit()`, `useEndpointResource()`, `usersWebHttpClient`
24
+ - `client-requests.md`
25
+ - playwright, browser test, e2e, ui verification, authenticated ui test, test auth, dev login as, dev auth bypass
26
+ - `ui-testing.md`
23
27
  - filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
24
28
  - `filters.md`
29
+ - fieldMeta, `repository.column`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`
30
+ - `crud-repository-mapping.md`
25
31
 
26
32
  ## Current Patterns
27
33
 
@@ -30,4 +36,7 @@ How to use it:
30
36
  - [child-cruds.md](./child-cruds.md)
31
37
  - [crud-links.md](./crud-links.md)
32
38
  - [live-actions.md](./live-actions.md)
39
+ - [client-requests.md](./client-requests.md)
40
+ - [ui-testing.md](./ui-testing.md)
33
41
  - [filters.md](./filters.md)
42
+ - [crud-repository-mapping.md](./crud-repository-mapping.md)
@@ -0,0 +1,56 @@
1
+ # Client Request Patterns
2
+
3
+ Use when:
4
+
5
+ - deciding which JSKIT client function to use for HTTP work
6
+ - custom endpoint reads or writes
7
+ - AJAX questions
8
+ - replacing raw `fetch(...)` calls
9
+ - choosing between `useCommand()`, `useList()`, `useView()`, `useAddEdit()`, and `useEndpointResource()`
10
+
11
+ Rules:
12
+
13
+ - Prefer the highest-level JSKIT runtime that matches the UI interaction.
14
+ - Do not hand-roll local AJAX helpers when an existing JSKIT runtime already fits.
15
+ - Do not use raw `fetch(...)` for normal app work.
16
+ - Use `usePaths().api(...)` for custom scoped API paths instead of concatenating route params into URLs by hand.
17
+ - Drop to `usersWebHttpClient.request(...)` only for exceptional low-level cases.
18
+
19
+ Choose the function like this:
20
+
21
+ ```js
22
+ // 1. Button/toggle/small mutation
23
+ const command = useCommand({ ... });
24
+
25
+ // 2. List endpoint
26
+ const list = useList({ ... });
27
+
28
+ // 3. Single-record endpoint
29
+ const view = useView({ ... });
30
+
31
+ // 4. Form save flow
32
+ const form = useAddEdit({ ... });
33
+
34
+ // 5. Truly custom endpoint
35
+ const resource = useEndpointResource({ ... });
36
+ ```
37
+
38
+ Use the CRUD wrappers when they fit:
39
+
40
+ - `useCrudList()` for routed CRUD lists
41
+ - `useCrudView()` for routed CRUD record loading
42
+ - `useCrudAddEdit()` for routed CRUD forms
43
+
44
+ Why this is the standard JSKIT shape:
45
+
46
+ - `useCommand()` resolves the scoped API path for the current route and surface.
47
+ - The higher-level list, view, add/edit, and command runtimes send requests through the shared HTTP runtime.
48
+ - `usersWebHttpClient` already handles credentials and CSRF behavior.
49
+ - `useEndpointResource()` is the shared endpoint primitive for loading, saving, and standard load/save error handling. Higher-level runtimes add UI feedback and field-error handling on top.
50
+
51
+ Avoid:
52
+
53
+ - raw `fetch(...)` for standard page or component work
54
+ - page-local HTTP helpers that duplicate JSKIT runtime seams
55
+ - manually concatenating scoped route params into API URLs
56
+ - using a lower-level seam when a higher-level routed CRUD or command runtime already fits
@@ -0,0 +1,81 @@
1
+ # CRUD Repository Mapping
2
+
3
+ Use when:
4
+ - a CRUD resource needs explicit DB column mapping
5
+ - a field should exist in API output but is not a real DB column
6
+ - the user mentions computed fields, projections, or `remainingBatchWeight`
7
+
8
+ Default JSKIT pattern:
9
+ 1. Treat the schema as the API contract only.
10
+ 2. Put persistence mapping in `resource.fieldMeta`.
11
+ 3. Use `repository.column` for explicit DB column overrides.
12
+ 4. Use `repository.storage: "virtual"` for computed output fields.
13
+ 5. Register computed SQL projections once in the repository runtime with `virtualFields`.
14
+ 6. Let generic CRUD read paths apply those projections automatically.
15
+
16
+ Field meta rules:
17
+ - for a normal override, write:
18
+
19
+ ```js
20
+ RESOURCE_FIELD_META.push({
21
+ key: "createdAt",
22
+ repository: {
23
+ column: "created_at"
24
+ }
25
+ });
26
+ ```
27
+
28
+ - for a computed output field, write:
29
+
30
+ ```js
31
+ RESOURCE_FIELD_META.push({
32
+ key: "remainingBatchWeight",
33
+ repository: {
34
+ storage: "virtual"
35
+ }
36
+ });
37
+ ```
38
+
39
+ - omit `repository` entirely when the field is column-backed and the default snake_case mapping is correct
40
+ - `repository.storage: "virtual"` cannot also define `repository.column`
41
+ - `repository.storage: "virtual"` fields must not appear in create/patch write schemas
42
+
43
+ Repository pattern:
44
+ - build the runtime with `createCrudResourceRuntime(resource, { ... })`
45
+ - register computed projections in `virtualFields`
46
+ - keep SQL in the repository, not in shared metadata
47
+
48
+ Example:
49
+
50
+ ```js
51
+ const repositoryRuntime = createCrudResourceRuntime(resource, {
52
+ context: "receivals repository",
53
+ list: LIST_CONFIG,
54
+ virtualFields: {
55
+ remainingBatchWeight: {
56
+ applyProjection(dbQuery, { knex, tableName, alias }) {
57
+ const { sql, bindings } = getRemainingBatchWeightSqlParts({ tableName });
58
+ dbQuery.select(knex.raw(`${sql} as ??`, [...bindings, alias]));
59
+ }
60
+ }
61
+ }
62
+ });
63
+ ```
64
+
65
+ What CRUD core now does for you:
66
+ - default select columns include only column-backed output fields
67
+ - `list`, `findById`, `listByIds`, and `listByForeignIds` apply registered virtual projections automatically
68
+ - search and parent-filter fallback derivation only use column-backed fields
69
+ - `listByIds(..., { valueKey })` requires that `valueKey` be column-backed
70
+
71
+ Avoid:
72
+ - manual `clearSelect()` / re-select hacks in individual repository methods just to add computed fields
73
+ - putting SQL fragments or joins into shared `fieldMeta`
74
+ - inventing `repository.storage` modes beyond the documented `virtual` contract
75
+
76
+ Review checks:
77
+ - schema defines contract, not storage
78
+ - `fieldMeta.repository` owns mapping
79
+ - computed fields use `repository.storage: "virtual"`
80
+ - repository runtime registers matching `virtualFields`
81
+ - no per-method projection duplication when generic CRUD reads already cover the field
@@ -0,0 +1,79 @@
1
+ # UI Testing Pattern
2
+
3
+ Use when:
4
+
5
+ - deciding how to verify a new or changed user-facing screen
6
+ - Playwright or browser-driven verification
7
+ - authenticated UI flows
8
+ - test auth, session bootstrap, dev login-as, or dev auth bypass
9
+
10
+ Rules:
11
+
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
+ - Record that Playwright run with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>`.
14
+ - `jskit doctor` expects `.jskit/verification/ui.json` to match the current dirty UI file set when UI files are changed.
15
+ - Do not rely on a live external auth provider for Playwright verification of normal app features.
16
+ - For authenticated UI in the standard JSKIT auth stack, use the development-only dev auth bypass route instead.
17
+ - The standard route is `POST /api/dev-auth/login-as`.
18
+ - The request body must include either `{ userId }` or `{ email }`.
19
+ - The route is available only when `AUTH_DEV_BYPASS_ENABLED=true` and `AUTH_DEV_BYPASS_SECRET` is set outside production.
20
+ - This route must never be enabled in production.
21
+ - Because the route is an unsafe POST, fetch `csrfToken` from `/api/session` first and send it back as the `csrf-token` header.
22
+ - Make the bootstrap request from the same browser context that will run the assertions so the auth cookies land in the page session.
23
+ - 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.
24
+
25
+ Recommended flow:
26
+
27
+ ```bash
28
+ npx jskit app verify-ui \
29
+ --command "npx playwright test tests/e2e/contacts.spec.ts -g filters" \
30
+ --feature "contacts filters" \
31
+ --auth-mode dev-auth-login-as
32
+ ```
33
+
34
+ The Playwright command itself should follow this setup shape when login is needed:
35
+
36
+ ```ts
37
+ await page.goto("/");
38
+
39
+ await page.evaluate(async ({ email }) => {
40
+ const sessionResponse = await fetch("/api/session", {
41
+ credentials: "include"
42
+ });
43
+ if (!sessionResponse.ok) {
44
+ throw new Error(`Session bootstrap failed: ${sessionResponse.status}`);
45
+ }
46
+
47
+ const sessionPayload = await sessionResponse.json();
48
+ const csrfToken = String(sessionPayload?.csrfToken || "");
49
+ if (!csrfToken) {
50
+ throw new Error("Missing csrfToken from /api/session.");
51
+ }
52
+
53
+ const loginResponse = await fetch("/api/dev-auth/login-as", {
54
+ method: "POST",
55
+ credentials: "include",
56
+ headers: {
57
+ "content-type": "application/json",
58
+ "csrf-token": csrfToken
59
+ },
60
+ body: JSON.stringify({ email })
61
+ });
62
+
63
+ if (!loginResponse.ok) {
64
+ throw new Error(`Dev login failed: ${loginResponse.status} ${await loginResponse.text()}`);
65
+ }
66
+ }, { email: "ada@example.com" });
67
+ ```
68
+
69
+ After that bootstrap:
70
+
71
+ - navigate to the protected page
72
+ - exercise the new UI behavior
73
+ - assert the actual outcome the chunk introduced
74
+
75
+ Do not mark the chunk done if:
76
+
77
+ - the feature changed user-facing UI but no Playwright flow ran
78
+ - the Playwright flow skipped the changed behavior itself
79
+ - authenticated UI work was left untested because no local auth bootstrap path was available and that gap was not called out
@@ -202,12 +202,19 @@ Exports
202
202
  - `loginResponseValidator`
203
203
  - `otpVerifyResponseValidator`
204
204
  - `oauthCompleteResponseValidator`
205
+ - `devLoginAsResponseValidator`
205
206
  - `logoutResponseValidator`
206
207
  - `oauthProviderCatalogEntryValidator`
207
208
  - `sessionResponseValidator`
208
209
  - `sessionUnavailableResponseValidator`
209
210
  - `createCommandMessages({ fields = {}, defaultMessage = "Invalid value." } = {})`
210
211
 
212
+ ### `src/shared/commands/authDevLoginAsCommand.js`
213
+ Exports
214
+ - `AUTH_DEV_LOGIN_AS_MESSAGES`
215
+ - `authDevLoginAsBodyValidator`
216
+ - `authDevLoginAsCommand`
217
+
211
218
  ### `src/shared/commands/authLoginOAuthCompleteCommand.js`
212
219
  Exports
213
220
  - `authLoginOAuthCompleteBodyValidator`
@@ -300,6 +307,7 @@ Exports
300
307
  - `authLoginOtpVerifyCommand`
301
308
  - `authLoginOAuthStartCommand`
302
309
  - `authLoginOAuthCompleteCommand`
310
+ - `authDevLoginAsCommand`
303
311
  - `authPasswordResetRequestCommand`
304
312
  - `authPasswordRecoveryCompleteCommand`
305
313
  - `authPasswordResetCommand`
@@ -26,7 +26,9 @@ Local functions
26
26
 
27
27
  ### `src/server/lib/actions/auth.contributor.js`
28
28
  Exports
29
- - `authActions`
29
+ - `baseAuthActions`
30
+ - `buildAuthActions({ includeDevLoginAs = false } = {})`
31
+ - `devLoginAsAction`
30
32
  Local functions
31
33
  - `requireRequestContext(context, actionId)`
32
34
 
@@ -106,6 +108,30 @@ Exports
106
108
  Exports
107
109
  - `createAuthSessionEventsService()`
108
110
 
111
+ ### `src/server/lib/devAuthBootstrap.js`
112
+ Exports
113
+ - `assertDevAuthBootstrapConfig(config, { usersRepository = null } = {})`
114
+ - `authenticateDevAuthRequest({ request, accessToken = "", refreshToken = "" }, { config, usersRepository = null } = {})`
115
+ - `createDevAuthSession(profile, config)`
116
+ - `ensureDevAuthBootstrapAvailable(config, request)`
117
+ - `isDevAuthToken(token)`
118
+ - `resolveDevAuthConfig({ enabled = false, secret = "", nodeEnv = "development", jwtAudience = "authenticated", accessTtlSeconds = DEFAULT_DEV_AUTH_ACCESS_TTL_SECONDS, refreshTtlSeconds = DEFAULT_DEV_AUTH_REFRESH_TTL_SECONDS } = {})`
119
+ - `resolveDevAuthProfile(input = {}, { usersRepository = null, validationError } = {})`
120
+ Local functions
121
+ - `parseBoolean(value, fallback = false)`
122
+ - `normalizePositiveInteger(value, fallback)`
123
+ - `normalizeRequestHostname(request)`
124
+ - `resolveDirectRemoteAddress(request)`
125
+ - `normalizeLoopbackIp(value)`
126
+ - `isLoopbackIp(value)`
127
+ - `isLoopbackHostname(value)`
128
+ - `isLocalDevAuthRequest(request)`
129
+ - `stripDevAuthTokenPrefix(token)`
130
+ - `buildProfileFromTokenClaims(payload)`
131
+ - `resolveProfileFromTokenClaims(payload, { usersRepository = null } = {})`
132
+ - `signDevAuthToken(kind, profile, config)`
133
+ - `verifyDevAuthToken(token, kind, config)`
134
+
109
135
  ### `src/server/lib/flowGuards.js`
110
136
  Exports
111
137
  - `requireNoFieldErrors(parsed, validationError)`
@@ -117,7 +143,9 @@ Exports
117
143
  Exports
118
144
  - `createService`
119
145
  - `__testables`
120
- - `authActions`
146
+ - `baseAuthActions`
147
+ - `buildAuthActions`
148
+ - `devLoginAsAction`
121
149
 
122
150
  ### `src/server/lib/oauthFlows.js`
123
151
  Exports
@@ -206,14 +234,18 @@ Exports
206
234
  - `AuthSupabaseServiceProvider`
207
235
  Local functions
208
236
  - `splitCsv(value)`
237
+ - `parseBoolean(value, fallback = false)`
209
238
  - `normalizeRecord(value)`
210
239
  - `normalizeOAuthProviderConfigList(value)`
211
240
  - `resolveOAuthConfigFromAppConfig(appConfig)`
212
241
  - `resolveAllowedReturnToOrigins({ appConfig = {}, appPublicUrl = "" } = {})`
213
242
  - `resolveAuthProviderConfig(env, appConfig = {})`
214
243
  - `resolveAuthProfileMode(env)`
244
+ - `isDevAuthBypassEnabledForRegistration(env)`
245
+ - `isDevAuthBypassRequested(env)`
215
246
  - `createInMemoryUserSettingsRepository()`
216
247
  - `resolveCommonDependencies(scope)`
248
+ - `resolveRuntimeEnv(scope)`
217
249
  - `resolveOptionalRepositories(scope)`
218
250
 
219
251
  ### root
@@ -217,7 +217,7 @@ Exports
217
217
 
218
218
  ### `src/server/routes/authRoutes.js`
219
219
  Exports
220
- - `buildRoutes(controller)`
220
+ - `buildRoutes(controller, { includeDevLoginAs = false } = {})`
221
221
 
222
222
  ### `src/server/services/AuthWebService.js`
223
223
  Exports
@@ -70,7 +70,7 @@ Exports
70
70
 
71
71
  ### `src/server/createCrudRepositoryFromResource.js`
72
72
  Exports
73
- - `createCrudRepositoryFromResource(resource = {}, { context = "crudRepository", list = {} } = {})`
73
+ - `createCrudRepositoryFromResource(resource = {}, { context = "crudRepository", list = {}, virtualFields = {} } = {})`
74
74
 
75
75
  ### `src/server/createCrudServiceFromResource.js`
76
76
  Exports
@@ -177,13 +177,13 @@ Local functions
177
177
  - `normalizeIncludePaths(include, { defaultInclude = DEFAULT_LOOKUP_INCLUDE } = {})`
178
178
  - `resolveChildIncludeFromPaths(paths = [])`
179
179
  - `buildLookupHydrationPlan(runtime = {}, include, { mode = "list", context = "crudRepository", includeWasExplicit = false, skippedNamespaces = new Set() } = {})`
180
- - `resolveLookupProviderResolver(repositoryOptions = {}, callOptions = {}, { context = "crudRepository" } = {})`
180
+ - `resolveLookupResolver(repositoryOptions = {}, callOptions = {}, { context = "crudRepository" } = {})`
181
181
  - `resolveLookupDepthRuntime(runtime = {}, repositoryOptions = {}, callOptions = {})`
182
182
  - `buildLookupGroupKey(relation = {})`
183
183
  - `normalizeLookupRelationValues(records = [], entries = [], childIncludeByKey = {})`
184
184
  - `resolveGroupChildInclude(group = {})`
185
- - `normalizeLookupProvider(provider, relation = {}, { context = "crudRepository" } = {})`
186
- - `resolveLookupProviderOwnershipFilter(provider = {}, { context = "crudRepository" } = {})`
185
+ - `normalizeLookup(provider, relation = {}, { context = "crudRepository" } = {})`
186
+ - `resolveLookupOwnershipFilter(provider = {}, { context = "crudRepository" } = {})`
187
187
  - `resolveLookupVisibilityContext(provider = {}, relation = {}, callOptions = {}, { context = "crudRepository" } = {})`
188
188
  - `buildLookupRecordMap(records = [], valueKey = "")`
189
189
  - `buildLookupCollectionMap(records = [], foreignKey = "")`
@@ -193,21 +193,21 @@ Local functions
193
193
  Exports
194
194
  - `normalizeCrudLookupApiPath`
195
195
  - `normalizeCrudLookupNamespace`
196
- - `requireCrudLookupNamespace(value = "", { context = "crudLookupProvider" } = {})`
197
- - `resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "crudLookupProvider" } = {})`
198
- - `resolveCrudLookupProviderToken(namespace = "", { context = "crudLookupProvider" } = {})`
196
+ - `requireCrudLookupNamespace(value = "", { context = "crudLookup" } = {})`
197
+ - `resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "crudLookup" } = {})`
198
+ - `resolveCrudLookupToken(namespace = "", { context = "crudLookup" } = {})`
199
199
 
200
- ### `src/server/lookupProviders.js`
200
+ ### `src/server/lookups.js`
201
201
  Exports
202
- - `resolveCrudLookupProviderToken`
203
- - `createCrudLookupProviderResolver(scope, { context = "crudLookupProvider" } = {})`
204
- - `createCrudLookupProvider(repository, { context = "crudLookupProvider", ownershipFilter = "" } = {})`
202
+ - `resolveCrudLookupToken`
203
+ - `createCrudLookupResolver(scope, { context = "crudLookup" } = {})`
204
+ - `createCrudLookup(repository, { context = "crudLookup", ownershipFilter = "" } = {})`
205
205
  Local functions
206
- - `normalizeLookupProviderOwnershipFilter(value, { context = "crudLookupProvider ownershipFilter" } = {})`
206
+ - `normalizeLookupOwnershipFilter(value, { context = "crudLookup ownershipFilter" } = {})`
207
207
 
208
208
  ### `src/server/repositoryMethods.js`
209
209
  Exports
210
- - `createCrudRepositoryRuntime(resource = {}, { context = "crudRepository", list = {} } = {})`
210
+ - `createCrudResourceRuntime(resource = {}, { context = "crudRepository", list = {}, virtualFields = {} } = {})`
211
211
  - `crudRepositoryList(runtime, knex, query = {}, repositoryOptions = {}, callOptions = {}, hooks = null)`
212
212
  - `crudRepositoryFindById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}, hooks = null)`
213
213
  - `crudRepositoryListByIds(runtime, knex, ids = [], repositoryOptions = {}, callOptions = {}, hooks = null)`
@@ -218,6 +218,8 @@ Exports
218
218
  Local functions
219
219
  - `requireCrudRecordId(value, { context = "crudRepository" } = {})`
220
220
  - `resolveRepositoryDefaults(resource = {}, repositoryMapping = {})`
221
+ - `normalizeCrudVirtualFieldHandlers(virtualFields = {}, repositoryMapping = {}, { context = "crudRepository" } = {})`
222
+ - `applyCrudRepositoryVirtualProjections(dbQuery, runtime = {}, { knex, tableName } = {})`
221
223
  - `normalizeSearchColumns(searchColumns = [], fallbackColumns = [])`
222
224
  - `normalizeListOrderDirection(value = LIST_ORDER_DIRECTION_ASC)`
223
225
  - `normalizeListOrderNulls(value = LIST_ORDER_NULLS_LAST)`
@@ -262,8 +264,9 @@ Exports
262
264
  - `buildWritePayload(sourcePayload = {}, fieldKeys = [], overrides = {})`
263
265
  - `resolveColumnName(fieldKey, overrides = {})`
264
266
  - `resolveCrudIdColumn(idColumn, { fallback = "id" } = {})`
265
- - `buildRepositoryColumnMetadata({ outputKeys = [], writeKeys = [], columnOverrides = {} } = {})`
267
+ - `buildRepositoryColumnMetadata({ outputKeys = [], writeKeys = [], columnOverrides = {}, fieldStorageByKey = {} } = {})`
266
268
  Local functions
269
+ - `resolveOptionalObjectSchemaProperties(schema, options = {})`
267
270
  - `requireObjectSchemaProperties(schema, { context = "crudRepository", schemaLabel = "schema" } = {})`
268
271
  - `normalizeResourceFieldMetaEntries(fieldMeta = [])`
269
272
  - `schemaIncludesStringType(schema = {})`
@@ -286,11 +289,14 @@ Local functions
286
289
 
287
290
  ### `src/shared/crudFieldMetaSupport.js`
288
291
  Exports
292
+ - `CRUD_FIELD_REPOSITORY_STORAGE_COLUMN`
293
+ - `CRUD_FIELD_REPOSITORY_STORAGE_VIRTUAL`
289
294
  - `CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE`
290
295
  - `CRUD_LOOKUP_FORM_CONTROL_SELECT`
291
296
  - `CRUD_RUNTIME_LOOKUPS_FIELD_KEY`
292
297
  - `checkCrudLookupFormControl(value, { context = "crud fieldMeta ui.formControl", defaultValue = CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE } = {})`
293
298
  - `isCrudRuntimeOutputOnlyFieldKey(value = "", { lookupContainerKey = CRUD_RUNTIME_LOOKUPS_FIELD_KEY } = {})`
299
+ - `normalizeCrudFieldRepositoryConfig(fieldMetaEntry = {}, { context = "crud fieldMeta repository", fieldKey = "" } = {})`
294
300
 
295
301
  ### `src/shared/crudNamespaceSupport.js`
296
302
  Exports
@@ -28,6 +28,7 @@ Exports
28
28
 
29
29
  ### `src/server/cliRuntime/appState.js`
30
30
  Exports
31
+ - `directoryLooksLikeJskitAppRoot(directoryPath)`
31
32
  - `resolveAppRootFromCwd(cwd)`
32
33
  - `loadAppPackageJson(appRoot)`
33
34
  - `createDefaultLock()`
@@ -41,8 +42,6 @@ Exports
41
42
  - `upsertEnvValue(content, key, value)`
42
43
  - `removeEnvValue(content, key, expectedValue, previous)`
43
44
  - `writeJsonFile`
44
- Local functions
45
- - `directoryLooksLikeJskitAppRoot(directoryPath)`
46
45
 
47
46
  ### `src/server/cliRuntime/capabilitySupport.js`
48
47
  Exports
@@ -379,6 +378,7 @@ Exports
379
378
  - `normalizeText(value = "")`
380
379
  - `isTruthyFlag(rawValue = "")`
381
380
  - `runExternalCommand(command, args = [], { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
381
+ - `runExternalShellCommand(commandText, { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
382
382
  - `formatUtcReleaseTimestamp(date = new Date())`
383
383
  - `resolveLocalJskitBin(appRoot = "")`
384
384
  - `runLocalJskit(appRoot, args = [], { stdout, stderr, createCliError, quiet = false } = {})`
@@ -402,6 +402,10 @@ Local functions
402
402
  Exports
403
403
  - `runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr })`
404
404
 
405
+ ### `src/server/commandHandlers/appCommands/verifyUi.js`
406
+ Exports
407
+ - `runAppVerifyUiCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr })`
408
+
405
409
  ### `src/server/commandHandlers/completion.js`
406
410
  Exports
407
411
  - `createCompletionCommands(ctx = {})`
@@ -672,6 +676,22 @@ Exports
672
676
  Local functions
673
677
  - `resolveCatalogPackagesPath()`
674
678
 
679
+ ### `src/server/shared/uiVerification.js`
680
+ Exports
681
+ - `UI_VERIFICATION_AUTH_MODES`
682
+ - `UI_VERIFICATION_RECEIPT_RELATIVE_PATH`
683
+ - `UI_VERIFICATION_RECEIPT_VERSION`
684
+ - `UI_VERIFICATION_RUNNER`
685
+ - `isUiVerificationAuthMode(value = "")`
686
+ - `isUiVerificationPath(relativePath = "")`
687
+ - `isValidUiVerificationReceipt(receipt)`
688
+ - `normalizeUiVerificationReceipt(rawReceipt = {})`
689
+ - `resolveChangedUiFilesFromGit(appRoot = "")`
690
+ - `sortUniqueStrings(values = [])`
691
+ Local functions
692
+ - `normalizeText(value = "")`
693
+ - `readGitPathList(appRoot = "", args = [])`
694
+
675
695
  ### bin
676
696
 
677
697
  ### `bin/jskit.js`
@@ -22,6 +22,37 @@ Use these references on demand:
22
22
  - `node_modules/@jskit-ai/agent-docs/templates/WORKBOARD.md`
23
23
  - `node_modules/@jskit-ai/agent-docs/skills/jskit-review/SKILL.md` for review passes when your Codex environment supports packaged skills
24
24
 
25
+ ## Mandatory Start Gate
26
+
27
+ Before any non-trivial change:
28
+
29
+ 1. Read this file and the workflow files above in the current turn. Do not rely on memory alone.
30
+ 2. If the task involves JSKIT UI, routing, surfaces, CRUDs, filters, placements, live actions, or similar implementation details, scan `node_modules/@jskit-ai/agent-docs/patterns/INDEX.md` and read the relevant pattern files before editing.
31
+ 3. For substantial or multi-chunk work, create or update `.jskit/WORKBOARD.md` before editing.
32
+ 4. Before editing, print a compact read receipt with:
33
+ - `Read receipt: ...`
34
+ - `Active chunk: ...`
35
+ - `Generator decision: ...`
36
+ - `Relevant patterns: ...`
37
+ - `Active rules from docs: ...`
38
+ 5. When files need to be created, prefer `jskit generate ...` over creating them from scratch, even if the generated output will need follow-up adaptation. If not using a generator, say why.
39
+ 6. Do not edit code until the read receipt is printed and the workboard step is complete when it applies.
40
+
41
+ ## Mandatory Done Gate
42
+
43
+ Before calling a chunk done, report:
44
+
45
+ - `Deslop review: ...`
46
+ - `JSKIT review: ...`
47
+ - `Material/Vuetify review: ...`
48
+ - `Playwright: ...` for any chunk that adds or changes user-facing UI
49
+ - `Verification: ...`
50
+ - `Files changed: ...`
51
+ - `Commands run: ...`
52
+ - `Remaining unverified: ...`
53
+
54
+ If a feature spans more than one chunk, repeat those passes on the whole changeset after the final chunk.
55
+
25
56
  Core rules:
26
57
 
27
58
  - Inspect the workspace before assuming a JSKIT app exists.
@@ -31,7 +62,6 @@ Core rules:
31
62
  - `Why this sticks: ...`
32
63
  - `Not doing: ...`
33
64
  - Keep that checkpoint compact. Do not expand it into a long preamble unless the developer asks for detail.
34
- - When a request involves JSKIT UI, routing, surfaces, CRUDs, filters, placements, live actions, or similar implementation details, scan `node_modules/@jskit-ai/agent-docs/patterns/INDEX.md` for matching keywords and read only the relevant pattern files.
35
65
  - Reuse existing JSKIT helpers and runtime seams before adding new local helpers.
36
66
  - A freshly scaffolded JSKIT app can still be in Stage 1. If the app was just created and platform decisions are not settled yet, continue with the initialize workflow before adding runtime packages.
37
67
  - Do not treat a missing `config.tenancyMode` line or an untouched minimal scaffold as a final tenancy decision.
@@ -42,8 +72,12 @@ Core rules:
42
72
  - Use the real env var names or option names instead of vague requests for "credentials". Values such as `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `AUTH_SUPABASE_URL`, `AUTH_SUPABASE_PUBLISHABLE_KEY`, and `APP_PUBLIC_URL` are routine setup inputs when the relevant package stack needs them.
43
73
  - For CRUD chunks, ask the developer which operations are allowed, which fields belong in the list view if one exists, and the intended look of the view and edit/new forms before generating code.
44
74
  - Every user-facing screen must pass a Material Design and Vuetify quality review before the chunk is considered done.
75
+ - Any chunk that adds or changes user-facing UI must include a Playwright check that exercises the changed behavior before the chunk is considered done.
76
+ - Record that Playwright run with `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>` so `jskit doctor` can verify the receipt.
77
+ - If the UI flow requires login, use the app's development-only auth bypass or session bootstrap path instead of a live external auth login flow.
78
+ - In JSKIT apps using the standard auth stack, that means enabling the dev auth bypass in development and using `POST /api/dev-auth/login-as` during Playwright setup.
79
+ - If authenticated UI work has no usable local auth bootstrap path yet, treat that as a testability gap and call it out before the chunk can be considered complete.
45
80
  - Do not implement app features before the blueprint has the database, surfaces, ownership model, and route/screen plan written down.
46
- - For substantial or multi-chunk work, create or update `.jskit/WORKBOARD.md` as the per-request execution tracker.
47
81
  - Break planned work into reviewable chunks before implementing. One CRUD is usually one chunk, but auth/platform setup, shell work, and cross-cutting integrations can be their own chunks.
48
82
  - Do not move to the next chunk until the current chunk has passed implementation, deslop review, JSKIT best-practices review, Material Design/Vuetify review, and verification.
49
83
  - If a feature spans more than one chunk, run a final whole-changeset deslop pass, JSKIT pass, Material Design/Vuetify pass, and verification pass after the last chunk.
@@ -22,7 +22,7 @@ For each chunk, follow this order:
22
22
  5. Deslop the chunk.
23
23
  6. Review the chunk against JSKIT reuse and best practices.
24
24
  7. Review user-facing screens against Material Design and Vuetify best practices, and improve any screens that do not meet that bar.
25
- 8. Verify the chunk with the relevant commands, including Playwright for meaningful UI flows.
25
+ 8. Verify the chunk with the relevant commands. Any chunk that adds or changes user-facing UI must include a Playwright flow that exercises the changed behavior, and that run must be recorded with `jskit app verify-ui`.
26
26
  9. Update `.jskit/WORKBOARD.md` with status, commands run, and anything still unverified.
27
27
  10. Only then move to the next chunk.
28
28
 
@@ -59,5 +59,9 @@ After the last chunk:
59
59
 
60
60
  Playwright note:
61
61
 
62
- - When login is required, prefer a test-only impersonation or session bootstrap path over dependence on a live external auth provider.
62
+ - When login is required, use a test-only auth bypass or session bootstrap path instead of dependence on a live external auth provider.
63
+ - Record the run through `jskit app verify-ui --command "<playwright command>" --feature "<label>" --auth-mode <mode>` so `jskit doctor` can verify the receipt against the current changed UI files.
64
+ - In the standard JSKIT auth stack, the default development path is `POST /api/dev-auth/login-as`, guarded by `AUTH_DEV_BYPASS_ENABLED=true` and `AUTH_DEV_BYPASS_SECRET=...`.
65
+ - That route is development-only and must not be enabled in production.
66
+ - Because it is still an unsafe POST, fetch `csrfToken` from `/api/session`, send it as the `csrf-token` header, and make the request in the same browser context that will run the Playwright assertions so the session cookies land in the page session.
63
67
  - If such a path does not exist yet, treat that as a testability gap and decide whether the chunk must add it before the feature is considered complete.
@@ -31,8 +31,11 @@ Before calling a chunk or a whole changeset done, review it in four passes:
31
31
  4. Verification review
32
32
  - run the smallest relevant verification commands for a chunk
33
33
  - run the widest relevant verification commands for a whole changeset
34
- - include Playwright for meaningful user-facing flows
35
- - if login is required, verify the chosen test-auth path or explicitly record that the lack of one is an open gap
34
+ - any added or changed user-facing UI must be exercised with Playwright
35
+ - that Playwright run should normally be recorded through `jskit app verify-ui`
36
+ - if login is required, use the chosen local test-auth path instead of live external auth
37
+ - in the standard JSKIT auth stack, prefer the development-only `POST /api/dev-auth/login-as` path
38
+ - if there is no usable local auth bootstrap path, explicitly record that as a blocking testability gap
36
39
  - note anything left unverified
37
40
 
38
41
  Minimum expectation: