@jskit-ai/agent-docs 0.1.7 → 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.
- package/guide/agent/app-setup/authentication.md +123 -0
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +19 -0
- package/guide/agent/generators/advanced-cruds.md +69 -0
- package/package.json +1 -1
- package/patterns/INDEX.md +6 -0
- package/patterns/crud-repository-mapping.md +81 -0
- package/patterns/ui-testing.md +79 -0
- package/reference/autogen/packages/auth-core.md +8 -0
- package/reference/autogen/packages/auth-provider-supabase-core.md +34 -2
- package/reference/autogen/packages/auth-web.md +1 -1
- package/reference/autogen/packages/crud-core.md +20 -14
- package/reference/autogen/tooling/jskit-cli.md +22 -2
- package/templates/app/AGENTS.md +6 -0
- package/workflow/feature-delivery.md +6 -2
- package/workflow/review.md +5 -2
|
@@ -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:
|
|
@@ -834,6 +834,75 @@ Usually nothing changes. The client can keep sending `q`.
|
|
|
834
834
|
- Do not dump every text column into search just because you can.
|
|
835
835
|
- In this tutorial CRUD, `notes` is a good example of a field you might leave out if you want fast, predictable list search.
|
|
836
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
|
+
|
|
837
906
|
### Pattern 3: structured list filters from one shared definition
|
|
838
907
|
|
|
839
908
|
This is the default pattern once a CRUD list needs real filters.
|
package/package.json
CHANGED
package/patterns/INDEX.md
CHANGED
|
@@ -22,8 +22,12 @@ How to use it:
|
|
|
22
22
|
- `live-actions.md`
|
|
23
23
|
- ajax, fetch, API call, request, endpoint, HTTP client, `useList()`, `useView()`, `useAddEdit()`, `useEndpointResource()`, `usersWebHttpClient`
|
|
24
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`
|
|
25
27
|
- filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
|
|
26
28
|
- `filters.md`
|
|
29
|
+
- fieldMeta, `repository.column`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`
|
|
30
|
+
- `crud-repository-mapping.md`
|
|
27
31
|
|
|
28
32
|
## Current Patterns
|
|
29
33
|
|
|
@@ -33,4 +37,6 @@ How to use it:
|
|
|
33
37
|
- [crud-links.md](./crud-links.md)
|
|
34
38
|
- [live-actions.md](./live-actions.md)
|
|
35
39
|
- [client-requests.md](./client-requests.md)
|
|
40
|
+
- [ui-testing.md](./ui-testing.md)
|
|
36
41
|
- [filters.md](./filters.md)
|
|
42
|
+
- [crud-repository-mapping.md](./crud-repository-mapping.md)
|
|
@@ -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
|
-
- `
|
|
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
|
-
- `
|
|
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
|
|
@@ -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
|
-
- `
|
|
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
|
-
- `
|
|
186
|
-
- `
|
|
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 = "
|
|
197
|
-
- `resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "
|
|
198
|
-
- `
|
|
196
|
+
- `requireCrudLookupNamespace(value = "", { context = "crudLookup" } = {})`
|
|
197
|
+
- `resolveCrudLookupNamespaceFromRelation(relation = {}, { context = "crudLookup" } = {})`
|
|
198
|
+
- `resolveCrudLookupToken(namespace = "", { context = "crudLookup" } = {})`
|
|
199
199
|
|
|
200
|
-
### `src/server/
|
|
200
|
+
### `src/server/lookups.js`
|
|
201
201
|
Exports
|
|
202
|
-
- `
|
|
203
|
-
- `
|
|
204
|
-
- `
|
|
202
|
+
- `resolveCrudLookupToken`
|
|
203
|
+
- `createCrudLookupResolver(scope, { context = "crudLookup" } = {})`
|
|
204
|
+
- `createCrudLookup(repository, { context = "crudLookup", ownershipFilter = "" } = {})`
|
|
205
205
|
Local functions
|
|
206
|
-
- `
|
|
206
|
+
- `normalizeLookupOwnershipFilter(value, { context = "crudLookup ownershipFilter" } = {})`
|
|
207
207
|
|
|
208
208
|
### `src/server/repositoryMethods.js`
|
|
209
209
|
Exports
|
|
210
|
-
- `
|
|
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`
|
package/templates/app/AGENTS.md
CHANGED
|
@@ -45,6 +45,7 @@ Before calling a chunk done, report:
|
|
|
45
45
|
- `Deslop review: ...`
|
|
46
46
|
- `JSKIT review: ...`
|
|
47
47
|
- `Material/Vuetify review: ...`
|
|
48
|
+
- `Playwright: ...` for any chunk that adds or changes user-facing UI
|
|
48
49
|
- `Verification: ...`
|
|
49
50
|
- `Files changed: ...`
|
|
50
51
|
- `Commands run: ...`
|
|
@@ -71,6 +72,11 @@ Core rules:
|
|
|
71
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.
|
|
72
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.
|
|
73
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.
|
|
74
80
|
- Do not implement app features before the blueprint has the database, surfaces, ownership model, and route/screen plan written down.
|
|
75
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.
|
|
76
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.
|
|
@@ -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
|
|
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,
|
|
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.
|
package/workflow/review.md
CHANGED
|
@@ -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
|
-
-
|
|
35
|
-
-
|
|
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:
|