@codyswann/lisa 2.191.13 → 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.
- 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/package.json +2 -2
- 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/skills/harper-auth/SKILL.md +48 -0
- 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/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-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/skills/harper-auth/SKILL.md +48 -0
- 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/skills/harper-auth/SKILL.md +48 -0
- 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/skills/harper-auth/SKILL.md +48 -0
- 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
|
@@ -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()
|
package/harper-fabric/copy-overwrite/ast-grep/rules/harper/require-statuscode-on-thrown-error.yml
ADDED
|
@@ -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.
|
|
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": {
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"vitest": "^4.1.9"
|
|
219
219
|
},
|
|
220
220
|
"devDependencies": {
|
|
221
|
-
"@codyswann/lisa": "^2.
|
|
221
|
+
"@codyswann/lisa": "^2.191.13",
|
|
222
222
|
"@types/js-yaml": "^4.0.9",
|
|
223
223
|
"@types/semver": "^7.7.1",
|
|
224
224
|
"eslint-plugin-oxlint": "^1.62.0",
|
|
@@ -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`.
|
|
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
|
|
@@ -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
|