@haverstack/server 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +108 -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 +42 -17
  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 @@
1
+ {"version":3,"file":"resumeBuffer.d.ts","sourceRoot":"","sources":["../../src/lib/resumeBuffer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,EAAE,CAAA;CAAE;AAC1C,4FAA4F;GAC1F;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE;AACxB,2EAA2E;GACzE;IAAE,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEjC,wGAAwG;AACxG,qBAAa,YAAY;IAYX,OAAO,CAAC,QAAQ,CAAC,KAAK;IAXlC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2C;IAErE,wEAAwE;IACxE,SAAS,SAAK;IACd,2EAA2E;IAC3E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAER,KAAK,EAAE,MAAM;IAE1C,+EAA+E;IAC/E,UAAU,IAAI,MAAM;IAIpB;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAmBzC,6DAA6D;IAC7D,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI;IAOjE;;;;OAIG;IACH,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,kBAAkB;CAK5C;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,6CAA6C;IAC7C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,YAAY,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAgBnE;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,6GAA6G;IAC7G,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAC/D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqC;IACpE;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;gBAEzC,IAAI,EAAE,2BAA2B;IAE9D;;;;;;;;;;OAUG;IACG,OAAO,CACX,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,KAAK,OAAO,CAAC,WAAW,CAAC,GACnF,OAAO,CAAC,YAAY,CAAC;IAuCxB,uGAAuG;IACvG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAW1B,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,KAAK;IASb,kEAAkE;IAClE,QAAQ,IAAI,IAAI;CAQjB"}
@@ -0,0 +1,216 @@
1
+ import { serializeChange } from '@haverstack/wire-types';
2
+ import { encodeCursor } from './resumeCursor.js';
3
+ /** One session+filter's resume buffer. Data and notification only — lifecycle lives in the registry. */
4
+ export class ResumeBuffer {
5
+ depth;
6
+ id = crypto.randomUUID();
7
+ entries = [];
8
+ currentN = 0;
9
+ evictedUpTo = 0;
10
+ listeners = new Set();
11
+ /** Connections currently holding this buffer open. Registry-managed. */
12
+ liveCount = 0;
13
+ /** Set by the registry when liveCount drops to 0; cleared on reacquire. */
14
+ disconnectedAt = null;
15
+ constructor(depth) {
16
+ this.depth = depth;
17
+ }
18
+ /** The cursor naming this buffer's current position — what `ready` reports. */
19
+ headCursor() {
20
+ return encodeCursor(this.id, this.currentN);
21
+ }
22
+ /**
23
+ * Record one already-permission-filtered change, minting its cursor.
24
+ * Notifies every currently attached live connection with the same frame
25
+ * a replay would later produce.
26
+ */
27
+ append(change) {
28
+ this.currentN += 1;
29
+ const seq = encodeCursor(this.id, this.currentN);
30
+ const frame = serializeChange({ ...change, seq });
31
+ const entry = {
32
+ n: this.currentN,
33
+ recordId: change.recordId,
34
+ isPurge: change.kind === 'purged',
35
+ frame,
36
+ };
37
+ this.entries.push(entry);
38
+ if (this.entries.length > this.depth) {
39
+ const dropped = this.entries.shift();
40
+ if (dropped)
41
+ this.evictedUpTo = dropped.n;
42
+ }
43
+ for (const listener of this.listeners)
44
+ listener(entry);
45
+ return entry;
46
+ }
47
+ /** Attach a live connection. Returns the detach function. */
48
+ subscribeLive(listener) {
49
+ this.listeners.add(listener);
50
+ return () => {
51
+ this.listeners.delete(listener);
52
+ };
53
+ }
54
+ /**
55
+ * Everything after position `n`, or why that can't be answered. `n` is
56
+ * this server's own minted position, already decoded and matched to
57
+ * this buffer's id by the caller.
58
+ */
59
+ entriesAfter(n) {
60
+ if (n > this.currentN || n < 0)
61
+ return { status: 'cursor_expired' };
62
+ if (n < this.evictedUpTo)
63
+ return { status: 'overflow' };
64
+ return { status: 'ok', entries: this.entries.filter((e) => e.n > n) };
65
+ }
66
+ }
67
+ /**
68
+ * Reproducible across a reconnect that re-sends the same query params —
69
+ * order-independent on the parts of `filter` that don't carry order of
70
+ * their own (`typeId`/`kinds` are matched as sets, not sequences, so
71
+ * re-sending them in a different order must key the same buffer).
72
+ *
73
+ * Two filters that mean different things must never key the same buffer.
74
+ * A buffer opens exactly one `ScopedStack.subscribe()`, carrying the
75
+ * filter of whichever connection minted it, so a collision hands one of
76
+ * the two colliding connections the *other's* filter: either a silent gap
77
+ * (events it asked for and never hears about, with no `reset` to tell it
78
+ * so) or frames outside its filter, which "filtering is exact, not
79
+ * advisory" forbids. See docs/spec/wire-format.md § Change feed.
80
+ *
81
+ * Hence an object rather than a positional array: `JSON.stringify` drops
82
+ * an undefined *property* but renders an undefined *array element* as
83
+ * `null`, which would collapse "no parentId filter" onto `parentId: null`
84
+ * — "root records only", a filter the wire format defines. An absent part
85
+ * is an absent key, which no present value can imitate. Key order is fixed
86
+ * by construction below.
87
+ */
88
+ export function resumeBufferKey(parts) {
89
+ const { filter } = parts;
90
+ const typeId = filter.typeId
91
+ ? [...(Array.isArray(filter.typeId) ? filter.typeId : [filter.typeId])].sort()
92
+ : undefined;
93
+ const kinds = filter.kinds ? [...filter.kinds].sort() : undefined;
94
+ return JSON.stringify({
95
+ principalId: parts.principalId,
96
+ subjectId: parts.subjectId,
97
+ includeRecords: parts.includeRecords,
98
+ includeUnlisted: parts.includeUnlisted,
99
+ ...(typeId !== undefined && { typeId }),
100
+ ...(filter.parentId !== undefined && { parentId: filter.parentId }),
101
+ ...(filter.entityId !== undefined && { entityId: filter.entityId }),
102
+ ...(kinds !== undefined && { kinds }),
103
+ });
104
+ }
105
+ /**
106
+ * Owns every live resume buffer, keyed by (session, filter, includeRecords).
107
+ * A buffer's underlying `ScopedStack.subscribe()` is opened once, when the
108
+ * buffer is first minted, and kept open — including across a gap with zero
109
+ * live connections — until the buffer itself is evicted.
110
+ */
111
+ export class ResumeBufferRegistry {
112
+ opts;
113
+ buffers = new Map();
114
+ unsubscribes = new Map();
115
+ evictionTimers = new Map();
116
+ /**
117
+ * Buffers whose subscription is still opening. A key lands in `buffers`
118
+ * only once its `ScopedStack.subscribe()` has resolved, so this is what
119
+ * keeps two connections arriving in the same tick from opening two
120
+ * subscriptions for one key — they await the same attempt and share
121
+ * whichever way it settles.
122
+ */
123
+ acquiring = new Map();
124
+ constructor(opts) {
125
+ this.opts = opts;
126
+ }
127
+ /**
128
+ * Get this key's buffer, creating one and opening its scoped subscription
129
+ * if none is currently retained. `subscribeToStack` is called at most
130
+ * once per buffer instance.
131
+ *
132
+ * A failed subscription registers nothing: the next connection on this
133
+ * key retries it. Registering a buffer that nothing is feeding would
134
+ * hand every later connection on that key a `ready` frame followed by
135
+ * permanent silence — a gap with no `reset` to announce it, which is
136
+ * the one failure the feed contract calls untrustworthy.
137
+ */
138
+ async acquire(key, subscribeToStack) {
139
+ const existing = this.buffers.get(key);
140
+ if (existing) {
141
+ this.cancelEviction(key);
142
+ existing.liveCount += 1;
143
+ existing.disconnectedAt = null;
144
+ return existing;
145
+ }
146
+ const inFlight = this.acquiring.get(key);
147
+ if (inFlight) {
148
+ const buffer = await inFlight;
149
+ buffer.liveCount += 1;
150
+ return buffer;
151
+ }
152
+ const attempt = (async () => {
153
+ const buffer = new ResumeBuffer(this.opts.depth);
154
+ // Awaited before either map is touched — the handler closes over
155
+ // `buffer`, so anything the stack emits while this is in flight is
156
+ // already being recorded by the time the buffer becomes reachable.
157
+ const unsubscribe = await subscribeToStack((change) => {
158
+ buffer.append(change);
159
+ });
160
+ this.buffers.set(key, buffer);
161
+ this.unsubscribes.set(key, unsubscribe);
162
+ return buffer;
163
+ })();
164
+ this.acquiring.set(key, attempt);
165
+ try {
166
+ const buffer = await attempt;
167
+ buffer.liveCount += 1;
168
+ return buffer;
169
+ }
170
+ finally {
171
+ this.acquiring.delete(key);
172
+ }
173
+ }
174
+ /** Detach one connection from this key's buffer, starting its retention countdown once none remain. */
175
+ release(key) {
176
+ const buffer = this.buffers.get(key);
177
+ if (!buffer)
178
+ return;
179
+ buffer.liveCount = Math.max(0, buffer.liveCount - 1);
180
+ if (buffer.liveCount > 0)
181
+ return;
182
+ buffer.disconnectedAt = Date.now();
183
+ const timer = setTimeout(() => this.evict(key, buffer), this.opts.retentionMs);
184
+ timer.unref?.();
185
+ this.evictionTimers.set(key, timer);
186
+ }
187
+ cancelEviction(key) {
188
+ const timer = this.evictionTimers.get(key);
189
+ if (timer) {
190
+ clearTimeout(timer);
191
+ this.evictionTimers.delete(key);
192
+ }
193
+ }
194
+ evict(key, buffer) {
195
+ if (this.buffers.get(key) !== buffer)
196
+ return; // already replaced
197
+ if (buffer.liveCount > 0)
198
+ return; // reacquired since the timer was set
199
+ this.buffers.delete(key);
200
+ this.evictionTimers.delete(key);
201
+ this.unsubscribes.get(key)?.();
202
+ this.unsubscribes.delete(key);
203
+ }
204
+ /** Tear down every retained buffer immediately — for shutdown. */
205
+ closeAll() {
206
+ for (const timer of this.evictionTimers.values())
207
+ clearTimeout(timer);
208
+ this.evictionTimers.clear();
209
+ this.acquiring.clear();
210
+ for (const unsubscribe of this.unsubscribes.values())
211
+ unsubscribe();
212
+ this.unsubscribes.clear();
213
+ this.buffers.clear();
214
+ }
215
+ }
216
+ //# sourceMappingURL=resumeBuffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumeBuffer.js","sourceRoot":"","sources":["../../src/lib/resumeBuffer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export type DecodedCursor = {
2
+ bufferId: string;
3
+ n: number;
4
+ };
5
+ /** Mint a cursor naming one buffer's identity and position. */
6
+ export declare function encodeCursor(bufferId: string, n: number): string;
7
+ /**
8
+ * Decode a presented cursor. Returns null for anything that doesn't parse
9
+ * as `bufferId + ":" + a non-negative integer` — never throws. A
10
+ * charset-valid cursor naming garbage is the same "cannot be honored" case
11
+ * an unrecognized buffer id is: answered with a `reset` frame, not a 400.
12
+ * (A charset-*invalid* cursor is refused before this is ever called — see
13
+ * `src/routes/changes.ts`.)
14
+ */
15
+ export declare function decodeCursor(seq: string): DecodedCursor | null;
16
+ //# sourceMappingURL=resumeCursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumeCursor.d.ts","sourceRoot":"","sources":["../../src/lib/resumeCursor.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAe9D"}
@@ -11,17 +11,10 @@
11
11
  * ":" delimiter, since it never appears in the encoded output.
12
12
  */
13
13
  import { base64urlEncode, base64urlDecode } from '@haverstack/core/wire';
14
-
15
- export type DecodedCursor = {
16
- bufferId: string;
17
- n: number;
18
- };
19
-
20
14
  /** Mint a cursor naming one buffer's identity and position. */
21
- export function encodeCursor(bufferId: string, n: number): string {
22
- return base64urlEncode(new TextEncoder().encode(`${bufferId}:${n}`));
15
+ export function encodeCursor(bufferId, n) {
16
+ return base64urlEncode(new TextEncoder().encode(`${bufferId}:${n}`));
23
17
  }
24
-
25
18
  /**
26
19
  * Decode a presented cursor. Returns null for anything that doesn't parse
27
20
  * as `bufferId + ":" + a non-negative integer` — never throws. A
@@ -30,19 +23,24 @@ export function encodeCursor(bufferId: string, n: number): string {
30
23
  * (A charset-*invalid* cursor is refused before this is ever called — see
31
24
  * `src/routes/changes.ts`.)
32
25
  */
33
- export function decodeCursor(seq: string): DecodedCursor | null {
34
- let text: string;
35
- try {
36
- text = new TextDecoder('utf-8', { fatal: true }).decode(base64urlDecode(seq));
37
- } catch {
38
- return null;
39
- }
40
- const colonIndex = text.lastIndexOf(':');
41
- if (colonIndex === -1) return null;
42
- const bufferId = text.slice(0, colonIndex);
43
- const nText = text.slice(colonIndex + 1);
44
- if (!/^\d+$/.test(nText)) return null;
45
- const n = Number(nText);
46
- if (!Number.isSafeInteger(n)) return null;
47
- return { bufferId, n };
26
+ export function decodeCursor(seq) {
27
+ let text;
28
+ try {
29
+ text = new TextDecoder('utf-8', { fatal: true }).decode(base64urlDecode(seq));
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ const colonIndex = text.lastIndexOf(':');
35
+ if (colonIndex === -1)
36
+ return null;
37
+ const bufferId = text.slice(0, colonIndex);
38
+ const nText = text.slice(colonIndex + 1);
39
+ if (!/^\d+$/.test(nText))
40
+ return null;
41
+ const n = Number(nText);
42
+ if (!Number.isSafeInteger(n))
43
+ return null;
44
+ return { bufferId, n };
48
45
  }
46
+ //# sourceMappingURL=resumeCursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumeCursor.js","sourceRoot":"","sources":["../../src/lib/resumeCursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAOzE,+DAA+D;AAC/D,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,CAAS;IACtD,OAAO,eAAe,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,QAAQ,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,UAAU,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AACzB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { Logger } from 'pino';
2
+ import type { StackTokenStore } from '@haverstack/core/wire';
3
+ /**
4
+ * Delete tokens past their `expiresAt`.
5
+ *
6
+ * `lookupToken()` already refuses an expired token, so this changes no
7
+ * authorization outcome — it reclaims rows, and nothing else does: the
8
+ * store deletes only on an explicit `revokeToken()`. Since `POST
9
+ * /auth/token` is unauthenticated, strangers drive the row count, and
10
+ * unbounded growth costs disk and slows every `listTokens()` caller.
11
+ *
12
+ * Returns a throttled sweep — call it freely; it runs at most once per
13
+ * interval and never rejects, since failing to reclaim rows must not fail
14
+ * the request that happened to trigger it.
15
+ */
16
+ export declare function createExpiredTokenSweeper(tokens: StackTokenStore, logger: Logger): () => Promise<void>;
17
+ //# sourceMappingURL=tokenSweep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenSweep.d.ts","sourceRoot":"","sources":["../../src/lib/tokenSweep.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAM7D;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,GACb,MAAM,OAAO,CAAC,IAAI,CAAC,CAmBrB"}
@@ -0,0 +1,40 @@
1
+ // Once an hour at most. The sweep is O(n) in tokens ever issued — cheap
2
+ // relative to that interval, and never on the hot path twice in a row.
3
+ const SWEEP_INTERVAL_MS = 60 * 60 * 1000;
4
+ /**
5
+ * Delete tokens past their `expiresAt`.
6
+ *
7
+ * `lookupToken()` already refuses an expired token, so this changes no
8
+ * authorization outcome — it reclaims rows, and nothing else does: the
9
+ * store deletes only on an explicit `revokeToken()`. Since `POST
10
+ * /auth/token` is unauthenticated, strangers drive the row count, and
11
+ * unbounded growth costs disk and slows every `listTokens()` caller.
12
+ *
13
+ * Returns a throttled sweep — call it freely; it runs at most once per
14
+ * interval and never rejects, since failing to reclaim rows must not fail
15
+ * the request that happened to trigger it.
16
+ */
17
+ export function createExpiredTokenSweeper(tokens, logger) {
18
+ let lastSweptAt = 0;
19
+ return async () => {
20
+ const now = Date.now();
21
+ if (now - lastSweptAt < SWEEP_INTERVAL_MS)
22
+ return;
23
+ // Claimed before the await, so two concurrent requests can't both start
24
+ // a sweep and race each other's revokeToken() calls.
25
+ lastSweptAt = now;
26
+ try {
27
+ const all = await tokens.listTokens();
28
+ const expired = all.filter((t) => t.expiresAt !== undefined && t.expiresAt.getTime() <= now);
29
+ for (const t of expired)
30
+ await tokens.revokeToken(t.id);
31
+ if (expired.length > 0) {
32
+ logger.info({ revoked: expired.length }, 'Swept expired tokens');
33
+ }
34
+ }
35
+ catch (err) {
36
+ logger.warn({ err }, 'Expired-token sweep failed');
37
+ }
38
+ };
39
+ }
40
+ //# sourceMappingURL=tokenSweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenSweep.js","sourceRoot":"","sources":["../../src/lib/tokenSweep.ts"],"names":[],"mappings":"AAGA,wEAAwE;AACxE,uEAAuE;AACvE,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEzC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAuB,EACvB,MAAc;IAEd,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,OAAO,KAAK,IAAI,EAAE;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,WAAW,GAAG,iBAAiB;YAAE,OAAO;QAClD,wEAAwE;QACxE,qDAAqD;QACrD,WAAW,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,CAAC;YAC7F,KAAK,MAAM,CAAC,IAAI,OAAO;gBAAE,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACxD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,sBAAsB,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,4BAA4B,CAAC,CAAC;QACrD,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
package/dist/main.js ADDED
@@ -0,0 +1,46 @@
1
+ import { serve } from '@hono/node-server';
2
+ import pino from 'pino';
3
+ import { loadConfig } from './config.js';
4
+ import { initStack } from './stack.js';
5
+ import { createApp } from './app.js';
6
+ import { createShutdownHandler } from './shutdown.js';
7
+ const logger = pino({
8
+ level: process.env['LOG_LEVEL'] ?? 'info',
9
+ transport: process.env['NODE_ENV'] !== 'production'
10
+ ? { target: 'pino-pretty', options: { colorize: true } }
11
+ : undefined,
12
+ });
13
+ // Set once initStack() resolves, so the fatal-error handler below can flush
14
+ // a crash that happens after startup. Undefined before then: nothing to
15
+ // flush yet.
16
+ let ctx;
17
+ async function main() {
18
+ const config = loadConfig();
19
+ ctx = await initStack(config, logger);
20
+ const app = createApp(ctx, config, logger);
21
+ logger.info({ dbPath: config.dbPath }, 'Stack initialized');
22
+ const server = serve({ fetch: app.fetch, port: config.port }, (info) => {
23
+ logger.info({ port: info.port }, 'Server listening');
24
+ });
25
+ const shutdown = createShutdownHandler(server, ctx, logger, config.shutdownTimeoutMs);
26
+ const onSignal = (signal) => {
27
+ shutdown(signal)
28
+ .then(() => process.exit(0))
29
+ .catch((err) => {
30
+ logger.error({ err }, 'Error during shutdown');
31
+ process.exit(1);
32
+ });
33
+ };
34
+ process.on('SIGTERM', () => onSignal('SIGTERM'));
35
+ process.on('SIGINT', () => onSignal('SIGINT'));
36
+ }
37
+ main().catch(async (err) => {
38
+ logger.error({ err }, 'Fatal startup error');
39
+ if (ctx) {
40
+ await ctx.stack.flush().catch((flushErr) => {
41
+ logger.error({ err: flushErr }, 'Failed to flush during fatal-error shutdown');
42
+ });
43
+ }
44
+ process.exit(1);
45
+ });
46
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAqB,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,MAAM;IACzC,SAAS,EACP,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY;QACtC,CAAC,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;QACxD,CAAC,CAAC,SAAS;CAChB,CAAC,CAAC;AAEH,4EAA4E;AAC5E,wEAAwE;AACxE,aAAa;AACb,IAAI,GAA6B,CAAC;AAElC,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,GAAG,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAE3C,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC;IAE5D,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;QACrE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,kBAAkB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,qBAAqB,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACtF,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,EAAE;QAClC,QAAQ,CAAC,MAAM,CAAC;aACb,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aAC3B,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,uBAAuB,CAAC,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IACzB,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,qBAAqB,CAAC,CAAC;IAC7C,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,EAAE;YACzC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,6CAA6C,CAAC,CAAC;QACjF,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { TokenSession } from '@haverstack/core';
3
+ import type { AppEnv } from '../types.js';
4
+ import type { StackContext } from '../stack.js';
5
+ /**
6
+ * Constant-time string equality, for comparing a presented credential
7
+ * against a configured secret. Exported so every comparison of the owner
8
+ * token uses the same one — `GET /changes` re-derives "is this the owner"
9
+ * to decide whether to arm its session re-check.
10
+ */
11
+ export declare function safeCompare(a: string, b: string): boolean;
12
+ export declare function authMiddleware(ownerToken: string, ctx: StackContext): MiddlewareHandler<AppEnv>;
13
+ export declare function requireAuth(): MiddlewareHandler<AppEnv>;
14
+ /**
15
+ * Whether a session is the stack owner acting alone: undelegated, and
16
+ * authenticated as the owner rather than merely delegated for it. Being the
17
+ * owner is never on its own sufficient under delegation — a delegated
18
+ * session with the owner as principal still fails this, matching
19
+ * `ScopedStack`'s own owner-only gates (e.g. hard delete). See
20
+ * docs/spec/access-control.md § Delegation.
21
+ */
22
+ export declare function isOwnerActingAlone(auth: TokenSession | null, ownerEntityId: string): boolean;
23
+ /** Gates a route to the owner acting alone; see `isOwnerActingAlone()`. */
24
+ export declare function requireOwner(ownerEntityId: string): MiddlewareHandler<AppEnv>;
25
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAyB/F;AAED,wBAAgB,WAAW,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAKvD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAE5F;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAS7E"}
@@ -0,0 +1,74 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { StackPermissionError } from '@haverstack/core';
3
+ import { wireError } from '../wireError.js';
4
+ /**
5
+ * Constant-time string equality, for comparing a presented credential
6
+ * against a configured secret. Exported so every comparison of the owner
7
+ * token uses the same one — `GET /changes` re-derives "is this the owner"
8
+ * to decide whether to arm its session re-check.
9
+ */
10
+ export function safeCompare(a, b) {
11
+ const ba = Buffer.from(a);
12
+ const bb = Buffer.from(b);
13
+ return ba.length === bb.length && timingSafeEqual(ba, bb);
14
+ }
15
+ export function authMiddleware(ownerToken, ctx) {
16
+ const ownerEntityId = ctx.stack.ownerEntityId;
17
+ return async (c, next) => {
18
+ const header = c.req.header('Authorization');
19
+ if (header === undefined) {
20
+ c.set('auth', null);
21
+ }
22
+ else if (!header.startsWith('Bearer ')) {
23
+ // A credential that isn't a bearer token (`Basic ...`, a missing
24
+ // space) is a 401, never a silent downgrade to anonymous.
25
+ return wireError(c, 401, 'unauthorized', 'Missing or invalid bearer token');
26
+ }
27
+ else {
28
+ const token = header.slice(7);
29
+ if (safeCompare(token, ownerToken)) {
30
+ c.set('auth', { principalId: ownerEntityId, subjectId: ownerEntityId });
31
+ }
32
+ else {
33
+ const session = await ctx.tokens.lookupToken(token);
34
+ // Unknown, expired, or revoked is distinct from "none offered",
35
+ // and must not read as anonymous on optional-auth routes.
36
+ if (!session)
37
+ return wireError(c, 401, 'unauthorized', 'Missing or invalid bearer token');
38
+ c.set('auth', session);
39
+ }
40
+ }
41
+ await next();
42
+ };
43
+ }
44
+ export function requireAuth() {
45
+ return async (c, next) => {
46
+ if (!c.get('auth'))
47
+ return wireError(c, 401, 'unauthorized', 'Missing or invalid bearer token');
48
+ await next();
49
+ };
50
+ }
51
+ /**
52
+ * Whether a session is the stack owner acting alone: undelegated, and
53
+ * authenticated as the owner rather than merely delegated for it. Being the
54
+ * owner is never on its own sufficient under delegation — a delegated
55
+ * session with the owner as principal still fails this, matching
56
+ * `ScopedStack`'s own owner-only gates (e.g. hard delete). See
57
+ * docs/spec/access-control.md § Delegation.
58
+ */
59
+ export function isOwnerActingAlone(auth, ownerEntityId) {
60
+ return auth?.principalId === ownerEntityId && auth?.subjectId === ownerEntityId;
61
+ }
62
+ /** Gates a route to the owner acting alone; see `isOwnerActingAlone()`. */
63
+ export function requireOwner(ownerEntityId) {
64
+ return async (c, next) => {
65
+ const auth = c.get('auth');
66
+ if (!auth)
67
+ return wireError(c, 401, 'unauthorized', 'Missing or invalid bearer token');
68
+ if (!isOwnerActingAlone(auth, ownerEntityId)) {
69
+ throw new StackPermissionError('Owner access required');
70
+ }
71
+ await next();
72
+ };
73
+ }
74
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ErrorHandler } from 'hono';
2
+ import type { Logger } from 'pino';
3
+ import { type Stack } from '@haverstack/core';
4
+ import type { AppEnv } from '../types.js';
5
+ /**
6
+ * Central error handler. `serializeError()` covers every core `StackError`
7
+ * (thrown directly by routes or surfaced from `Stack`/`ScopedStack`) with
8
+ * the wire-format's typed `{ error: { code, message } }` body. Anything
9
+ * else is an unanticipated bug: log it and return a bodyless 500 rather
10
+ * than guessing at a taxonomy it doesn't belong to.
11
+ *
12
+ * `stack` is the unscoped root Stack, used only to probe existence for the
13
+ * refusal log below — never to read content or bypass a permission check
14
+ * for the response itself.
15
+ */
16
+ export declare function errorMiddleware(logger: Logger, stack: Stack): ErrorHandler<AppEnv>;
17
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/middleware/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAA4C,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACxF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAsDlF"}