@fitzzero/quickdraw-core 5.1.1 → 5.1.3
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/README.md +1308 -219
- package/dist/{admin-DKwkIh5i.d.ts → admin-CS7JB_UH.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-29cGgrg5.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DtdYjrZ9.d.ts} +51 -21
- package/dist/{caller-CXTFswBO.d.ts → caller-BTwGl7vm.d.ts} +296 -33
- package/dist/chunk-2FDSKLBO.js +82 -0
- package/dist/chunk-2FDSKLBO.js.map +1 -0
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/{chunk-OECZPRPW.js → chunk-64I6MYBC.js} +162 -775
- package/dist/chunk-64I6MYBC.js.map +1 -0
- package/dist/chunk-7473HK24.js +17 -0
- package/dist/chunk-7473HK24.js.map +1 -0
- package/dist/chunk-7Y5ROPXL.js +33 -0
- package/dist/chunk-7Y5ROPXL.js.map +1 -0
- package/dist/chunk-A2RBQD66.js +7 -0
- package/dist/chunk-A2RBQD66.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/chunk-ASAMUGHG.js +651 -0
- package/dist/chunk-ASAMUGHG.js.map +1 -0
- package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
- package/dist/chunk-CYE4UBOB.js.map +1 -0
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.js.map +1 -0
- package/dist/chunk-DNP65TD5.js +832 -0
- package/dist/chunk-DNP65TD5.js.map +1 -0
- package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
- package/dist/chunk-DUBU3BB3.js.map +1 -0
- package/dist/chunk-GYLWEJHW.js +19 -0
- package/dist/chunk-GYLWEJHW.js.map +1 -0
- package/dist/{chunk-SHEGLZEP.js → chunk-I3JCFS7H.js} +34 -13
- package/dist/chunk-I3JCFS7H.js.map +1 -0
- package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
- package/dist/chunk-IIQ3WMJK.js.map +1 -0
- package/dist/{chunk-RZTP4IEW.js → chunk-JVTONQFI.js} +6339 -4226
- package/dist/chunk-JVTONQFI.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-KKQANRLG.js} +2 -2
- package/dist/chunk-KKQANRLG.js.map +1 -0
- package/dist/{chunk-CDKG4TFA.js → chunk-MPQAT55F.js} +38 -205
- package/dist/chunk-MPQAT55F.js.map +1 -0
- package/dist/chunk-NO4M6PO5.js +101 -0
- package/dist/chunk-NO4M6PO5.js.map +1 -0
- package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
- package/dist/chunk-NW6U45HX.js.map +1 -0
- package/dist/chunk-NX2V6SIL.js +202 -0
- package/dist/chunk-NX2V6SIL.js.map +1 -0
- package/dist/{chunk-ZIVPHYRG.js → chunk-NZHA4TZD.js} +72 -175
- package/dist/chunk-NZHA4TZD.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-OCZJ4EYV.js} +518 -27
- package/dist/chunk-OCZJ4EYV.js.map +1 -0
- package/dist/{chunk-REX5YPHX.js → chunk-PHCURHC7.js} +76 -21
- package/dist/chunk-PHCURHC7.js.map +1 -0
- package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
- package/dist/chunk-Q4OGQM5I.js.map +1 -0
- package/dist/chunk-QSU47XEX.js +13 -0
- package/dist/chunk-QSU47XEX.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-RZXJTDH5.js} +13 -2
- package/dist/chunk-RZXJTDH5.js.map +1 -0
- package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
- package/dist/chunk-SN3OZVEQ.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-SQ5VUG77.js} +28 -8
- package/dist/chunk-SQ5VUG77.js.map +1 -0
- package/dist/chunk-STH5DLQP.js +1192 -0
- package/dist/chunk-STH5DLQP.js.map +1 -0
- package/dist/{chunk-QCHKWIRS.js → chunk-VZFJTKIW.js} +13 -13
- package/dist/{chunk-QCHKWIRS.js.map → chunk-VZFJTKIW.js.map} +1 -1
- package/dist/{chunk-VSQBHPWH.js → chunk-X4ZU4QH5.js} +54 -22
- package/dist/chunk-X4ZU4QH5.js.map +1 -0
- package/dist/chunk-XD7SRDUY.js +188 -0
- package/dist/chunk-XD7SRDUY.js.map +1 -0
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/chunk-Z7I6SONO.js +825 -0
- package/dist/chunk-Z7I6SONO.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +132 -112
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/core.d.ts +387 -0
- package/dist/client/core.js +109 -0
- package/dist/client/index.d.ts +52 -203
- package/dist/client/index.js +59 -218
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-2EeQ_Et0.d.ts} +22 -11
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Bx5HOcYK.d.ts} +67 -17
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-DAMANT79.d.ts} +46 -13
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-BMBaxppV.d.ts} +216 -10
- package/dist/{http-Djj2Inuu.d.ts → http-YzLAar3w.d.ts} +76 -60
- package/dist/index.d.ts +8 -7
- package/dist/index.js +82 -25
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-Br62cnyW.d.ts} +82 -11
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/parser.js +2 -1
- package/dist/prisma/index.d.ts +14 -7
- package/dist/prisma/index.js +279 -70
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/rateLimit-5RKBFLUR.js.map +1 -0
- package/dist/remote-N7OUAGOW.js +15 -0
- package/dist/remote-N7OUAGOW.js.map +1 -0
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +277 -270
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/express/index.d.ts +9 -2
- package/dist/server/express/index.js +1 -1
- package/dist/server/index.d.ts +268 -84
- package/dist/server/index.js +664 -418
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +89 -27
- package/dist/server/mcp/index.js +281 -62
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-DVa8W-yR.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +19 -10
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +50 -13
- package/dist/testing/index.js +113 -21
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +17 -9
- package/dist/tracked-BR5b2O-U.d.ts +43 -0
- package/dist/{types-Cu0vmx7c.d.ts → types-CLRCQGnO.d.ts} +4 -3
- package/dist/types-tMagHbjy.d.ts +151 -0
- package/dist/utils/index.d.ts +16 -4
- package/dist/utils/index.js +6 -3
- package/package.json +5 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-4YO5H5ON.js.map +0 -1
- package/dist/chunk-B2P7AZ25.js.map +0 -1
- package/dist/chunk-CDKG4TFA.js.map +0 -1
- package/dist/chunk-DD4SKCNO.js.map +0 -1
- package/dist/chunk-H23KEFBA.js.map +0 -1
- package/dist/chunk-OECZPRPW.js.map +0 -1
- package/dist/chunk-QEKVYG5F.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-RZTP4IEW.js.map +0 -1
- package/dist/chunk-SHEGLZEP.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VSQBHPWH.js.map +0 -1
- package/dist/chunk-X5CWPX7H.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
- package/dist/chunk-ZIVPHYRG.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → client/core.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/protocol/envelope.ts"],"sourcesContent":["// Protocol v5 frames (RFC 0003 sections 8.2 and 8.3): what each `qd:*` event\n// carries and what its acknowledgement receives. `ClientToServerEvents` and\n// `ServerToClientEvents` key every frame by its event name from\n// `contract/names.ts`, in the event-map form Socket.IO's `Server` and the\n// client's `Socket` take as type parameters.\n//\n// Section 8.2 spells out the `qd:call`, `qd:cancel`, `qd:watch` and `qd:ch`\n// payloads, and sections 6, 7, 11.3 and 12.5 describe the rest. Fields the RFC\n// does not name follow its style: `s` service, `m` method, `c` collection,\n// `i` input, `d` data, `e` error, `v` version, `rev` revision.\n//\n// 4.1 used one event per service method and a `{ success, data }` reply\n// (4.1 `src/shared/types.ts:88-90`). Here every call travels as one\n// `qd:call` envelope and is answered `{ ok: true, d }` or `{ ok: false, e }`.\n//\n// Runtime guards cover the frames the dispatcher reads from an untrusted\n// client (`qd:call`, `qd:cancel`). They are written by hand so the package\n// root stays free of schema libraries.\n//\n// One rule for every frame, both ways: a receiver ignores the object fields\n// it does not know and the array elements after the last one it reads, and\n// a later revision of protocol 5 may only add fields to objects and append\n// elements to arrays (`qd:event`, `qd:stream`, `qd:ch`), never remove,\n// rename, retype or reorder what is here. So a client that reads only what\n// it knows keeps working against a newer server, and a server against a\n// newer client. Anything else needs a new protocol number (`qd:hello`'s\n// `protocol`), which the handshake refuses with `PROTOCOL_MISMATCH`.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport type { CLIENT_EVENTS, SERVER_EVENTS } from \"../contract/names\";\nimport type { WireError } from \"./errors\";\nimport { isName, isRecord } from \"./guards\";\nimport type { HelloFrame } from \"./version\";\n\n/**\n * A revision: a whole number of microseconds since the epoch that every\n * flush raises (RFC 0003 section 5.3), `max(Date.now() * 1000, last + 1)` on\n * one process and the shared counter's next value (Valkey's clock in\n * microseconds) behind a cluster; a safe integer, about 1.8e15. Compare\n * revisions as numbers; do not read them as times.\n */\nexport type Revision = number;\n\n/**\n * The version of a result the caller holds, sent back as a call's `v` so the\n * server can answer \"not modified\" (RFC 0003 section 9, step 5): a row's\n * revision, or what the method's `version(input, ctx)` returns. Opaque to the\n * client.\n */\nexport type Version = number | string;\n\n/** A call's number among its socket's calls in flight: a non-negative safe integer. */\nexport type CallId = number;\n\n/** An acknowledgement that succeeded with nothing to return. */\nexport interface Ok {\n readonly ok: true;\n}\n\n/** An acknowledgement that failed, and why. */\nexport interface Failure {\n readonly ok: false;\n readonly e: WireError;\n}\n\n// ---------------------------------------------------------------------------\n// Calls (section 8.2)\n// ---------------------------------------------------------------------------\n\n/** `qd:call`: one method call, answered through the acknowledgement. */\nexport interface CallEnvelope {\n /** Names the call for `qd:cancel`; unique among the socket's calls in flight. */\n readonly id: CallId;\n /** The service name. */\n readonly s: string;\n /** The method name. */\n readonly m: string;\n /** The input, not yet validated. Absent when the input is `undefined`. */\n readonly i?: unknown;\n /** The version of this call's result the caller already holds. */\n readonly v?: Version;\n}\n\n/** A call that returned data, with the version of that data when the method has one. */\nexport interface CallSuccess<Output = unknown> {\n readonly ok: true;\n readonly d: Output;\n readonly v?: Version;\n readonly nm?: undefined;\n}\n\n/** A call whose result is still the version the caller sent: keep the cached copy. */\nexport interface CallNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly v: Version;\n}\n\n/** The acknowledgement of `qd:call`. */\nexport type CallReply<Output = unknown> = CallSuccess<Output> | CallNotModified | Failure;\n\n/** `qd:cancel`: abort a call in flight. The call's acknowledgement still arrives. */\nexport interface CancelFrame {\n readonly id: CallId;\n}\n\n// ---------------------------------------------------------------------------\n// Entity subscriptions (section 6)\n// ---------------------------------------------------------------------------\n\n/** `qd:sub`: subscribe to rows of one service, sending the revision held for each. */\nexport interface EntitySubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n /** The revision held for each id, by position in `ids`; `null` where none is cached. */\n readonly revs?: readonly (Revision | null)[];\n}\n\n/** One id's row, as `qd:sub` answers it. */\nexport interface EntityRow<Row = unknown> {\n readonly ok: true;\n readonly d: Row;\n readonly rev: Revision;\n readonly nm?: undefined;\n}\n\n/** The revision the client sent for this id is current. */\nexport interface EntityNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly rev: Revision;\n}\n\n/** One id's answer to `qd:sub`: the row, \"not modified\", or why it was refused. */\nexport type EntityResult<Row = unknown> = EntityRow<Row> | EntityNotModified | Failure;\n\n/** The acknowledgement of `qd:sub`: one result per requested id, in request order. */\nexport type EntitySubscribeReply<Row = unknown> =\n | { readonly ok: true; readonly r: readonly EntityResult<Row>[] }\n | Failure;\n\n/** `qd:unsub`: stop receiving frames for these rows. */\nexport interface EntityUnsubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n}\n\n/** `qd:e` `u`: the full row. Replace the cached row when `rev` is newer. */\nexport interface EntityUpdate<Row = unknown> {\n readonly t: \"u\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Row;\n}\n\n/** `qd:e` `p`: changed fields only. Merge when `rev` is newer; fetch the row when it is not cached. */\nexport interface EntityPatch<Row = unknown> {\n readonly t: \"p\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Partial<Row>;\n}\n\n/** `qd:e` `r`: the row is gone. Keep a tombstone until a newer `u`. */\nexport interface EntityRemove {\n readonly t: \"r\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n}\n\n/** `qd:e`: a change to one row, sent to the room of each access tier that has subscribers. */\nexport type EntityFrame<Row = unknown> = EntityUpdate<Row> | EntityPatch<Row> | EntityRemove;\n\n// ---------------------------------------------------------------------------\n// Collections (section 7)\n// ---------------------------------------------------------------------------\n\n/** One scope of one collection of a service. */\nexport interface CollectionScopeRef {\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n}\n\n/** `qd:col:sub`: the first page of a scope, a later page by `cursor`, or a resume from `since`. */\nexport interface CollectionSubscribe extends CollectionScopeRef {\n /** The revision the client holds the scope at; the server resumes from it when it can. */\n readonly since?: Revision;\n readonly limit?: number;\n readonly cursor?: string;\n}\n\n/**\n * A scope member in the collection's index (RFC 0003 section 7.4): its id,\n * its revision, then its index field values in the order `index` declares.\n * The revision is the time in the service's `versionColumn` when it declares\n * one (in microseconds, as revisions are), else the revision the snapshot\n * (or, in an `added` delta, the flush) was read at.\n */\nexport type WireIndexRow = readonly [id: string, rev: Revision, ...fields: unknown[]];\n\n/** A page of a scope: the answer to `qd:col:sub` without `since`, or when `since` is too old. */\nexport interface CollectionSnapshot<Item = unknown> {\n readonly ok: true;\n readonly resumed?: undefined;\n /** The revision the page was read at; resume from it with `since`. */\n readonly rev: Revision;\n readonly items: readonly Item[];\n /** How many members the scope has. */\n readonly total: number;\n /** The next page's cursor; `null` on the last page. */\n readonly cursor: string | null;\n /** The page size used: the request's, or `maxLimit` when the request asked for more. */\n readonly limit: number;\n /** Present when the requested limit was above `maxLimit` and was lowered to it. */\n readonly clamped?: true;\n /**\n * One row per member in `order`, on the first page of a collection that\n * declares `index` (not on a page read with `cursor`). Absent when the\n * scope has more members than the index holds: see `indexTruncated`.\n */\n readonly index?: readonly WireIndexRow[];\n /** Present instead of `index` when the scope has more members than the index holds (50,000). */\n readonly indexTruncated?: true;\n}\n\n/** The answer to `qd:col:sub` when the server still holds every change since `since`. */\nexport interface CollectionResumed<Item = unknown> {\n readonly ok: true;\n readonly resumed: true;\n /** The scope's revision after these deltas. */\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n/** The acknowledgement of `qd:col:sub`. */\nexport type CollectionSubscribeReply<Item = unknown> =\n | CollectionSnapshot<Item>\n | CollectionResumed<Item>\n | Failure;\n\n/** `qd:col:items`: load full items of a scope by id. */\nexport interface CollectionItemsRequest extends CollectionScopeRef {\n readonly ids: readonly string[];\n}\n\n/**\n * The acknowledgement of `qd:col:items`: the items found, in request order,\n * and the revision they were read at, taken before the read as a\n * snapshot's is: a client drops an item older than what a delta brought it.\n */\nexport type CollectionItemsReply<Item = unknown> =\n | { readonly ok: true; readonly rev: Revision; readonly items: readonly Item[] }\n | Failure;\n\n/**\n * One change to a scope (RFC 0003 section 7.2). In a collection that declares\n * `index`, `added` carries the member's index row, built from the same row as\n * its item, and `patched` and `updated` carry every changed index field\n * (index fields are item fields): a client keeping the index updates the\n * row's fields from them, and its `rev` to the frame's `rev`.\n */\nexport type CollectionDelta<Item = unknown> =\n | { readonly t: \"added\"; readonly item: Item; readonly index?: WireIndexRow }\n | { readonly t: \"updated\"; readonly item: Item }\n | { readonly t: \"patched\"; readonly id: string; readonly d: Partial<Item> }\n | { readonly t: \"removed\"; readonly id: string }\n | { readonly t: \"reset\" };\n\n/** `qd:c`: one flush's changes to one scope, in order. */\nexport interface CollectionFrame<Item = unknown> extends CollectionScopeRef {\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n// ---------------------------------------------------------------------------\n// Topics, streams, channels and events (sections 8.2, 11.3 and 12.5)\n// ---------------------------------------------------------------------------\n\n/**\n * `qd:watch` and `qd:unwatch`: join or leave a change topic of service `s`:\n * `{collection}:{scope}` for one scope of a collection, or `service` for the\n * service-wide topic (RFC 0003 section 11.3).\n */\nexport interface WatchFrame {\n readonly s: string;\n readonly topic: string;\n}\n\n/**\n * `qd:changed`: a watched topic changed in the flush at `rev`; invalidate the\n * queries that watch it. Sent once per flush per topic, and carries no data.\n * On the `service` topic, `models` names the models whose writes changed it\n * (rc.5, additive: the client's model names, first letter lowercased), so a\n * query that watches only some of them (`watch: { service: [models] }`)\n * ignores the rest; a frame without it may have changed any of them.\n */\nexport interface ChangedFrame {\n readonly s: string;\n readonly topic: string;\n readonly rev: Revision;\n readonly models?: readonly string[];\n}\n\n/**\n * `qd:stream:sub` and `qd:stream:unsub`: one stream of a service, and for a\n * scoped stream one scope of it (absent for a global stream).\n */\nexport interface StreamSubscribe {\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n}\n\n/**\n * The acknowledgement of `qd:stream:sub`: the stream's seed, oldest first,\n * read as the socket joined the stream's room, so items pushed after it\n * arrive as `qd:stream` frames (possibly before this acknowledgement). The\n * seed is the latest items the server kept, or what the service computed\n * for this subscriber (the current state the items change); a computed one\n * the server had to wait for may also reflect items that arrived as frames\n * meanwhile. A client replaces what it held with it.\n */\nexport type StreamSubscribeReply<Item = unknown> =\n | { readonly ok: true; readonly seed: readonly Item[] }\n | Failure;\n\n/**\n * `qd:stream`: an item pushed to a stream, as one array argument\n * `[service, stream, scope, item]` (like `qd:event`), so a fast stream sends\n * no key names: `scope` is `null` for a global stream. The positions are\n * fixed; a later revision may append elements after `item`, never insert\n * them, so a client reads the four it knows and ignores the rest. Before\n * `5.0.0-rc.4` it was the object `{ s, stream, scope?, item }`.\n */\nexport type StreamFrame<Item = unknown> = readonly [\n s: string,\n stream: string,\n scope: string | null,\n item: Item,\n];\n\n/**\n * `qd:ch`, sent volatile and never acknowledged: `[service, channel,\n * payload]`. A later revision may append elements after `payload`; the\n * server reads the three it knows and ignores the rest.\n */\nexport type ChannelFrame<Payload = unknown> = readonly [\n s: string,\n channel: string,\n payload: Payload,\n];\n\n/**\n * `qd:event`: a custom room event declared in a contract's `events`:\n * `[service, event, payload]`. A later revision may append elements after\n * `payload`; a client reads the three it knows and ignores the rest.\n */\nexport type EventFrame<Payload = unknown> = readonly [s: string, event: string, payload: Payload];\n\n/**\n * `qd:presence`: who is in an app room (one a method joined with\n * `ctx.rooms.join`) the socket is in, by user id (RFC 0003 section 12.5).\n * Sent to a socket as it joins with `users`, the whole list (its own user\n * included); to the room's other sockets with `joined` when a user's first\n * socket joins, and with `left` when a user's last socket leaves; and to a\n * socket that leaves with `users: []`, since it no longer sees the room. A\n * socket the server took out of the room (`rooms.leave(room, { userId })`)\n * gets that `users: []` unasked: it is out, and its channels requiring the\n * room are dropped until a joining call lets it back. Exactly one of\n * `users`, `joined` and `left` is present. Anonymous sockets are in no list.\n */\nexport interface PresenceFrame {\n readonly room: string;\n /** Every user in the room: replaces what the client holds for it. */\n readonly users?: readonly string[];\n /** A user who joined the room. */\n readonly joined?: string;\n /** A user who left the room: their last socket in it left or disconnected. */\n readonly left?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Connection-level frames (sections 4.4, 7.2 and 8.3)\n// ---------------------------------------------------------------------------\n\n/** Why the server ended a subscription. */\nexport type RevokeReason = \"access\" | \"anchor-deleted\";\n\n/**\n * `qd:revoked`: the server removed this socket from a subscription's room,\n * because the principal's access was lowered or removed (`access`), or the\n * row a collection scope is anchored on was deleted (`anchor-deleted`). A\n * stream feed's subscription is revoked for `access` only; `scope` is absent\n * for a global stream.\n */\nexport type RevokedFrame =\n | {\n readonly kind: \"entity\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly id: string;\n }\n | {\n readonly kind: \"collection\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n }\n | {\n readonly kind: \"stream\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n };\n\n/** `qd:rotate`: stay connected until a random moment within `withinMs`, then reconnect. */\nexport interface RotateFrame {\n readonly withinMs: number;\n}\n\n/** `qd:access`: the user's service grants changed. */\nexport interface AccessFrame {\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n\n// ---------------------------------------------------------------------------\n// Event maps\n// ---------------------------------------------------------------------------\n\n/** The function a listener calls to acknowledge an event. */\ntype Ack<Reply> = (reply: Reply) => void;\n\n/** The listener of each client event, by its key in `CLIENT_EVENTS`. */\ninterface ClientListeners {\n call: (envelope: CallEnvelope, ack: Ack<CallReply>) => void;\n cancel: (frame: CancelFrame) => void;\n sub: (frame: EntitySubscribe, ack: Ack<EntitySubscribeReply>) => void;\n unsub: (frame: EntityUnsubscribe, ack?: Ack<Ok | Failure>) => void;\n collectionSub: (frame: CollectionSubscribe, ack: Ack<CollectionSubscribeReply>) => void;\n collectionUnsub: (frame: CollectionScopeRef, ack?: Ack<Ok | Failure>) => void;\n collectionItems: (frame: CollectionItemsRequest, ack: Ack<CollectionItemsReply>) => void;\n watch: (frame: WatchFrame, ack: Ack<Ok | Failure>) => void;\n unwatch: (frame: WatchFrame, ack?: Ack<Ok | Failure>) => void;\n streamSub: (frame: StreamSubscribe, ack: Ack<StreamSubscribeReply>) => void;\n streamUnsub: (frame: StreamSubscribe, ack?: Ack<Ok | Failure>) => void;\n channel: (frame: ChannelFrame) => void;\n}\n\n/** The listener of each server event, by its key in `SERVER_EVENTS`. */\ninterface ServerListeners {\n hello: (frame: HelloFrame) => void;\n entity: (frame: EntityFrame) => void;\n collection: (frame: CollectionFrame) => void;\n changed: (frame: ChangedFrame) => void;\n revoked: (frame: RevokedFrame) => void;\n stream: (frame: StreamFrame) => void;\n event: (frame: EventFrame) => void;\n presence: (frame: PresenceFrame) => void;\n rotate: (frame: RotateFrame) => void;\n access: (frame: AccessFrame) => void;\n}\n\n/**\n * The events a v5 client sends, keyed by name (`\"qd:call\"`, ...). The server\n * types its sockets with it: `new Server<ClientToServerEvents, ServerToClientEvents>()`.\n */\nexport type ClientToServerEvents = {\n [Key in keyof typeof CLIENT_EVENTS as (typeof CLIENT_EVENTS)[Key]]: ClientListeners[Key];\n};\n\n/**\n * The events a v5 server sends, keyed by name (`\"qd:hello\"`, ...). The client\n * types its socket with it: `Socket<ServerToClientEvents, ClientToServerEvents>`.\n */\nexport type ServerToClientEvents = {\n [Key in keyof typeof SERVER_EVENTS as (typeof SERVER_EVENTS)[Key]]: ServerListeners[Key];\n};\n\n// ---------------------------------------------------------------------------\n// Guards\n// ---------------------------------------------------------------------------\n\nfunction isCallId(value: unknown): value is CallId {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction isVersion(value: unknown): value is Version {\n return typeof value === \"string\" || (typeof value === \"number\" && Number.isFinite(value));\n}\n\n/**\n * True when `value` has the shape of a `qd:call` envelope: a call id, service\n * and method names, and a version when one is present. The input is not\n * looked at (the method's schema validates it), and unknown keys are ignored.\n */\nexport function isCallEnvelope(value: unknown): value is CallEnvelope {\n return (\n isRecord(value) &&\n isCallId(value.id) &&\n isName(value.s) &&\n isName(value.m) &&\n (value.v === undefined || isVersion(value.v))\n );\n}\n\n/** True when `value` has the shape of a `qd:cancel` frame. Unknown keys are ignored. */\nexport function isCancel(value: unknown): value is CancelFrame {\n return isRecord(value) && isCallId(value.id);\n}\n"],"mappings":";;;;;;AAweA,SAAS,SAAS,OAAiC;AACjD,SAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS;AAC9E;AAEA,SAAS,UAAU,OAAkC;AACnD,SAAO,OAAO,UAAU,YAAa,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AACzF;AAOO,SAAS,eAAe,OAAuC;AACpE,SACE,SAAS,KAAK,KACd,SAAS,MAAM,EAAE,KACjB,OAAO,MAAM,CAAC,KACd,OAAO,MAAM,CAAC,MACb,MAAM,MAAM,UAAa,UAAU,MAAM,CAAC;AAE/C;AAGO,SAAS,SAAS,OAAsC;AAC7D,SAAO,SAAS,KAAK,KAAK,SAAS,MAAM,EAAE;AAC7C;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/pipeline/request.ts","../src/server/transports/auth.ts","../src/server/auth/jwt.ts","../src/server/auth/sessionCookie.ts","../src/server/auth/restMiddleware.ts","../src/server/transports/ack.ts","../src/server/transports/body.ts"],"sourcesContent":["// What a transport hands the dispatcher for one call, and what it gets back.\n// Every transport (Socket.IO, HTTP, MCP, in-process, the 4.x shim) builds a\n// `DispatchRequest`; the reply goes out through `respond`, so the pipeline\n// can measure its size and flush only after it was sent (RFC 0003 section 9,\n// step 9).\n\nimport type { CallReply, CallSuccess, Version } from \"../../protocol/envelope\";\nimport { toWire, type QuickdrawError } from \"../../protocol/errors\";\nimport type { McpContext, Principal, Transport } from \"../types\";\n\n/** How one call ended. The dispatcher's `call` resolves with it and never rejects. */\nexport type DispatchResult =\n | {\n readonly ok: true;\n readonly data: unknown;\n /** The version of `data`, when the query has one; the caller sends it back as `v`. */\n readonly version?: Version;\n readonly notModified?: undefined;\n }\n | {\n /** The caller's version is current: it keeps its cached result. */\n readonly ok: true;\n readonly notModified: true;\n readonly version: Version;\n }\n | {\n readonly ok: false;\n /** Why the call failed. For `INTERNAL`, `cause` holds the original error; `toWire` never sends it. */\n readonly error: QuickdrawError;\n };\n\n/**\n * A shared run's result as one group of its callers receives it: every\n * caller whose levels hide the same fields gets the same `data`, and its JSON\n * text is written once for all of them (RFC 0003 section 9, step 6). The\n * dispatcher hands it to `respond` beside the result.\n */\nexport interface SharedData {\n /** The `data` of the result it was handed with. */\n readonly data: unknown;\n /**\n * `data` as JSON text, written the first time any caller of the group asks\n * and reused after: what `JSON.stringify(data)` returns, `undefined`\n * included. Throws what `JSON.stringify` throws.\n */\n json(): string | undefined;\n}\n\n/** One method call, as a transport hands it to the dispatcher. */\nexport interface DispatchRequest {\n /** The service name. */\n readonly service: string;\n /** The method name. */\n readonly method: string;\n /** The input as received; the method's schema validates it. */\n readonly input: unknown;\n /** The authenticated caller, or `null` when anonymous. */\n readonly principal: Principal | null;\n readonly transport: Transport;\n /**\n * The connection the call arrived on. Queries are capped per connection;\n * a call without one (in-process, HTTP) is not capped.\n */\n readonly connectionId?: string;\n /** Aborts when the caller cancels the call or goes away. Only queries can be cancelled. */\n readonly signal?: AbortSignal;\n /** Identifies the call in logs; generated when absent. */\n readonly requestId?: string;\n /** The version of the result the caller already holds. */\n readonly v?: Version;\n /**\n * The fields the MCP bridge's `context` option produced for this call,\n * which the handler reads as `ctx.mcp`. Only the MCP transport sets it.\n */\n readonly mcp?: McpContext;\n /**\n * Sends the result to the caller and returns the reply's size in bytes,\n * when the transport measured it. Called once, before the flush and the\n * completion record. An error it throws is logged and does not change the\n * result. For data taken from a shared run, `shared` carries that data's\n * JSON text, written once for every caller that receives the same copy;\n * a transport that writes JSON may send it instead of encoding the data\n * again.\n */\n readonly respond?: (result: DispatchResult, shared?: SharedData) => number | undefined;\n}\n\n/** A dispatch result in the acknowledgement shape of `qd:call` (RFC 0003 section 8.2). */\nexport function toCallReply(result: DispatchResult): CallReply {\n if (!result.ok) {\n return { ok: false, e: toWire(result.error) };\n }\n if (result.notModified === true) {\n return { ok: true, nm: true, v: result.version };\n }\n return result.version === undefined\n ? { ok: true, d: result.data }\n : { ok: true, d: result.data, v: result.version };\n}\n\n/**\n * The JSON text of a `qd:call` acknowledgement that carries data, written\n * around `dataJson`, the data's own JSON text: what `JSON.stringify(reply)`\n * writes, without encoding the data again.\n */\nexport function callReplyJson(reply: CallSuccess, dataJson: string): string {\n const version = reply.v === undefined ? \"\" : `,\"v\":${JSON.stringify(reply.v)}`;\n return `{\"ok\":true,\"d\":${dataJson}${version}}`;\n}\n","// Who a socket connection or an HTTP request acts for (RFC 0003 sections 3\n// and 10). Ported from 4.1's socket middleware\n// (4.1 `src/server/createServer.ts:85-113`): `authenticate` returns a user\n// id or a principal, and `loadServiceAccess(userId)` supplies the service\n// grants when the principal carries none. 4.1 asked only about sockets; here\n// the same hooks also authenticate HTTP calls, so `authenticate` receives the\n// transport, the credentials and the headers instead of the socket alone.\n\nimport type { IncomingHttpHeaders, IncomingMessage } from \"node:http\";\nimport type { Socket } from \"socket.io\";\nimport type { AccessLevel } from \"../../contract/access\";\nimport type { Logger } from \"../../contract/logger\";\nimport { QuickdrawError } from \"../../protocol/errors\";\nimport { modelKey } from \"../storage\";\nimport type { MaybePromise, Principal } from \"../types\";\nimport type { FlushSink } from \"../uow/flushSink\";\nimport { ANY_FIELD } from \"../uow/types\";\nimport type { HttpCredentialSource } from \"./body\";\n\nexport type { HttpCredentialSource };\n\n/** A principal's service-wide grants by service name: `{ taskService: \"Admin\" }`. */\nexport type ServiceGrants = Readonly<Record<string, AccessLevel>>;\n\ninterface AuthenticateRequestBase {\n /**\n * The credentials. For a socket, its handshake `auth` as the client sent\n * it (`{ token, qd }` from a v5 client). For an HTTP request, `{ token }`\n * with its session cookie or bearer token, or `{}` when it has neither.\n */\n readonly auth: Readonly<Record<string, unknown>>;\n /** The request's headers, or the socket handshake's. */\n readonly headers: IncomingHttpHeaders;\n}\n\n/** `authenticate`'s question about a connecting socket. */\nexport interface SocketAuthenticateRequest extends AuthenticateRequestBase {\n readonly transport: \"socket\";\n readonly socket: Socket;\n}\n\n/** `authenticate`'s question about an HTTP call. */\nexport interface HttpAuthenticateRequest extends AuthenticateRequestBase {\n readonly transport: \"http\";\n readonly req: IncomingMessage;\n /**\n * Where `auth.token` came from (finding F7.1): `\"cookie\"`, the session\n * cookie, which a browser sends with a request any page makes, so an\n * `authenticate` that accepts it checks the request's `Origin` as\n * `socketAuth` does; `\"bearer\"`, an `Authorization: Bearer` header, which\n * a page sends only on purpose. Absent without a token.\n */\n readonly credential?: HttpCredentialSource;\n}\n\n/** What `authenticate` is asked about: a socket connecting, or an HTTP call. */\nexport type AuthenticateRequest = SocketAuthenticateRequest | HttpAuthenticateRequest;\n\n/**\n * What `authenticate` returns: the principal; its user id, when a bare\n * `{ userId }` is a valid principal of the app's type; or `null` or\n * `undefined` for an anonymous caller.\n */\nexport type AuthenticateResult<P extends Principal> =\n | P\n | ({ readonly userId: string } extends P ? string : never)\n | null\n | undefined;\n\n/** How a server authenticates sockets and HTTP calls. Without it every caller is anonymous. */\nexport interface ServerAuth<P extends Principal = Principal> {\n /**\n * Says who is calling. Throwing refuses the socket connection (\"Authentication\n * failed\", with `{ code: \"UNAUTHENTICATED\" }` as the `connect_error` data) or\n * answers the HTTP call with `UNAUTHENTICATED`; returning nothing lets the\n * caller in anonymously, so only `\"public\"` methods pass. Throw a\n * `QuickdrawError(\"UNAUTHENTICATED\", ...)` for a refusal that is the\n * caller's doing (it logs at debug; anything else thrown logs at error).\n * On an HTTP call a thrown `QuickdrawError(\"FORBIDDEN\", ...)` is answered\n * as it is, with 403 (logged at debug): a credential the request may not\n * use from where it came, as `socketAuth` answers a session cookie sent\n * from a page outside `allowedOrigins`.\n */\n readonly authenticate?: (request: AuthenticateRequest) => MaybePromise<AuthenticateResult<P>>;\n /**\n * Loads a user's service grants (for example `User.serviceAccess`) when\n * the principal `authenticate` returned has none. `access.refresh(userId)`\n * calls it again.\n */\n readonly loadServiceAccess?: (userId: string) => MaybePromise<ServiceGrants | null | undefined>;\n /**\n * Where `loadServiceAccess` reads grants from, as `{ model: \"user\", column:\n * \"serviceAccess\" }`, the row's id being the user's. A tracked write that\n * sets that column, or creates, deletes or touches such a row, refreshes\n * that user's grants once it is flushed (`server.access.refresh`), so a\n * changed grant reaches their sockets without a reconnect (RFC 0003\n * section 4.4). Needs `loadServiceAccess`.\n */\n readonly serviceAccessSource?: ServiceAccessSource;\n}\n\n/** Where a server's grants are stored: one row per user, the row's id being the user's. */\nexport interface ServiceAccessSource {\n /** The model, as the client names it: `\"user\"`. */\n readonly model: string;\n /** The column holding the grants: `\"serviceAccess\"`. */\n readonly column: string;\n}\n\n/** Resolves a request to its principal, or `null`; rejects when authentication fails. */\nexport type ResolvePrincipal = (request: AuthenticateRequest) => Promise<Principal | null>;\n\nconst SOCKET_SESSIONS = new WeakMap<object, string>();\n\n/**\n * Records the session a socket authenticated with, so that\n * `server.access.disconnectUser(userId, { sessionId })` can end that\n * session's sockets alone. `socketAuth` calls it for the auth routes'\n * sessions; an app's own `authenticate` may too.\n */\nexport function recordSocketSession(socket: object, sessionId: string): void {\n SOCKET_SESSIONS.set(socket, sessionId);\n}\n\n/** The session a socket authenticated with, when its `authenticate` recorded one. */\nexport function socketSessionOf(socket: object): string | undefined {\n return SOCKET_SESSIONS.get(socket);\n}\n\n/**\n * True for a refusal `authenticate` made on purpose, by throwing a\n * `QuickdrawError` with code `UNAUTHENTICATED` (a revoked session, a socket\n * from a page the cookie may not be used from). The transports log those at\n * debug, as the client's doing, and every other failure at error.\n */\nexport function isRefusal(error: unknown): boolean {\n return error instanceof QuickdrawError && error.code === \"UNAUTHENTICATED\";\n}\n\n/**\n * True for an HTTP call's refusal that `authenticate` made on purpose and\n * the transport answers as it is: a `QuickdrawError` with code `FORBIDDEN`\n * (a session cookie sent from a page `socketAuth` does not allow).\n */\nexport function isForbiddenCredential(error: unknown): error is QuickdrawError {\n return error instanceof QuickdrawError && error.code === \"FORBIDDEN\";\n}\n\n/** True when `value` has a principal's shape: an object with a non-empty string `userId`. */\nexport function isPrincipal(value: unknown): value is Principal {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { readonly userId?: unknown }).userId === \"string\" &&\n (value as { readonly userId: string }).userId.length > 0\n );\n}\n\n/**\n * The principal an `authenticate` result stands for: a principal as is, a\n * user id as `{ userId }`, nothing as `null`. Throws `TypeError` for anything\n * else. The MCP bridge reads its `principal` hook's result the same way.\n */\nexport function toPrincipal(result: unknown): Principal | null {\n if (result === null || result === undefined || result === \"\") {\n return null;\n }\n if (typeof result === \"string\") {\n return { userId: result };\n }\n if (isPrincipal(result)) {\n return result;\n }\n throw new TypeError(\n \"authenticate must return a principal with a non-empty string userId, a user id, or null\",\n );\n}\n\n/** Builds the function both transports authenticate with. */\nexport function createPrincipalResolver<P extends Principal>(\n auth: ServerAuth<P> | undefined,\n): ResolvePrincipal {\n const authenticate = auth?.authenticate;\n const loadServiceAccess = auth?.loadServiceAccess;\n return async (request) => {\n if (authenticate === undefined) {\n return null;\n }\n const principal = toPrincipal(await authenticate(request));\n const carried = principal?.serviceAccess;\n if (principal === null || (carried !== undefined && carried !== null)) {\n return principal;\n }\n if (loadServiceAccess === undefined) {\n return principal;\n }\n const serviceAccess = (await loadServiceAccess(principal.userId)) ?? {};\n return { ...principal, serviceAccess };\n };\n}\n\n/**\n * The flush sink that refreshes the grants of the users a flush wrote stored\n * grants for, or `undefined` without `serviceAccessSource`. `refresh` is\n * `server.access.refresh`, given once the server exists. Throws a\n * `TypeError` for a source without `loadServiceAccess`, or a malformed one.\n */\nexport function createGrantsSink<P extends Principal>(\n auth: ServerAuth<P> | undefined,\n refresh: () => ((userId: string) => Promise<unknown>) | undefined,\n logger: Logger,\n): FlushSink | undefined {\n const source = auth?.serviceAccessSource;\n if (source === undefined) {\n return undefined;\n }\n const named = (value: unknown): boolean => typeof value === \"string\" && value.length > 0;\n if (!named(source.model) || !named(source.column)) {\n throw new TypeError(\"createServer: auth.serviceAccessSource must be { model, column }\");\n }\n if (auth?.loadServiceAccess === undefined) {\n throw new TypeError(\n \"createServer: auth.serviceAccessSource needs auth.loadServiceAccess to reload the grants it stores\",\n );\n }\n const model = modelKey(source.model);\n return Object.freeze({\n async flush(writes): Promise<void> {\n const users = new Set<string>();\n for (const write of writes) {\n const sets = write.fields.includes(source.column) || write.fields.includes(ANY_FIELD);\n if (modelKey(write.model) === model && (write.op !== \"update\" || sets)) {\n users.add(write.id);\n }\n }\n await Promise.all(\n [...users].map(async (userId) => {\n try {\n await refresh()?.(userId);\n } catch (error) {\n logger.error(\"Refreshing a user's written grants failed\", {\n category: \"quickdraw.access\",\n userId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }),\n );\n },\n } satisfies FlushSink);\n}\n","import * as jose from \"jose\";\n\nexport interface JWTPayload {\n userId: string;\n email?: string;\n /**\n * The session the token belongs to: the id of the auth routes kit's\n * `SessionStore` entry, which `socketAuth` checks on every handshake.\n */\n sid?: string;\n exp?: number;\n iat?: number;\n}\n\nconst DEFAULT_EXPIRATION = \"7d\";\n\n/**\n * Create a JWT token for authentication.\n *\n * @param payload - The payload to encode in the JWT\n * @param secret - The secret key for signing\n * @param expiresIn - Expiration time (default: 7 days)\n */\nexport async function createJWT(\n payload: Omit<JWTPayload, \"exp\" | \"iat\">,\n secret: string,\n expiresIn: string = DEFAULT_EXPIRATION,\n): Promise<string> {\n const secretKey = new TextEncoder().encode(secret);\n\n const jwt = await new jose.SignJWT(payload as jose.JWTPayload)\n .setProtectedHeader({ alg: \"HS256\" })\n .setIssuedAt()\n .setExpirationTime(expiresIn)\n .sign(secretKey);\n\n return jwt;\n}\n\n/**\n * Verify and decode a JWT token.\n *\n * @param token - The JWT token to verify\n * @param secret - The secret key for verification\n * @returns The decoded payload, or null if invalid\n */\nexport async function verifyJWT(token: string, secret: string): Promise<JWTPayload | null> {\n try {\n const secretKey = new TextEncoder().encode(secret);\n const { payload } = await jose.jwtVerify(token, secretKey);\n\n return {\n userId: payload.userId as string,\n email: payload.email as string | undefined,\n ...(typeof payload.sid === \"string\" && { sid: payload.sid }),\n exp: payload.exp,\n iat: payload.iat,\n };\n } catch {\n return null;\n }\n}\n","/**\n * The session cookie: the one rule its name follows, written and read, and\n * the 4.x helpers that set and clear it.\n *\n * Uses structural request and response types so the module works with\n * Express (or any compatible framework) without a runtime/type dependency on\n * it.\n */\n\nimport type { IncomingHttpHeaders } from \"node:http\";\n\nexport const SESSION_COOKIE = \"session\";\n\n/**\n * The session cookie's name on a secure request when no cookie domain is\n * configured. The `__Host-` prefix makes a browser keep it only when it is\n * Secure, has `Path=/` and no `Domain`, so no other site under the same\n * parent domain can set or replace it.\n */\nexport const HOST_SESSION_COOKIE = \"__Host-session\";\n\n// Matches the default JWT expiry (\"7d\" in jwt.ts) — a cookie that outlives\n// its JWT just keeps sending a token the server will reject. Pass maxAgeMs\n// if your JWT lifetime differs.\nconst DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;\n\n/**\n * A request or a socket handshake, as far as the session cookie's name\n * depends on it: its headers, Express's `req.secure` (which follows\n * `X-Forwarded-Proto` behind `trust proxy`) or a Socket.IO handshake's\n * `secure`, and its connection.\n */\nexport interface SessionCookieRequest {\n readonly headers: IncomingHttpHeaders;\n readonly secure?: unknown;\n /** The connection: TLS ended at this server when it is `encrypted`. */\n readonly socket?: unknown;\n}\n\n/** How an app named its session cookie: a name it gave, and the domain it shares the cookie with. */\nexport interface SessionCookieNaming {\n readonly cookieName?: string | undefined;\n /** `undefined` or `\"\"`: no domain, the API's host only. */\n readonly domain?: string | undefined;\n}\n\n/**\n * True when a request or a socket handshake came over HTTPS: `tls` says its\n * connection did (TLS ended at this server, Express's `req.secure`, or a\n * Socket.IO handshake's `secure`), a proxy says so (`X-Forwarded-Proto:\n * https`), or it came from an `https:` page (`Origin`), which a browser lets\n * reach only `https:` URLs and loopback hosts. Each signal only narrows the\n * session cookies read (`sessionCookieNamesFor`), so none of them needs to\n * be trusted.\n */\nexport function isSecureRequest(headers: IncomingHttpHeaders, tls: boolean): boolean {\n if (tls) {\n return true;\n }\n const forwarded = headers[\"x-forwarded-proto\"];\n const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(\",\", 1)[0];\n if (proto?.trim().toLowerCase() === \"https\") {\n return true;\n }\n const { origin } = headers;\n return typeof origin === \"string\" && origin.toLowerCase().startsWith(\"https:\");\n}\n\n/** {@link isSecureRequest} for a request: its TLS connection, or its `secure` flag, and its headers. */\nexport function isSecureSessionRequest(req: SessionCookieRequest): boolean {\n const connection = req.socket;\n const encrypted =\n typeof connection === \"object\" &&\n connection !== null &&\n (connection as { readonly encrypted?: unknown }).encrypted === true;\n return isSecureRequest(req.headers, req.secure === true || encrypted);\n}\n\nfunction hasDomain(domain: string | undefined): domain is string {\n return domain !== undefined && domain !== \"\";\n}\n\n/** The cookie domain from the environment (`COOKIE_DOMAIN`), or `undefined` for none. */\nexport function cookieDomainFromEnv(): string | undefined {\n const domain = process.env.COOKIE_DOMAIN;\n return hasDomain(domain) ? domain : undefined;\n}\n\n/**\n * The session cookie's name on `req`, the one rule the auth routes,\n * `setSessionCookie`, `socketAuth` and the HTTP transport all follow: the\n * name the app gave; else `session` when the cookie has a domain (a domain\n * cookie cannot be `__Host-`); else `__Host-session` on a request that came\n * over HTTPS ({@link isSecureRequest}), and `session` on a plain HTTP one.\n */\nexport function sessionCookieNameFor(\n req: SessionCookieRequest,\n naming: SessionCookieNaming = {},\n): string {\n if (naming.cookieName !== undefined) {\n return naming.cookieName;\n }\n if (hasDomain(naming.domain)) {\n return SESSION_COOKIE;\n }\n return isSecureSessionRequest(req) ? HOST_SESSION_COOKIE : SESSION_COOKIE;\n}\n\nconst SECURE_SESSION_COOKIES: readonly string[] = Object.freeze([HOST_SESSION_COOKIE]);\n\nconst PLAIN_SESSION_COOKIES: readonly string[] = Object.freeze([\n SESSION_COOKIE,\n HOST_SESSION_COOKIE,\n]);\n\n/**\n * The cookie names a session is read from on `req`, the first of them being\n * the name {@link sessionCookieNameFor} writes for the same request. A name\n * the app gave is the only one read. Otherwise a secure request reads only\n * `__Host-session`: a site under the same parent domain can set a `session`\n * cookie for the whole domain, and while the user holds no `__Host-session`\n * that planted cookie would sign them in as whoever planted it. A plain HTTP\n * request, or a cookie with a domain, reads `session` and then\n * `__Host-session`, which only this host can have set.\n */\nexport function sessionCookieNamesFor(\n req: SessionCookieRequest,\n naming: SessionCookieNaming = {},\n): readonly string[] {\n if (naming.cookieName !== undefined) {\n return [naming.cookieName];\n }\n return sessionCookieNameFor(req, naming) === HOST_SESSION_COOKIE\n ? SECURE_SESSION_COOKIES\n : PLAIN_SESSION_COOKIES;\n}\n\nexport interface CookieSettings {\n httpOnly: boolean;\n secure: boolean;\n sameSite: \"lax\" | \"none\";\n path: string;\n domain?: string;\n maxAge?: number;\n}\n\nexport interface CookieResponse {\n cookie(name: string, value: string, options: CookieSettings): unknown;\n clearCookie(name: string, options: CookieSettings): unknown;\n}\n\nexport interface SessionCookieOptions {\n /**\n * Cookie name. Default: the name the auth routes use on the same request\n * (`__Host-session` over HTTPS without a domain, else `session`), which\n * `socketAuth` and the HTTP transport read; it is read from the response's\n * request (`res.req`, which Express and Node set), and is `session`\n * without one.\n */\n cookieName?: string;\n /** Lifetime in ms. Default: 7 days (matches the default JWT expiry). */\n maxAgeMs?: number;\n /** Cookie domain. Defaults to process.env.COOKIE_DOMAIN. */\n domain?: string;\n /**\n * SameSite. Default: \"lax\", as the auth routes set it (before rc.5,\n * \"none\" in production; finding F7.5 of the quickdraw-chat review):\n * the cookie rides top-level navigations and same-site requests, never a\n * request another site's page makes. \"none\" serves a web app on another\n * site, and is always Secure, because browsers drop one that is not.\n */\n sameSite?: \"lax\" | \"none\";\n /** Secure. Default: true in production or over HTTPS; a `__Host-` cookie always is. */\n secure?: boolean;\n}\n\nfunction isSessionCookieRequest(value: unknown): value is SessionCookieRequest {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n const { headers } = value as { readonly headers?: unknown };\n return typeof headers === \"object\" && headers !== null;\n}\n\n/** The request a response answers (`res.req`), when it has one. */\nfunction requestOf(res: CookieResponse): SessionCookieRequest {\n const req: unknown = Reflect.get(res, \"req\");\n return isSessionCookieRequest(req) ? req : { headers: {} };\n}\n\nfunction sessionCookie(\n res: CookieResponse,\n options: SessionCookieOptions,\n): { readonly name: string; readonly settings: CookieSettings } {\n const isProd = process.env.NODE_ENV === \"production\";\n const domain = options.domain ?? cookieDomainFromEnv();\n const req = requestOf(res);\n const name = sessionCookieNameFor(req, { cookieName: options.cookieName, domain });\n const hostOnly = name.startsWith(\"__Host-\");\n // Lax, as the auth routes' own cookie: SameSite=None (always Secure) is the\n // app's choice for a web app on another site, never a default (F7.5).\n const sameSite = options.sameSite ?? \"lax\";\n return {\n name,\n settings: {\n httpOnly: true,\n secure:\n hostOnly ||\n sameSite === \"none\" ||\n (options.secure ?? (isProd || isSecureSessionRequest(req))),\n sameSite,\n path: \"/\",\n ...(hasDomain(domain) && !hostOnly && { domain }),\n },\n };\n}\n\n/**\n * Sets the session cookie, under the name {@link sessionCookieNameFor} gives\n * the response's request, so `socketAuth` and the HTTP transport read it.\n */\nexport function setSessionCookie(\n res: CookieResponse,\n jwt: string,\n options: SessionCookieOptions = {},\n): void {\n const { name, settings } = sessionCookie(res, options);\n res.cookie(name, jwt, { ...settings, maxAge: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS });\n}\n\n/** Clears the session cookie `setSessionCookie` sets on the same request. */\nexport function clearSessionCookie(res: CookieResponse, options: SessionCookieOptions = {}): void {\n const { name, settings } = sessionCookie(res, options);\n res.clearCookie(name, settings);\n}\n","/**\n * REST authentication middleware factory.\n *\n * Authenticates requests via session cookie or Bearer token, verifies the JWT,\n * then checks session revocation via an injected lookup (the app owns the\n * database). Uses structural request/response types so there is no runtime or\n * type dependency on Express.\n */\n\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport { verifyJWT } from \"./jwt\";\nimport { cookieDomainFromEnv, sessionCookieNamesFor } from \"./sessionCookie\";\n\nexport interface AuthRequest {\n cookies?: Record<string, string>;\n /** Its headers: `authorization`, and what says whether it came over HTTPS (`sessionCookieNamesFor`). */\n headers: IncomingHttpHeaders;\n /** Express's `req.secure`. */\n secure?: unknown;\n userId?: string;\n}\n\nexport interface AuthResponse {\n status(code: number): { json(body: unknown): unknown };\n}\n\nexport interface RequireAuthOptions {\n /**\n * Look up the session for a token. Return null when the session does not\n * exist; sessions past expiresAt are rejected. This mirrors the socket auth\n * path so a revoked/logged-out session can no longer authenticate REST\n * requests until the JWT naturally expires.\n */\n getSession: (token: string) => Promise<{ expiresAt: Date } | null>;\n /** JWT signing secret. Defaults to process.env.JWT_SECRET. */\n jwtSecret?: string;\n /**\n * Cookie to read the JWT from. Default: the names `setSessionCookie` and\n * the auth routes set on the same request (`sessionCookieNamesFor`):\n * `__Host-session` over HTTPS, `session` (then `__Host-session`) over\n * plain HTTP or when `COOKIE_DOMAIN` is set.\n */\n cookieName?: string;\n}\n\n/**\n * Extract a JWT from the session cookie (`req.cookies`, from `cookie-parser`)\n * or Authorization header (cookie takes priority). Without a `cookieName`,\n * the cookie is read under the names `setSessionCookie` sets on the same\n * request.\n */\nexport function extractBearerOrCookieToken(req: AuthRequest, cookieName?: string): string | null {\n const { cookies } = req;\n const names = sessionCookieNamesFor(req, { cookieName, domain: cookieDomainFromEnv() });\n const name = names.find((candidate) => cookies?.[candidate] !== undefined);\n const cookieToken = name === undefined ? undefined : cookies?.[name];\n if (cookieToken) return cookieToken;\n\n const authHeader = req.headers.authorization;\n if (authHeader?.startsWith(\"Bearer \")) return authHeader.slice(7);\n\n return null;\n}\n\n/**\n * Create an Express-compatible middleware that attaches req.userId on success\n * and responds 401 on failure.\n */\nexport function createRequireAuth(\n options: RequireAuthOptions,\n): (req: AuthRequest, res: AuthResponse, next: () => void) => Promise<void> {\n return async (req, res, next) => {\n const token = extractBearerOrCookieToken(req, options.cookieName);\n if (!token) {\n res.status(401).json({ error: \"Missing or invalid authorization\" });\n return;\n }\n\n const secret = options.jwtSecret ?? process.env.JWT_SECRET;\n if (!secret) {\n res.status(401).json({ error: \"Authentication is not configured\" });\n return;\n }\n\n const payload = await verifyJWT(token, secret);\n if (!payload?.userId) {\n res.status(401).json({ error: \"Invalid or expired token\" });\n return;\n }\n\n const session = await options.getSession(token);\n if (!session || session.expiresAt < new Date()) {\n res.status(401).json({ error: \"Session expired or revoked\" });\n return;\n }\n\n req.userId = payload.userId;\n next();\n };\n}\n","// Replies a transport sends itself, and acknowledging a socket call while\n// measuring its reply (RFC 0003 section 9, step 9). The JSON parser reports\n// each packet it encodes; an acknowledgement is encoded synchronously inside\n// the `ack()` call that sends it, so the packet reported while `ack()` runs is\n// that reply. The size reaches the completion record through the\n// dispatcher's `respond`.\n//\n// Socket.IO's server-side `ack()` marks itself sent only after the packet was\n// written, so when encoding the reply throws (a `BigInt` or a cycle that\n// `JSON.stringify` refuses), the same `ack()` can still send a fallback.\n//\n// A reply whose data came from a shared run is handed to `ack()` pre-encoded\n// around that data's JSON text, which every caller of the same copy shares,\n// so the run's result is encoded once rather than once per caller.\n\nimport { PacketType } from \"socket.io-parser\";\nimport type { Failure } from \"../../protocol/envelope\";\nimport { INTERNAL_MESSAGE, QuickdrawError, type WireIssue } from \"../../protocol/errors\";\nimport { createJsonParser, type JsonParser } from \"../../protocol/parser\";\nimport { PreEncoded } from \"../../protocol/preEncoded\";\nimport {\n callReplyJson,\n toCallReply,\n type DispatchResult,\n type SharedData,\n} from \"../pipeline/request\";\n\n/**\n * A `VALIDATION` error for a frame or request the transport could not read,\n * before any method ran, with its one issue in `data.issues` as RFC 0003\n * section 3 requires.\n */\nexport function unreadable(message: string, path: WireIssue[\"path\"] = []): QuickdrawError {\n return new QuickdrawError(\"VALIDATION\", message, { issues: [{ path, message }] });\n}\n\n/** The reply a call gets when its own reply could not be encoded. */\nexport const INTERNAL_FAILURE: Failure = Object.freeze({\n ok: false,\n e: Object.freeze({ code: \"INTERNAL\", message: INTERNAL_MESSAGE }),\n});\n\n/** The function Socket.IO hands a listener to acknowledge an event. */\nexport type Acknowledge = (reply: unknown) => void;\n\n/** Picks the parser a server uses and measures the acknowledgements it writes. */\nexport interface ReplyMeter {\n /** The parser to give Socket.IO: the JSON parser, or `undefined` for the stock one. */\n readonly parser: JsonParser | undefined;\n /**\n * Runs `send`, which writes one acknowledgement, and returns that reply's\n * size in UTF-8 bytes. `undefined` when nothing was measured: the stock\n * parser measures nothing, and a closed socket writes nothing.\n */\n measure(send: () => void): number | undefined;\n}\n\ninterface Measurement {\n bytes: number | undefined;\n}\n\n/** The JSON parser with a meter on its encoder, or the stock parser when `binary` is true. */\nexport function createReplyMeter(binary: boolean): ReplyMeter {\n if (binary) {\n return {\n parser: undefined,\n measure(send) {\n send();\n return undefined;\n },\n };\n }\n let current: Measurement | undefined;\n const parser = createJsonParser({\n onEncoded(packet, bytes) {\n if (current !== undefined && packet.type === PacketType.ACK) {\n current.bytes = bytes;\n }\n },\n });\n return {\n parser,\n measure(send) {\n const measurement: Measurement = { bytes: undefined };\n const outer = current;\n current = measurement;\n try {\n send();\n } finally {\n current = outer;\n }\n return measurement.bytes;\n },\n };\n}\n\n/**\n * What a call's `ack` is given for `result`: its `qd:call` reply. When the\n * data came from a shared run and the JSON parser writes the reply, the\n * reply is pre-encoded around the data's shared JSON text, so every caller\n * of that copy is sent the same text, encoded once (RFC 0003 section 9,\n * step 6). The text is written when the reply is encoded, inside `ack()`,\n * so a value `JSON.stringify` refuses still gets the `INTERNAL` fallback.\n */\nexport function callAcknowledgement(\n meter: ReplyMeter,\n result: DispatchResult,\n shared: SharedData | undefined,\n): unknown {\n const reply = toCallReply(result);\n if (shared === undefined || meter.parser === undefined || !(\"d\" in reply)) {\n return reply;\n }\n return new PreEncoded(reply, () => {\n const data = shared.json();\n return data === undefined ? JSON.stringify(reply) : callReplyJson(reply, data);\n });\n}\n\n/**\n * Sends `reply` through `ack` and returns its size. When the reply cannot be\n * encoded, reports the error and sends `fallback` through the same `ack`\n * instead. Never throws.\n */\nexport function acknowledge(\n meter: ReplyMeter,\n ack: Acknowledge,\n reply: unknown,\n fallback: unknown,\n report: (error: unknown) => void,\n): number | undefined {\n try {\n return meter.measure(() => ack(reply));\n } catch (error) {\n report(error);\n }\n try {\n return meter.measure(() => ack(fallback));\n } catch (error) {\n report(error);\n return undefined;\n }\n}\n","// Reading an HTTP call's credentials and JSON input from a Node request, so\n// the HTTP transport works the same under Express 4, Express 5 or a bare\n// Node HTTP server, with or without the app's own body and cookie parsers.\n\nimport type { IncomingMessage } from \"node:http\";\nimport { extractBearerOrCookieToken } from \"../auth/restMiddleware\";\nimport {\n cookieDomainFromEnv,\n sessionCookieNamesFor,\n type SessionCookieNaming,\n} from \"../auth/sessionCookie\";\nimport { unreadable } from \"./ack\";\n\n/**\n * A request as Express leaves it: maybe with `body` from `express.json()`,\n * `cookies` from `cookie-parser`, and Express's own `secure`.\n */\nexport type HttpRequest = IncomingMessage & {\n readonly body?: unknown;\n readonly cookies?: unknown;\n readonly secure?: unknown;\n};\n\n/**\n * True when the request declares a JSON body (`application/json` or a\n * `+json` type). The HTTP transport requires it of every call, with or\n * without a body: a cross-site page can send a form or a body-less POST\n * without a CORS preflight, but not one with this content type, so a session\n * cookie alone cannot make the browser call a method on a user's behalf.\n * Since rc.5 `socketAuth` also checks the `Origin` of a call that sends the\n * cookie (finding F7.1), so a permissive CORS policy does not open it.\n */\nexport function isJsonRequest(req: IncomingMessage): boolean {\n const header = req.headers[\"content-type\"];\n if (typeof header !== \"string\") {\n return false;\n }\n const type = (header.split(\";\", 1)[0] ?? \"\").trim().toLowerCase();\n return type === \"application/json\" || type.endsWith(\"+json\");\n}\n\nfunction hasBody(req: IncomingMessage): boolean {\n if (req.headers[\"transfer-encoding\"] !== undefined) {\n return true;\n }\n const length = Number(req.headers[\"content-length\"] ?? 0);\n return Number.isFinite(length) && length > 0;\n}\n\nfunction readText(req: IncomingMessage, maxBytes: number): Promise<string> {\n if (req.readableEnded) {\n return Promise.resolve(\"\");\n }\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n function stop(): void {\n req.off(\"data\", onData);\n req.off(\"end\", onEnd);\n req.off(\"error\", onError);\n }\n function onData(chunk: Buffer | string): void {\n const buffer = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n size += buffer.length;\n if (size <= maxBytes) {\n chunks.push(buffer);\n return;\n }\n stop();\n // Discard the rest, so the response can still be sent on this connection.\n req.resume();\n reject(unreadable(`The request body is larger than ${maxBytes} bytes`));\n }\n function onEnd(): void {\n stop();\n resolve(Buffer.concat(chunks).toString(\"utf8\"));\n }\n function onError(error: Error): void {\n stop();\n reject(error);\n }\n req.on(\"data\", onData);\n req.on(\"end\", onEnd);\n req.on(\"error\", onError);\n });\n}\n\n/**\n * The call's input: `undefined` when the request has no body; the body the\n * app's JSON parser already read (`express.json()` sets `req.body`); or else\n * the body read here, up to `maxBytes`, and parsed. A body that is too large\n * or not JSON rejects with `VALIDATION`.\n *\n * `req.body` counts only once the request stream was consumed. Express 4's\n * body parsers set `req.body = {}` before they decide whether a request is\n * theirs, so a request that `express.urlencoded()` passed over, or that\n * `express.json()` passed over because its type was `application/vnd.api+json`,\n * arrives with `req.body` set to `{}` and its body still unread.\n */\nexport async function readJsonInput(req: HttpRequest, maxBytes: number): Promise<unknown> {\n if (!hasBody(req)) {\n return undefined;\n }\n if (req.readableEnded) {\n return req.body;\n }\n const text = await readText(req, maxBytes);\n if (text.trim() === \"\") {\n return undefined;\n }\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw unreadable(\"The request body is not valid JSON\");\n }\n}\n\nfunction isStringRecord(value: unknown): value is Record<string, string> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n Object.values(value).every((entry) => typeof entry === \"string\")\n );\n}\n\nfunction decode(raw: string): string {\n const quoted = raw.length > 1 && raw.startsWith('\"') && raw.endsWith('\"');\n const value = quoted ? raw.slice(1, -1) : raw;\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/** Where cookies are read from: a request, or a socket handshake's `{ headers }`. */\nexport interface CookieSource {\n readonly cookies?: unknown;\n readonly headers: { readonly cookie?: string | undefined };\n}\n\n/**\n * The request's cookies, from its `Cookie` header, or else `req.cookies`\n * (`cookie-parser`) for a request without one. A name the header repeats\n * maps to `\"\"`, no credential: a browser sends two cookies of one name when\n * another site under the same parent domain, or a path below this one, set\n * the second (cookie tossing), and the server cannot tell which is its own,\n * so it trusts neither. `cookie-parser` keeps the first, which may be the\n * planted one, so the header is read whenever there is one.\n */\nexport function cookiesOf(req: CookieSource): Record<string, string> {\n const header = req.headers.cookie;\n if (typeof header !== \"string\") {\n return isStringRecord(req.cookies) ? req.cookies : {};\n }\n const cookies: Record<string, string> = Object.create(null) as Record<string, string>;\n for (const pair of header.split(\";\")) {\n const equals = pair.indexOf(\"=\");\n const name = pair.slice(0, equals).trim();\n if (equals <= 0 || name === \"\") {\n continue;\n }\n cookies[name] = Object.hasOwn(cookies, name) ? \"\" : decode(pair.slice(equals + 1).trim());\n }\n return cookies;\n}\n\n/**\n * The session token among `cookies`, under the first of `names` the request\n * holds: its value, or `null` when it is empty or repeated. A later name is\n * read only when no earlier one is there, so a repeated name never lets\n * another stand in for it.\n */\nexport function cookieToken(\n cookies: Readonly<Record<string, string>>,\n names: readonly string[],\n): string | null {\n const name = names.find((candidate) => Object.hasOwn(cookies, candidate));\n const value = name === undefined ? undefined : cookies[name];\n return value === undefined || value === \"\" ? null : value;\n}\n\nexport type { SessionCookieNaming };\n\n/**\n * How a transport names the session cookie: the `cookieName` it was given,\n * and the domain `COOKIE_DOMAIN` gives the cookie, read now, as the auth\n * routes read it (a cookie with a domain is `session`).\n */\nexport function transportCookieNaming(cookieName: string | undefined): SessionCookieNaming {\n return { cookieName, domain: cookieDomainFromEnv() };\n}\n\n/**\n * Where an HTTP call's token came from. `\"cookie\"`: the session cookie, which\n * a browser sends with a request any page makes, so whoever accepts it checks\n * the request's `Origin` (`socketAuth` does). `\"bearer\"`: an\n * `Authorization: Bearer` header, which a page sends only on purpose.\n */\nexport type HttpCredentialSource = \"cookie\" | \"bearer\";\n\n/** An HTTP call's token and where it came from. */\nexport interface HttpCredential {\n readonly token: string;\n readonly from: HttpCredentialSource;\n}\n\n/**\n * The credential an HTTP call authenticates with: its session cookie, under\n * the names `sessionCookieNamesFor` gives the request (by how the app named\n * the cookie and whether the request came over HTTPS), or else its bearer\n * token; `null` with neither.\n */\nexport function httpCredentialOf(\n req: HttpRequest,\n naming: SessionCookieNaming,\n): HttpCredential | null {\n const cookie = cookieToken(cookiesOf(req), sessionCookieNamesFor(req, naming));\n if (cookie !== null) {\n return { token: cookie, from: \"cookie\" };\n }\n const bearer = extractBearerOrCookieToken({\n headers: { authorization: req.headers.authorization },\n });\n return bearer === null || bearer === \"\" ? null : { token: bearer, from: \"bearer\" };\n}\n\n/** The token of {@link httpCredentialOf}, or `null`. */\nexport function tokenOf(req: HttpRequest, naming: SessionCookieNaming): string | null {\n return httpCredentialOf(req, naming)?.token ?? null;\n}\n"],"mappings":"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","names":["cookieToken"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contract/collections.ts","../src/contract/realtime.ts"],"sourcesContent":["// Collection declarations for a contract (RFC 0003 section 7.1). A collection\n// is the rows of one service grouped by a scope value. Membership is declared\n// (a scope column or a junction table, plus an equality filter), never\n// computed by a function, so the framework knows which writes can move a row.\n// This replaces 4.1's `defineCollection({ resolveScopeId, checkScopeAccess,\n// snapshot, toItem })`.\n\nimport type { AccessLevel } from \"./access\";\n\n/** Page size of a collection snapshot when the collection sets no `limit`. */\nexport const DEFAULT_COLLECTION_LIMIT = 100;\n\n/** Largest page a client may ask for when the collection sets no `maxLimit`. */\nexport const DEFAULT_COLLECTION_MAX_LIMIT = 500;\n\n/**\n * A scope that comes from a junction table instead of a column on the row:\n * the rows of `model` link an entry (`entry` holds the row's id) to a scope\n * value (`scope`). For example, a chat appears in each member's list through\n * `via({ model: \"chatMember\", entry: \"chatId\", scope: \"userId\" })`.\n */\nexport interface ViaScope<\n Model extends string = string,\n Entry extends string = string,\n Scope extends string = string,\n> {\n readonly kind: \"via\";\n readonly model: Model;\n readonly entry: Entry;\n readonly scope: Scope;\n /**\n * The item is computed from the junction (a member count): every junction\n * write sends its entry again, whole, to each scope that still holds it.\n * Without it a junction write only adds or removes the entry in the scope\n * it links, so a count over the junction goes stale everywhere else.\n */\n readonly refreshEntry?: boolean;\n}\n\n/** What `via(...)` takes: the junction model, its entry and scope columns, and `refreshEntry`. */\nexport interface ViaOptions<\n Model extends string = string,\n Entry extends string = string,\n Scope extends string = string,\n> {\n /** The junction model, as the database client names it: `\"chatMember\"`. */\n readonly model: Model;\n /** The junction column holding the entry's id: `\"chatId\"`. */\n readonly entry: Entry;\n /** The junction column holding the scope value: `\"userId\"`. */\n readonly scope: Scope;\n /**\n * Set when the item reads the junction, as a `memberCount` mapped from a\n * relation count does: a junction create, update or delete then sends the\n * entry again, as `updated`, to every scope that still holds it (one read\n * of the entry's item per flush). Default `false`: a junction write adds\n * or removes the entry for the scope it links, and nothing else.\n */\n readonly refreshEntry?: boolean;\n}\n\n/**\n * Declares a collection scope that comes from a junction table.\n *\n * @example\n * myChats: {\n * scope: via({ model: \"chatMember\", entry: \"chatId\", scope: \"userId\", refreshEntry: true }),\n * item: \"listItem\",\n * order: [[\"id\", \"asc\"]],\n * }\n */\nexport function via<\n const Model extends string,\n const Entry extends string,\n const Scope extends string,\n>(junction: ViaOptions<Model, Entry, Scope>): ViaScope<Model, Entry, Scope> {\n return Object.freeze({\n kind: \"via\",\n model: junction.model,\n entry: junction.entry,\n scope: junction.scope,\n ...(junction.refreshEntry === undefined ? {} : { refreshEntry: junction.refreshEntry }),\n });\n}\n\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Sort columns for the keyset cursor, ending in `id` so every row has a\n * unique position: `[[\"ordinal\", \"asc\"], [\"id\", \"asc\"]]`.\n */\nexport type OrderBy<Column extends string = string> = readonly [\n ...(readonly [Column, SortDirection])[],\n readonly [\"id\", SortDirection],\n];\n\n/** Who a view is evaluated for, on the client. */\nexport interface Viewer {\n readonly userId: string;\n}\n\n/**\n * A view: a named pure predicate evaluated on the client over the index rows\n * of a scope (RFC 0003 section 7.5). `row` holds only `id` and the\n * collection's `index` fields, which the type enforces.\n */\nexport type ViewPredicate<Row> = (row: Row, who: Viewer) => boolean;\n\n/** The equality filter that decides membership, for example `{ archived: false }`. */\nexport interface CollectionWhere {\n readonly [column: string]: string | number | boolean | null;\n}\n\n/** Any collection of any contract, as `defineContract` stores it. */\nexport interface CollectionDef {\n /** What the list holds, in a sentence or two, for the generated docs. */\n readonly describe?: string | undefined;\n /** A string column of the entity, or `via(...)` for a junction table. */\n readonly scope: string | ViaScope;\n /** The projection each item is sent as: `\"entity\"` or a named projection. */\n readonly item: string;\n /** Sort columns for the keyset cursor; the last one is `id`. */\n readonly order: OrderBy;\n /** Equality filter that decides membership. */\n readonly where?: CollectionWhere | undefined;\n /** Page size. Default {@link DEFAULT_COLLECTION_LIMIT}. */\n readonly limit?: number | undefined;\n /** Largest page a client may ask for. Default {@link DEFAULT_COLLECTION_MAX_LIMIT}. */\n readonly maxLimit?: number | undefined;\n /** Item fields sent for every member of the scope (RFC 0003 section 7.4). */\n readonly index?: readonly string[] | undefined;\n /** Named predicates over index rows. Requires `index`. */\n readonly views?: { readonly [view: string]: ViewPredicate<never> } | undefined;\n /** Level required on the scope's anchor row. Default `\"Read\"`, through the anchor's policy. */\n readonly access?: AccessLevel | undefined;\n}\n","// Streams, channels and typed room events in a contract (RFC 0003 section\n// 12.5). A stream is server-to-client append-only data a subscriber receives\n// with a seed of recent items; a channel is fire-and-forget client-to-server\n// input (cursors, typing) behind a per-socket token bucket; an event is a\n// typed custom frame a handler sends to a room. 4.1 declared channels on the\n// service class (4.1 `src/server/BaseService.ts:915-942`) and typed room\n// events through the augmentable `QuickdrawEventMap`; here all three live in\n// the contract, so the client is typed from it.\n//\n// Methods, collections, streams, channels and events share one namespace:\n// the client exposes each as `qd.<service>.<name>`.\n\nimport type { AccessLevel } from \"./access\";\nimport type { AnyContract } from \"./defineContract\";\nimport type { StandardSchemaV1 } from \"./standardSchema\";\n\n/** The `scope` of a stream with one feed for the whole service: the default. */\nexport const GLOBAL_STREAM = \"global\";\n\n/** The most items a stream may keep per scope as the seed for new subscribers. */\nexport const STREAM_MAX_SEED = 1000;\n\n/** A channel's sustained rate when it declares none: messages per second per socket. */\nexport const CHANNEL_DEFAULT_RATE = 30;\n\n/**\n * The app room a stream's subscriber must be in (`access: { room }`): the\n * room's name, `{ prefix }` for any room whose name starts with it, or for a\n * scoped stream a function of the scope (`(worldId) => \\`world:${worldId}\\``).\n */\nexport type StreamRoom = string | RoomPrefix | ((scope: string) => string | null | undefined);\n\n/**\n * Who may subscribe to a stream (RFC 0003 section 4.1's forms, as data). A\n * scoped stream's scope value is the row an `entry` or `scope` form is about:\n * `{ entry: L }` needs level `L` on the row of this service whose id is the\n * scope, `{ scope: L, of }` level `L` on that row of `of`'s service.\n * `{ room }` opens it to the sockets in an app room a method joined them to\n * (signed in or not), as a channel's `requires: { room }` does its messages:\n * a socket that leaves the room, or is taken out of it, leaves the feed too.\n * A stream that declares none is closed: no client may subscribe.\n */\nexport type StreamAccess =\n | \"public\"\n | \"authenticated\"\n | {\n readonly service: AccessLevel;\n readonly entry?: undefined;\n readonly scope?: undefined;\n readonly of?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly entry: AccessLevel;\n readonly service?: AccessLevel;\n readonly scope?: undefined;\n readonly of?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly scope: AccessLevel;\n readonly of: AnyContract;\n readonly service?: undefined;\n readonly entry?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly room: StreamRoom;\n readonly service?: undefined;\n readonly entry?: undefined;\n readonly scope?: undefined;\n readonly of?: undefined;\n };\n\n/** A server-to-client stream of append-only items (RFC 0003 section 12.5). */\nexport interface StreamDef<Item extends StandardSchemaV1 = StandardSchemaV1> {\n /** One item's schema. Pushed items are checked against it before they are sent. */\n readonly item: Item;\n /**\n * What a scope value is (`\"taskId\"`): the stream has one feed per scope.\n * `\"global\"`, the default, makes one feed for the whole service.\n */\n readonly scope?: string;\n /**\n * How many of the latest items the server keeps per scope, in memory on\n * each process, and sends a new subscriber first. Default 0; at most 1,000.\n */\n readonly seed?: number;\n /** Send items volatile: dropped for a client whose connection is backed up. Default `false`. */\n readonly volatile?: boolean;\n /** Who may subscribe. Without it the stream is closed. */\n readonly access?: StreamAccess;\n /** What the stream carries, in a sentence or two, for the generated docs. */\n readonly describe?: string;\n}\n\n/**\n * Where a channel requirement finds its row id or scope value in the parsed\n * payload: the name of a key holding a string, or a function of the payload.\n */\nexport type PayloadSelector = string | ((payload: never) => string | null | undefined);\n\n/**\n * Any app room whose name starts with `prefix` (`{ prefix: \"world:\" }`, a\n * game of many worlds): the sending socket passes when it is in one, and\n * the channel's handler gets the one it matched as `ctx.room`, so the\n * payload need not repeat which room it is for.\n */\nexport interface RoomPrefix {\n readonly prefix: string;\n}\n\n/**\n * The app room a channel requirement names: the room itself (`\"world\"`, a\n * game's one world), a function of the parsed payload that returns it\n * (`(payload) => \\`lobby:${payload.lobbyId}\\``), or `{ prefix }`, any room\n * whose name starts with it (`{ prefix: \"world:\" }`). Unlike a\n * {@link PayloadSelector}, a string here is the room's name, not a payload\n * key. A name starting with `qd:` or `user:` is never an app room: a literal\n * one (or such a prefix) is refused when the contract is defined, and a\n * computed one drops the message.\n */\nexport type RoomSelector = string | ((payload: never) => string | null | undefined) | RoomPrefix;\n\n/**\n * What a channel message requires of the socket that sends it: a live\n * subscription (`qd:sub`) to the row of this service `entity` names, or\n * (`qd:col:sub`) to the scope of `collection` that `scope` names; or that\n * the socket is in the app room `room` names (or, for `{ prefix }`, in any\n * app room whose name starts with it), which a method called over that\n * same socket joined with `ctx.rooms.join` (a room another socket of the\n * user joined does not count, and a reconnected socket is in none until it\n * joins again). The handler gets the room that matched as `ctx.room`. Each\n * is checked in memory against the sending socket's own records, on the\n * node it is connected to. A message whose payload names none is dropped.\n */\nexport type ChannelRequires =\n | {\n readonly entity: PayloadSelector;\n readonly collection?: undefined;\n readonly scope?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly collection: string;\n readonly scope: PayloadSelector;\n readonly entity?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly room: RoomSelector;\n readonly entity?: undefined;\n readonly collection?: undefined;\n readonly scope?: undefined;\n };\n\n/** A client-to-server fire-and-forget channel (RFC 0003 section 12.5). */\nexport interface ChannelDef<Payload extends StandardSchemaV1 = StandardSchemaV1> {\n /** One message's schema. A message that fails it is dropped. */\n readonly payload: Payload;\n /** Messages one socket may send per second, sustained. Default 30. */\n readonly ratePerSecond?: number;\n /** How many messages one socket may send at once. Default twice `ratePerSecond`. */\n readonly burst?: number;\n /** What the sender must already be subscribed to, or the app room it must be in. */\n readonly requires?: ChannelRequires;\n /** What a message says, in a sentence or two, for the generated docs. */\n readonly describe?: string;\n}\n\n/** A typed custom room event, delivered as `qd:event` (RFC 0003 section 8.3). */\nexport interface EventDef<Payload extends StandardSchemaV1 = StandardSchemaV1> {\n readonly payload: Payload;\n /** What the event announces, in a sentence or two, for the generated docs. */\n readonly describe?: string;\n}\n\n/** True when a stream has one feed per scope value; false for a global stream. */\nexport function isScopedStream(def: Pick<StreamDef, \"scope\">): boolean {\n return def.scope !== undefined && def.scope !== GLOBAL_STREAM;\n}\n"],"mappings":";AAUO,IAAM,2BAA2B;AAGjC,IAAM,+BAA+B;AA0DrC,SAAS,IAId,UAA0E;AAC1E,SAAO,OAAO,OAAO;AAAA,IACnB,MAAM;AAAA,IACN,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB,GAAI,SAAS,iBAAiB,SAAY,CAAC,IAAI,EAAE,cAAc,SAAS,aAAa;AAAA,EACvF,CAAC;AACH;;;AClEO,IAAM,gBAAgB;AAGtB,IAAM,kBAAkB;AAGxB,IAAM,uBAAuB;AA2J7B,SAAS,eAAe,KAAwC;AACrE,SAAO,IAAI,UAAU,UAAa,IAAI,UAAU;AAClD;","names":[]}
|