@haverstack/server 0.8.0 → 0.9.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 (197) hide show
  1. package/dist/app.d.ts +8 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +70 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/config.d.ts +21 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +115 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +12 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/changeStreams.d.ts +17 -0
  14. package/dist/lib/changeStreams.d.ts.map +1 -0
  15. package/{src/lib/changeStreams.ts → dist/lib/changeStreams.js} +12 -12
  16. package/dist/lib/changeStreams.js.map +1 -0
  17. package/dist/lib/frameGate.d.ts +26 -0
  18. package/dist/lib/frameGate.d.ts.map +1 -0
  19. package/dist/lib/frameGate.js +44 -0
  20. package/dist/lib/frameGate.js.map +1 -0
  21. package/dist/lib/json.d.ts +10 -0
  22. package/dist/lib/json.d.ts.map +1 -0
  23. package/dist/lib/json.js +27 -0
  24. package/dist/lib/json.js.map +1 -0
  25. package/dist/lib/nonceStore.d.ts +50 -0
  26. package/dist/lib/nonceStore.d.ts.map +1 -0
  27. package/dist/lib/nonceStore.js +96 -0
  28. package/dist/lib/nonceStore.js.map +1 -0
  29. package/dist/lib/queryLimit.d.ts +4 -0
  30. package/dist/lib/queryLimit.d.ts.map +1 -0
  31. package/{src/lib/queryLimit.ts → dist/lib/queryLimit.js} +5 -6
  32. package/dist/lib/queryLimit.js.map +1 -0
  33. package/dist/lib/queryWorker/pool.d.ts +60 -0
  34. package/dist/lib/queryWorker/pool.d.ts.map +1 -0
  35. package/dist/lib/queryWorker/pool.js +278 -0
  36. package/dist/lib/queryWorker/pool.js.map +1 -0
  37. package/{src/lib/queryWorker/protocol.ts → dist/lib/queryWorker/protocol.d.ts} +21 -18
  38. package/dist/lib/queryWorker/protocol.d.ts.map +1 -0
  39. package/dist/lib/queryWorker/protocol.js +2 -0
  40. package/dist/lib/queryWorker/protocol.js.map +1 -0
  41. package/dist/lib/queryWorker/worker.d.ts +2 -0
  42. package/dist/lib/queryWorker/worker.d.ts.map +1 -0
  43. package/{src/lib/queryWorker/worker.ts → dist/lib/queryWorker/worker.js} +19 -22
  44. package/dist/lib/queryWorker/worker.js.map +1 -0
  45. package/dist/lib/resumeBuffer.d.ts +143 -0
  46. package/dist/lib/resumeBuffer.d.ts.map +1 -0
  47. package/dist/lib/resumeBuffer.js +216 -0
  48. package/dist/lib/resumeBuffer.js.map +1 -0
  49. package/dist/lib/resumeCursor.d.ts +16 -0
  50. package/dist/lib/resumeCursor.d.ts.map +1 -0
  51. package/{src/lib/resumeCursor.ts → dist/lib/resumeCursor.js} +22 -24
  52. package/dist/lib/resumeCursor.js.map +1 -0
  53. package/dist/lib/tokenSweep.d.ts +17 -0
  54. package/dist/lib/tokenSweep.d.ts.map +1 -0
  55. package/dist/lib/tokenSweep.js +40 -0
  56. package/dist/lib/tokenSweep.js.map +1 -0
  57. package/dist/main.d.ts +2 -0
  58. package/dist/main.d.ts.map +1 -0
  59. package/dist/main.js +46 -0
  60. package/dist/main.js.map +1 -0
  61. package/dist/middleware/auth.d.ts +25 -0
  62. package/dist/middleware/auth.d.ts.map +1 -0
  63. package/dist/middleware/auth.js +74 -0
  64. package/dist/middleware/auth.js.map +1 -0
  65. package/dist/middleware/errors.d.ts +17 -0
  66. package/dist/middleware/errors.d.ts.map +1 -0
  67. package/dist/middleware/errors.js +64 -0
  68. package/dist/middleware/errors.js.map +1 -0
  69. package/dist/routes/attachments.d.ts +5 -0
  70. package/dist/routes/attachments.d.ts.map +1 -0
  71. package/dist/routes/attachments.js +104 -0
  72. package/dist/routes/attachments.js.map +1 -0
  73. package/dist/routes/auth.d.ts +6 -0
  74. package/dist/routes/auth.d.ts.map +1 -0
  75. package/dist/routes/auth.js +110 -0
  76. package/dist/routes/auth.js.map +1 -0
  77. package/dist/routes/changes.d.ts +22 -0
  78. package/dist/routes/changes.d.ts.map +1 -0
  79. package/dist/routes/changes.js +233 -0
  80. package/dist/routes/changes.js.map +1 -0
  81. package/dist/routes/entity.d.ts +5 -0
  82. package/dist/routes/entity.d.ts.map +1 -0
  83. package/dist/routes/entity.js +30 -0
  84. package/dist/routes/entity.js.map +1 -0
  85. package/dist/routes/health.d.ts +4 -0
  86. package/dist/routes/health.d.ts.map +1 -0
  87. package/dist/routes/health.js +7 -0
  88. package/dist/routes/health.js.map +1 -0
  89. package/dist/routes/records.d.ts +5 -0
  90. package/dist/routes/records.d.ts.map +1 -0
  91. package/dist/routes/records.js +231 -0
  92. package/dist/routes/records.js.map +1 -0
  93. package/dist/routes/tokens.d.ts +5 -0
  94. package/dist/routes/tokens.d.ts.map +1 -0
  95. package/dist/routes/tokens.js +65 -0
  96. package/dist/routes/tokens.js.map +1 -0
  97. package/dist/routes/types.d.ts +5 -0
  98. package/dist/routes/types.d.ts.map +1 -0
  99. package/dist/routes/types.js +46 -0
  100. package/dist/routes/types.js.map +1 -0
  101. package/dist/routes/wellknown.d.ts +6 -0
  102. package/dist/routes/wellknown.d.ts.map +1 -0
  103. package/dist/routes/wellknown.js +45 -0
  104. package/dist/routes/wellknown.js.map +1 -0
  105. package/dist/shutdown.d.ts +23 -0
  106. package/dist/shutdown.d.ts.map +1 -0
  107. package/dist/shutdown.js +32 -0
  108. package/dist/shutdown.js.map +1 -0
  109. package/dist/stack.d.ts +20 -0
  110. package/dist/stack.d.ts.map +1 -0
  111. package/dist/stack.js +50 -0
  112. package/dist/stack.js.map +1 -0
  113. package/dist/testing.d.ts +49 -0
  114. package/dist/testing.d.ts.map +1 -0
  115. package/dist/testing.js +130 -0
  116. package/dist/testing.js.map +1 -0
  117. package/{src/types.ts → dist/types.d.ts} +5 -5
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/{src/wireError.ts → dist/wireError.d.ts} +2 -9
  122. package/dist/wireError.d.ts.map +1 -0
  123. package/dist/wireError.js +13 -0
  124. package/dist/wireError.js.map +1 -0
  125. package/package.json +37 -12
  126. package/.claude/settings.local.json +0 -22
  127. package/.dockerignore +0 -10
  128. package/.env.example +0 -90
  129. package/.github/pull_request_template.md +0 -23
  130. package/.github/workflows/ci.yml +0 -55
  131. package/.github/workflows/release.yml +0 -99
  132. package/.prettierignore +0 -3
  133. package/.prettierrc +0 -7
  134. package/AGENTS.md +0 -91
  135. package/CONTRIBUTING.md +0 -234
  136. package/Dockerfile +0 -30
  137. package/SECURITY.md +0 -54
  138. package/docs/api.md +0 -277
  139. package/docs/deployment.md +0 -212
  140. package/eslint.config.mjs +0 -18
  141. package/pnpm-workspace.yaml +0 -13
  142. package/src/app.ts +0 -86
  143. package/src/config.ts +0 -176
  144. package/src/index.ts +0 -54
  145. package/src/lib/frameGate.ts +0 -42
  146. package/src/lib/json.ts +0 -27
  147. package/src/lib/nonceStore.ts +0 -103
  148. package/src/lib/queryWorker/pool.ts +0 -324
  149. package/src/lib/resumeBuffer.ts +0 -268
  150. package/src/lib/tokenSweep.ts +0 -43
  151. package/src/middleware/auth.ts +0 -77
  152. package/src/middleware/errors.ts +0 -73
  153. package/src/routes/attachments.ts +0 -126
  154. package/src/routes/auth.ts +0 -137
  155. package/src/routes/changes.ts +0 -279
  156. package/src/routes/entity.ts +0 -33
  157. package/src/routes/health.ts +0 -8
  158. package/src/routes/records.ts +0 -259
  159. package/src/routes/tokens.ts +0 -85
  160. package/src/routes/types.ts +0 -57
  161. package/src/routes/wellknown.ts +0 -55
  162. package/src/shutdown.ts +0 -56
  163. package/src/stack.ts +0 -96
  164. package/tests/app.test.ts +0 -86
  165. package/tests/changeFeedClient.ts +0 -156
  166. package/tests/config.test.ts +0 -112
  167. package/tests/conformance.test.ts +0 -2322
  168. package/tests/fixtures/blockingWorker.ts +0 -27
  169. package/tests/fixtures/brokenWorker.ts +0 -7
  170. package/tests/lib/changeFeedClient.test.ts +0 -126
  171. package/tests/lib/frameGate.test.ts +0 -93
  172. package/tests/lib/queryLimit.test.ts +0 -33
  173. package/tests/lib/queryWorker.test.ts +0 -60
  174. package/tests/lib/queryWorkerPool.test.ts +0 -129
  175. package/tests/lib/resumeBuffer.test.ts +0 -341
  176. package/tests/lib/resumeCursor.test.ts +0 -46
  177. package/tests/middleware/errors.test.ts +0 -176
  178. package/tests/routes/associations.test.ts +0 -170
  179. package/tests/routes/attachments.test.ts +0 -621
  180. package/tests/routes/auth.test.ts +0 -448
  181. package/tests/routes/changes.test.ts +0 -567
  182. package/tests/routes/contentQuery.test.ts +0 -68
  183. package/tests/routes/entity.test.ts +0 -170
  184. package/tests/routes/groups.test.ts +0 -69
  185. package/tests/routes/records.test.ts +0 -1965
  186. package/tests/routes/relatedToQuery.test.ts +0 -182
  187. package/tests/routes/tokens.test.ts +0 -157
  188. package/tests/routes/tombstones.test.ts +0 -93
  189. package/tests/routes/types.test.ts +0 -129
  190. package/tests/routes/versions.test.ts +0 -177
  191. package/tests/routes/wellknown.test.ts +0 -71
  192. package/tests/setup.ts +0 -154
  193. package/tests/shutdown.test.ts +0 -121
  194. package/tests/stack.test.ts +0 -198
  195. package/tsconfig.build.json +0 -9
  196. package/tsconfig.json +0 -19
  197. package/vitest.config.ts +0 -12
@@ -0,0 +1,64 @@
1
+ import { StackPermissionError, StackNotFoundError } from '@haverstack/core';
2
+ import { serializeError } from '@haverstack/wire-types';
3
+ /**
4
+ * Central error handler. `serializeError()` covers every core `StackError`
5
+ * (thrown directly by routes or surfaced from `Stack`/`ScopedStack`) with
6
+ * the wire-format's typed `{ error: { code, message } }` body. Anything
7
+ * else is an unanticipated bug: log it and return a bodyless 500 rather
8
+ * than guessing at a taxonomy it doesn't belong to.
9
+ *
10
+ * `stack` is the unscoped root Stack, used only to probe existence for the
11
+ * refusal log below — never to read content or bypass a permission check
12
+ * for the response itself.
13
+ */
14
+ export function errorMiddleware(logger, stack) {
15
+ return async (err, c) => {
16
+ const wire = serializeError(err);
17
+ if (wire) {
18
+ // A denial with a resolved auth is a verified DID that lacked the
19
+ // grant, which docs/spec/identity.md asks be logged. Anonymous
20
+ // denials are bodyless 401s from wireError() and never reach here.
21
+ const auth = c.get('auth');
22
+ if (err instanceof StackPermissionError && auth) {
23
+ logger.warn({
24
+ requestId: c.get('requestId'),
25
+ principalId: auth.principalId,
26
+ subjectId: auth.subjectId,
27
+ }, 'Denied a verified requester');
28
+ }
29
+ // On the wire a refused record is indistinguishable from a missing
30
+ // one (docs/spec/wire-format.md § Server implementation checklist),
31
+ // but the operator is not the adversary: an unscoped probe, on a path
32
+ // already failing, recovers the distinction for the log alone. Debug
33
+ // rather than warn — this is the sharing graph, which the spec asks
34
+ // stay out of a general-purpose aggregator by default.
35
+ if (err instanceof StackNotFoundError && auth) {
36
+ const recordId = c.req.param('id');
37
+ if (recordId) {
38
+ const existing = await stack.get(recordId);
39
+ logger.debug({
40
+ requestId: c.get('requestId'),
41
+ principalId: auth.principalId,
42
+ subjectId: auth.subjectId,
43
+ recordId,
44
+ existed: existing !== null,
45
+ // Always 'read': ScopedStack collapses to StackNotFoundError
46
+ // exactly when canRead() is false, whatever verb was asked.
47
+ check: 'read',
48
+ }, 'Refused a verified requester a 404');
49
+ }
50
+ }
51
+ // A 401 or 403 to an anonymous caller would confirm the record
52
+ // exists to someone who presented no credential, so a private record
53
+ // and a missing one answer alike. `Bearer` (RFC 6750 §3) — not the
54
+ // `did-challenge` exchange discovery advertises for obtaining a
55
+ // token — keeps the login prompt reachable without reopening that.
56
+ if (wire.status === 404 && !auth)
57
+ c.header('WWW-Authenticate', 'Bearer');
58
+ return c.json(wire.body, wire.status);
59
+ }
60
+ logger.error({ err, requestId: c.get('requestId') }, 'Unhandled request error');
61
+ return c.json({ error: { code: 'internal', message: 'Internal server error' } }, 500);
62
+ };
63
+ }
64
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/middleware/errors.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAc,MAAM,kBAAkB,CAAC;AAExF,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,KAAY;IAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE;QACtB,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,IAAI,EAAE,CAAC;YACT,kEAAkE;YAClE,+DAA+D;YAC/D,mEAAmE;YACnE,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC3B,IAAI,GAAG,YAAY,oBAAoB,IAAI,IAAI,EAAE,CAAC;gBAChD,MAAM,CAAC,IAAI,CACT;oBACE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;oBAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;oBAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;iBAC1B,EACD,6BAA6B,CAC9B,CAAC;YACJ,CAAC;YACD,mEAAmE;YACnE,oEAAoE;YACpE,sEAAsE;YACtE,qEAAqE;YACrE,oEAAoE;YACpE,uDAAuD;YACvD,IAAI,GAAG,YAAY,kBAAkB,IAAI,IAAI,EAAE,CAAC;gBAC9C,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACnC,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;oBAC3C,MAAM,CAAC,KAAK,CACV;wBACE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;wBAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;wBAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;wBACzB,QAAQ;wBACR,OAAO,EAAE,QAAQ,KAAK,IAAI;wBAC1B,6DAA6D;wBAC7D,4DAA4D;wBAC5D,KAAK,EAAE,MAAM;qBACd,EACD,oCAAoC,CACrC,CAAC;gBACJ,CAAC;YACH,CAAC;YACD,+DAA+D;YAC/D,qEAAqE;YACrE,mEAAmE;YACnE,gEAAgE;YAChE,mEAAmE;YACnE,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI;gBAAE,CAAC,CAAC,MAAM,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;YACzE,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAA8B,CAAC,CAAC;QAChE,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,yBAAyB,CAAC,CAAC;QAChF,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,uBAAuB,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IACxF,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { Hono } from 'hono';
2
+ import type { AppEnv } from '../types.js';
3
+ import type { StackContext } from '../stack.js';
4
+ export declare function attachmentRoutes(ctx: StackContext, maxAttachmentBytes: number): Hono<AppEnv>;
5
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../../src/routes/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CA8G5F"}
@@ -0,0 +1,104 @@
1
+ import { Hono } from 'hono';
2
+ import { bodyLimit } from 'hono/body-limit';
3
+ import { StackPermissionError, StackPayloadTooLargeError } from '@haverstack/core';
4
+ import { resolveAttachmentDownloadContentType, firstRecordedAttachment, parseUploadFilename, NOSNIFF_HEADER_NAME, NOSNIFF_HEADER_VALUE, } from '@haverstack/core/wire';
5
+ import { serializeRecord } from '@haverstack/wire-types';
6
+ import { requireAuth, requireOwner } from '../middleware/auth.js';
7
+ export function attachmentRoutes(ctx, maxAttachmentBytes) {
8
+ const app = new Hono();
9
+ const { stack } = ctx;
10
+ const { ownerEntityId } = stack;
11
+ // Aborts while reading the body, the moment the byte count passes the
12
+ // limit — no full-body buffer for an oversized upload, chunked or not.
13
+ const attachmentBodyLimit = bodyLimit({
14
+ maxSize: maxAttachmentBytes,
15
+ onError: () => {
16
+ throw new StackPayloadTooLargeError(`Attachment exceeds the ${maxAttachmentBytes}-byte limit`);
17
+ },
18
+ });
19
+ // POST /attachments — stores the bytes and creates the _attachment@1
20
+ // metadata record in the same request, returning that record. Routed
21
+ // through ScopedStack.putAttachment() so the create-grant check on
22
+ // _attachment@1 runs before a single byte is written: an authenticated
23
+ // requester with no grant is refused, not merely denied a metadata
24
+ // record afterward. See docs/spec/wire-format.md § Upload.
25
+ app.post('/', attachmentBodyLimit, requireAuth(), async (c) => {
26
+ const auth = c.get('auth');
27
+ const mimeType = c.req.header('Content-Type') || 'application/octet-stream';
28
+ const filename = parseUploadFilename(c.req.header('Content-Disposition'));
29
+ const appId = c.req.query('appId') || undefined;
30
+ const data = new Uint8Array(await c.req.arrayBuffer());
31
+ const record = await stack.forSession(auth).putAttachment(data, mimeType, filename, appId);
32
+ return c.json(serializeRecord(record), 200);
33
+ });
34
+ // GET /attachments/:fileId — download
35
+ app.get('/:fileId', async (c) => {
36
+ const fileId = c.req.param('fileId');
37
+ const auth = c.get('auth');
38
+ let data;
39
+ try {
40
+ data = await (auth ? stack.forSession(auth) : stack.asEntity(null)).getAttachment(fileId);
41
+ }
42
+ catch (e) {
43
+ // Anonymous and denied is a transport-auth failure, distinct from an
44
+ // authenticated requester lacking access; everything else belongs to
45
+ // errorMiddleware. ScopedStack.getAttachment() already throws alike
46
+ // for missing and forbidden, so this handler owes no anti-oracle
47
+ // logic of its own — only to leave that answer intact.
48
+ if (!auth && e instanceof StackPermissionError) {
49
+ return c.json({ error: 'Unauthorized' }, 401);
50
+ }
51
+ throw e;
52
+ }
53
+ const metaRecords = await stack.getAttachmentRecords(fileId);
54
+ const firstRecord = firstRecordedAttachment(metaRecords);
55
+ const ownRecord = auth
56
+ ? firstRecordedAttachment(metaRecords.filter((r) => r.entityId === auth.subjectId))
57
+ : undefined;
58
+ const contentTypeParam = c.req.query('contentType');
59
+ const filenameParam = c.req.query('filename');
60
+ const { contentType, forced } = resolveAttachmentDownloadContentType({
61
+ contentTypeParam,
62
+ filenameParam,
63
+ storedMimeType: firstRecord?.content.mimeType,
64
+ });
65
+ // When the dangerous-type policy overrides the candidate, the filename
66
+ // goes too: forcing Content-Type alone won't stop a browser sniffing
67
+ // the body back into the original type without both nosniff and a
68
+ // non-inline disposition.
69
+ const filename = forced
70
+ ? undefined
71
+ : (filenameParam ?? ownRecord?.content.filename ?? firstRecord?.content.filename);
72
+ const disposition = filename
73
+ ? `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`
74
+ : 'attachment';
75
+ return c.newResponse(data, 200, {
76
+ 'Content-Type': contentType,
77
+ 'Content-Length': String(data.byteLength),
78
+ 'Content-Disposition': disposition,
79
+ [NOSNIFF_HEADER_NAME]: NOSNIFF_HEADER_VALUE,
80
+ });
81
+ });
82
+ // DELETE /attachments/:fileId
83
+ app.delete('/:fileId', requireOwner(ownerEntityId), async (c) => {
84
+ const fileId = c.req.param('fileId');
85
+ await stack.deleteAttachment(fileId);
86
+ return c.body(null, 204);
87
+ });
88
+ // POST /attachments/gc — sweep and delete orphaned attachment bytes.
89
+ // Owner-only, invoke-only (no built-in scheduling): dryRun makes a
90
+ // cron-from-outside workflow safe. Body is entirely optional — every
91
+ // field defaults inside ScopedStack.collectAttachmentGarbage().
92
+ app.post('/gc', requireOwner(ownerEntityId), async (c) => {
93
+ const auth = c.get('auth');
94
+ const raw = await c.req.text();
95
+ const body = raw ? JSON.parse(raw) : {};
96
+ const result = await stack.forSession(auth).collectAttachmentGarbage({
97
+ ...(typeof body.graceMs === 'number' && { graceMs: body.graceMs }),
98
+ ...(body.dryRun === true && { dryRun: true }),
99
+ });
100
+ return c.json(result, 200);
101
+ });
102
+ return app;
103
+ }
104
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/routes/attachments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { Hono } from 'hono';
2
+ import type { Logger } from 'pino';
3
+ import type { AppEnv } from '../types.js';
4
+ import type { StackContext } from '../stack.js';
5
+ export declare function authRoutes(ctx: StackContext, authOrigin: string, logger: Logger): Hono<AppEnv>;
6
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAa5B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0BhD,wBAAgB,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CA+F9F"}
@@ -0,0 +1,110 @@
1
+ import { Hono } from 'hono';
2
+ import { verifyAuthChallenge, base64urlDecode } from '@haverstack/core/wire';
3
+ import { isValidDidKey } from '@haverstack/core/did';
4
+ import { WIRE_AUTH_ERROR_STATUS } from '@haverstack/wire-types';
5
+ import { readJson } from '../lib/json.js';
6
+ import { createExpiredTokenSweeper } from '../lib/tokenSweep.js';
7
+ // The handshake never issues a delegated token: proving key possession
8
+ // proves the principal and nothing about whom it may act for. It gets a
9
+ // default TTL rather than the owner's choice of one, because a token
10
+ // anyone can mint for themselves owes a bounded lifetime.
11
+ const AUTH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
12
+ // Stamped on every handshake-issued token. GET /tokens is the owner's only
13
+ // view of what exists, and without this a self-service token is
14
+ // indistinguishable from one the owner minted deliberately — so "revoke
15
+ // everything the handshake handed out" would be an unanswerable question.
16
+ const AUTH_TOKEN_LABEL = 'did-challenge';
17
+ /**
18
+ * `{ error: { code, message } }` for the four handshake-specific codes,
19
+ * deliberately outside WireErrorCode — no Stack operation has begun at
20
+ * this point, so none of these maps to a StackError. See
21
+ * docs/spec/wire-format.md § Authentication.
22
+ */
23
+ function authError(c, code, message) {
24
+ return c.json({ error: { code, message } }, WIRE_AUTH_ERROR_STATUS[code]);
25
+ }
26
+ export function authRoutes(ctx, authOrigin, logger) {
27
+ const app = new Hono();
28
+ const sweepExpiredTokens = createExpiredTokenSweeper(ctx.tokens, logger);
29
+ // POST /auth/challenge — issues a nonce bound to the requested DID.
30
+ // Unauthenticated: this is how a token is earned in the first place.
31
+ app.post('/challenge', async (c) => {
32
+ const body = await readJson(c);
33
+ if (typeof body.did !== 'string' || !isValidDidKey(body.did)) {
34
+ return authError(c, 'invalid_did', 'Not a valid did:key');
35
+ }
36
+ const { nonce, expiresAt } = ctx.nonces.issue(body.did);
37
+ const response = { nonce, expiresAt: expiresAt.toISOString() };
38
+ return c.json(response);
39
+ });
40
+ // POST /auth/token — redeems a signed nonce for a bearer token.
41
+ // Unauthenticated for the same reason.
42
+ app.post('/token', async (c) => {
43
+ const body = await readJson(c);
44
+ if (typeof body.did !== 'string' || !isValidDidKey(body.did)) {
45
+ return authError(c, 'invalid_did', 'Not a valid did:key');
46
+ }
47
+ // Checked ahead of the signature so a nonce issued to a different DID
48
+ // is refused as unknown_nonce even when the caller's own signature over
49
+ // it is perfectly valid — the nonce belongs to the DID it was issued
50
+ // for, not to whoever can produce a valid signature.
51
+ if (typeof body.nonce !== 'string') {
52
+ return authError(c, 'unknown_nonce', 'Unknown or already-used nonce');
53
+ }
54
+ if (typeof body.signature !== 'string') {
55
+ return authError(c, 'invalid_signature', 'Signature does not verify');
56
+ }
57
+ // Spent on this call regardless of what happens next — single-use
58
+ // means a redemption attempt consumes the nonce, not merely a
59
+ // successful one.
60
+ const result = ctx.nonces.redeem(body.did, body.nonce);
61
+ if (result === 'unknown')
62
+ return authError(c, 'unknown_nonce', 'Unknown or already-used nonce');
63
+ if (result === 'expired')
64
+ return authError(c, 'expired_nonce', 'Challenge has expired');
65
+ let signatureBytes;
66
+ try {
67
+ signatureBytes = base64urlDecode(body.signature);
68
+ }
69
+ catch {
70
+ return authError(c, 'invalid_signature', 'Signature does not verify');
71
+ }
72
+ // Built from our own configured public origin (authOrigin), never from
73
+ // a request header — Host/X-Forwarded-Host are client-controlled, and
74
+ // deriving the signed origin from either would let a client choose
75
+ // which origin it signs for, silently reopening the relay attack the
76
+ // origin binding exists to prevent.
77
+ const verified = await verifyAuthChallenge({ origin: authOrigin, did: body.did, nonce: body.nonce }, signatureBytes).catch(() => false);
78
+ if (!verified)
79
+ return authError(c, 'invalid_signature', 'Signature does not verify');
80
+ // Reclaim expired rows before adding one. Throttled internally, so this
81
+ // is a no-op on all but the first handshake of each interval.
82
+ await sweepExpiredTokens();
83
+ // Undelegated by construction: principalId and subjectId are both the
84
+ // proven DID. Never let a client name its own subject.
85
+ const expiresAt = new Date(Date.now() + AUTH_TOKEN_TTL_MS);
86
+ const { token } = await ctx.tokens.createToken(body.did, {
87
+ expiresAt,
88
+ label: AUTH_TOKEN_LABEL,
89
+ });
90
+ // Reported from the value just written rather than read back through
91
+ // listTokens(), an unpaginated scan of every token ever issued — on an
92
+ // unauthenticated route that makes each handshake cost more than the
93
+ // last. The store persists the Date unchanged, so a read-back could
94
+ // only return what is already here.
95
+ const response = {
96
+ token,
97
+ expiresAt: expiresAt.toISOString(),
98
+ principalId: body.did,
99
+ subjectId: body.did,
100
+ };
101
+ // The one place a bearer token comes into existence without the owner
102
+ // doing anything, so it is the one place a log line is the only record
103
+ // that it happened. errorMiddleware already logs denied-but-verified
104
+ // requests; this is its counterpart on the granting side.
105
+ logger.info({ requestId: c.get('requestId'), did: body.did, expiresAt: expiresAt.toISOString() }, 'Issued a token via the DID challenge-response handshake');
106
+ return c.json(response);
107
+ });
108
+ return app;
109
+ }
110
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { Hono } from 'hono';
2
+ import type { AppEnv } from '../types.js';
3
+ import type { StackContext } from '../stack.js';
4
+ import type { Config } from '../config.js';
5
+ import type { Logger } from 'pino';
6
+ export type ChangeRouteOptions = {
7
+ keepaliveMs?: number;
8
+ sessionCheckMs?: number;
9
+ maxPendingFrames?: number;
10
+ /**
11
+ * Whether a presented cursor is honored at all. Default true and never
12
+ * false in production, since discovery advertises resume unconditionally.
13
+ * It exists because `resume: false` is real, spec-defined behavior
14
+ * (`ready` with no `seq`, then `reset` with reason `not_supported`) that
15
+ * a conformance fixture needs a way to reach.
16
+ */
17
+ resume?: boolean;
18
+ resumeBufferDepth?: number;
19
+ resumeRetentionMs?: number;
20
+ };
21
+ export declare function changeRoutes(ctx: StackContext, config: Config, logger: Logger, opts?: ChangeRouteOptions): Hono<AppEnv>;
22
+ //# sourceMappingURL=changes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../../src/routes/changes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAM3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAuBnC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAYF,wBAAgB,YAAY,CAC1B,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,kBAAuB,GAC5B,IAAI,CAAC,MAAM,CAAC,CAqNd"}
@@ -0,0 +1,233 @@
1
+ import { Hono } from 'hono';
2
+ import { streamSSE } from 'hono/streaming';
3
+ import { StackQueryError, StackPermissionError } from '@haverstack/core';
4
+ import { parseChangeParams } from '@haverstack/core/wire';
5
+ import { serializeChange, isValidSeq } from '@haverstack/wire-types';
6
+ import { safeCompare, isOwnerActingAlone } from '../middleware/auth.js';
7
+ import { FrameGate } from '../lib/frameGate.js';
8
+ import { decodeCursor } from '../lib/resumeCursor.js';
9
+ import { ResumeBufferRegistry, resumeBufferKey } from '../lib/resumeBuffer.js';
10
+ // Keepalive cadence, and how often an authenticated session is re-checked
11
+ // against the token store — a stream's authority is fixed at connect, so
12
+ // without the re-check a revoked token keeps delivering for as long as the
13
+ // connection lives. Neither value reaches a client, so neither is an env var.
14
+ const DEFAULT_KEEPALIVE_MS = 15_000;
15
+ const DEFAULT_SESSION_CHECK_MS = 30_000;
16
+ // A client whose in-flight frame count crosses this is closed rather than
17
+ // left to queue: one that can't tell it missed a frame can't repair it
18
+ // either, so dropping silently is worse than disconnecting.
19
+ const DEFAULT_MAX_PENDING_FRAMES = 1000;
20
+ // Per-buffer ring depth and how long a buffer keeps being fed after its
21
+ // last connection disconnects — see src/lib/resumeBuffer.ts. Not part of
22
+ // the wire contract either: a client never learns these numbers, only
23
+ // their consequence (`overflow` or `cursor_expired`).
24
+ const DEFAULT_RESUME_BUFFER_DEPTH = 1000;
25
+ const DEFAULT_RESUME_RETENTION_MS = 5 * 60 * 1000;
26
+ /**
27
+ * Raw cursor text presented on this connection, if any — `Last-Event-ID`
28
+ * takes priority over `?since=` (a browser EventSource-style reconnect
29
+ * sends the header; `?since=` exists for a client whose transport can't
30
+ * set one). Not yet validated or decoded — see isValidSeq()/decodeCursor().
31
+ */
32
+ function presentedCursorRaw(c, url) {
33
+ return c.req.header('Last-Event-ID') ?? url.searchParams.get('since') ?? undefined;
34
+ }
35
+ export function changeRoutes(ctx, config, logger, opts = {}) {
36
+ const app = new Hono();
37
+ const keepaliveMs = opts.keepaliveMs ?? DEFAULT_KEEPALIVE_MS;
38
+ const sessionCheckMs = opts.sessionCheckMs ?? DEFAULT_SESSION_CHECK_MS;
39
+ const maxPendingFrames = opts.maxPendingFrames ?? DEFAULT_MAX_PENDING_FRAMES;
40
+ const resumeEnabled = opts.resume ?? true;
41
+ const resumeBuffers = new ResumeBufferRegistry({
42
+ depth: opts.resumeBufferDepth ?? DEFAULT_RESUME_BUFFER_DEPTH,
43
+ retentionMs: opts.resumeRetentionMs ?? DEFAULT_RESUME_RETENTION_MS,
44
+ });
45
+ /** Scope to a session if authenticated, else the anonymous (public-only) view. */
46
+ function scopeFor(auth) {
47
+ return auth ? ctx.stack.forSession(auth) : ctx.stack.asEntity(null);
48
+ }
49
+ // Single-process only: events exist only in the process owning the
50
+ // stack's storage (docs/spec/wire-format.md § Feed implementation
51
+ // checklist), and query workers only read. A second process subscribing
52
+ // to its own Stack over the same storage would see nothing and look fine
53
+ // in testing — worth knowing before scaling this horizontally.
54
+ app.get('/', async (c) => {
55
+ const url = new URL(c.req.url);
56
+ const auth = c.get('auth');
57
+ const { filter, includeRecords, includeUnlisted } = parseChangeParams(url);
58
+ const presentedRaw = presentedCursorRaw(c, url);
59
+ // Refused before the stream opens: a throw from subscribe() after
60
+ // streaming starts is caught below and merely closes the connection,
61
+ // silently, having already answered 200. includeUnlisted owes a real
62
+ // 403, the same one GET /records answers with.
63
+ if (includeUnlisted && !isOwnerActingAlone(auth, ctx.stack.ownerEntityId)) {
64
+ throw new StackPermissionError('includeUnlisted is owner-only');
65
+ }
66
+ // A charset-invalid cursor is refused rather than treated as a cache
67
+ // miss: no conformant server could have minted it, so there is nothing
68
+ // to reconcile. isValidSeq() is the rule minted cursors meet on the way
69
+ // out, applied here on the way in.
70
+ if (resumeEnabled && presentedRaw !== undefined && !isValidSeq(presentedRaw)) {
71
+ throw new StackQueryError(`Invalid cursor: "${presentedRaw}"`);
72
+ }
73
+ // Never accepted: a bearer token is read only from the Authorization
74
+ // header (authMiddleware), never from a query param — the feed is
75
+ // consumed via fetch with a streaming body precisely so a client never
76
+ // needs to put a credential somewhere a browser might log or cache it.
77
+ const bearerToken = c.req.header('Authorization')?.slice('Bearer '.length);
78
+ return streamSSE(c, async (stream) => {
79
+ let resolveDone;
80
+ const done = new Promise((resolve) => {
81
+ resolveDone = resolve;
82
+ });
83
+ stream.onAbort(() => resolveDone());
84
+ const unregister = ctx.changeStreams.add(() => resolveDone());
85
+ const gate = new FrameGate(maxPendingFrames, () => resolveDone());
86
+ /** Returns false once the gate has tripped — this connection is closing. */
87
+ function send(event, data, id) {
88
+ return gate.send(() => stream.writeSSE({ event, data: JSON.stringify(data), ...(id !== undefined && { id }) }));
89
+ }
90
+ // Assigned inside the try below; torn down in the finally whether the
91
+ // connection ended by abort, by a revoked session, or by a throw. A
92
+ // throw is caught rather than left to hono, which would answer it by
93
+ // writing the raw error message to the client as an `error` frame.
94
+ let unsubscribe;
95
+ let keepalive;
96
+ let sessionCheck;
97
+ try {
98
+ if (!resumeEnabled) {
99
+ // `resume: false` — a discovery-conformant server that mints no
100
+ // cursors at all, reachable only through WellknownRouteOptions'
101
+ // matching test toggle.
102
+ send('ready', {});
103
+ if (presentedRaw !== undefined) {
104
+ const reason = 'not_supported';
105
+ send('reset', { reason });
106
+ }
107
+ unsubscribe = await scopeFor(auth).subscribe((change) => {
108
+ send('record', serializeChange(change));
109
+ }, { filter, includeRecords, includeUnlisted });
110
+ }
111
+ else {
112
+ const key = resumeBufferKey({
113
+ principalId: auth?.principalId ?? null,
114
+ subjectId: auth?.subjectId ?? null,
115
+ filter,
116
+ includeRecords,
117
+ includeUnlisted,
118
+ });
119
+ const scoped = scopeFor(auth);
120
+ const buffer = await resumeBuffers.acquire(key, (onChange) => scoped.subscribe(onChange, { filter, includeRecords, includeUnlisted }));
121
+ // Attached before anything below awaits, so there is no gap
122
+ // between what the backlog snapshot covers and what the live
123
+ // buffer reports. `replaying` holds live arrivals until the
124
+ // permission-rechecking replay finishes, so a change appended
125
+ // mid-replay can't overtake an older, still-pending backlog frame.
126
+ let replaying = true;
127
+ const queued = [];
128
+ const detachLive = buffer.subscribeLive((entry) => {
129
+ if (replaying)
130
+ queued.push(entry);
131
+ else
132
+ send('record', entry.frame, entry.frame.seq);
133
+ });
134
+ // Registered before the first `await` below, so a subscription
135
+ // opened above is always released — including when that await
136
+ // throws. Everything after this point runs under the finally.
137
+ unsubscribe = () => {
138
+ detachLive();
139
+ resumeBuffers.release(key);
140
+ };
141
+ let resetReason;
142
+ let backlog = [];
143
+ if (presentedRaw !== undefined) {
144
+ const decoded = decodeCursor(presentedRaw);
145
+ if (!decoded || decoded.bufferId !== buffer.id) {
146
+ // Unrecognized outright, or names a buffer instance that isn't
147
+ // this key's current one (evicted past its retention window,
148
+ // or minted for a filter this cursor doesn't actually match).
149
+ resetReason = 'cursor_expired';
150
+ }
151
+ else {
152
+ const outcome = buffer.entriesAfter(decoded.n);
153
+ if (outcome.status === 'ok')
154
+ backlog = outcome.entries;
155
+ else
156
+ resetReason = outcome.status;
157
+ }
158
+ }
159
+ send('ready', { seq: buffer.headCursor() });
160
+ if (resetReason) {
161
+ send('reset', { reason: resetReason });
162
+ }
163
+ else {
164
+ for (const entry of backlog) {
165
+ // A purge frame isn't re-checkable: the mutation-time
166
+ // decision is the only one there will ever be
167
+ // (docs/spec/events.md). Everything else still has its record
168
+ // ID, so a grant revoked during the gap is caught here.
169
+ if (!entry.isPurge) {
170
+ const stillReadable = await scoped.get(entry.recordId);
171
+ if (!stillReadable)
172
+ continue;
173
+ }
174
+ // Once the gate trips this connection is closing, so the
175
+ // remaining entries are permission checks nobody will read.
176
+ if (!send('record', entry.frame, entry.frame.seq))
177
+ break;
178
+ }
179
+ }
180
+ replaying = false;
181
+ for (const entry of queued) {
182
+ if (!send('record', entry.frame, entry.frame.seq))
183
+ break;
184
+ }
185
+ }
186
+ // Through the gate like any other write: a client not draining its
187
+ // keepalives isn't draining anything, and counting them is what
188
+ // keeps "in-flight frames are bounded" true of the whole stream
189
+ // rather than of `record` frames alone.
190
+ keepalive = setInterval(() => {
191
+ gate.send(() => stream.write(': keepalive\n\n'));
192
+ }, keepaliveMs);
193
+ // A revoked or expired token must stop delivering. The owner's
194
+ // static bearer token never expires (authMiddleware compares it
195
+ // directly, not via the token store), so only a minted session needs
196
+ // re-checking; an anonymous connection has no token to revoke.
197
+ const isOwnerToken = bearerToken !== undefined && safeCompare(bearerToken, config.ownerToken);
198
+ sessionCheck =
199
+ auth && !isOwnerToken && bearerToken
200
+ ? setInterval(() => {
201
+ void ctx.tokens.lookupToken(bearerToken).then((session) => {
202
+ if (!session)
203
+ resolveDone();
204
+ },
205
+ // The token store is unreachable. Closing is the honest
206
+ // answer — the client reconnects and re-authenticates
207
+ // through the path that already handles a 401 — and it
208
+ // keeps a rejection here from going unhandled.
209
+ () => resolveDone());
210
+ }, sessionCheckMs)
211
+ : undefined;
212
+ await done;
213
+ }
214
+ catch (err) {
215
+ // Hono answers an uncaught throw here by writing the raw error
216
+ // message to the client as an `error` frame. Catching keeps internal
217
+ // detail off a stream any anonymous caller can open, and a closed
218
+ // connection is a repair the client already knows how to make.
219
+ logger.error({ err, requestId: c.get('requestId') }, 'Change feed connection ended with an error');
220
+ }
221
+ finally {
222
+ if (keepalive)
223
+ clearInterval(keepalive);
224
+ if (sessionCheck)
225
+ clearInterval(sessionCheck);
226
+ unsubscribe?.();
227
+ unregister();
228
+ }
229
+ });
230
+ });
231
+ return app;
232
+ }
233
+ //# sourceMappingURL=changes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changes.js","sourceRoot":"","sources":["../../src/routes/changes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { Hono } from 'hono';
2
+ import type { AppEnv } from '../types.js';
3
+ import type { StackContext } from '../stack.js';
4
+ export declare function entityRoutes(ctx: StackContext): Hono<AppEnv>;
5
+ //# sourceMappingURL=entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../src/routes/entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,wBAAgB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,CAwB5D"}