@codyswann/lisa 2.191.14 → 2.192.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/harper-fabric/copy-overwrite/.github/dependabot.yml +47 -0
  2. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-early-return-in-search-loop.yml +27 -0
  3. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-empty-conditions-with-sort.yml +45 -0
  4. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-full-table-scan.yml +33 -0
  5. package/harper-fabric/copy-overwrite/ast-grep/rules/harper/require-statuscode-on-thrown-error.yml +41 -0
  6. package/package.json +1 -1
  7. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa-agy/plugin.json +1 -1
  10. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  13. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-expo-agy/plugin.json +1 -1
  20. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-harper-fabric/skills/harper-auth/SKILL.md +48 -0
  25. package/plugins/lisa-harper-fabric/skills/harper-resources/SKILL.md +35 -0
  26. package/plugins/lisa-harper-fabric/skills/harper-rest-queries/SKILL.md +40 -3
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-agy/skills/harper-auth/SKILL.md +48 -0
  29. package/plugins/lisa-harper-fabric-agy/skills/harper-resources/SKILL.md +35 -0
  30. package/plugins/lisa-harper-fabric-agy/skills/harper-rest-queries/SKILL.md +40 -3
  31. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-copilot/skills/harper-auth/SKILL.md +48 -0
  33. package/plugins/lisa-harper-fabric-copilot/skills/harper-resources/SKILL.md +35 -0
  34. package/plugins/lisa-harper-fabric-copilot/skills/harper-rest-queries/SKILL.md +40 -3
  35. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/skills/harper-auth/SKILL.md +48 -0
  37. package/plugins/lisa-harper-fabric-cursor/skills/harper-resources/SKILL.md +35 -0
  38. package/plugins/lisa-harper-fabric-cursor/skills/harper-rest-queries/SKILL.md +40 -3
  39. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  52. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-agy/plugin.json +1 -1
  57. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  62. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  67. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/src/harper-fabric/skills/harper-auth/SKILL.md +48 -0
  70. package/plugins/src/harper-fabric/skills/harper-resources/SKILL.md +35 -0
  71. package/plugins/src/harper-fabric/skills/harper-rest-queries/SKILL.md +40 -3
@@ -0,0 +1,47 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+
4
+ # Harper/Fabric override of the TypeScript dependabot template.
5
+ # A fresh Harper/Fabric repo ships with only a `main` branch, so this file
6
+ # omits `target-branch` — Dependabot then opens PRs against the repository's
7
+ # default branch instead of a `dev` branch that does not exist. If a project
8
+ # adopts a long-lived integration branch, set `target-branch` back per updater.
9
+
10
+ version: 2
11
+ updates:
12
+ # JavaScript/Node.js dependencies
13
+ - package-ecosystem: npm
14
+ directory: /
15
+ schedule:
16
+ interval: weekly
17
+ day: monday
18
+ open-pull-requests-limit: 10
19
+ groups:
20
+ production-dependencies:
21
+ dependency-type: production
22
+ update-types:
23
+ - minor
24
+ - patch
25
+ development-dependencies:
26
+ dependency-type: development
27
+ update-types:
28
+ - minor
29
+ - patch
30
+ labels:
31
+ - dependencies
32
+ commit-message:
33
+ prefix: 'chore(deps)'
34
+ prefix-development: 'chore(deps-dev)'
35
+
36
+ # GitHub Actions
37
+ - package-ecosystem: github-actions
38
+ directory: /
39
+ schedule:
40
+ interval: weekly
41
+ day: monday
42
+ open-pull-requests-limit: 5
43
+ labels:
44
+ - dependencies
45
+ - github-actions
46
+ commit-message:
47
+ prefix: 'ci(deps)'
@@ -0,0 +1,27 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+ id: harper-no-early-return-in-search-loop
4
+ language: typescript
5
+ severity: warning
6
+ message: |
7
+ Early `return`/`break` out of a `for await (... of tables.X.search(...))` loop
8
+ leaves Harper's read transaction open — the iterator is abandoned before the
9
+ scan completes and the transaction is not released, leaking a read handle.
10
+ note: >-
11
+ Drain or explicitly close the iterator on early exit. Prefer bounding the query
12
+ so the loop finishes naturally (`search({ ..., limit: 1 })` when you only need
13
+ the first row), or drive the iterator manually and call `iterator.return?.()`
14
+ in a `finally` block so Harper releases the read transaction. This is a
15
+ heuristic — a `break` that belongs to an inner `switch` or a `return` inside a
16
+ nested callback is a false positive; suppress those with an inline
17
+ `// ast-grep-ignore: harper-no-early-return-in-search-loop`.
18
+ rule:
19
+ any:
20
+ - kind: return_statement
21
+ - kind: break_statement
22
+ inside:
23
+ kind: for_in_statement
24
+ stopBy: end
25
+ has:
26
+ field: right
27
+ pattern: $ITER.search($$$ARGS)
@@ -0,0 +1,45 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+ id: harper-no-empty-conditions-with-sort
4
+ language: typescript
5
+ severity: error
6
+ message: |
7
+ A Harper `search()` with `sort` but an empty `conditions: []` array crashes
8
+ the query planner — it cannot seed the btree scan from the sort attribute and
9
+ throws at runtime (a latent 500 the first time an unfiltered, sorted page is
10
+ requested).
11
+ note: >-
12
+ Seed a floor/sentinel condition on the sort attribute so the planner has an
13
+ index range to walk, e.g. `{ attribute: "lastName", comparator:
14
+ "greater_than_equal", value: "" }` (or `not_equal: null`), then apply the sort.
15
+ Centralize this in the page helper (`searchPageAndCount`) rather than
16
+ duplicating a sentinel at each call site. If a sort-only scan is genuinely
17
+ intended and the attribute is guaranteed present, suppress with an inline
18
+ `// ast-grep-ignore: harper-no-empty-conditions-with-sort` and a comment.
19
+ rule:
20
+ kind: object
21
+ all:
22
+ - has:
23
+ kind: pair
24
+ all:
25
+ - has:
26
+ field: key
27
+ regex: ^conditions$
28
+ - has:
29
+ field: value
30
+ kind: array
31
+ regex: "^\\[\\s*\\]$"
32
+ - has:
33
+ kind: pair
34
+ has:
35
+ field: key
36
+ regex: ^sort$
37
+ inside:
38
+ kind: arguments
39
+ stopBy: end
40
+ inside:
41
+ kind: call_expression
42
+ stopBy: end
43
+ has:
44
+ field: function
45
+ regex: search$
@@ -0,0 +1,33 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+ id: harper-no-full-table-scan
4
+ language: typescript
5
+ severity: warning
6
+ message: |
7
+ Possible full table scan in a Harper resource. Loading a whole table on every
8
+ request does not scale — one large table (e.g. an employment/event history) was
9
+ the root cause of multi-second `/Search` and directory responses and a day-long
10
+ red deploy gate in production.
11
+ note: >-
12
+ Fetch only the rows you need through an indexed attribute
13
+ (`rowsByAttribute(tables.$TABLE, "someIndexedId", value)`), or page with
14
+ `searchPageAndCount` / `search({ conditions, limit, offset })`. A conditionless
15
+ `tables.$TABLE.search({})` / `search()` and the `allRows` / `optionalAll`
16
+ helpers read every row. Small lookup tables can be legitimate to scan — this is
17
+ a warning; if a full scan is genuinely intended (a one-off offline script, never
18
+ a Resource handler), suppress with an inline
19
+ `// ast-grep-ignore: harper-no-full-table-scan` and a comment saying why.
20
+ rule:
21
+ any:
22
+ # Non-generic full-table reads via common directory helpers.
23
+ - pattern: allRows(tables.$TABLE)
24
+ - pattern: optionalAll(tables.$TABLE)
25
+ # Generic form, e.g. allRows<AdvisorRow>(tables.Advisor): the type argument
26
+ # nests the callee in an instantiation_expression, so the plain patterns
27
+ # above miss it. Any single-arg generic call passing the whole table is a
28
+ # full scan regardless of helper name (the scoped rowsByAttribute helper
29
+ # takes 3 args and is not matched).
30
+ - pattern: $FN<$TYPE>(tables.$TABLE)
31
+ # Raw unconditional table scans.
32
+ - pattern: tables.$TABLE.search({})
33
+ - pattern: tables.$TABLE.search()
@@ -0,0 +1,41 @@
1
+ # This file is managed by Lisa.
2
+ # Do not edit directly — changes will be overwritten on the next `lisa` run.
3
+ id: harper-require-statuscode-on-thrown-error
4
+ language: typescript
5
+ severity: error
6
+ message: |
7
+ This error carries `status` but not `statusCode`. Harper's thrown-error HTTP
8
+ response writer reads `error.statusCode` (falling back to 500); a plain
9
+ `status` is ignored, so every intended 4xx becomes a 500 on the wire.
10
+ note: >-
11
+ Set `statusCode` (keep `status` too if callers/tests rely on it):
12
+ `Object.assign(error, { status, statusCode: status })`, or in an Error
13
+ subclass assign `this.statusCode = status` alongside `this.status`. Verified
14
+ live on harperdb 4.7.32 — a 401/403/404 thrown with only `status` was served
15
+ as 500. Direct `err.status = code` assignments are not auto-detected; when you
16
+ attach an HTTP code to a thrown error, always set `statusCode`.
17
+ rule:
18
+ any:
19
+ # `Object.assign(error, { status })` with only a `status` property — the
20
+ # exact object match excludes the correct `{ status, statusCode }` form.
21
+ - pattern: 'Object.assign($E, { status: $S })'
22
+ # Error subclass that assigns `this.status` but never mentions `statusCode`
23
+ # anywhere in the class body.
24
+ - pattern: this.status = $S
25
+ inside:
26
+ kind: class_declaration
27
+ stopBy: end
28
+ all:
29
+ - has:
30
+ kind: class_heritage
31
+ stopBy: end
32
+ has:
33
+ kind: extends_clause
34
+ has:
35
+ field: value
36
+ regex: Error$
37
+ - not:
38
+ has:
39
+ kind: property_identifier
40
+ regex: ^statusCode$
41
+ stopBy: end
package/package.json CHANGED
@@ -95,7 +95,7 @@
95
95
  "ws": ">=8.20.1"
96
96
  },
97
97
  "name": "@codyswann/lisa",
98
- "version": "2.191.14",
98
+ "version": "2.192.0",
99
99
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
100
100
  "main": "dist/index.js",
101
101
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Harper/Fabric-specific rules for TypeScript component apps",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-harper-fabric",
3
- "version": "2.191.14",
3
+ "version": "2.192.0",
4
4
  "description": "Harper/Fabric-specific Lisa 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
@@ -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.0",
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
@@ -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