@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.
- package/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +70 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/changeStreams.d.ts +17 -0
- package/dist/lib/changeStreams.d.ts.map +1 -0
- package/{src/lib/changeStreams.ts → dist/lib/changeStreams.js} +12 -12
- package/dist/lib/changeStreams.js.map +1 -0
- package/dist/lib/frameGate.d.ts +26 -0
- package/dist/lib/frameGate.d.ts.map +1 -0
- package/dist/lib/frameGate.js +44 -0
- package/dist/lib/frameGate.js.map +1 -0
- package/dist/lib/json.d.ts +10 -0
- package/dist/lib/json.d.ts.map +1 -0
- package/dist/lib/json.js +27 -0
- package/dist/lib/json.js.map +1 -0
- package/dist/lib/nonceStore.d.ts +50 -0
- package/dist/lib/nonceStore.d.ts.map +1 -0
- package/dist/lib/nonceStore.js +96 -0
- package/dist/lib/nonceStore.js.map +1 -0
- package/dist/lib/queryLimit.d.ts +4 -0
- package/dist/lib/queryLimit.d.ts.map +1 -0
- package/{src/lib/queryLimit.ts → dist/lib/queryLimit.js} +5 -6
- package/dist/lib/queryLimit.js.map +1 -0
- package/dist/lib/queryWorker/pool.d.ts +60 -0
- package/dist/lib/queryWorker/pool.d.ts.map +1 -0
- package/dist/lib/queryWorker/pool.js +278 -0
- package/dist/lib/queryWorker/pool.js.map +1 -0
- package/{src/lib/queryWorker/protocol.ts → dist/lib/queryWorker/protocol.d.ts} +21 -18
- package/dist/lib/queryWorker/protocol.d.ts.map +1 -0
- package/dist/lib/queryWorker/protocol.js +2 -0
- package/dist/lib/queryWorker/protocol.js.map +1 -0
- package/dist/lib/queryWorker/worker.d.ts +2 -0
- package/dist/lib/queryWorker/worker.d.ts.map +1 -0
- package/{src/lib/queryWorker/worker.ts → dist/lib/queryWorker/worker.js} +19 -22
- package/dist/lib/queryWorker/worker.js.map +1 -0
- package/dist/lib/resumeBuffer.d.ts +143 -0
- package/dist/lib/resumeBuffer.d.ts.map +1 -0
- package/dist/lib/resumeBuffer.js +216 -0
- package/dist/lib/resumeBuffer.js.map +1 -0
- package/dist/lib/resumeCursor.d.ts +16 -0
- package/dist/lib/resumeCursor.d.ts.map +1 -0
- package/{src/lib/resumeCursor.ts → dist/lib/resumeCursor.js} +22 -24
- package/dist/lib/resumeCursor.js.map +1 -0
- package/dist/lib/tokenSweep.d.ts +17 -0
- package/dist/lib/tokenSweep.d.ts.map +1 -0
- package/dist/lib/tokenSweep.js +40 -0
- package/dist/lib/tokenSweep.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +46 -0
- package/dist/main.js.map +1 -0
- package/dist/middleware/auth.d.ts +25 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +74 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/errors.d.ts +17 -0
- package/dist/middleware/errors.d.ts.map +1 -0
- package/dist/middleware/errors.js +64 -0
- package/dist/middleware/errors.js.map +1 -0
- package/dist/routes/attachments.d.ts +5 -0
- package/dist/routes/attachments.d.ts.map +1 -0
- package/dist/routes/attachments.js +108 -0
- package/dist/routes/attachments.js.map +1 -0
- package/dist/routes/auth.d.ts +6 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +110 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/changes.d.ts +22 -0
- package/dist/routes/changes.d.ts.map +1 -0
- package/dist/routes/changes.js +233 -0
- package/dist/routes/changes.js.map +1 -0
- package/dist/routes/entity.d.ts +5 -0
- package/dist/routes/entity.d.ts.map +1 -0
- package/dist/routes/entity.js +30 -0
- package/dist/routes/entity.js.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +7 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/records.d.ts +5 -0
- package/dist/routes/records.d.ts.map +1 -0
- package/dist/routes/records.js +231 -0
- package/dist/routes/records.js.map +1 -0
- package/dist/routes/tokens.d.ts +5 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +65 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/types.d.ts +5 -0
- package/dist/routes/types.d.ts.map +1 -0
- package/dist/routes/types.js +46 -0
- package/dist/routes/types.js.map +1 -0
- package/dist/routes/wellknown.d.ts +6 -0
- package/dist/routes/wellknown.d.ts.map +1 -0
- package/dist/routes/wellknown.js +45 -0
- package/dist/routes/wellknown.js.map +1 -0
- package/dist/shutdown.d.ts +23 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +32 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/stack.d.ts +20 -0
- package/dist/stack.d.ts.map +1 -0
- package/dist/stack.js +50 -0
- package/dist/stack.js.map +1 -0
- package/dist/testing.d.ts +49 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +130 -0
- package/dist/testing.js.map +1 -0
- package/{src/types.ts → dist/types.d.ts} +5 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/{src/wireError.ts → dist/wireError.d.ts} +2 -9
- package/dist/wireError.d.ts.map +1 -0
- package/dist/wireError.js +13 -0
- package/dist/wireError.js.map +1 -0
- package/package.json +42 -17
- package/.claude/settings.local.json +0 -22
- package/.dockerignore +0 -10
- package/.env.example +0 -90
- package/.github/pull_request_template.md +0 -23
- package/.github/workflows/ci.yml +0 -55
- package/.github/workflows/release.yml +0 -99
- package/.prettierignore +0 -3
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -91
- package/CONTRIBUTING.md +0 -234
- package/Dockerfile +0 -30
- package/SECURITY.md +0 -54
- package/docs/api.md +0 -277
- package/docs/deployment.md +0 -212
- package/eslint.config.mjs +0 -18
- package/pnpm-workspace.yaml +0 -13
- package/src/app.ts +0 -86
- package/src/config.ts +0 -176
- package/src/index.ts +0 -54
- package/src/lib/frameGate.ts +0 -42
- package/src/lib/json.ts +0 -27
- package/src/lib/nonceStore.ts +0 -103
- package/src/lib/queryWorker/pool.ts +0 -324
- package/src/lib/resumeBuffer.ts +0 -268
- package/src/lib/tokenSweep.ts +0 -43
- package/src/middleware/auth.ts +0 -77
- package/src/middleware/errors.ts +0 -73
- package/src/routes/attachments.ts +0 -126
- package/src/routes/auth.ts +0 -137
- package/src/routes/changes.ts +0 -279
- package/src/routes/entity.ts +0 -33
- package/src/routes/health.ts +0 -8
- package/src/routes/records.ts +0 -259
- package/src/routes/tokens.ts +0 -85
- package/src/routes/types.ts +0 -57
- package/src/routes/wellknown.ts +0 -55
- package/src/shutdown.ts +0 -56
- package/src/stack.ts +0 -96
- package/tests/app.test.ts +0 -86
- package/tests/changeFeedClient.ts +0 -156
- package/tests/config.test.ts +0 -112
- package/tests/conformance.test.ts +0 -2322
- package/tests/fixtures/blockingWorker.ts +0 -27
- package/tests/fixtures/brokenWorker.ts +0 -7
- package/tests/lib/changeFeedClient.test.ts +0 -126
- package/tests/lib/frameGate.test.ts +0 -93
- package/tests/lib/queryLimit.test.ts +0 -33
- package/tests/lib/queryWorker.test.ts +0 -60
- package/tests/lib/queryWorkerPool.test.ts +0 -129
- package/tests/lib/resumeBuffer.test.ts +0 -341
- package/tests/lib/resumeCursor.test.ts +0 -46
- package/tests/middleware/errors.test.ts +0 -176
- package/tests/routes/associations.test.ts +0 -170
- package/tests/routes/attachments.test.ts +0 -621
- package/tests/routes/auth.test.ts +0 -448
- package/tests/routes/changes.test.ts +0 -567
- package/tests/routes/contentQuery.test.ts +0 -68
- package/tests/routes/entity.test.ts +0 -170
- package/tests/routes/groups.test.ts +0 -69
- package/tests/routes/records.test.ts +0 -1965
- package/tests/routes/relatedToQuery.test.ts +0 -182
- package/tests/routes/tokens.test.ts +0 -157
- package/tests/routes/tombstones.test.ts +0 -93
- package/tests/routes/types.test.ts +0 -129
- package/tests/routes/versions.test.ts +0 -177
- package/tests/routes/wellknown.test.ts +0 -71
- package/tests/setup.ts +0 -154
- package/tests/shutdown.test.ts +0 -121
- package/tests/stack.test.ts +0 -198
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -19
- 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":"AAiBA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAiBjD,wGAAwG;AACxG,MAAM,OAAO,YAAY;IAYM;IAXpB,EAAE,GAAW,MAAM,CAAC,UAAU,EAAE,CAAC;IACzB,OAAO,GAAkB,EAAE,CAAC;IACrC,QAAQ,GAAG,CAAC,CAAC;IACb,WAAW,GAAG,CAAC,CAAC;IACP,SAAS,GAAG,IAAI,GAAG,EAAgC,CAAC;IAErE,wEAAwE;IACxE,SAAS,GAAG,CAAC,CAAC;IACd,2EAA2E;IAC3E,cAAc,GAAkB,IAAI,CAAC;IAErC,YAA6B,KAAa;QAAb,UAAK,GAAL,KAAK,CAAQ;IAAG,CAAC;IAE9C,+EAA+E;IAC/E,UAAU;QACR,OAAO,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,MAAoB;QACzB,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QACnB,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAClD,MAAM,KAAK,GAAgB;YACzB,CAAC,EAAE,IAAI,CAAC,QAAQ;YAChB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,OAAO,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ;YACjC,KAAK;SACN,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrC,IAAI,OAAO;gBAAE,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;QAC5C,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,6DAA6D;IAC7D,aAAa,CAAC,QAAsC;QAClD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,CAAS;QACpB,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QACpE,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW;YAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QACxD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IACxE,CAAC;CACF;AAWD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IACzB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM;QAC1B,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;QAC9E,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAClE,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,CAAC;QACvC,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACnE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACnE,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,CAAC;KACtC,CAAC,CAAC;AACL,CAAC;AASD;;;;;GAKG;AACH,MAAM,OAAO,oBAAoB;IAaF;IAZZ,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC1C,YAAY,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,cAAc,GAAG,IAAI,GAAG,EAA0B,CAAC;IACpE;;;;;;OAMG;IACc,SAAS,GAAG,IAAI,GAAG,EAAiC,CAAC;IAEtE,YAA6B,IAAiC;QAAjC,SAAI,GAAJ,IAAI,CAA6B;IAAG,CAAC;IAElE;;;;;;;;;;OAUG;IACH,KAAK,CAAC,OAAO,CACX,GAAW,EACX,gBAAoF;QAEpF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;YACzB,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAC;YACxB,QAAQ,CAAC,cAAc,GAAG,IAAI,CAAC;YAC/B,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC;YAC9B,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;YACtB,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjD,iEAAiE;YACjE,mEAAmE;YACnE,mEAAmE;YACnE,MAAM,WAAW,GAAG,MAAM,gBAAgB,CAAC,CAAC,MAAM,EAAE,EAAE;gBACpD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACxB,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC9B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;YACxC,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,EAAE,CAAC;QACL,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAEjC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;YAC7B,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;YACtB,OAAO,MAAM,CAAC;QAChB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,uGAAuG;IACvG,OAAO,CAAC,GAAW;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QACrD,IAAI,MAAM,CAAC,SAAS,GAAG,CAAC;YAAE,OAAO;QACjC,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC/E,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QAChB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtC,CAAC;IAEO,cAAc,CAAC,GAAW;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,KAAK,EAAE,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,GAAW,EAAE,MAAoB;QAC7C,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,MAAM;YAAE,OAAO,CAAC,mBAAmB;QACjE,IAAI,MAAM,CAAC,SAAS,GAAG,CAAC;YAAE,OAAO,CAAC,qCAAqC;QACvE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QAC/B,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,kEAAkE;IAClE,QAAQ;QACN,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACtE,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,KAAK,MAAM,WAAW,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;YAAE,WAAW,EAAE,CAAC;QACpE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -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
|
|
22
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 @@
|
|
|
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
|
package/dist/main.js.map
ADDED
|
@@ -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"}
|