@haverstack/server 0.8.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/.claude/settings.local.json +22 -0
- package/.dockerignore +10 -0
- package/.env.example +90 -0
- package/.github/pull_request_template.md +23 -0
- package/.github/workflows/ci.yml +55 -0
- package/.github/workflows/release.yml +99 -0
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/AGENTS.md +91 -0
- package/CONTRIBUTING.md +234 -0
- package/Dockerfile +30 -0
- package/LICENSE +121 -0
- package/README.md +107 -0
- package/SECURITY.md +54 -0
- package/docs/api.md +277 -0
- package/docs/deployment.md +212 -0
- package/eslint.config.mjs +18 -0
- package/package.json +42 -0
- package/pnpm-workspace.yaml +13 -0
- package/src/app.ts +86 -0
- package/src/config.ts +176 -0
- package/src/index.ts +54 -0
- package/src/lib/changeStreams.ts +23 -0
- package/src/lib/frameGate.ts +42 -0
- package/src/lib/json.ts +27 -0
- package/src/lib/nonceStore.ts +103 -0
- package/src/lib/queryLimit.ts +11 -0
- package/src/lib/queryWorker/pool.ts +324 -0
- package/src/lib/queryWorker/protocol.ts +39 -0
- package/src/lib/queryWorker/worker.ts +51 -0
- package/src/lib/resumeBuffer.ts +268 -0
- package/src/lib/resumeCursor.ts +48 -0
- package/src/lib/tokenSweep.ts +43 -0
- package/src/middleware/auth.ts +77 -0
- package/src/middleware/errors.ts +73 -0
- package/src/routes/attachments.ts +126 -0
- package/src/routes/auth.ts +137 -0
- package/src/routes/changes.ts +279 -0
- package/src/routes/entity.ts +33 -0
- package/src/routes/health.ts +8 -0
- package/src/routes/records.ts +259 -0
- package/src/routes/tokens.ts +85 -0
- package/src/routes/types.ts +57 -0
- package/src/routes/wellknown.ts +55 -0
- package/src/shutdown.ts +56 -0
- package/src/stack.ts +96 -0
- package/src/types.ts +9 -0
- package/src/wireError.ts +21 -0
- package/tests/app.test.ts +86 -0
- package/tests/changeFeedClient.ts +156 -0
- package/tests/config.test.ts +112 -0
- package/tests/conformance.test.ts +2322 -0
- package/tests/fixtures/blockingWorker.ts +27 -0
- package/tests/fixtures/brokenWorker.ts +7 -0
- package/tests/lib/changeFeedClient.test.ts +126 -0
- package/tests/lib/frameGate.test.ts +93 -0
- package/tests/lib/queryLimit.test.ts +33 -0
- package/tests/lib/queryWorker.test.ts +60 -0
- package/tests/lib/queryWorkerPool.test.ts +129 -0
- package/tests/lib/resumeBuffer.test.ts +341 -0
- package/tests/lib/resumeCursor.test.ts +46 -0
- package/tests/middleware/errors.test.ts +176 -0
- package/tests/routes/associations.test.ts +170 -0
- package/tests/routes/attachments.test.ts +621 -0
- package/tests/routes/auth.test.ts +448 -0
- package/tests/routes/changes.test.ts +567 -0
- package/tests/routes/contentQuery.test.ts +68 -0
- package/tests/routes/entity.test.ts +170 -0
- package/tests/routes/groups.test.ts +69 -0
- package/tests/routes/records.test.ts +1965 -0
- package/tests/routes/relatedToQuery.test.ts +182 -0
- package/tests/routes/tokens.test.ts +157 -0
- package/tests/routes/tombstones.test.ts +93 -0
- package/tests/routes/types.test.ts +129 -0
- package/tests/routes/versions.test.ts +177 -0
- package/tests/routes/wellknown.test.ts +71 -0
- package/tests/setup.ts +154 -0
- package/tests/shutdown.test.ts +121 -0
- package/tests/stack.test.ts +198 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(openssl rand *)",
|
|
5
|
+
"Bash(pnpm ls *)",
|
|
6
|
+
"Bash(find / -path \"*haverstack/core*\" -name \"*.js\")",
|
|
7
|
+
"Read(//var/home/jen/Dev/haverstack/core/packages/core/**)",
|
|
8
|
+
"Bash(node -e \"const {generateId} = require\\('./dist/id.js'\\); console.log\\(generateId\\(\\)\\)\")",
|
|
9
|
+
"Bash(node --input-type=module -e \"import {generateId} from '/var/home/jen/Dev/haverstack/core/packages/core/dist/id.js'; console.log\\(generateId\\(\\)\\)\")",
|
|
10
|
+
"Bash(node *)",
|
|
11
|
+
"Bash(echo \"exit: $?\")",
|
|
12
|
+
"Bash(pnpm -v)",
|
|
13
|
+
"Bash(pnpm help *)",
|
|
14
|
+
"Bash(python3 -c ' *)",
|
|
15
|
+
"Bash(gh issue *)",
|
|
16
|
+
"Bash(pnpm typecheck *)",
|
|
17
|
+
"Bash(pnpm test *)",
|
|
18
|
+
"Bash(pnpm lint *)",
|
|
19
|
+
"Bash(pnpm format *)"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/.dockerignore
ADDED
package/.env.example
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Port to listen on (default: 3000)
|
|
2
|
+
PORT=3000
|
|
3
|
+
|
|
4
|
+
# Path to the SQLite database file.
|
|
5
|
+
# On first run (file does not exist), the database is initialized.
|
|
6
|
+
# On subsequent runs, the existing database is opened.
|
|
7
|
+
DB_PATH=./stack.db
|
|
8
|
+
|
|
9
|
+
# Owner entity ID. Required only on first run when the DB is being initialized.
|
|
10
|
+
# Must be a DID (e.g. "did:key:z6Mk..."). Generate one with:
|
|
11
|
+
# node -e "require('@haverstack/core/did').generateDidKeypair().then(k => console.log(k.did))"
|
|
12
|
+
# Keep the corresponding private key — this only prints the public DID.
|
|
13
|
+
ENTITY_ID=
|
|
14
|
+
|
|
15
|
+
# Owner's display name (optional). When set, the server ensures the owner's
|
|
16
|
+
# own _entity profile record exists on every open — safe to leave set
|
|
17
|
+
# permanently. No profile record is created if omitted.
|
|
18
|
+
OWNER_NAME=
|
|
19
|
+
|
|
20
|
+
# Owner's handle (optional). Only used alongside OWNER_NAME.
|
|
21
|
+
OWNER_HANDLE=
|
|
22
|
+
|
|
23
|
+
# IANA timezone string (optional). Used only on first run. No default — an
|
|
24
|
+
# absent timezone stays absent (omitted from discovery), rather than
|
|
25
|
+
# asserting a timezone the stack was never actually given.
|
|
26
|
+
TIMEZONE=
|
|
27
|
+
|
|
28
|
+
# Master bearer token for the stack owner. This is a break-glass credential:
|
|
29
|
+
# the DID challenge-response handshake lets a client holding the owner's key
|
|
30
|
+
# authenticate without it, but OWNER_TOKEN remains required to bootstrap the
|
|
31
|
+
# server and manage other tokens via POST /tokens. Keep this secret and
|
|
32
|
+
# treat it like a root password. All other tokens are issued via the API
|
|
33
|
+
# and stored in the DB.
|
|
34
|
+
# Generate with `openssl rand -hex 32`
|
|
35
|
+
OWNER_TOKEN=
|
|
36
|
+
|
|
37
|
+
# Allowed CORS origins. Default: empty (no cross-origin access).
|
|
38
|
+
# Set to * to allow all origins, or a comma-separated list:
|
|
39
|
+
# CORS_ORIGINS=https://app.example.com,https://admin.example.com
|
|
40
|
+
# See docs/deployment.md for guidance.
|
|
41
|
+
CORS_ORIGINS=
|
|
42
|
+
|
|
43
|
+
# Canonical public base URL of this server. Required: it's the origin the
|
|
44
|
+
# DID challenge-response handshake (POST /auth/challenge, POST /auth/token)
|
|
45
|
+
# signs and verifies against, and MUST be this server's own public origin —
|
|
46
|
+
# never auto-detected from a request header, which a client controls.
|
|
47
|
+
# Example: https://stack.example.com
|
|
48
|
+
BASE_URL=
|
|
49
|
+
|
|
50
|
+
# Maximum upload size for attachments in bytes (default: 52428800 = 50 MB).
|
|
51
|
+
MAX_ATTACHMENT_BYTES=52428800
|
|
52
|
+
|
|
53
|
+
# Maximum size of a JSON request body in bytes — record bodies, PATCH merge
|
|
54
|
+
# patches, and every other JSON-accepting route (default: 1048576 = 1 MB).
|
|
55
|
+
# Applies globally, except POST /attachments, which is bounded by
|
|
56
|
+
# MAX_ATTACHMENT_BYTES instead. Declared to clients via maxContentBytes in
|
|
57
|
+
# GET /.well-known/stack.
|
|
58
|
+
MAX_CONTENT_BYTES=1048576
|
|
59
|
+
|
|
60
|
+
# Deadline in milliseconds for a GET /records or POST /records/query search
|
|
61
|
+
# (default: 10000 = 10s). Bounds execution only — the clock starts when the
|
|
62
|
+
# search reaches a worker, so waiting for a free worker makes a request slow,
|
|
63
|
+
# never failed. A query still executing past it is abandoned with 503 (code
|
|
64
|
+
# "timeout"). See docs/deployment.md#bounding-query-cost.
|
|
65
|
+
QUERY_TIMEOUT_MS=10000
|
|
66
|
+
|
|
67
|
+
# Number of worker threads a slow search can occupy without blocking other
|
|
68
|
+
# requests (default: 2, max 32). See docs/deployment.md#bounding-query-cost.
|
|
69
|
+
QUERY_WORKER_POOL_SIZE=2
|
|
70
|
+
|
|
71
|
+
# How many searches may queue for a free worker before the server sheds load
|
|
72
|
+
# with 503 (code "timeout") rather than queueing further (default: 64). This
|
|
73
|
+
# is what bounds the waiting that QUERY_TIMEOUT_MS deliberately does not.
|
|
74
|
+
# See docs/deployment.md#bounding-query-cost.
|
|
75
|
+
QUERY_QUEUE_LIMIT=64
|
|
76
|
+
|
|
77
|
+
# Register the Schema Commons types (org.haverstack/note, bookmark, task,
|
|
78
|
+
# contact, article, place, page, photo) from @haverstack/commons on startup
|
|
79
|
+
# (default: false). Off by default because the package is Draft status and
|
|
80
|
+
# registering types is not free — they show up in GET /types and in every
|
|
81
|
+
# app's type cache. See docs/deployment.md#schema-commons-seeding.
|
|
82
|
+
SEED_COMMONS_TYPES=false
|
|
83
|
+
|
|
84
|
+
# How long shutdown waits (in milliseconds) for open/keep-alive connections
|
|
85
|
+
# to drain on server.close() before forcing them closed and finishing the
|
|
86
|
+
# flush/close sequence anyway (default: 10000 = 10s).
|
|
87
|
+
SHUTDOWN_TIMEOUT_MS=10000
|
|
88
|
+
|
|
89
|
+
# See docs/deployment.md for production deployment guidance (TLS, CORS,
|
|
90
|
+
# rate limiting, and public endpoint exposure).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What changed, and why. Link any issue this closes. -->
|
|
4
|
+
|
|
5
|
+
## Docs
|
|
6
|
+
|
|
7
|
+
<!-- Does this change observable behavior — a route, status code, error code,
|
|
8
|
+
auth rule, or environment variable? If so, link the docs/ sections updated
|
|
9
|
+
in this PR (docs/api.md, docs/deployment.md, README's config table,
|
|
10
|
+
.env.example). If not, say "No behavior change."
|
|
11
|
+
|
|
12
|
+
If the change is to the wire contract itself rather than to this server's
|
|
13
|
+
implementation of it, the spec and conformance fixtures live in
|
|
14
|
+
haverstack/core — link that PR here. -->
|
|
15
|
+
|
|
16
|
+
## Verification
|
|
17
|
+
|
|
18
|
+
<!-- What you ran, and anything you checked by hand.
|
|
19
|
+
pnpm run format:check && pnpm run lint && pnpm typecheck && pnpm test -->
|
|
20
|
+
|
|
21
|
+
## Notes for reviewers
|
|
22
|
+
|
|
23
|
+
<!-- Optional: trade-offs, alternatives considered, follow-up work worth filing. -->
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
format:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: pnpm/action-setup@v4
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with:
|
|
16
|
+
node-version: 22
|
|
17
|
+
cache: pnpm
|
|
18
|
+
- run: pnpm install --frozen-lockfile
|
|
19
|
+
- run: pnpm run format:check
|
|
20
|
+
|
|
21
|
+
lint:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: pnpm/action-setup@v4
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 22
|
|
29
|
+
cache: pnpm
|
|
30
|
+
- run: pnpm install --frozen-lockfile
|
|
31
|
+
- run: pnpm run lint
|
|
32
|
+
|
|
33
|
+
typecheck:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: pnpm/action-setup@v4
|
|
38
|
+
- uses: actions/setup-node@v4
|
|
39
|
+
with:
|
|
40
|
+
node-version: 22
|
|
41
|
+
cache: pnpm
|
|
42
|
+
- run: pnpm install --frozen-lockfile
|
|
43
|
+
- run: pnpm run typecheck
|
|
44
|
+
|
|
45
|
+
test:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: pnpm/action-setup@v4
|
|
50
|
+
- uses: actions/setup-node@v4
|
|
51
|
+
with:
|
|
52
|
+
node-version: 22
|
|
53
|
+
cache: pnpm
|
|
54
|
+
- run: pnpm install --frozen-lockfile
|
|
55
|
+
- run: pnpm test
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Runs on every push to main except the version-bump commit this workflow
|
|
4
|
+
# produces, preventing an infinite loop.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
release:
|
|
11
|
+
if: "!startsWith(github.event.head_commit.message, 'chore: release')"
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write # push version bump + tag, create release
|
|
15
|
+
packages: write # push Docker image to ghcr.io
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0 # full history so we can inspect commits since last tag
|
|
21
|
+
|
|
22
|
+
- uses: pnpm/action-setup@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-node@v4
|
|
25
|
+
with:
|
|
26
|
+
node-version: 22
|
|
27
|
+
cache: pnpm
|
|
28
|
+
|
|
29
|
+
- run: pnpm install --frozen-lockfile
|
|
30
|
+
|
|
31
|
+
# Inspect commit messages since the last tag (or all commits if no tag
|
|
32
|
+
# yet) and pick a semver bump level using conventional commits:
|
|
33
|
+
# BREAKING CHANGE or ! suffix → major
|
|
34
|
+
# feat: → minor
|
|
35
|
+
# anything else → patch
|
|
36
|
+
- name: Determine version bump
|
|
37
|
+
id: semver
|
|
38
|
+
run: |
|
|
39
|
+
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
40
|
+
|
|
41
|
+
if [ -z "$LATEST_TAG" ]; then
|
|
42
|
+
COMMITS=$(git log --pretty=format:"%s%n%b")
|
|
43
|
+
else
|
|
44
|
+
COMMITS=$(git log "${LATEST_TAG}..HEAD" --pretty=format:"%s%n%b")
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
BUMP="patch"
|
|
48
|
+
while IFS= read -r line; do
|
|
49
|
+
if echo "$line" | grep -qE "BREAKING[- ]CHANGE|^[a-z]+(\(.+\))?!:"; then
|
|
50
|
+
BUMP="major"
|
|
51
|
+
break
|
|
52
|
+
elif echo "$line" | grep -qE "^feat(\(.+\))?:"; then
|
|
53
|
+
if [ "$BUMP" = "patch" ]; then BUMP="minor"; fi
|
|
54
|
+
fi
|
|
55
|
+
done <<< "$COMMITS"
|
|
56
|
+
|
|
57
|
+
echo "bump=$BUMP" >> "$GITHUB_OUTPUT"
|
|
58
|
+
|
|
59
|
+
- name: Bump version in package.json
|
|
60
|
+
id: version
|
|
61
|
+
run: |
|
|
62
|
+
NEW_VERSION=$(npm version ${{ steps.semver.outputs.bump }} --no-git-tag-version)
|
|
63
|
+
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
|
64
|
+
|
|
65
|
+
- name: Log in to GitHub Container Registry
|
|
66
|
+
uses: docker/login-action@v3
|
|
67
|
+
with:
|
|
68
|
+
registry: ghcr.io
|
|
69
|
+
username: ${{ github.actor }}
|
|
70
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
71
|
+
|
|
72
|
+
- name: Build and push Docker image
|
|
73
|
+
uses: docker/build-push-action@v6
|
|
74
|
+
with:
|
|
75
|
+
context: .
|
|
76
|
+
push: true
|
|
77
|
+
tags: |
|
|
78
|
+
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.new_version }}
|
|
79
|
+
ghcr.io/${{ github.repository }}:latest
|
|
80
|
+
|
|
81
|
+
# Commit the package.json bump, tag it, and push. Requires that the
|
|
82
|
+
# github-actions bot is allowed to push to main (configure via branch
|
|
83
|
+
# protection → "Allow specific actors to bypass required pull requests").
|
|
84
|
+
- name: Commit, tag, and push
|
|
85
|
+
run: |
|
|
86
|
+
git config user.name "github-actions[bot]"
|
|
87
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
88
|
+
git add package.json
|
|
89
|
+
git commit -m "chore: release ${{ steps.version.outputs.new_version }}"
|
|
90
|
+
git tag -a "${{ steps.version.outputs.new_version }}" -m "Release ${{ steps.version.outputs.new_version }}"
|
|
91
|
+
git push origin main --follow-tags
|
|
92
|
+
|
|
93
|
+
- name: Create GitHub release
|
|
94
|
+
env:
|
|
95
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
96
|
+
run: |
|
|
97
|
+
gh release create "${{ steps.version.outputs.new_version }}" \
|
|
98
|
+
--title "${{ steps.version.outputs.new_version }}" \
|
|
99
|
+
--generate-notes
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/AGENTS.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Agent Guide
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding agents working in this repository. Humans should read [CONTRIBUTING.md](./CONTRIBUTING.md), which explains the reasoning behind everything here; this file is the short, checkable form.
|
|
4
|
+
|
|
5
|
+
This is the reference server for [Haverstack](https://github.com/haverstack/core). The contract it implements lives in core, and most of the rules below follow from that.
|
|
6
|
+
|
|
7
|
+
## Before you push
|
|
8
|
+
|
|
9
|
+
Run all four. They mirror `.github/workflows/ci.yml`, so a clean local run means a green PR.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm install
|
|
13
|
+
pnpm run format:check # pnpm run format to fix in place
|
|
14
|
+
pnpm run lint
|
|
15
|
+
pnpm typecheck
|
|
16
|
+
pnpm test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
No build step is needed first — `typecheck` runs against `src/`, and `@haverstack/*` resolve from `node_modules`.
|
|
20
|
+
|
|
21
|
+
Report results honestly. If something fails, say so with the output rather than describing the change as complete.
|
|
22
|
+
|
|
23
|
+
A change that depends on unpublished core changes cannot pass CI here, which installs from the registry. Land the core release first; don't edit a version range to point at a local checkout.
|
|
24
|
+
|
|
25
|
+
## Which repository
|
|
26
|
+
|
|
27
|
+
Two repositories, one contract. Filing or fixing in the wrong one leaves the problem standing everywhere else.
|
|
28
|
+
|
|
29
|
+
- **Here**: routes, status codes, config and environment variables, the Docker image, deployment guidance, the change feed, the query worker pool, token issuance and storage.
|
|
30
|
+
- **[core](https://github.com/haverstack/core)**: the data model, permission semantics, identity and DIDs, the wire format and error taxonomy, the conformance fixtures, and the `@haverstack/*` packages.
|
|
31
|
+
|
|
32
|
+
**When it isn't clear which side a bug or a security finding sits on, raise it in core** and say which server behavior prompted it. The wire contract is what multiple implementations must agree on, so an ambiguous defect is more usefully argued there, and a spec section or fixture is the durable form of the answer. For a suspected vulnerability use a private advisory, not an issue — see [SECURITY.md](./SECURITY.md).
|
|
33
|
+
|
|
34
|
+
## Writing comments
|
|
35
|
+
|
|
36
|
+
These rules are enforced in review. Applying them to code you touch is expected; a sweeping unrelated comment refactor is not.
|
|
37
|
+
|
|
38
|
+
- **Answer _why_, not _how_.** The how should be readable in the code. Don't narrate the next few lines.
|
|
39
|
+
- **Four or five lines is the maximum**, not the target. Longer explanations link out: `See docs/deployment.md § Bounding query cost.`
|
|
40
|
+
- **File-top module comments are exempt** — they may be as long as needed to say what the module is and why it exists.
|
|
41
|
+
- **Never cite GitHub issues** (`#44`, `core#52`) in code comments. They belong in commit messages and PR descriptions only.
|
|
42
|
+
- **Never describe previous implementations.** No "the old path", "this used to…", "before this fix". State the current invariant. This applies to test comments too: say what the test pins, not what bug prompted it.
|
|
43
|
+
|
|
44
|
+
## The spec is the source of truth, and it lives in core
|
|
45
|
+
|
|
46
|
+
Design lives in core's [`docs/spec/`](https://github.com/haverstack/core/tree/main/docs/spec): data model, identity, access control, versioning, attachments, adapters, wire format. This server implements it and does not redefine it.
|
|
47
|
+
|
|
48
|
+
- A change to the **wire contract** is a core PR first — spec, then `@haverstack/conformance-fixtures`, then the implementation here. Changing what this server sends so a fixture passes, without the fixture moving, is divergence.
|
|
49
|
+
- A change to **this server's** observable behavior (a route, status code, error code, auth rule, env var) updates this repo's docs in the same change: `docs/api.md`, `docs/deployment.md`, the README config table, `.env.example`. A new env var touches at least three of those.
|
|
50
|
+
- Section names are load-bearing — comments reference them as `docs/spec/<file>.md § Section`. Verify a section exists before linking to it.
|
|
51
|
+
- **Prose states the system as it is, never how it got there.** "now runs on workers", "as of 0.6", "once #88 lands" date the document and describe a system the reader can't see.
|
|
52
|
+
|
|
53
|
+
## No backward compatibility
|
|
54
|
+
|
|
55
|
+
There is **no install base**. Nothing depends on this server's current behavior.
|
|
56
|
+
|
|
57
|
+
- Don't add compatibility shims, deprecation paths, or handler branches for request shapes this server never accepted. Delete rather than deprecate.
|
|
58
|
+
- Don't use the word "legacy". A third-party implementation of the wire protocol is _foreign_, not legacy.
|
|
59
|
+
- Genuinely foreign input — an optional field a client omits, a stack file from another adapter — is current-behavior handling and stays.
|
|
60
|
+
- **Stored data is the exception.** A change to what lives under `DB_PATH` (database, token store, nonce store, attachments) meets running deployments with no migration path. Flag it explicitly in the PR.
|
|
61
|
+
|
|
62
|
+
## Architecture rules
|
|
63
|
+
|
|
64
|
+
- **The server is a thin, enforcing layer over `Stack`.** Validation, ID rules, and permission logic belong to `@haverstack/core`. Don't reimplement a model invariant in a route handler.
|
|
65
|
+
- **Routes are sub-apps.** One file per URL prefix in `src/routes/`, mounted in `src/app.ts`. Handlers parse, call `Stack`, serialize. Mechanism goes in `src/lib/`, cross-cutting concerns in `src/middleware/`.
|
|
66
|
+
- **Errors take one of two paths, never a hand-rolled body.** A thrown core `StackError` is mapped by `errorMiddleware` via `serializeError()`. Failures with no core class use `wireError()`. Never `c.json()` an error directly; never let an engine error or a stack trace reach the client.
|
|
67
|
+
- **Compare secrets with `safeCompare()`** from `src/middleware/auth.ts`. Every owner-token comparison goes through it.
|
|
68
|
+
- **Anything that can scan the whole store runs on `QueryWorkerPool`**, because `node:sqlite` is synchronous and uncancellable. Point lookups and writes stay on the request thread.
|
|
69
|
+
- **Config is parsed once** in `src/config.ts`; downstream code takes the `Config` object. New variables land in `src/config.ts`, `.env.example`, and the README table together, with a default that is safe for a public deployment.
|
|
70
|
+
- **Register cleanup in `src/shutdown.ts`** for anything holding a file handle, a thread, or a long-lived response.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
Vitest, in `tests/`, mirroring `src/`. Use `tests/setup.ts` rather than building a server by hand:
|
|
75
|
+
|
|
76
|
+
- `buildTestApp()` — real Hono app over a real SQLite database in an isolated temp dir, plus `cleanup()`. Always `await cleanup()`; a leaked worker pool keeps the run alive.
|
|
77
|
+
- `req(app, method, path, opts)` — returns `{ status, data }`.
|
|
78
|
+
- `TEST_BASE_URL` matches `@haverstack/conformance-fixtures`' `AUTH_FIXTURE_ORIGIN` — the auth fixtures carry real signatures over that exact origin. Don't change it.
|
|
79
|
+
|
|
80
|
+
`tests/conformance.test.ts` is an acceptance gate: every fixture is dispatched or listed in a `SKIPPED` set with a reason, and each block's coverage test fails when core adds or renames one. Handle the new fixture or skip it with a reason — never loosen the coverage check.
|
|
81
|
+
|
|
82
|
+
Name tests after the invariant they pin, not the defect that prompted them.
|
|
83
|
+
|
|
84
|
+
## Commits and pull requests
|
|
85
|
+
|
|
86
|
+
- Conventional Commits, and they drive the release: `feat:` → minor, `feat!:` or a `BREAKING CHANGE` footer → major, everything else (`fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `perf:`) → patch. Scope optional: `fix(attachments): …`. Getting the type wrong produces the wrong version bump silently — when in doubt, `fix:`.
|
|
87
|
+
- Imperative subject; the body explains why. On a squash merge, the squashed subject is what the release workflow reads.
|
|
88
|
+
- Issue references are welcome in commit messages and PR bodies.
|
|
89
|
+
- One concern per PR. Fill in `.github/pull_request_template.md`, including the Docs section.
|
|
90
|
+
- Never hand-edit `version` in `package.json` — `.github/workflows/release.yml` owns it, and merging to `main` is the act of releasing.
|
|
91
|
+
- Don't open a PR unless asked to.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for working on this. This document covers how to get set up, what to run before you push, where to file what, and the conventions the codebase already follows — the ones that aren't obvious from reading a single file.
|
|
4
|
+
|
|
5
|
+
This is the reference server. The contract it implements — the data model, the permission rules, the wire format — lives in [`haverstack/core`](https://github.com/haverstack/core), and that split shapes most of what follows.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Node 22.9+ and pnpm 11; CI runs Node 22 and the `packageManager` pin.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
cp .env.example .env
|
|
15
|
+
# Edit .env — set OWNER_TOKEN, ENTITY_ID, and BASE_URL at minimum
|
|
16
|
+
pnpm install
|
|
17
|
+
pnpm dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`pnpm dev` starts with live reload on `PORT` (default `3000`) and initializes a SQLite database at `DB_PATH` on first run.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Before you push
|
|
25
|
+
|
|
26
|
+
Run all four:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pnpm run format:check # or: pnpm run format, to fix in place
|
|
30
|
+
pnpm run lint
|
|
31
|
+
pnpm typecheck
|
|
32
|
+
pnpm test
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
These mirror `.github/workflows/ci.yml` exactly, so a clean local run means a green PR. Failures block merge.
|
|
36
|
+
|
|
37
|
+
Unlike core, nothing here needs a build first — `pnpm typecheck` runs against `src/` directly, and the `@haverstack/*` packages resolve from `node_modules` as published builds.
|
|
38
|
+
|
|
39
|
+
**Testing against unreleased core changes.** When a change here depends on something not yet published from core, don't hand-edit the version range: link the workspace (`pnpm link --global` in the core package, or a `pnpm.overrides` entry you drop before pushing) and land the core release first. A PR here that only passes against a local core checkout will fail CI, which installs from the registry.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Where to file an issue
|
|
44
|
+
|
|
45
|
+
Two repositories, one contract. Filing in the wrong one usually means a fix that lands in one place and quietly leaves the problem standing everywhere else.
|
|
46
|
+
|
|
47
|
+
- **Here** — a route's behavior, a status code this server returns, config and environment variables, the Docker image, deployment guidance, the change feed, the query worker pool, token issuance and storage.
|
|
48
|
+
- **[`haverstack/core`](https://github.com/haverstack/core/issues)** — the data model, permission semantics, identity and DID handling, the wire format and its error taxonomy, the conformance fixtures, and anything in `@haverstack/core`, `@haverstack/adapter-local`, or `@haverstack/wire-types`.
|
|
49
|
+
|
|
50
|
+
**When it isn't clear which side something sits on, file it in core** and say which server behavior prompted it. The wire contract is the thing multiple implementations have to agree on, so an ambiguous bug is more usefully argued there — and a fixture or spec section is the durable form of the answer. Moving an issue afterwards is cheap; discovering six months later that a protocol flaw was filed as a server bug is not.
|
|
51
|
+
|
|
52
|
+
For a suspected **security** issue, don't open an issue at all — see [SECURITY.md](./SECURITY.md), which draws the same line for advisories.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Commit style
|
|
57
|
+
|
|
58
|
+
This project uses [Conventional Commits](https://www.conventionalcommits.org/). Every commit message must start with a type prefix. The release workflow reads commit messages since the last tag to determine the version bump automatically:
|
|
59
|
+
|
|
60
|
+
| Commit type | Example | Version bump |
|
|
61
|
+
| ------------------------------------------------------------------------ | ----------------------------------- | ------------ |
|
|
62
|
+
| `feat:` | `feat: add token expiry` | minor |
|
|
63
|
+
| `feat!:` or `BREAKING CHANGE` footer | `feat!: rename /entity to /owner` | major |
|
|
64
|
+
| Anything else (`fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `perf:`) | `fix: return 404 on missing record` | patch |
|
|
65
|
+
|
|
66
|
+
A scope is optional: `fix(attachments): reject empty filename`.
|
|
67
|
+
|
|
68
|
+
Getting the type wrong produces the wrong version bump with no warning, so when in doubt use `fix:` for patches and `feat:` only for genuinely new capabilities.
|
|
69
|
+
|
|
70
|
+
Write the subject in the imperative, and use the body to explain _why_ the change was made, not what the diff already shows.
|
|
71
|
+
|
|
72
|
+
Issue references (`#123`, `core#52`) belong in commit messages, PR titles, and PR bodies. They do **not** belong in code comments — see [Comments](#comments).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Pull requests
|
|
77
|
+
|
|
78
|
+
- One concern per PR. Split unrelated changes.
|
|
79
|
+
- The PR description should explain _why_, not just _what_ — the diff already shows what changed.
|
|
80
|
+
- Link to any relevant issue with `Closes #N`.
|
|
81
|
+
- Keep PRs small enough to review in one sitting; large refactors are fine but flag them early.
|
|
82
|
+
- Fill in [`.github/pull_request_template.md`](./.github/pull_request_template.md), including the Docs section — answer it even when the answer is "no behavior change."
|
|
83
|
+
- Work on a branch; CI must be green before merge.
|
|
84
|
+
|
|
85
|
+
If you're an AI coding agent, [AGENTS.md](./AGENTS.md) is the condensed, checkable form of this document.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Releasing
|
|
90
|
+
|
|
91
|
+
Releases are automatic. `.github/workflows/release.yml` runs on every push to `main` (skipping its own `chore: release` commit), reads the conventional-commit prefixes since the last tag to pick a bump, then:
|
|
92
|
+
|
|
93
|
+
1. bumps `version` in `package.json`,
|
|
94
|
+
2. builds and pushes `ghcr.io/haverstack/server` at that version and `:latest`,
|
|
95
|
+
3. commits the bump, tags it, pushes to `main`, and cuts a GitHub release with generated notes.
|
|
96
|
+
|
|
97
|
+
Nobody edits `version` by hand and nobody builds the image by hand. Two things are load-bearing: the workflow needs `contents: write` and `packages: write`, and the `github-actions[bot]` push to `main` requires a branch-protection bypass (Settings → Branches → _Allow specified actors to bypass required pull requests_). Merging to `main` is the act of releasing, which is why `main` is protected.
|
|
98
|
+
|
|
99
|
+
The version bump is derived from commit subjects, so a squash merge means the squashed subject is what counts — check it before confirming the merge.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Comments
|
|
104
|
+
|
|
105
|
+
The convention this codebase follows, in order of how often it comes up:
|
|
106
|
+
|
|
107
|
+
**Comments answer _why_. The _how_ should be visible in the code itself.** If a comment is narrating what the next few lines do, delete it and let the code speak. If the code is hard enough to follow that it needs narration, that's usually a signal to simplify the code.
|
|
108
|
+
|
|
109
|
+
**Keep them short — four or five lines is the extreme case, not the target.** Anything needing more explanation than that links out to the docs or to core's spec instead of carrying the argument inline:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// A search past QUERY_TIMEOUT_MS is abandoned and its worker replaced: a
|
|
113
|
+
// stuck query costs one pool slot rather than every request. See
|
|
114
|
+
// docs/deployment.md § Bounding query cost.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**File-top module comments are exempt.** A comment at the top of a file may be as long as it needs to be to explain what the module does and why it exists.
|
|
118
|
+
|
|
119
|
+
**No GitHub issue references.** An issue captured something that needed to change, and the change is now reflected in the code and the docs. A `(#44)` in a comment adds nothing a reader can act on, and it makes the source unreadable anywhere the issue tracker isn't at hand. If the rationale matters, state it or link to the section that holds it.
|
|
120
|
+
|
|
121
|
+
**No references to previous implementations.** Comments describe the code as it is now. Phrases like "the old single-threaded path", "this used to run on the request thread", or "before this fix" date the comment and describe code that no longer exists. State the invariant instead:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// Don't:
|
|
125
|
+
// Auth used to fall through to anonymous on a malformed header.
|
|
126
|
+
|
|
127
|
+
// Do:
|
|
128
|
+
// A present-but-unparseable Authorization header is a 401, never a silent
|
|
129
|
+
// downgrade to anonymous — a client that sent *something* meant to.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The same applies to tests. A regression test's name and comment should say what invariant it pins, not what bug prompted it.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## The spec is the source of truth, and it lives in core
|
|
137
|
+
|
|
138
|
+
Design decisions — the data model, identity, access control, versioning, attachments, the wire format — live in core's [`docs/spec/`](https://github.com/haverstack/core/tree/main/docs/spec). This server implements them; it does not get to redefine them.
|
|
139
|
+
|
|
140
|
+
That means a change to the wire contract is a core PR first: spec section, then `@haverstack/conformance-fixtures`, then the implementation here. A server-side "fix" that makes a fixture pass by changing what this server sends, without the fixture moving, is this server diverging from the contract — which `tests/conformance.test.ts` exists to catch.
|
|
141
|
+
|
|
142
|
+
This repository's own docs cover what is genuinely the server's:
|
|
143
|
+
|
|
144
|
+
- [`docs/api.md`](./docs/api.md) — routes, auth handshake, query parameters, permissions as this server exposes them
|
|
145
|
+
- [`docs/deployment.md`](./docs/deployment.md) — TLS, proxies, CORS, body limits, query cost, single-writer topology
|
|
146
|
+
- [`README.md`](./README.md) — the configuration table
|
|
147
|
+
- [`.env.example`](./.env.example) — every variable, with its default
|
|
148
|
+
|
|
149
|
+
A change to observable behavior updates the relevant one **in the same PR**. A new environment variable touches three of them (`.env.example`, the README table, and usually a deployment section); they drift the moment they're updated separately.
|
|
150
|
+
|
|
151
|
+
When you link to a core spec section from a comment, use the `docs/spec/<file>.md § Section` form and make sure the section actually exists.
|
|
152
|
+
|
|
153
|
+
**Prose describes the system as it is, not how it got there.** The no-references-to-previous-implementations rule above applies to documentation, and bites harder there: an operator reading the deployment guide is deciding what to configure, and "this now runs on workers", "as of 0.6 the timeout also covers queueing", or "once #88 lands" describe a system they cannot see. They also rot silently — the sentence stays true-sounding long after it stopped being news.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## No backward compatibility yet
|
|
158
|
+
|
|
159
|
+
**There is no install base.** Nothing depends on this server's current behavior, so there is nothing to preserve. Prefer changing things in place over carrying compatibility shims:
|
|
160
|
+
|
|
161
|
+
- No accepting an older request shape "just in case" — no such client exists.
|
|
162
|
+
- No deprecation cycles; delete the old route.
|
|
163
|
+
- No `legacy` branches in handlers, and no `legacy` in prose. A third-party implementation of the wire protocol is _foreign_, not legacy — that distinction is real and worth keeping.
|
|
164
|
+
|
|
165
|
+
Note that this is about _our_ history, not about inputs we genuinely receive. Handling a client that omits an optional field, or a stack database written by another adapter, is current-behavior compatibility and stays.
|
|
166
|
+
|
|
167
|
+
One thing does not get this freedom: **stored data**. A change to what lives in `DB_PATH` — the database, the token store, the nonce store, the attachments directory — meets a running deployment with data in it and no migration path this repository provides. Say so explicitly in the PR and in the release notes.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Architecture conventions
|
|
172
|
+
|
|
173
|
+
**The server is a thin, enforcing layer over `Stack`.** Validation, ID rules, permission logic, and the record model belong to `@haverstack/core` and are inherited, not reimplemented. What this repository owns is everything the library can't see: HTTP shape, authentication, config, transport limits, and process lifecycle. A permission check written inside a route handler is a bug waiting to diverge from the model — the exceptions are the ones the wire format assigns to the server, like deciding what an anonymous caller may reach.
|
|
174
|
+
|
|
175
|
+
**One module per concern**, following the layout in [Where things live](#where-things-live):
|
|
176
|
+
|
|
177
|
+
- `src/routes/*` — one file per URL prefix, each exporting a `Hono` sub-app mounted in `src/app.ts`. Handlers parse, call `Stack`, and serialize; anything longer than that usually belongs in `src/lib/`.
|
|
178
|
+
- `src/middleware/*` — cross-cutting: `auth.ts` resolves a credential to a principal/subject pair (or anonymous), `errors.ts` maps thrown `StackError`s to wire responses.
|
|
179
|
+
- `src/lib/*` — mechanism with no HTTP in it: the query worker pool, change streams, resume cursors, the nonce store, token sweeping.
|
|
180
|
+
|
|
181
|
+
**Errors go out through one of two paths, never by hand.** A thrown core `StackError` is mapped by `errorMiddleware` via `serializeError()`, which is what keeps the [error taxonomy](https://github.com/haverstack/core/blob/main/docs/spec/wire-format.md) intact. Failures with no core class — a missing bearer token, a `404` for an unrouted path — use `wireError()` so the `{ error: { code, message } }` shape stays uniform. Never `c.json()` an error body directly, and never let an engine error reach the client as itself.
|
|
182
|
+
|
|
183
|
+
**Compare credentials in constant time.** `safeCompare()` in `src/middleware/auth.ts` is the only way to check a presented secret against a configured one; every owner-token comparison goes through it, including the ones that re-derive "is this the owner" outside the middleware.
|
|
184
|
+
|
|
185
|
+
**Anything that can scan the whole store runs on the worker pool.** `GET /records` and `POST /records/query` go through `QueryWorkerPool` because `node:sqlite` is synchronous and uncancellable in-process. Point lookups and writes stay on the request thread — they're index-bound. A new route that can trigger a full scan belongs on the pool; see [Deployment § Bounding query cost](./docs/deployment.md#bounding-query-cost).
|
|
186
|
+
|
|
187
|
+
**Config is parsed once, at the edge.** `src/config.ts` reads and validates the environment into a `Config` object, and everything downstream takes that object rather than reaching for `process.env`. The one exception is `src/index.ts`, which reads `LOG_LEVEL` and `NODE_ENV` to build the logger before config parsing runs — so that a config error is logged rather than thrown into the void. A new variable is added to `src/config.ts`, `.env.example`, and the README table together, and gets a default that is safe for a public deployment, or is required outright.
|
|
188
|
+
|
|
189
|
+
**Shutdown is part of the contract.** `src/shutdown.ts` drains open connections within `SHUTDOWN_TIMEOUT_MS` and then closes the stack, token store, nonce store, and worker pool. Anything new holding a file handle, a thread, or a long-lived response (the change feed does all three) registers its cleanup there.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Testing conventions
|
|
194
|
+
|
|
195
|
+
Tests use [Vitest](https://vitest.dev/) and live in `tests/`, mirroring `src/`.
|
|
196
|
+
|
|
197
|
+
`tests/setup.ts` is the shared harness. Use it rather than building a server by hand:
|
|
198
|
+
|
|
199
|
+
- **`buildTestApp()`** — a real `Hono` app over a real SQLite database in an isolated temp directory, plus a `cleanup()` that closes everything and removes it. Tests exercise the actual HTTP surface, not a mocked one.
|
|
200
|
+
- **`req(app, method, path, opts)`** — fires a request and returns `{ status, data }`.
|
|
201
|
+
- **`TEST_ENTITY_ID` / `OTHER_ENTITY_ID` / `TEST_TOKEN` / `TEST_BASE_URL`** — the fixed identities. `TEST_BASE_URL` matches `@haverstack/conformance-fixtures`' `AUTH_FIXTURE_ORIGIN`, because the auth fixtures carry real signatures over that exact origin; changing it breaks the handshake fixtures.
|
|
202
|
+
|
|
203
|
+
Each test gets its own temp directory so parallel runs don't collide over the database's sibling `attachments/` folder. Always `await cleanup()` — a leaked worker pool keeps the run alive.
|
|
204
|
+
|
|
205
|
+
**`tests/conformance.test.ts` is an acceptance gate, not a snapshot.** It runs `@haverstack/conformance-fixtures` against the real HTTP surface, and every fixture in each imported array is either dispatched or named in that block's `SKIPPED` set with a reason. Each block's final coverage test fails when core adds, removes, or renames a fixture this file hasn't been told about. When that happens, the fix is to handle the new fixture or skip it with a reason — never to loosen the coverage check.
|
|
206
|
+
|
|
207
|
+
Name tests after the behavior they pin, not the defect that prompted them. `'a revoked token is rejected on a route that allows anonymous reads'` survives refactoring; `'regression for #44'` doesn't.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Where things live
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
docs/
|
|
215
|
+
api.md # Routes, auth, query parameters, permissions
|
|
216
|
+
deployment.md # TLS, proxies, CORS, limits, query cost, topology
|
|
217
|
+
src/
|
|
218
|
+
index.ts # Entry point: config, stack, server, signals
|
|
219
|
+
app.ts # Hono app: middleware order and route mounting
|
|
220
|
+
config.ts # Environment parsing and validation
|
|
221
|
+
stack.ts # StackContext: adapter, stack, tokens, nonces, workers
|
|
222
|
+
shutdown.ts # Connection draining and orderly resource close
|
|
223
|
+
wireError.ts # `{ error: { code, message } }` for non-StackError failures
|
|
224
|
+
types.ts # Hono environment bindings
|
|
225
|
+
middleware/
|
|
226
|
+
auth.ts # Bearer resolution, principal/subject, constant-time compare
|
|
227
|
+
errors.ts # StackError → wire response mapping
|
|
228
|
+
routes/ # One sub-app per URL prefix
|
|
229
|
+
lib/ # Mechanism: query workers, change streams, cursors, nonces
|
|
230
|
+
tests/
|
|
231
|
+
setup.ts # buildTestApp(), req(), shared identities
|
|
232
|
+
conformance.test.ts # @haverstack/conformance-fixtures acceptance gate
|
|
233
|
+
routes/ lib/ # Mirrors src/
|
|
234
|
+
```
|