@codyswann/lisa 2.191.14 → 2.192.1

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.
Files changed (98) hide show
  1. package/dist/configs/eslint/harper-fabric.d.ts.map +1 -1
  2. package/dist/configs/eslint/harper-fabric.js +5 -0
  3. package/dist/configs/eslint/harper-fabric.js.map +1 -1
  4. package/harper-fabric/copy-contents/.prettierignore +1 -0
  5. package/harper-fabric/copy-contents/gitignore +7 -0
  6. package/harper-fabric/copy-overwrite/.github/dependabot.yml +47 -0
  7. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-early-return-in-search-loop.yml +27 -0
  8. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-empty-conditions-with-sort.yml +45 -0
  9. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-full-table-scan.yml +33 -0
  10. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/require-statuscode-on-thrown-error.yml +41 -0
  11. package/harper-fabric/copy-overwrite/knip.json +1 -0
  12. package/harper-fabric/copy-overwrite/tsconfig.eslint.json +1 -0
  13. package/harper-fabric/merge/.oxlintrc.json +1 -0
  14. package/oxlint/harper-fabric.json +1 -0
  15. package/package.json +1 -1
  16. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo-agy/plugin.json +1 -1
  29. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/generated-artifact-globs.txt +1 -0
  34. package/plugins/lisa-harper-fabric/hooks/block-generated-artifact-edits.sh +51 -0
  35. package/plugins/lisa-harper-fabric/rules/harper-fabric.md +1 -1
  36. package/plugins/lisa-harper-fabric/skills/harper-auth/SKILL.md +48 -0
  37. package/plugins/lisa-harper-fabric/skills/harper-build-and-deploy/SKILL.md +20 -5
  38. package/plugins/lisa-harper-fabric/skills/harper-resources/SKILL.md +35 -0
  39. package/plugins/lisa-harper-fabric/skills/harper-rest-queries/SKILL.md +40 -3
  40. package/plugins/lisa-harper-fabric-agy/generated-artifact-globs.txt +1 -0
  41. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-agy/skills/harper-auth/SKILL.md +48 -0
  43. package/plugins/lisa-harper-fabric-agy/skills/harper-build-and-deploy/SKILL.md +20 -5
  44. package/plugins/lisa-harper-fabric-agy/skills/harper-resources/SKILL.md +35 -0
  45. package/plugins/lisa-harper-fabric-agy/skills/harper-rest-queries/SKILL.md +40 -3
  46. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-copilot/generated-artifact-globs.txt +1 -0
  48. package/plugins/lisa-harper-fabric-copilot/hooks/block-generated-artifact-edits.sh +51 -0
  49. package/plugins/lisa-harper-fabric-copilot/rules/harper-fabric.md +1 -1
  50. package/plugins/lisa-harper-fabric-copilot/skills/harper-auth/SKILL.md +48 -0
  51. package/plugins/lisa-harper-fabric-copilot/skills/harper-build-and-deploy/SKILL.md +20 -5
  52. package/plugins/lisa-harper-fabric-copilot/skills/harper-resources/SKILL.md +35 -0
  53. package/plugins/lisa-harper-fabric-copilot/skills/harper-rest-queries/SKILL.md +40 -3
  54. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-cursor/generated-artifact-globs.txt +1 -0
  56. package/plugins/lisa-harper-fabric-cursor/hooks/block-generated-artifact-edits.sh +51 -0
  57. package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric.mdc +1 -1
  58. package/plugins/lisa-harper-fabric-cursor/skills/harper-auth/SKILL.md +48 -0
  59. package/plugins/lisa-harper-fabric-cursor/skills/harper-build-and-deploy/SKILL.md +20 -5
  60. package/plugins/lisa-harper-fabric-cursor/skills/harper-resources/SKILL.md +35 -0
  61. package/plugins/lisa-harper-fabric-cursor/skills/harper-rest-queries/SKILL.md +40 -3
  62. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  75. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-agy/plugin.json +1 -1
  80. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  85. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  90. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/src/harper-fabric/generated-artifact-globs.txt +1 -0
  93. package/plugins/src/harper-fabric/hooks/block-generated-artifact-edits.sh +51 -0
  94. package/plugins/src/harper-fabric/rules/harper-fabric.md +1 -1
  95. package/plugins/src/harper-fabric/skills/harper-auth/SKILL.md +48 -0
  96. package/plugins/src/harper-fabric/skills/harper-build-and-deploy/SKILL.md +20 -5
  97. package/plugins/src/harper-fabric/skills/harper-resources/SKILL.md +35 -0
  98. package/plugins/src/harper-fabric/skills/harper-rest-queries/SKILL.md +40 -3
@@ -74,6 +74,41 @@ See [[harper-config-yaml]] for the extension wiring, [[harper-schema-graphql]] f
74
74
  how the schema defines the tables resources extend, and [[harper-realtime]] when
75
75
  `subscribe`, `publish`, or WebSocket behavior is part of the feature.
76
76
 
77
+ ## Throwing HTTP status errors
78
+
79
+ Harper's thrown-error response writer reads **`error.statusCode`** (falling back
80
+ to `500`). A plain `error.status` is **ignored** — throw an error with only
81
+ `status` set and every intended `4xx` is served as a `500`. Verified on
82
+ harperdb 4.7.32.
83
+
84
+ Always set `statusCode` (keep `status` too only if callers or tests read it):
85
+
86
+ ```javascript
87
+ static async post(target, data, context) {
88
+ if (!context.user) {
89
+ const error = new Error('Authentication required');
90
+ error.statusCode = 401; // NOT `error.status` — Harper reads statusCode
91
+ throw error;
92
+ }
93
+ // ...
94
+ }
95
+ ```
96
+
97
+ A shared helper keeps every throw site correct:
98
+
99
+ ```javascript
100
+ function throwStatus(message, status) {
101
+ // Set both: `statusCode` is what Harper serves; `status` is kept for
102
+ // returned-response symmetry and any caller/test that reads it.
103
+ throw Object.assign(new Error(message), { status, statusCode: status });
104
+ }
105
+ ```
106
+
107
+ The `harper-require-statuscode-on-thrown-error` ast-grep rule flags errors that
108
+ carry `status` without `statusCode`. Pass `context` through to `super` and nested
109
+ table calls so authorization and the request transaction stay aligned — see
110
+ [[harper-rest-queries]] for context propagation and iterator draining.
111
+
77
112
  ## Project conventions (TS is source)
78
113
 
79
114
  - **Write resources in TypeScript under `src/`. `harper-app/resources.js` is a
@@ -121,6 +121,38 @@ const products = await tables.Products.search({
121
121
  });
122
122
  ```
123
123
 
124
+ ## Sort with no conditions crashes the planner
125
+
126
+ A `search()` that carries a `sort` but an **empty `conditions: []`** array throws
127
+ at runtime — the planner cannot seed the btree scan from the sort attribute. This
128
+ is a latent `500` the first time an unfiltered, sorted collection page is
129
+ requested (verified on harperdb 4.7.32).
130
+
131
+ ```javascript
132
+ // ✗ crashes: sort with nothing to seed the scan
133
+ tables.Advisor.search({
134
+ conditions: [],
135
+ sort: { attribute: 'lastName' },
136
+ });
137
+ ```
138
+
139
+ Seed a floor/sentinel condition on the sort attribute so the planner has an index
140
+ range to walk, then apply the sort:
141
+
142
+ ```javascript
143
+ // ✓ floor condition on the sort attribute seeds the scan
144
+ tables.Advisor.search({
145
+ conditions: [
146
+ { attribute: 'lastName', comparator: 'greater_than_equal', value: '' },
147
+ ],
148
+ sort: { attribute: 'lastName' },
149
+ });
150
+ ```
151
+
152
+ Centralize this in the page helper (`searchPageAndCount`) rather than duplicating
153
+ a sentinel at each call site. The `harper-no-empty-conditions-with-sort` ast-grep
154
+ rule flags the crashing shape.
155
+
124
156
  ## Relationship queries
125
157
 
126
158
  Relationship attributes can be queried with dot syntax when the relationship is
@@ -200,9 +232,14 @@ Useful query keys:
200
232
  | `select` | Projection list. Keep admin-only fields out of public responses. |
201
233
  | `explain` | Debug execution order and index usage while tuning. |
202
234
 
203
- `search()` can return an `AsyncIterable`. When iterating manually or stopping
204
- early, drain it or call the iterator's `return()` in `finally` so Harper can
205
- release the read transaction:
235
+ `search()` can return an `AsyncIterable`. An early `return`/`break` out of a
236
+ `for await (... of tables.X.search(...))` loop abandons the iterator before the
237
+ scan completes and **leaks the open read transaction** (verified on
238
+ harperdb 4.7.32). When iterating manually or stopping early, drain it or call the
239
+ iterator's `return()` in `finally` so Harper releases the read transaction — or
240
+ bound the query (`search({ ..., limit: 1 })`) when you only need the first row.
241
+ The `harper-no-early-return-in-search-loop` ast-grep rule flags early exits from
242
+ these loops:
206
243
 
207
244
  ```javascript
208
245
  const iterator = tables.Products
@@ -1,4 +1,5 @@
1
1
  harper-app/resources.js
2
2
  harper-app/resource-*.js
3
+ harper-app/*.js
3
4
  harper-app/web/**
4
5
  harper-app/lib/**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.191.14",
3
+ "version": "2.192.1",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -260,6 +260,54 @@ Guidance:
260
260
  webhook must bypass Harper auth, verify its signature first and keep its table
261
261
  writes narrowly scoped.
262
262
 
263
+ ## Session cookies are `SameSite=None` — guard state-changing POSTs
264
+
265
+ Harper hardcodes the session cookie's `SameSite=None` attribute on HTTPS. The
266
+ browser therefore attaches the session cookie to **cross-site** requests, so a
267
+ same-origin app cannot rely on `SameSite` to block CSRF on cookie-authenticated,
268
+ state-changing routes (`POST`/`PUT`/`PATCH`/`DELETE`).
269
+
270
+ Add an app-side origin check in the resource before mutating. Reject requests
271
+ whose `Origin` (or `Referer` fallback) is not an allowed origin:
272
+
273
+ ```javascript
274
+ const ALLOWED_ORIGINS = new Set([
275
+ 'https://app.example.com',
276
+ ]);
277
+
278
+ function requireSameOrigin(context) {
279
+ const headers = context.requestContext?.headers ?? context.headers;
280
+ const origin =
281
+ headers?.get?.('origin') ??
282
+ headers?.get?.('referer');
283
+ const ok =
284
+ typeof origin === 'string' &&
285
+ [...ALLOWED_ORIGINS].some((allowed) => origin.startsWith(allowed));
286
+ if (!ok) {
287
+ const error = new Error('Cross-origin request rejected');
288
+ error.statusCode = 403; // Harper reads statusCode, not status
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ export class Watchlists extends tables.Watchlists {
294
+ static async post(target, data, context) {
295
+ requireSameOrigin(context);
296
+ // ...authorization and write
297
+ return super.post(target, await data, context);
298
+ }
299
+ }
300
+ ```
301
+
302
+ Guidance:
303
+
304
+ - Call the check on every cookie-authenticated state-changing route. Read-only
305
+ `GET` handlers and token/`Authorization: Bearer` APIs (not cookie-driven) do
306
+ not need it.
307
+ - Prefer an allowlist of exact origins over substring matching that a lookalike
308
+ host could satisfy.
309
+ - This is defense the app owns; Harper will not add it for you.
310
+
263
311
  ## Verification matrix
264
312
 
265
313
  Run the local app, create the test users, and check unauthenticated, reader, and
@@ -50,15 +50,30 @@ What the build actually does:
50
50
 
51
51
  Generated Harper deploy artifacts usually include:
52
52
 
53
- - `harper-app/resources.js`
54
- - `harper-app/resource-*.js`
53
+ - `harper-app/*.js` — every compiled module the build emits to the harper-app
54
+ root (`resources.js`, `resource-*.js`, and any other output such as a route
55
+ negotiation module). The single-star does **not** cross a directory separator,
56
+ so it does not match hand-written shims one level down.
55
57
  - `harper-app/web/**`
56
58
  - `harper-app/lib/**`
57
59
 
60
+ The guard surfaces (`generated-artifact-globs.txt` for the PreToolUse block hook,
61
+ `.gitignore`, `.prettierignore`, the ESLint/oxlint/knip ignores, and
62
+ `tsconfig.eslint.json`) all key off `harper-app/*.js` so a newly-named compiled
63
+ module is protected automatically. **Name compiled resource modules
64
+ `resource-*.ts`** so their JS output is unambiguously generated.
65
+
66
+ If you keep a *hand-written* `.js` at the harper-app root (e.g. an SEO shell),
67
+ the root-level rule would otherwise treat it as generated: re-include it with a
68
+ `!harper-app/<file>.js` line below the managed gitignore block, and add it to
69
+ `.lisa/harper-generated-artifact-allowlist.txt` so the block hook lets you edit
70
+ it. Hand-written shims nested under `harper-app/<route>/index.js` need no
71
+ exemption — the root-level rule never matches them.
72
+
58
73
  Every lint, format, dead-code, search, or generated-artifact guard must ignore
59
- those generated paths unless it is explicitly validating the build output itself.
60
- When a new generated path appears, add it to every relevant ignore surface in the
61
- same change; partial ignores fail later gates in non-obvious ways.
74
+ generated paths unless it is explicitly validating the build output itself. When
75
+ a new generated *directory* appears, add it to every relevant ignore surface in
76
+ the same change; partial ignores fail later gates in non-obvious ways.
62
77
 
63
78
  - **TypeScript under `src/` is source.** `bun run build` produces the deployable
64
79
  Harper assets from it.
@@ -74,6 +74,41 @@ See [[harper-config-yaml]] for the extension wiring, [[harper-schema-graphql]] f
74
74
  how the schema defines the tables resources extend, and [[harper-realtime]] when
75
75
  `subscribe`, `publish`, or WebSocket behavior is part of the feature.
76
76
 
77
+ ## Throwing HTTP status errors
78
+
79
+ Harper's thrown-error response writer reads **`error.statusCode`** (falling back
80
+ to `500`). A plain `error.status` is **ignored** — throw an error with only
81
+ `status` set and every intended `4xx` is served as a `500`. Verified on
82
+ harperdb 4.7.32.
83
+
84
+ Always set `statusCode` (keep `status` too only if callers or tests read it):
85
+
86
+ ```javascript
87
+ static async post(target, data, context) {
88
+ if (!context.user) {
89
+ const error = new Error('Authentication required');
90
+ error.statusCode = 401; // NOT `error.status` — Harper reads statusCode
91
+ throw error;
92
+ }
93
+ // ...
94
+ }
95
+ ```
96
+
97
+ A shared helper keeps every throw site correct:
98
+
99
+ ```javascript
100
+ function throwStatus(message, status) {
101
+ // Set both: `statusCode` is what Harper serves; `status` is kept for
102
+ // returned-response symmetry and any caller/test that reads it.
103
+ throw Object.assign(new Error(message), { status, statusCode: status });
104
+ }
105
+ ```
106
+
107
+ The `harper-require-statuscode-on-thrown-error` ast-grep rule flags errors that
108
+ carry `status` without `statusCode`. Pass `context` through to `super` and nested
109
+ table calls so authorization and the request transaction stay aligned — see
110
+ [[harper-rest-queries]] for context propagation and iterator draining.
111
+
77
112
  ## Project conventions (TS is source)
78
113
 
79
114
  - **Write resources in TypeScript under `src/`. `harper-app/resources.js` is a
@@ -121,6 +121,38 @@ const products = await tables.Products.search({
121
121
  });
122
122
  ```
123
123
 
124
+ ## Sort with no conditions crashes the planner
125
+
126
+ A `search()` that carries a `sort` but an **empty `conditions: []`** array throws
127
+ at runtime — the planner cannot seed the btree scan from the sort attribute. This
128
+ is a latent `500` the first time an unfiltered, sorted collection page is
129
+ requested (verified on harperdb 4.7.32).
130
+
131
+ ```javascript
132
+ // ✗ crashes: sort with nothing to seed the scan
133
+ tables.Advisor.search({
134
+ conditions: [],
135
+ sort: { attribute: 'lastName' },
136
+ });
137
+ ```
138
+
139
+ Seed a floor/sentinel condition on the sort attribute so the planner has an index
140
+ range to walk, then apply the sort:
141
+
142
+ ```javascript
143
+ // ✓ floor condition on the sort attribute seeds the scan
144
+ tables.Advisor.search({
145
+ conditions: [
146
+ { attribute: 'lastName', comparator: 'greater_than_equal', value: '' },
147
+ ],
148
+ sort: { attribute: 'lastName' },
149
+ });
150
+ ```
151
+
152
+ Centralize this in the page helper (`searchPageAndCount`) rather than duplicating
153
+ a sentinel at each call site. The `harper-no-empty-conditions-with-sort` ast-grep
154
+ rule flags the crashing shape.
155
+
124
156
  ## Relationship queries
125
157
 
126
158
  Relationship attributes can be queried with dot syntax when the relationship is
@@ -200,9 +232,14 @@ Useful query keys:
200
232
  | `select` | Projection list. Keep admin-only fields out of public responses. |
201
233
  | `explain` | Debug execution order and index usage while tuning. |
202
234
 
203
- `search()` can return an `AsyncIterable`. When iterating manually or stopping
204
- early, drain it or call the iterator's `return()` in `finally` so Harper can
205
- release the read transaction:
235
+ `search()` can return an `AsyncIterable`. An early `return`/`break` out of a
236
+ `for await (... of tables.X.search(...))` loop abandons the iterator before the
237
+ scan completes and **leaks the open read transaction** (verified on
238
+ harperdb 4.7.32). When iterating manually or stopping early, drain it or call the
239
+ iterator's `return()` in `finally` so Harper releases the read transaction — or
240
+ bound the query (`search({ ..., limit: 1 })`) when you only need the first row.
241
+ The `harper-no-early-return-in-search-loop` ast-grep rule flags early exits from
242
+ these loops:
206
243
 
207
244
  ```javascript
208
245
  const iterator = tables.Products
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.191.14",
3
+ "version": "2.192.1",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,4 +1,5 @@
1
1
  harper-app/resources.js
2
2
  harper-app/resource-*.js
3
+ harper-app/*.js
3
4
  harper-app/web/**
4
5
  harper-app/lib/**
@@ -33,6 +33,7 @@ matches_glob() {
33
33
  local file="$1"
34
34
  local glob="$2"
35
35
 
36
+ # Recursive directory globs: "dir/**" matches everything under dir.
36
37
  if [ "${glob: -3}" = "/**" ]; then
37
38
  local dir="${glob%/**}"
38
39
  case "$file" in
@@ -41,6 +42,38 @@ matches_glob() {
41
42
  return 1
42
43
  fi
43
44
 
45
+ # Single-star globs like "harper-app/*.js": the star must not cross a
46
+ # directory separator, so a broadened root-level pattern protects compiled
47
+ # modules emitted straight into harper-app/ (resources.js, resource-*.js, and
48
+ # any other build output such as detail-shell-negotiation.js) WITHOUT matching
49
+ # hand-written files nested one level down (e.g. harper-app/<route>/index.js).
50
+ case "$glob" in
51
+ */\** | \**)
52
+ local base_glob="${glob##*/}"
53
+ local dir_glob=""
54
+ case "$glob" in
55
+ */*) dir_glob="${glob%/*}" ;;
56
+ esac
57
+ local file_base="${file##*/}"
58
+ local file_dir=""
59
+ case "$file" in
60
+ */*) file_dir="${file%/*}" ;;
61
+ esac
62
+ case "$file_base" in
63
+ $base_glob) ;;
64
+ *) return 1 ;;
65
+ esac
66
+ [ -z "$dir_glob" ] && return 0
67
+ # Match the directory exactly, or as a suffix so absolute/prefixed paths
68
+ # (e.g. /repo/harper-app/foo.js) still resolve.
69
+ case "$file_dir" in
70
+ $dir_glob | */"$dir_glob") return 0 ;;
71
+ esac
72
+ return 1
73
+ ;;
74
+ esac
75
+
76
+ # Literal (wildcard-free) globs.
44
77
  case "$file" in
45
78
  $glob | */$glob) return 0 ;;
46
79
  esac
@@ -50,6 +83,24 @@ matches_glob() {
50
83
 
51
84
  NORMALIZED_FILE=$(normalize_path "$FILE_PATH")
52
85
 
86
+ # Project-owned allowlist of hand-written files that live under harper-app/ and
87
+ # must NOT be treated as generated (e.g. a hand-authored SEO shell, or route
88
+ # index.js shims kept at the harper-app root). The broadened root-level
89
+ # `harper-app/*.js` protection would otherwise block editing them. One glob per
90
+ # line (same syntax as the globs file); blank lines and `#` comments ignored.
91
+ # Prefer naming compiled resource modules `resource-*.ts` so their JS output is
92
+ # unambiguously generated and never needs allowlisting.
93
+ ALLOWLIST_FILE="${CLAUDE_PROJECT_DIR:-.}/.lisa/harper-generated-artifact-allowlist.txt"
94
+ if [ -f "$ALLOWLIST_FILE" ]; then
95
+ while IFS= read -r allow || [ -n "$allow" ]; do
96
+ [ -n "$allow" ] || continue
97
+ case "$allow" in \#*) continue ;; esac
98
+ if matches_glob "$NORMALIZED_FILE" "$allow"; then
99
+ exit 0
100
+ fi
101
+ done <"$ALLOWLIST_FILE"
102
+ fi
103
+
53
104
  while IFS= read -r glob || [ -n "$glob" ]; do
54
105
  [ -n "$glob" ] || continue
55
106
  case "$glob" in \#*) continue ;; esac
@@ -13,7 +13,7 @@ These rules apply to Harper/Fabric component apps managed by Lisa.
13
13
  - TypeScript under `src/` is the source of truth for Harper resources, browser modules, shared libraries, and operational scripts.
14
14
  - `harper-app/config.yaml`, `harper-app/schema.graphql`, HTML, CSS, docs, and research fixtures are source assets.
15
15
  - `harper-app/config.yaml` does not merge with Harper defaults. Keep every required top-level extension declared when editing it; the Harper Fabric hook blocks accidental extension drops unless the removal is documented in `.lisa/harper-config-extension-allowlist.json`.
16
- - `harper-app/resources.js` and `harper-app/web/**/*.js` are generated deploy artifacts. Never edit them directly; change the matching TypeScript and run `bun run build`.
16
+ - Every `.js` at the harper-app root (`harper-app/*.js` — `resources.js`, `resource-*.js`, and any other compiled module) plus `harper-app/web/**/*.js` and `harper-app/lib/**` are generated deploy artifacts. Never edit them directly; change the matching TypeScript and run `bun run build`. Name compiled resource modules `resource-*.ts` so their output is unambiguously generated; a hand-written `.js` kept at the harper-app root must be re-included in `.gitignore` and listed in `.lisa/harper-generated-artifact-allowlist.txt` so the block hook allows editing it.
17
17
  - Deployment, bootstrap, smoke, seed, verify, preview, token, crawl, ingest, and extraction commands must run from compiled JavaScript or generated Harper assets, not stale checked-in JavaScript.
18
18
 
19
19
  ## Harper/Fabric Deploy Surface
@@ -260,6 +260,54 @@ Guidance:
260
260
  webhook must bypass Harper auth, verify its signature first and keep its table
261
261
  writes narrowly scoped.
262
262
 
263
+ ## Session cookies are `SameSite=None` — guard state-changing POSTs
264
+
265
+ Harper hardcodes the session cookie's `SameSite=None` attribute on HTTPS. The
266
+ browser therefore attaches the session cookie to **cross-site** requests, so a
267
+ same-origin app cannot rely on `SameSite` to block CSRF on cookie-authenticated,
268
+ state-changing routes (`POST`/`PUT`/`PATCH`/`DELETE`).
269
+
270
+ Add an app-side origin check in the resource before mutating. Reject requests
271
+ whose `Origin` (or `Referer` fallback) is not an allowed origin:
272
+
273
+ ```javascript
274
+ const ALLOWED_ORIGINS = new Set([
275
+ 'https://app.example.com',
276
+ ]);
277
+
278
+ function requireSameOrigin(context) {
279
+ const headers = context.requestContext?.headers ?? context.headers;
280
+ const origin =
281
+ headers?.get?.('origin') ??
282
+ headers?.get?.('referer');
283
+ const ok =
284
+ typeof origin === 'string' &&
285
+ [...ALLOWED_ORIGINS].some((allowed) => origin.startsWith(allowed));
286
+ if (!ok) {
287
+ const error = new Error('Cross-origin request rejected');
288
+ error.statusCode = 403; // Harper reads statusCode, not status
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ export class Watchlists extends tables.Watchlists {
294
+ static async post(target, data, context) {
295
+ requireSameOrigin(context);
296
+ // ...authorization and write
297
+ return super.post(target, await data, context);
298
+ }
299
+ }
300
+ ```
301
+
302
+ Guidance:
303
+
304
+ - Call the check on every cookie-authenticated state-changing route. Read-only
305
+ `GET` handlers and token/`Authorization: Bearer` APIs (not cookie-driven) do
306
+ not need it.
307
+ - Prefer an allowlist of exact origins over substring matching that a lookalike
308
+ host could satisfy.
309
+ - This is defense the app owns; Harper will not add it for you.
310
+
263
311
  ## Verification matrix
264
312
 
265
313
  Run the local app, create the test users, and check unauthenticated, reader, and
@@ -50,15 +50,30 @@ What the build actually does:
50
50
 
51
51
  Generated Harper deploy artifacts usually include:
52
52
 
53
- - `harper-app/resources.js`
54
- - `harper-app/resource-*.js`
53
+ - `harper-app/*.js` — every compiled module the build emits to the harper-app
54
+ root (`resources.js`, `resource-*.js`, and any other output such as a route
55
+ negotiation module). The single-star does **not** cross a directory separator,
56
+ so it does not match hand-written shims one level down.
55
57
  - `harper-app/web/**`
56
58
  - `harper-app/lib/**`
57
59
 
60
+ The guard surfaces (`generated-artifact-globs.txt` for the PreToolUse block hook,
61
+ `.gitignore`, `.prettierignore`, the ESLint/oxlint/knip ignores, and
62
+ `tsconfig.eslint.json`) all key off `harper-app/*.js` so a newly-named compiled
63
+ module is protected automatically. **Name compiled resource modules
64
+ `resource-*.ts`** so their JS output is unambiguously generated.
65
+
66
+ If you keep a *hand-written* `.js` at the harper-app root (e.g. an SEO shell),
67
+ the root-level rule would otherwise treat it as generated: re-include it with a
68
+ `!harper-app/<file>.js` line below the managed gitignore block, and add it to
69
+ `.lisa/harper-generated-artifact-allowlist.txt` so the block hook lets you edit
70
+ it. Hand-written shims nested under `harper-app/<route>/index.js` need no
71
+ exemption — the root-level rule never matches them.
72
+
58
73
  Every lint, format, dead-code, search, or generated-artifact guard must ignore
59
- those generated paths unless it is explicitly validating the build output itself.
60
- When a new generated path appears, add it to every relevant ignore surface in the
61
- same change; partial ignores fail later gates in non-obvious ways.
74
+ generated paths unless it is explicitly validating the build output itself. When
75
+ a new generated *directory* appears, add it to every relevant ignore surface in
76
+ the same change; partial ignores fail later gates in non-obvious ways.
62
77
 
63
78
  - **TypeScript under `src/` is source.** `bun run build` produces the deployable
64
79
  Harper assets from it.
@@ -74,6 +74,41 @@ See [[harper-config-yaml]] for the extension wiring, [[harper-schema-graphql]] f
74
74
  how the schema defines the tables resources extend, and [[harper-realtime]] when
75
75
  `subscribe`, `publish`, or WebSocket behavior is part of the feature.
76
76
 
77
+ ## Throwing HTTP status errors
78
+
79
+ Harper's thrown-error response writer reads **`error.statusCode`** (falling back
80
+ to `500`). A plain `error.status` is **ignored** — throw an error with only
81
+ `status` set and every intended `4xx` is served as a `500`. Verified on
82
+ harperdb 4.7.32.
83
+
84
+ Always set `statusCode` (keep `status` too only if callers or tests read it):
85
+
86
+ ```javascript
87
+ static async post(target, data, context) {
88
+ if (!context.user) {
89
+ const error = new Error('Authentication required');
90
+ error.statusCode = 401; // NOT `error.status` — Harper reads statusCode
91
+ throw error;
92
+ }
93
+ // ...
94
+ }
95
+ ```
96
+
97
+ A shared helper keeps every throw site correct:
98
+
99
+ ```javascript
100
+ function throwStatus(message, status) {
101
+ // Set both: `statusCode` is what Harper serves; `status` is kept for
102
+ // returned-response symmetry and any caller/test that reads it.
103
+ throw Object.assign(new Error(message), { status, statusCode: status });
104
+ }
105
+ ```
106
+
107
+ The `harper-require-statuscode-on-thrown-error` ast-grep rule flags errors that
108
+ carry `status` without `statusCode`. Pass `context` through to `super` and nested
109
+ table calls so authorization and the request transaction stay aligned — see
110
+ [[harper-rest-queries]] for context propagation and iterator draining.
111
+
77
112
  ## Project conventions (TS is source)
78
113
 
79
114
  - **Write resources in TypeScript under `src/`. `harper-app/resources.js` is a
@@ -121,6 +121,38 @@ const products = await tables.Products.search({
121
121
  });
122
122
  ```
123
123
 
124
+ ## Sort with no conditions crashes the planner
125
+
126
+ A `search()` that carries a `sort` but an **empty `conditions: []`** array throws
127
+ at runtime — the planner cannot seed the btree scan from the sort attribute. This
128
+ is a latent `500` the first time an unfiltered, sorted collection page is
129
+ requested (verified on harperdb 4.7.32).
130
+
131
+ ```javascript
132
+ // ✗ crashes: sort with nothing to seed the scan
133
+ tables.Advisor.search({
134
+ conditions: [],
135
+ sort: { attribute: 'lastName' },
136
+ });
137
+ ```
138
+
139
+ Seed a floor/sentinel condition on the sort attribute so the planner has an index
140
+ range to walk, then apply the sort:
141
+
142
+ ```javascript
143
+ // ✓ floor condition on the sort attribute seeds the scan
144
+ tables.Advisor.search({
145
+ conditions: [
146
+ { attribute: 'lastName', comparator: 'greater_than_equal', value: '' },
147
+ ],
148
+ sort: { attribute: 'lastName' },
149
+ });
150
+ ```
151
+
152
+ Centralize this in the page helper (`searchPageAndCount`) rather than duplicating
153
+ a sentinel at each call site. The `harper-no-empty-conditions-with-sort` ast-grep
154
+ rule flags the crashing shape.
155
+
124
156
  ## Relationship queries
125
157
 
126
158
  Relationship attributes can be queried with dot syntax when the relationship is
@@ -200,9 +232,14 @@ Useful query keys:
200
232
  | `select` | Projection list. Keep admin-only fields out of public responses. |
201
233
  | `explain` | Debug execution order and index usage while tuning. |
202
234
 
203
- `search()` can return an `AsyncIterable`. When iterating manually or stopping
204
- early, drain it or call the iterator's `return()` in `finally` so Harper can
205
- release the read transaction:
235
+ `search()` can return an `AsyncIterable`. An early `return`/`break` out of a
236
+ `for await (... of tables.X.search(...))` loop abandons the iterator before the
237
+ scan completes and **leaks the open read transaction** (verified on
238
+ harperdb 4.7.32). When iterating manually or stopping early, drain it or call the
239
+ iterator's `return()` in `finally` so Harper releases the read transaction — or
240
+ bound the query (`search({ ..., limit: 1 })`) when you only need the first row.
241
+ The `harper-no-early-return-in-search-loop` ast-grep rule flags early exits from
242
+ these loops:
206
243
 
207
244
  ```javascript
208
245
  const iterator = tables.Products
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.191.14",
3
+ "version": "2.192.1",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,4 +1,5 @@
1
1
  harper-app/resources.js
2
2
  harper-app/resource-*.js
3
+ harper-app/*.js
3
4
  harper-app/web/**
4
5
  harper-app/lib/**