@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.
Files changed (82) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/.dockerignore +10 -0
  3. package/.env.example +90 -0
  4. package/.github/pull_request_template.md +23 -0
  5. package/.github/workflows/ci.yml +55 -0
  6. package/.github/workflows/release.yml +99 -0
  7. package/.prettierignore +3 -0
  8. package/.prettierrc +7 -0
  9. package/AGENTS.md +91 -0
  10. package/CONTRIBUTING.md +234 -0
  11. package/Dockerfile +30 -0
  12. package/LICENSE +121 -0
  13. package/README.md +107 -0
  14. package/SECURITY.md +54 -0
  15. package/docs/api.md +277 -0
  16. package/docs/deployment.md +212 -0
  17. package/eslint.config.mjs +18 -0
  18. package/package.json +42 -0
  19. package/pnpm-workspace.yaml +13 -0
  20. package/src/app.ts +86 -0
  21. package/src/config.ts +176 -0
  22. package/src/index.ts +54 -0
  23. package/src/lib/changeStreams.ts +23 -0
  24. package/src/lib/frameGate.ts +42 -0
  25. package/src/lib/json.ts +27 -0
  26. package/src/lib/nonceStore.ts +103 -0
  27. package/src/lib/queryLimit.ts +11 -0
  28. package/src/lib/queryWorker/pool.ts +324 -0
  29. package/src/lib/queryWorker/protocol.ts +39 -0
  30. package/src/lib/queryWorker/worker.ts +51 -0
  31. package/src/lib/resumeBuffer.ts +268 -0
  32. package/src/lib/resumeCursor.ts +48 -0
  33. package/src/lib/tokenSweep.ts +43 -0
  34. package/src/middleware/auth.ts +77 -0
  35. package/src/middleware/errors.ts +73 -0
  36. package/src/routes/attachments.ts +126 -0
  37. package/src/routes/auth.ts +137 -0
  38. package/src/routes/changes.ts +279 -0
  39. package/src/routes/entity.ts +33 -0
  40. package/src/routes/health.ts +8 -0
  41. package/src/routes/records.ts +259 -0
  42. package/src/routes/tokens.ts +85 -0
  43. package/src/routes/types.ts +57 -0
  44. package/src/routes/wellknown.ts +55 -0
  45. package/src/shutdown.ts +56 -0
  46. package/src/stack.ts +96 -0
  47. package/src/types.ts +9 -0
  48. package/src/wireError.ts +21 -0
  49. package/tests/app.test.ts +86 -0
  50. package/tests/changeFeedClient.ts +156 -0
  51. package/tests/config.test.ts +112 -0
  52. package/tests/conformance.test.ts +2322 -0
  53. package/tests/fixtures/blockingWorker.ts +27 -0
  54. package/tests/fixtures/brokenWorker.ts +7 -0
  55. package/tests/lib/changeFeedClient.test.ts +126 -0
  56. package/tests/lib/frameGate.test.ts +93 -0
  57. package/tests/lib/queryLimit.test.ts +33 -0
  58. package/tests/lib/queryWorker.test.ts +60 -0
  59. package/tests/lib/queryWorkerPool.test.ts +129 -0
  60. package/tests/lib/resumeBuffer.test.ts +341 -0
  61. package/tests/lib/resumeCursor.test.ts +46 -0
  62. package/tests/middleware/errors.test.ts +176 -0
  63. package/tests/routes/associations.test.ts +170 -0
  64. package/tests/routes/attachments.test.ts +621 -0
  65. package/tests/routes/auth.test.ts +448 -0
  66. package/tests/routes/changes.test.ts +567 -0
  67. package/tests/routes/contentQuery.test.ts +68 -0
  68. package/tests/routes/entity.test.ts +170 -0
  69. package/tests/routes/groups.test.ts +69 -0
  70. package/tests/routes/records.test.ts +1965 -0
  71. package/tests/routes/relatedToQuery.test.ts +182 -0
  72. package/tests/routes/tokens.test.ts +157 -0
  73. package/tests/routes/tombstones.test.ts +93 -0
  74. package/tests/routes/types.test.ts +129 -0
  75. package/tests/routes/versions.test.ts +177 -0
  76. package/tests/routes/wellknown.test.ts +71 -0
  77. package/tests/setup.ts +154 -0
  78. package/tests/shutdown.test.ts +121 -0
  79. package/tests/stack.test.ts +198 -0
  80. package/tsconfig.build.json +9 -0
  81. package/tsconfig.json +19 -0
  82. package/vitest.config.ts +12 -0
package/Dockerfile ADDED
@@ -0,0 +1,30 @@
1
+ # deps: install production dependencies only
2
+ FROM node:22-slim AS deps
3
+ RUN npm install -g pnpm@10
4
+ WORKDIR /app
5
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
6
+ RUN pnpm install --frozen-lockfile --prod
7
+
8
+ # builder: compile TypeScript
9
+ FROM node:22-slim AS builder
10
+ RUN npm install -g pnpm@10
11
+ WORKDIR /app
12
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json tsconfig.build.json ./
13
+ RUN pnpm install --frozen-lockfile
14
+ COPY src ./src
15
+ RUN pnpm build
16
+
17
+ # runner: minimal production image
18
+ FROM node:22-slim AS runner
19
+ RUN groupadd -r app && useradd -r -g app app
20
+ WORKDIR /app
21
+ COPY --from=deps --chown=app:app /app/node_modules ./node_modules
22
+ COPY --from=builder --chown=app:app /app/dist ./dist
23
+ COPY --from=builder --chown=app:app /app/package.json ./
24
+ RUN mkdir -p /app/data && chown app:app /app/data
25
+ VOLUME ["/app/data"]
26
+ ENV NODE_ENV=production
27
+ ENV DB_PATH=/app/data/stack.db
28
+ EXPOSE 3000
29
+ USER app
30
+ CMD ["node", "dist/index.js"]
package/LICENSE ADDED
@@ -0,0 +1,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Haverstack Server
2
+
3
+ Reference HTTP server implementation for [Haverstack](https://github.com/haverstack/core).
4
+
5
+ Exposes a Haverstack stack over a REST API so apps can read and write records remotely. Uses SQLite for storage via `@haverstack/adapter-local`.
6
+
7
+ > **Status:** Early development. APIs are unstable.
8
+
9
+ ---
10
+
11
+ ## Quick start
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
+ The server listens on `PORT` (default `3000`). On first run it initializes a new SQLite database at `DB_PATH`.
21
+
22
+ ---
23
+
24
+ ## Docker
25
+
26
+ ```sh
27
+ docker run -d \
28
+ -e OWNER_TOKEN=<secret> \
29
+ -e ENTITY_ID=<your-entity-id> \
30
+ -e BASE_URL=https://stack.example.com \
31
+ -p 3000:3000 \
32
+ -v haverstack-data:/app/data \
33
+ ghcr.io/haverstack/server:latest
34
+ ```
35
+
36
+ `/app/data` holds the SQLite database, its WAL sidecar files (`stack.db-wal`, `stack.db-shm`), a storage-ownership lock file (`stack.db.lock`), and attachments — mount a volume there for persistence. Set `ENTITY_ID` only on first run; it is ignored once the database exists, though a value that no longer matches the stack's stored owner logs a startup warning rather than failing silently.
37
+
38
+ Only one server process may hold a given `DB_PATH` at a time: a second process against the same path fails at startup with a clear error rather than silently sharing (or corrupting) the database. This is enforced by the storage adapter, independent of any reverse proxy or orchestrator setting — see [Deployment: single-writer topology](./docs/deployment.md#single-writer-topology).
39
+
40
+ ---
41
+
42
+ ## Configuration
43
+
44
+ All configuration is via environment variables. See `.env.example` for the full list.
45
+
46
+ | Variable | Required | Default | Description |
47
+ | ------------------------ | --------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `OWNER_TOKEN` | Yes | — | Break-glass bearer token for the stack owner. Treat like a password. |
49
+ | `ENTITY_ID` | First run | — | Owner entity ID — must be a DID (e.g. `did:key:z6Mk...`). Only needed when initializing a new DB. |
50
+ | `OWNER_NAME` | No | — | Owner's display name. When set, ensures the owner's `_entity` profile record exists. |
51
+ | `OWNER_HANDLE` | No | — | Owner's handle. Only used alongside `OWNER_NAME`. |
52
+ | `DB_PATH` | No | `/app/data/stack.db` (Docker) / `./stack.db` | Path to the SQLite database file. |
53
+ | `PORT` | No | `3000` | Port to listen on. |
54
+ | `TIMEZONE` | No | — (none) | IANA timezone. Only used on first run; omitted from discovery when unset. |
55
+ | `CORS_ORIGINS` | No | `` (none) | Allowed origins, comma-separated or `*`. |
56
+ | `BASE_URL` | Yes | — | Canonical public base URL of this server. Signed/verified against by the DID challenge-response handshake — see [API reference](./docs/api.md#authentication). |
57
+ | `MAX_ATTACHMENT_BYTES` | No | `52428800` (50 MB) | Maximum attachment upload size. |
58
+ | `MAX_CONTENT_BYTES` | No | `1048576` (1 MB) | Maximum JSON request body size (records, patches, etc). See [Deployment: request body size limits](./docs/deployment.md#request-body-size-limits). |
59
+ | `QUERY_TIMEOUT_MS` | No | `10000` (10s) | Execution deadline for a `GET /records` or `POST /records/query` search, timed from when it reaches a worker. Exceeding it answers `503` (code `timeout`). See [Deployment: bounding query cost](./docs/deployment.md#bounding-query-cost). |
60
+ | `QUERY_WORKER_POOL_SIZE` | No | `2` | Number of worker threads a slow search can run on without blocking other requests (max 32). See [Deployment: bounding query cost](./docs/deployment.md#bounding-query-cost). |
61
+ | `QUERY_QUEUE_LIMIT` | No | `64` | Searches allowed to queue for a worker before the server sheds load with `503` (code `timeout`). See [Deployment: bounding query cost](./docs/deployment.md#bounding-query-cost). |
62
+ | `SEED_COMMONS_TYPES` | No | `false` | Registers the [Schema Commons](https://github.com/haverstack/core/blob/main/docs/commons/README.md) types from `@haverstack/commons` on startup. See [Deployment: Schema Commons seeding](./docs/deployment.md#schema-commons-seeding). |
63
+ | `SHUTDOWN_TIMEOUT_MS` | No | `10000` (10s) | How long shutdown waits for open connections to drain before forcing them closed and finishing cleanup anyway. |
64
+
65
+ ---
66
+
67
+ ## Development
68
+
69
+ ```sh
70
+ pnpm install
71
+ pnpm dev # Start with live reload
72
+ pnpm test # Run tests
73
+ pnpm typecheck # Type check
74
+ pnpm lint # Lint
75
+ pnpm format:check # Check formatting
76
+ ```
77
+
78
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full pre-push checklist, where to file what, and the comment, commit, and architecture conventions this codebase follows. [AGENTS.md](./AGENTS.md) is the same material in condensed form, for AI coding agents.
79
+
80
+ Releases are automatic: [Conventional Commits](https://www.conventionalcommits.org/) since the last tag decide the version bump, and every push to `main` bumps `package.json`, publishes the `ghcr.io/haverstack/server` image, tags, and cuts a GitHub release. See [CONTRIBUTING.md § Releasing](./CONTRIBUTING.md#releasing).
81
+
82
+ ---
83
+
84
+ ## Docs
85
+
86
+ - [API reference](./docs/api.md) — routes, auth, and permissions
87
+ - [Deployment guide](./docs/deployment.md) — TLS, CORS, and rate limiting
88
+ - [Contributing](./CONTRIBUTING.md) — setup, conventions, and releases
89
+ - [Security policy](./SECURITY.md) — reporting, scope, and documented non-vulnerabilities
90
+
91
+ ---
92
+
93
+ ## Security
94
+
95
+ Found a weakness? Report it privately through [GitHub Security Advisories](https://github.com/haverstack/server/security/advisories/new). [SECURITY.md](./SECURITY.md) says what's in scope, which properties are documented decisions rather than bugs, and when a finding belongs in [`haverstack/core`](https://github.com/haverstack/core) instead — which is where to send anything you can't confidently place.
96
+
97
+ ---
98
+
99
+ ## Related
100
+
101
+ - [`haverstack/core`](https://github.com/haverstack/core) — core library, types, adapters, and spec
102
+
103
+ ---
104
+
105
+ ## License
106
+
107
+ [CC0 1.0 Universal](./LICENSE) — public domain. No rights reserved.
package/SECURITY.md ADDED
@@ -0,0 +1,54 @@
1
+ # Security Policy
2
+
3
+ This server holds someone's personal data and the tokens that speak for them. It is also the only place the Haverstack permission model is actually enforced — everything behind it is full trust. This document says what to do when you find a weakness, which repository to report it to, and which parts of the system carry a security promise at all.
4
+
5
+ ## Reporting a vulnerability
6
+
7
+ **Report privately, through [GitHub Security Advisories](https://github.com/haverstack/server/security/advisories/new).** That opens a channel visible only to you and the maintainers. Please don't open a public issue or PR for a suspected vulnerability — a public report is a disclosure, and it lands before there's anything to upgrade to.
8
+
9
+ Include what you have: the version or commit (`GET /.well-known/stack` and the image tag both name it), the request that triggers it, what an attacker gets, and the smallest reproduction you can manage. A failing test against `tests/routes/` is the most useful form, but prose is fine — a clear description of the wrong behavior beats a polished exploit.
10
+
11
+ Expect an acknowledgement within a week. If a report is valid, you'll get the fix plan and the release it lands in; if it isn't, you'll get the reasoning, which is often the more interesting answer. Credit in the advisory unless you'd rather not be named.
12
+
13
+ Nothing here is a bug bounty — this is a public-domain project with no budget behind it.
14
+
15
+ ## Which repository
16
+
17
+ This repository is one implementation of a contract that lives elsewhere. The split matters for a report, because a fix in the wrong place leaves every other implementation exposed:
18
+
19
+ - **Here** — a finding that is this server's alone: a route that enforces a permission incorrectly, a token that outlives its revocation, a header this server sets, a config default that is unsafe, something in the published Docker image.
20
+ - **[`haverstack/core`](https://github.com/haverstack/core/security/advisories/new)** — a finding in the contract or the libraries: the access-control model itself, DID verification, the wire format, the error taxonomy, or anything in `@haverstack/core`, `@haverstack/adapter-local`, or `@haverstack/wire-types`. A bug there is a bug in every server and client at once.
21
+
22
+ **When it isn't clear which side a finding sits on, report it to core** and say so. A weakness that reaches through this server into the permission model, a wire shape, or a signature check is nearly always the more serious of the two readings, and core is where the people who can adjudicate that are looking. The reverse mistake — filing a protocol-level flaw against one implementation — gets it fixed in one place and quietly leaves it standing everywhere else. Cross-repository advisories are routine; nobody will mind the reroute.
23
+
24
+ ## What is in scope
25
+
26
+ The server in this repository, its default configuration (`.env.example`), and the `ghcr.io/haverstack/server` image built from it.
27
+
28
+ **Especially interesting**, because they're where a mistake is least visible:
29
+
30
+ - Any request that reads or writes a record the [permission rules](./docs/api.md#permissions) say it cannot — particularly through delegation, where effective authority is the intersection of the principal's and the subject's, or through a query filter that leaks a record the same caller couldn't fetch by id.
31
+ - A response that discloses whether a record exists to a caller who couldn't read it — a `404` and a `403` that differ where they shouldn't, or a total count that changes.
32
+ - Anything that lets a bearer token outlive its revocation or expiry, cross between principal and subject, or be minted for a DID the caller doesn't hold the key for: a challenge redeemable more than once, a nonce accepted after its window, a signature that verifies against a different `BASE_URL`.
33
+ - An attachment served with a `Content-Type` or `Content-Disposition` that lets a browser execute it, or an attachment path that escapes its blob directory.
34
+ - Input that reaches SQL as syntax rather than as a bound parameter, or that escapes the [error taxonomy](./docs/api.md#auth-errors) as a raw engine error with a stack trace or a filesystem path in it.
35
+ - Anything that lets an unauthenticated caller consume resources without bound in a way the [query worker pool](./docs/deployment.md#bounding-query-cost) doesn't already cap — the deadline and queue limit are the intended bound, so a way around _them_ is a finding.
36
+ - Secrets in the wrong place: `OWNER_TOKEN` or a session token in a log line, an error body, or an image layer.
37
+
38
+ ## What is not a vulnerability
39
+
40
+ These are documented properties, not oversights. Each is a design decision with its reasoning in the docs; argue with the decision by opening an issue, not an advisory.
41
+
42
+ - **The DID handshake is open.** `POST /auth/challenge` and `POST /auth/token` are unauthenticated: anyone reachable can generate a keypair and obtain a token for the `did:key` it derives. That is the point — the handshake proves _which_ DID is calling and authorizes nothing on its own. See [Deployment § The DID handshake is open by design](./docs/deployment.md#the-did-handshake-is-open-by-design).
43
+ - **A grant with no grantee is a grant to the public**, and with the handshake open that means anyone. It is the documented meaning of `grant(null, ...)`, not a bypass. See [API § Permissions](./docs/api.md#permissions).
44
+ - **`GET /.well-known/stack` is public** and exposes the owner entity ID, timezone, and capability list. Clients need it to bootstrap. A private stack restricts it by network policy, not by auth. See [Deployment § Public endpoints](./docs/deployment.md#public-endpoints).
45
+ - **Read routes serve anonymous callers.** `GET /records`, `POST /records/query`, `GET /records/:id`, and `GET /types` answer without a token, subject to record-level permissions. A token changes who you are, not whether the route responds.
46
+ - **Rate limiting and abuse control belong to the reverse proxy.** The server bounds query _cost_ (`QUERY_TIMEOUT_MS`, `QUERY_WORKER_POOL_SIZE`, `QUERY_QUEUE_LIMIT`) and body _size_ (`MAX_CONTENT_BYTES`, `MAX_ATTACHMENT_BYTES`), and deliberately does not implement request rate limiting. A deployment without a proxy limit is a misconfiguration; the guide gives Caddy, nginx, and Traefik configs. See [Deployment](./docs/deployment.md).
47
+ - **TLS terminates upstream.** The server speaks plain HTTP and expects a proxy in front. Running it directly on a public port is a deployment error.
48
+ - **`OWNER_TOKEN` is a shared secret with full owner authority** and no expiry. Treat it like a password; it is break-glass access, not a session.
49
+ - **One process per `DB_PATH`.** Permissions exist only behind this server process, so a second writer would be a second, unenforced trust boundary — which is why starting one fails loudly. Direct access to `stack.db` by anything else is full trust by construction. See [Deployment § Single-writer topology](./docs/deployment.md#single-writer-topology).
50
+ - **A lost `did:key` private key is unrecoverable**, and key rotation is deferred. That is a protocol property; see core's [Identity](https://github.com/haverstack/core/blob/main/docs/spec/identity.md) spec.
51
+
52
+ ## Supported versions
53
+
54
+ This server is `0.x` and its API is unstable. Fixes land on `main` and ship in the next release, as a tag and a `ghcr.io/haverstack/server` image; there are no maintained release branches and no backports to older minors. `:latest` tracks the newest release. The practical advice is to track it, and to pin a digest only if you also watch for advisories.