@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.
- package/dist/configs/eslint/harper-fabric.d.ts.map +1 -1
- package/dist/configs/eslint/harper-fabric.js +5 -0
- package/dist/configs/eslint/harper-fabric.js.map +1 -1
- package/harper-fabric/copy-contents/.prettierignore +1 -0
- package/harper-fabric/copy-contents/gitignore +7 -0
- package/harper-fabric/copy-overwrite/.github/dependabot.yml +47 -0
- package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-early-return-in-search-loop.yml +27 -0
- package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-empty-conditions-with-sort.yml +45 -0
- package/harper-fabric/copy-overwrite/ast-grep/rules/harper/no-full-table-scan.yml +33 -0
- package/harper-fabric/copy-overwrite/ast-grep/rules/harper/require-statuscode-on-thrown-error.yml +41 -0
- package/harper-fabric/copy-overwrite/knip.json +1 -0
- package/harper-fabric/copy-overwrite/tsconfig.eslint.json +1 -0
- package/harper-fabric/merge/.oxlintrc.json +1 -0
- package/oxlint/harper-fabric.json +1 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/generated-artifact-globs.txt +1 -0
- package/plugins/lisa-harper-fabric/hooks/block-generated-artifact-edits.sh +51 -0
- package/plugins/lisa-harper-fabric/rules/harper-fabric.md +1 -1
- package/plugins/lisa-harper-fabric/skills/harper-auth/SKILL.md +48 -0
- package/plugins/lisa-harper-fabric/skills/harper-build-and-deploy/SKILL.md +20 -5
- package/plugins/lisa-harper-fabric/skills/harper-resources/SKILL.md +35 -0
- package/plugins/lisa-harper-fabric/skills/harper-rest-queries/SKILL.md +40 -3
- package/plugins/lisa-harper-fabric-agy/generated-artifact-globs.txt +1 -0
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/skills/harper-auth/SKILL.md +48 -0
- package/plugins/lisa-harper-fabric-agy/skills/harper-build-and-deploy/SKILL.md +20 -5
- package/plugins/lisa-harper-fabric-agy/skills/harper-resources/SKILL.md +35 -0
- package/plugins/lisa-harper-fabric-agy/skills/harper-rest-queries/SKILL.md +40 -3
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/generated-artifact-globs.txt +1 -0
- package/plugins/lisa-harper-fabric-copilot/hooks/block-generated-artifact-edits.sh +51 -0
- package/plugins/lisa-harper-fabric-copilot/rules/harper-fabric.md +1 -1
- package/plugins/lisa-harper-fabric-copilot/skills/harper-auth/SKILL.md +48 -0
- package/plugins/lisa-harper-fabric-copilot/skills/harper-build-and-deploy/SKILL.md +20 -5
- package/plugins/lisa-harper-fabric-copilot/skills/harper-resources/SKILL.md +35 -0
- package/plugins/lisa-harper-fabric-copilot/skills/harper-rest-queries/SKILL.md +40 -3
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/generated-artifact-globs.txt +1 -0
- package/plugins/lisa-harper-fabric-cursor/hooks/block-generated-artifact-edits.sh +51 -0
- package/plugins/lisa-harper-fabric-cursor/rules/harper-fabric.mdc +1 -1
- package/plugins/lisa-harper-fabric-cursor/skills/harper-auth/SKILL.md +48 -0
- package/plugins/lisa-harper-fabric-cursor/skills/harper-build-and-deploy/SKILL.md +20 -5
- package/plugins/lisa-harper-fabric-cursor/skills/harper-resources/SKILL.md +35 -0
- package/plugins/lisa-harper-fabric-cursor/skills/harper-rest-queries/SKILL.md +40 -3
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/harper-fabric/generated-artifact-globs.txt +1 -0
- package/plugins/src/harper-fabric/hooks/block-generated-artifact-edits.sh +51 -0
- package/plugins/src/harper-fabric/rules/harper-fabric.md +1 -1
- package/plugins/src/harper-fabric/skills/harper-auth/SKILL.md +48 -0
- package/plugins/src/harper-fabric/skills/harper-build-and-deploy/SKILL.md +20 -5
- package/plugins/src/harper-fabric/skills/harper-resources/SKILL.md +35 -0
- 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`.
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
@@ -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
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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`.
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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`.
|
|
204
|
-
|
|
205
|
-
|
|
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
|