@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.
- 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 +104 -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 +37 -12
- 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,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 @@
|
|
|
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"}
|