@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
|
@@ -0,0 +1 @@
|
|
|
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`. The MCP transport sets it, and a\n * call's `ctx.services` passes its own on to the calls it makes, whose\n * transport is `\"internal\"`.\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 { errorText, 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 bearer token, or `{}` without one; for `socketAuth`'s\n * `authenticate` alone, the session cookie when the request carries one,\n * else the bearer token. Every other `authenticate` finds the cookie only\n * in `sessionCookie`.\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 * The credential the transport chose (finding F7.1): `\"cookie\"` when the\n * request carries the session cookie, which a browser sends with a\n * request any page makes, so an `authenticate` that accepts it checks the\n * request's `Origin` as `socketAuth` does; else `\"bearer\"` for an\n * `Authorization: Bearer` header, which a page sends only on purpose.\n * Absent with neither.\n */\n readonly credential?: HttpCredentialSource;\n /**\n * The session cookie's value, when the request carries one. It is\n * ambient (a browser sends it with a request any page makes), so an\n * `authenticate` that accepts it first checks\n * `cookieOriginAllowed(request, allowedOrigins)`, then the session store,\n * on every call (`liveSession`). It is in `auth.token` only for\n * `socketAuth`'s `authenticate`.\n */\n readonly sessionCookie?: string;\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\") or answers the HTTP call with `UNAUTHENTICATED`; returning\n * nothing lets the caller in anonymously, so only `\"public\"` methods pass.\n * Throw a `QuickdrawError(\"UNAUTHENTICATED\", ...)` (or `\"FORBIDDEN\"`) for a\n * refusal that is the caller's doing: it logs at debug, and a socket's\n * `connect_error` data is `{ code: \"UNAUTHENTICATED\" }`, which signs a\n * client out. Anything else thrown is the server's failure (its session\n * store down, say): it logs at error, and the data is `{ code: \"INTERNAL\" }`,\n * which a client retries, keeping the user, so never throw a plain `Error`\n * for bad credentials.\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). `createServer` gives it to its dispatcher, whose admin kit\n * on that model treats the column as the grants field, whatever its name:\n * hidden unless `grants: true`, and then for service-wide Admins only. A\n * dispatcher made without `createServer` takes it as\n * `createDispatcher({ serviceAccessSource })`, and `createHttpRouter`\n * refuses an `auth.serviceAccessSource` its dispatcher does not name.\n * 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\n/** The session a socket authenticated with: its id, and when it expires (epoch milliseconds). */\ninterface SocketSession {\n readonly id: string;\n readonly expiresAt: number | undefined;\n}\n\nconst SOCKET_SESSIONS = new WeakMap<object, SocketSession>();\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, and, with `expiresAt`, so that the server\n * disconnects the socket when the session expires (the 5.1 audit's AUTH-4)\n * and refuses its handshake when it already has. `expiresAt` is a `Date`, a\n * date string, or epoch milliseconds: a JWT's `exp` is in seconds, so pass\n * `exp * 1000`. `socketAuth` calls it for the auth routes' sessions; an\n * app's own `authenticate` may too.\n */\nexport function recordSocketSession(\n socket: object,\n sessionId: string,\n expiresAt?: Date | number | string,\n): void {\n const time = expiresAt === undefined ? Number.NaN : new Date(expiresAt).getTime();\n SOCKET_SESSIONS.set(socket, {\n id: sessionId,\n expiresAt: Number.isNaN(time) ? undefined : time,\n });\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)?.id;\n}\n\n/** When the session a socket authenticated with expires (epoch milliseconds), when it was recorded with one. */\nexport function socketSessionExpiryOf(socket: object): number | undefined {\n return SOCKET_SESSIONS.get(socket)?.expiresAt;\n}\n\n/** What the server knows of an `authenticate` that `socketAuth` made. */\nexport interface SocketAuthInfo {\n /** Whether it was given `devCredentials`, which sign a socket in by a user id alone. */\n readonly devCredentials: boolean;\n}\n\nconst SOCKET_AUTHS = new WeakMap<object, SocketAuthInfo>();\n\n/**\n * Marks `authenticate` as `socketAuth`'s: the HTTP transport hands only such\n * an `authenticate` the session cookie as `auth.token`, since it checks the\n * cookie's `Origin` and the session store; every other gets the cookie as\n * `request.sessionCookie` alone.\n */\nexport function markSocketAuth(authenticate: object, info: SocketAuthInfo): void {\n SOCKET_AUTHS.set(authenticate, Object.freeze({ ...info }));\n}\n\n/** What `socketAuth` recorded of `authenticate`, or `undefined` for an `authenticate` it did not make. */\nexport function socketAuthOf(authenticate: unknown): SocketAuthInfo | undefined {\n return typeof authenticate === \"function\" ? SOCKET_AUTHS.get(authenticate) : undefined;\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,\n * and `isForbiddenCredential`'s, at debug, as the client's doing, and every\n * other failure at error; the socket transport refuses another failure's\n * handshake with `INTERNAL`, since the credentials may well be good.\n */\nexport function isRefusal(error: unknown): boolean {\n return error instanceof QuickdrawError && error.code === \"UNAUTHENTICATED\";\n}\n\n/**\n * True for a refusal that `authenticate` made on purpose with code\n * `FORBIDDEN`, a `QuickdrawError` (a session cookie sent from a page\n * `socketAuth` does not allow): the HTTP transport answers it as it is, and\n * the socket transport refuses the handshake as it refuses an\n * `UNAUTHENTICATED` one.\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: errorText(error),\n });\n }\n }),\n );\n },\n } satisfies FlushSink);\n}\n","import * as jose from \"jose\";\nimport { consoleLogger } from \"../../contract/logger\";\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 * The token's payload when its signature and expiry verify with `secret`,\n * or `null`: `verifyJWT` without its warning, for the kit's own reads, which\n * check the session store themselves (`liveSession`, the logout route,\n * `createRequireAuth`).\n */\nexport async function readVerifiedJWT(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/** Whether `verifyJWT` already warned, in this process, that it checks no session store. */\nlet warnedSessionToken = false;\n\n/**\n * Verify and decode a JWT token: its signature and expiry, nothing more.\n * It is never a session check: a token of a session that was signed out,\n * logged out everywhere or revoked still verifies until the JWT expires (7\n * days by default). Check a session token with `liveSession({ sessions,\n * jwtSecret }, token)`, `bearerAuth(...)` or `socketAuth(...)`, which ask the\n * session store on every call. In development it warns once per process\n * when it verifies a token that carries a session (`sid`).\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 const payload = await readVerifiedJWT(token, secret);\n if (payload?.sid !== undefined && process.env.NODE_ENV !== \"production\" && !warnedSessionToken) {\n warnedSessionToken = true;\n consoleLogger.warn(\n \"[quickdraw] verifyJWT verified a session token (it carries sid): verifyJWT checks the signature and expiry only, never the session store, so a signed-out or revoked session still passes. Check session tokens with liveSession({ sessions, jwtSecret }, token), bearerAuth(...) or socketAuth(...).\",\n );\n }\n return payload;\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 HOST_SESSION_COOKIE,\n SESSION_COOKIE,\n]);\n\n/**\n * The cookie names a session is read from on `req`, among them the name\n * {@link sessionCookieNameFor} writes for the same request. A name the app\n * gave is the only one read. Otherwise `__Host-session` wins wherever it is\n * present: a browser keeps it only as a Secure, host-only cookie on `/`, so\n * only this host, over a secure context, can have set it. A secure request\n * reads only `__Host-session`: a site under the same parent domain can set\n * a `session` cookie for the whole domain, and while the user holds no\n * `__Host-session` that planted cookie would sign them in as whoever\n * planted it. A plain HTTP request, or a cookie with a domain, reads\n * `__Host-session` and then `session`, so a planted `session` never stands\n * in for a `__Host-session` the request carries (the 5.1 audit's AUTH-2a).\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/** True when the request carries a `__Host-session` cookie: in its `Cookie` header, or else in `req.cookies`. */\nfunction carriesHostSession(req: SessionCookieRequest): boolean {\n const header = req.headers.cookie;\n if (typeof header === \"string\") {\n return header.split(\";\").some((pair) => pair.trimStart().startsWith(`${HOST_SESSION_COOKIE}=`));\n }\n const cookies: unknown = Reflect.get(req, \"cookies\");\n return (\n typeof cookies === \"object\" && cookies !== null && Object.hasOwn(cookies, HOST_SESSION_COOKIE)\n );\n}\n\n/**\n * Clears a `__Host-session` the request carries when a session cookie of\n * another name (`session`: over plain HTTP, or with a domain) was just set\n * or cleared. Readers take `__Host-session` first wherever it is present\n * ({@link sessionCookieNamesFor}), so a stale one, left by an earlier\n * response that looked secure (a proxy that sends `X-Forwarded-Proto` only\n * some of the time), would shadow the session just written, or outlive a\n * sign-out. A caller whose app named the cookie skips it: that name is then\n * the only one read.\n */\nexport function clearStaleHostSession(\n res: CookieResponse,\n req: SessionCookieRequest,\n written: string,\n sameSite: \"lax\" | \"none\",\n): void {\n if (written === HOST_SESSION_COOKIE || !carriesHostSession(req)) {\n return;\n }\n res.clearCookie(HOST_SESSION_COOKIE, { httpOnly: true, secure: true, sameSite, path: \"/\" });\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 * and clears a stale `__Host-session` that would shadow 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 if (options.cookieName === undefined) {\n clearStaleHostSession(res, requestOf(res), name, settings.sameSite);\n }\n}\n\n/** Clears the session cookie `setSessionCookie` sets on the same request, and a stale `__Host-session` beside it. */\nexport function clearSessionCookie(res: CookieResponse, options: SessionCookieOptions = {}): void {\n const { name, settings } = sessionCookie(res, options);\n res.clearCookie(name, settings);\n if (options.cookieName === undefined) {\n clearStaleHostSession(res, requestOf(res), name, settings.sameSite);\n }\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 { readVerifiedJWT } 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, `__Host-session` then `session` over plain\n * 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 readVerifiedJWT(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 * The credential the HTTP transport chose for a call (the cookie first).\n * `\"cookie\"`: the session cookie, which a browser sends with a request any\n * page makes, so whoever accepts it checks the request's `Origin`\n * (`socketAuth` does). `\"bearer\"`: an `Authorization: Bearer` header, which a\n * page sends only on purpose. For an `authenticate` `socketAuth` did not\n * make, `auth.token` holds the bearer token alone, even when this says\n * `\"cookie\"`; the cookie is `request.sessionCookie`.\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/** An HTTP call's session cookie and bearer token, each `null` when it has none. */\nexport interface HttpCredentials {\n readonly cookie: string | null;\n readonly bearer: string | null;\n}\n\n/**\n * Both credentials an HTTP call carries: its session cookie, under the names\n * `sessionCookieNamesFor` gives the request (by how the app named the cookie\n * and whether the request came over HTTPS), and its bearer token.\n */\nexport function httpCredentialsOf(req: HttpRequest, naming: SessionCookieNaming): HttpCredentials {\n const cookie = cookieToken(cookiesOf(req), sessionCookieNamesFor(req, naming));\n const bearer = extractBearerOrCookieToken({\n headers: { authorization: req.headers.authorization },\n });\n return { cookie, bearer: bearer === \"\" ? null : bearer };\n}\n\n/**\n * The credential an HTTP call authenticates with: its session cookie (see\n * `httpCredentialsOf`), or else its bearer token; `null` with neither.\n */\nexport function httpCredentialOf(\n req: HttpRequest,\n naming: SessionCookieNaming,\n): HttpCredential | null {\n const { cookie, bearer } = httpCredentialsOf(req, naming);\n if (cookie !== null) {\n return { token: cookie, from: \"cookie\" };\n }\n return bearer === null ? 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,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
function isName(value) {
|
|
6
|
-
return typeof value === "string" && value.length > 0;
|
|
7
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
isRecord
|
|
3
|
+
} from "./chunk-QSU47XEX.js";
|
|
8
4
|
|
|
9
5
|
// src/protocol/errors.ts
|
|
10
6
|
var ERROR_CODES = Object.freeze([
|
|
@@ -66,8 +62,6 @@ function fromWire(payload) {
|
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
export {
|
|
69
|
-
isRecord,
|
|
70
|
-
isName,
|
|
71
65
|
ERROR_CODES,
|
|
72
66
|
INTERNAL_MESSAGE,
|
|
73
67
|
isErrorCode,
|
|
@@ -76,4 +70,4 @@ export {
|
|
|
76
70
|
toWire,
|
|
77
71
|
fromWire
|
|
78
72
|
};
|
|
79
|
-
//# sourceMappingURL=chunk-
|
|
73
|
+
//# sourceMappingURL=chunk-Q4OGQM5I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol/errors.ts"],"sourcesContent":["// Error codes and the error every failed call carries (RFC 0003 section 3).\n// In 4.1 a failed call answered `{ success: false, error, code? }`\n// (4.1 `src/shared/types.ts:88-90`), every thrown error became code 500\n// with its own message (4.1 `src/server/ServiceRegistry.ts:398-402`), and\n// the client rethrew it as `ServiceCallError` with that number\n// (4.1 `src/client/serviceError.ts:6`). In 5.0 a handler picks the code by\n// throwing `QuickdrawError`; the same code reaches the caller on every\n// transport, and the HTTP transport maps it to a status with `httpStatus`.\n\nimport { isRecord } from \"./guards\";\n\n/** Every code a call can fail with, in the order of RFC 0003 section 3. */\nexport const ERROR_CODES = Object.freeze([\n \"UNAUTHENTICATED\",\n \"FORBIDDEN\",\n \"NOT_FOUND\",\n \"CONFLICT\",\n \"VALIDATION\",\n \"RATE_LIMITED\",\n \"CANCELLED\",\n \"TIMEOUT\",\n \"INTERNAL\",\n] as const);\n\n/**\n * Why a call failed:\n *\n * - `UNAUTHENTICATED` (401): there is no principal.\n * - `FORBIDDEN` (403): access was denied.\n * - `NOT_FOUND` (404): an unknown service, method or row.\n * - `CONFLICT` (409): a unique or state conflict.\n * - `VALIDATION` (422): the input failed its schema; see {@link ValidationErrorData}.\n * - `RATE_LIMITED` (429): a limiter or a full queue; see {@link RateLimitedErrorData}.\n * - `CANCELLED` (499): the caller cancelled.\n * - `TIMEOUT` (504): the handler ran past its time limit.\n * - `INTERNAL` (500): anything else.\n */\nexport type ErrorCode = (typeof ERROR_CODES)[number];\n\nconst HTTP_STATUS: Readonly<Record<ErrorCode, number>> = Object.freeze({\n UNAUTHENTICATED: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n VALIDATION: 422,\n RATE_LIMITED: 429,\n CANCELLED: 499,\n TIMEOUT: 504,\n INTERNAL: 500,\n});\n\n/** The message every `INTERNAL` error carries on the wire, whatever was thrown. */\nexport const INTERNAL_MESSAGE = \"Internal error\";\n\n/** True when `value` is one of the error codes. */\nexport function isErrorCode(value: unknown): value is ErrorCode {\n return typeof value === \"string\" && Object.hasOwn(HTTP_STATUS, value);\n}\n\n/** The HTTP status for an error code, as the table in RFC 0003 section 3 gives it. */\nexport function httpStatus(code: ErrorCode): number {\n return HTTP_STATUS[code];\n}\n\n/** One problem with a call's input, in a `VALIDATION` error's `data.issues`. */\nexport interface WireIssue {\n /** Where the problem is, as object keys and array indexes; empty for the input itself. */\n readonly path: readonly (string | number)[];\n readonly message: string;\n}\n\n/** The `data` of a `VALIDATION` error. */\nexport interface ValidationErrorData {\n readonly issues: readonly WireIssue[];\n}\n\n/** The `data` of a `RATE_LIMITED` error. */\nexport interface RateLimitedErrorData {\n /** How long to wait before trying again, in milliseconds. */\n readonly retryAfterMs: number;\n}\n\n/**\n * The error a failed call carries. A handler throws it to choose the code the\n * caller sees; the client rejects with it. Anything else a handler throws\n * reaches the caller as `INTERNAL` with a generic message (see {@link toWire}).\n */\nexport class QuickdrawError extends Error {\n /** Why the call failed. */\n readonly code: ErrorCode;\n /**\n * Details for the caller, sent as JSON: `issues` for `VALIDATION`,\n * `retryAfterMs` for `RATE_LIMITED`. `INTERNAL` errors never send it.\n */\n readonly data: unknown;\n\n constructor(code: ErrorCode, message: string, data?: unknown) {\n super(message);\n this.name = \"QuickdrawError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/** An error as it travels: the `e` of a failed acknowledgement. */\nexport interface WireError {\n readonly code: ErrorCode;\n readonly message: string;\n readonly data?: unknown;\n}\n\n/**\n * The wire form of whatever a call failed with. A `QuickdrawError` keeps its\n * code, message and data. Anything else, and every `INTERNAL` error, becomes\n * `{ code: \"INTERNAL\", message: \"Internal error\" }`: no stack, message or data\n * of an internal failure ever reaches the caller, so log the original first.\n */\nexport function toWire(error: unknown): WireError {\n if (!(error instanceof QuickdrawError) || error.code === \"INTERNAL\" || !isErrorCode(error.code)) {\n return { code: \"INTERNAL\", message: INTERNAL_MESSAGE };\n }\n return error.data === undefined\n ? { code: error.code, message: error.message }\n : { code: error.code, message: error.message, data: error.data };\n}\n\n/**\n * The `QuickdrawError` a wire error describes. A code this version does not\n * know becomes `INTERNAL`, keeping the message and data. A payload that is not\n * a wire error (not an object, or no string `message`) becomes a generic\n * `INTERNAL`. Either way the caller can branch on `code`.\n */\nexport function fromWire(payload: unknown): QuickdrawError {\n if (!isRecord(payload) || typeof payload.message !== \"string\") {\n return new QuickdrawError(\"INTERNAL\", INTERNAL_MESSAGE);\n }\n const code = isErrorCode(payload.code) ? payload.code : \"INTERNAL\";\n return new QuickdrawError(code, payload.message, payload.data);\n}\n"],"mappings":";;;;;AAYO,IAAM,cAAc,OAAO,OAAO;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAU;AAiBV,IAAM,cAAmD,OAAO,OAAO;AAAA,EACrE,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,WAAW;AAAA,EACX,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AAGM,IAAM,mBAAmB;AAGzB,SAAS,YAAY,OAAoC;AAC9D,SAAO,OAAO,UAAU,YAAY,OAAO,OAAO,aAAa,KAAK;AACtE;AAGO,SAAS,WAAW,MAAyB;AAClD,SAAO,YAAY,IAAI;AACzB;AAyBO,IAAM,iBAAN,cAA6B,MAAM;AAAA;AAAA,EAE/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EAET,YAAY,MAAiB,SAAiB,MAAgB;AAC5D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAeO,SAAS,OAAO,OAA2B;AAChD,MAAI,EAAE,iBAAiB,mBAAmB,MAAM,SAAS,cAAc,CAAC,YAAY,MAAM,IAAI,GAAG;AAC/F,WAAO,EAAE,MAAM,YAAY,SAAS,iBAAiB;AAAA,EACvD;AACA,SAAO,MAAM,SAAS,SAClB,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,QAAQ,IAC3C,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,SAAS,MAAM,MAAM,KAAK;AACnE;AAQO,SAAS,SAAS,SAAkC;AACzD,MAAI,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,YAAY,UAAU;AAC7D,WAAO,IAAI,eAAe,YAAY,gBAAgB;AAAA,EACxD;AACA,QAAM,OAAO,YAAY,QAAQ,IAAI,IAAI,QAAQ,OAAO;AACxD,SAAO,IAAI,eAAe,MAAM,QAAQ,SAAS,QAAQ,IAAI;AAC/D;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/protocol/guards.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isName(value) {
|
|
6
|
+
return typeof value === "string" && value.length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
isRecord,
|
|
11
|
+
isName
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=chunk-QSU47XEX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol/guards.ts"],"sourcesContent":["// Shape checks shared by the protocol's hand-written guards. Internal: the\n// package root exports the guards built on these, not these.\n\n/** An object that is not an array or `null`: what a JSON object decodes to. */\nexport type UnknownRecord = { readonly [key: string]: unknown };\n\n/** True when `value` is an object that is not an array or `null`. */\nexport function isRecord(value: unknown): value is UnknownRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** True when `value` is a non-empty string, as service and method names are. */\nexport function isName(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n"],"mappings":";AAOO,SAAS,SAAS,OAAwC;AAC/D,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAGO,SAAS,OAAO,OAAiC;AACtD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;","names":[]}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPrismaError,
|
|
3
|
+
prismaSummary
|
|
4
|
+
} from "./chunk-DKE6QO26.js";
|
|
1
5
|
import {
|
|
2
6
|
QuickdrawError
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-Q4OGQM5I.js";
|
|
4
8
|
|
|
5
9
|
// src/server/pipeline/metrics.ts
|
|
6
10
|
function describeError(error, depth = 0) {
|
|
7
11
|
if (!(error instanceof Error)) {
|
|
8
12
|
return { value: String(error) };
|
|
9
13
|
}
|
|
14
|
+
if (isPrismaError(error)) {
|
|
15
|
+
const summary = prismaSummary(error);
|
|
16
|
+
if (error.cause !== void 0 && depth < 3) {
|
|
17
|
+
summary.cause = describeError(error.cause, depth + 1);
|
|
18
|
+
}
|
|
19
|
+
return summary;
|
|
20
|
+
}
|
|
10
21
|
const described = {
|
|
11
22
|
name: error.name,
|
|
12
23
|
message: error.message,
|
|
@@ -83,4 +94,4 @@ export {
|
|
|
83
94
|
failureLevel,
|
|
84
95
|
createRecorder
|
|
85
96
|
};
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
97
|
+
//# sourceMappingURL=chunk-RZXJTDH5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/pipeline/metrics.ts"],"sourcesContent":["// The completion record (RFC 0003 section 9, step 9) and call logging. Every\n// call produces exactly one record, handed to `onCall`, and one log entry:\n// `debug` normally, `warn` when slower than `slowMs` or larger than\n// `maxResponseBytes`, `error` when the call failed with `INTERNAL` or\n// `TIMEOUT` (the failures that are the server's fault). An `INTERNAL`\n// entry carries the original error with its stack. This replaces 4.1's two\n// `info` entries per call (4.1 `src/server/ServiceRegistry.ts:331-373`).\n\nimport { isPrismaError, prismaSummary, type Logger } from \"../../contract/logger\";\nimport type { MethodKind } from \"../../contract/methods\";\nimport { QuickdrawError, type ErrorCode } from \"../../protocol/errors\";\nimport type { DevWarnings } from \"../devWarnings\";\nimport type { Transport } from \"../types\";\n\n/** How a call ended: `\"ok\"`, `\"not-modified\"`, or the error code it failed with. */\nexport type CallOutcome = \"ok\" | \"not-modified\" | ErrorCode;\n\n/** One call's completion record, handed to the dispatcher's `onCall`. */\nexport interface CallRecord {\n readonly service: string;\n readonly method: string;\n /** `undefined` when the method does not exist. */\n readonly kind: MethodKind | undefined;\n readonly transport: Transport;\n readonly requestId: string;\n readonly outcome: CallOutcome;\n /** From the call's arrival until its result was ready, in milliseconds. */\n readonly durationMs: number;\n /** Time spent waiting for a query slot, in milliseconds. */\n readonly queueMs: number;\n /** The reply's size as the transport measured it; 0 when it measured nothing. */\n readonly bytes: number;\n /** True when the result came from another call's run of the same query, or its `ttlMs` cache. */\n readonly shared: boolean;\n /**\n * Database statements the handler run issued, when the unit of work counts\n * them: the handler's own reads and writes, the reads a kit handler makes\n * to filter by access included. The access check before the handler (its\n * reads run before the unit opens), the flush after it and the per-caller\n * strip of field tiers are not counted. A shared run is counted once, on\n * the call that started it; a call that joined it reports `undefined`.\n */\n readonly sqlStatements: number | undefined;\n}\n\n/** Options of {@link createRecorder}. */\nexport interface RecorderOptions {\n readonly logger: Logger;\n readonly onCall: ((record: CallRecord) => void) | undefined;\n readonly slowMs: number;\n readonly maxResponseBytes: number;\n /** Raises `oversized-response` for a reply over `maxResponseBytes`. */\n readonly warnings?: DevWarnings;\n}\n\n/** What a record is logged with besides the record itself. */\nexport interface RecordDetails {\n /** The error the call failed with. */\n readonly error?: QuickdrawError;\n /** The caller's user id, for the log only. */\n readonly userId?: string;\n}\n\n/**\n * The parts of an error worth logging, including its stack and cause. A\n * Prisma error is summarized: its message renders the call, values\n * included.\n */\nexport function describeError(error: unknown, depth = 0): Record<string, unknown> {\n if (!(error instanceof Error)) {\n return { value: String(error) };\n }\n if (isPrismaError(error)) {\n const summary = prismaSummary(error);\n if (error.cause !== undefined && depth < 3) {\n summary.cause = describeError(error.cause, depth + 1);\n }\n return summary;\n }\n const described: Record<string, unknown> = {\n name: error.name,\n message: error.message,\n stack: error.stack,\n };\n if (\"code\" in error) {\n described.code = error.code;\n }\n if (\"data\" in error && error.data !== undefined) {\n described.data = error.data;\n }\n if (error.cause !== undefined && depth < 3) {\n described.cause = describeError(error.cause, depth + 1);\n }\n return described;\n}\n\ntype Level = \"debug\" | \"warn\" | \"error\";\n\n/** True for the codes of server faults, which log at error; every other code the client caused. */\nfunction isFault(code: unknown): boolean {\n return code === \"INTERNAL\" || code === \"TIMEOUT\";\n}\n\n/**\n * The level a failure outside a call logs at, as a call's failure does:\n * `debug` for a `QuickdrawError` the client caused (any code but `INTERNAL`\n * and `TIMEOUT`), else `error`. A channel handler refusing a payload must\n * not fill the error log.\n */\nexport function failureLevel(error: unknown): \"debug\" | \"error\" {\n return error instanceof QuickdrawError && !isFault(error.code) ? \"debug\" : \"error\";\n}\n\nfunction levelOf(record: CallRecord, options: RecorderOptions): Level {\n if (isFault(record.outcome)) {\n return \"error\";\n }\n const slow = record.durationMs > options.slowMs;\n return slow || record.bytes > options.maxResponseBytes ? \"warn\" : \"debug\";\n}\n\n/** Hands `record` to `onCall`; what it throws is logged, never thrown. */\nfunction emit(options: RecorderOptions, record: CallRecord): void {\n if (options.onCall === undefined) {\n return;\n }\n try {\n options.onCall(record);\n } catch (error) {\n options.logger.error(\"onCall threw; the call was not affected\", {\n category: \"quickdraw.call\",\n error: describeError(error),\n });\n }\n}\n\n/**\n * The `oversized-response` development warning, for a method whose reply\n * was larger than `maxResponseBytes`. It is raised after the record went to\n * `onCall`, because a strict test app throws it.\n */\nfunction warnOversized(options: RecorderOptions, record: CallRecord): void {\n if (record.kind === undefined || record.bytes <= options.maxResponseBytes) {\n return;\n }\n options.warnings?.warn({\n kind: \"oversized-response\",\n service: record.service,\n method: record.method,\n message:\n `replied with ${record.bytes} bytes, more than maxResponseBytes (${options.maxResponseBytes}); ` +\n \"page the result (take and a cursor), return a leaner projection, or serve it as a collection\",\n meta: { bytes: record.bytes, maxResponseBytes: options.maxResponseBytes },\n });\n}\n\n/**\n * Returns the function that logs a call's record and hands it to `onCall`.\n * In a strict test app it throws an oversized reply's `DevWarningError`\n * once it has done both.\n */\nexport function createRecorder(\n options: RecorderOptions,\n): (record: CallRecord, details: RecordDetails) => void {\n return (record, details) => {\n const meta: Record<string, unknown> = { category: \"quickdraw.call\", ...record };\n if (details.userId !== undefined) {\n meta.userId = details.userId;\n }\n if (details.error !== undefined) {\n meta.error = describeError(details.error);\n }\n const duration = Math.round(record.durationMs);\n const message = `${record.service}.${record.method} ${record.outcome} in ${duration} ms`;\n options.logger[levelOf(record, options)](message, meta);\n emit(options, record);\n warnOversized(options, record);\n };\n}\n"],"mappings":";;;;;;;;;AAoEO,SAAS,cAAc,OAAgB,QAAQ,GAA4B;AAChF,MAAI,EAAE,iBAAiB,QAAQ;AAC7B,WAAO,EAAE,OAAO,OAAO,KAAK,EAAE;AAAA,EAChC;AACA,MAAI,cAAc,KAAK,GAAG;AACxB,UAAM,UAAU,cAAc,KAAK;AACnC,QAAI,MAAM,UAAU,UAAa,QAAQ,GAAG;AAC1C,cAAQ,QAAQ,cAAc,MAAM,OAAO,QAAQ,CAAC;AAAA,IACtD;AACA,WAAO;AAAA,EACT;AACA,QAAM,YAAqC;AAAA,IACzC,MAAM,MAAM;AAAA,IACZ,SAAS,MAAM;AAAA,IACf,OAAO,MAAM;AAAA,EACf;AACA,MAAI,UAAU,OAAO;AACnB,cAAU,OAAO,MAAM;AAAA,EACzB;AACA,MAAI,UAAU,SAAS,MAAM,SAAS,QAAW;AAC/C,cAAU,OAAO,MAAM;AAAA,EACzB;AACA,MAAI,MAAM,UAAU,UAAa,QAAQ,GAAG;AAC1C,cAAU,QAAQ,cAAc,MAAM,OAAO,QAAQ,CAAC;AAAA,EACxD;AACA,SAAO;AACT;AAKA,SAAS,QAAQ,MAAwB;AACvC,SAAO,SAAS,cAAc,SAAS;AACzC;AAQO,SAAS,aAAa,OAAmC;AAC9D,SAAO,iBAAiB,kBAAkB,CAAC,QAAQ,MAAM,IAAI,IAAI,UAAU;AAC7E;AAEA,SAAS,QAAQ,QAAoB,SAAiC;AACpE,MAAI,QAAQ,OAAO,OAAO,GAAG;AAC3B,WAAO;AAAA,EACT;AACA,QAAM,OAAO,OAAO,aAAa,QAAQ;AACzC,SAAO,QAAQ,OAAO,QAAQ,QAAQ,mBAAmB,SAAS;AACpE;AAGA,SAAS,KAAK,SAA0B,QAA0B;AAChE,MAAI,QAAQ,WAAW,QAAW;AAChC;AAAA,EACF;AACA,MAAI;AACF,YAAQ,OAAO,MAAM;AAAA,EACvB,SAAS,OAAO;AACd,YAAQ,OAAO,MAAM,2CAA2C;AAAA,MAC9D,UAAU;AAAA,MACV,OAAO,cAAc,KAAK;AAAA,IAC5B,CAAC;AAAA,EACH;AACF;AAOA,SAAS,cAAc,SAA0B,QAA0B;AACzE,MAAI,OAAO,SAAS,UAAa,OAAO,SAAS,QAAQ,kBAAkB;AACzE;AAAA,EACF;AACA,UAAQ,UAAU,KAAK;AAAA,IACrB,MAAM;AAAA,IACN,SAAS,OAAO;AAAA,IAChB,QAAQ,OAAO;AAAA,IACf,SACE,gBAAgB,OAAO,KAAK,uCAAuC,QAAQ,gBAAgB;AAAA,IAE7F,MAAM,EAAE,OAAO,OAAO,OAAO,kBAAkB,QAAQ,iBAAiB;AAAA,EAC1E,CAAC;AACH;AAOO,SAAS,eACd,SACsD;AACtD,SAAO,CAAC,QAAQ,YAAY;AAC1B,UAAM,OAAgC,EAAE,UAAU,kBAAkB,GAAG,OAAO;AAC9E,QAAI,QAAQ,WAAW,QAAW;AAChC,WAAK,SAAS,QAAQ;AAAA,IACxB;AACA,QAAI,QAAQ,UAAU,QAAW;AAC/B,WAAK,QAAQ,cAAc,QAAQ,KAAK;AAAA,IAC1C;AACA,UAAM,WAAW,KAAK,MAAM,OAAO,UAAU;AAC7C,UAAM,UAAU,GAAG,OAAO,OAAO,IAAI,OAAO,MAAM,IAAI,OAAO,OAAO,OAAO,QAAQ;AACnF,YAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC,EAAE,SAAS,IAAI;AACtD,SAAK,SAAS,MAAM;AACpB,kBAAc,SAAS,MAAM;AAAA,EAC/B;AACF;","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
utf8ByteLength
|
|
3
|
+
} from "./chunk-7Y5ROPXL.js";
|
|
4
|
+
|
|
1
5
|
// src/protocol/parser.ts
|
|
2
6
|
import { Decoder, Encoder, PacketType } from "socket.io-parser";
|
|
3
7
|
|
|
@@ -20,35 +24,6 @@ function isPreEncoded(value) {
|
|
|
20
24
|
return value instanceof PreEncoded;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
// src/protocol/utf8.ts
|
|
24
|
-
var nodeBuffer = globalThis.Buffer;
|
|
25
|
-
function isHighSurrogate(unit) {
|
|
26
|
-
return unit >= 55296 && unit <= 56319;
|
|
27
|
-
}
|
|
28
|
-
function isLowSurrogate(unit) {
|
|
29
|
-
return unit >= 56320 && unit <= 57343;
|
|
30
|
-
}
|
|
31
|
-
function countUtf8Bytes(text) {
|
|
32
|
-
let bytes = 0;
|
|
33
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
34
|
-
const unit = text.charCodeAt(index);
|
|
35
|
-
if (unit < 128) {
|
|
36
|
-
bytes += 1;
|
|
37
|
-
} else if (unit < 2048) {
|
|
38
|
-
bytes += 2;
|
|
39
|
-
} else if (isHighSurrogate(unit) && isLowSurrogate(text.charCodeAt(index + 1))) {
|
|
40
|
-
bytes += 4;
|
|
41
|
-
index += 1;
|
|
42
|
-
} else {
|
|
43
|
-
bytes += 3;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return bytes;
|
|
47
|
-
}
|
|
48
|
-
function utf8ByteLength(text) {
|
|
49
|
-
return nodeBuffer === void 0 ? countUtf8Bytes(text) : nodeBuffer.byteLength(text, "utf8");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
27
|
// src/protocol/parser.ts
|
|
53
28
|
var hasBlob = typeof Blob === "function";
|
|
54
29
|
function isBinary(value) {
|
|
@@ -124,7 +99,6 @@ function createJsonParser(options = {}) {
|
|
|
124
99
|
|
|
125
100
|
export {
|
|
126
101
|
PreEncoded,
|
|
127
|
-
utf8ByteLength,
|
|
128
102
|
createJsonParser
|
|
129
103
|
};
|
|
130
|
-
//# sourceMappingURL=chunk-
|
|
104
|
+
//# sourceMappingURL=chunk-SN3OZVEQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol/parser.ts","../src/protocol/preEncoded.ts"],"sourcesContent":["// A JSON-only Socket.IO parser (RFC 0003 section 8.4). The stock encoder\n// walks every event and acknowledgement looking for binary values, then\n// serializes it; this one skips the walk. For a packet without binary its\n// output is the stock output byte for byte, because it is the stock encoder's\n// own string encoding: `encode` is overridden to call `encodeAsString`\n// directly (socket.io-parser 4.2.5 `Encoder.encode`). The 4.x legacy shim\n// therefore needs no second parser. The decoder is the stock decoder, so a\n// server using this parser still reads packets from a client that uses the\n// stock one, binary included.\n//\n// Apps that send binary use the stock parser instead (`binary: true` on the\n// server). This module imports `socket.io-parser`, so it is its own export,\n// `@fitzzero/quickdraw-core/parser`, and the package root stays free of\n// dependencies.\n//\n// The server hands it pre-encoded arguments (`preEncoded.ts`) for text that\n// several packets share, such as a shared run's reply to each of its callers:\n// their text is spliced in as written, so it is encoded once, not per packet.\n\nimport { Decoder, Encoder, PacketType, type Packet } from \"socket.io-parser\";\nimport { isPreEncoded, type PreEncoded } from \"./preEncoded\";\nimport { utf8ByteLength } from \"./utf8\";\n\n/** Options of {@link createJsonParser}. */\nexport interface JsonParserOptions {\n /**\n * Called with every packet the encoder writes and its size in UTF-8 bytes:\n * events, acknowledgements (a call's response travels as one) and connection\n * packets. A broadcast is encoded once, so it is reported once, not once per\n * recipient. The hook runs inside `emit`: keep it cheap. It must not throw;\n * if it does, the error is ignored and the packet is still sent.\n */\n readonly onEncoded?: (packet: Packet, byteLength: number) => void;\n}\n\n/** A Socket.IO parser: the `parser` option of both `new Server()` and `io()`. */\nexport interface JsonParser {\n readonly Encoder: typeof Encoder;\n readonly Decoder: typeof Decoder;\n}\n\ntype EncodedHook = NonNullable<JsonParserOptions[\"onEncoded\"]>;\n\n/** The stock encoder's string encoding; `private` in its type declarations only. */\ninterface StringEncoding {\n encodeAsString(this: Encoder, packet: Packet): string;\n}\n\nconst hasBlob = typeof Blob === \"function\";\n\n/** Binary as the stock parser defines it: an `ArrayBuffer`, a view of one (`Buffer` too) or a `Blob`. */\nfunction isBinary(value: unknown): value is object {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n (hasBlob && value instanceof Blob))\n );\n}\n\nfunction binaryMessage(packet: Packet, index: number, value: object): string {\n const kind = Object.prototype.toString.call(value).slice(8, -1);\n const where =\n packet.type === PacketType.ACK\n ? `Argument ${index + 1} of acknowledgement ${String(packet.id)}`\n : `Argument ${index} of event \"${String(packet.data[0])}\"`;\n return (\n `${where} is binary (${kind}), which the JSON-only Socket.IO parser cannot send. ` +\n \"Use the stock Socket.IO parser to send binary; a quickdraw server takes `binary: true`.\"\n );\n}\n\n/**\n * Throws when an argument of an event or an acknowledgement is binary. It\n * looks at the arguments themselves, never inside them: that search is the\n * cost this parser exists to remove. Binary nested in an argument is written\n * the way `JSON.stringify` writes it.\n */\nfunction rejectBinaryArguments(packet: Packet): void {\n if (packet.type !== PacketType.EVENT && packet.type !== PacketType.ACK) {\n return;\n }\n const args: unknown = packet.data;\n if (!Array.isArray(args)) {\n return;\n }\n for (let index = 0; index < args.length; index += 1) {\n const arg: unknown = args[index];\n if (isBinary(arg)) {\n throw new TypeError(binaryMessage(packet, index, arg));\n }\n }\n}\n\n/** The stock `encodeAsString`, or a clear error when a socket.io-parser release drops it. */\nfunction stockEncodeAsString(): StringEncoding[\"encodeAsString\"] {\n const { encodeAsString } = Encoder.prototype as unknown as Partial<StringEncoding>;\n if (typeof encodeAsString !== \"function\") {\n throw new TypeError(\n \"createJsonParser: socket.io-parser's Encoder has no encodeAsString method, \" +\n \"which the JSON parser is built on. Install socket.io-parser 4.2.4 or a later 4.x.\",\n );\n }\n return encodeAsString;\n}\n\n/** The arguments of an event or acknowledgement when every one is pre-encoded. */\nfunction preEncodedArguments(packet: Packet): readonly PreEncoded[] | undefined {\n const data: unknown = packet.data;\n if (\n (packet.type !== PacketType.EVENT && packet.type !== PacketType.ACK) ||\n !Array.isArray(data)\n ) {\n return undefined;\n }\n const args: readonly unknown[] = data;\n return args.length > 0 && args.every(isPreEncoded) ? args : undefined;\n}\n\n/**\n * The text of an event or acknowledgement whose arguments are all\n * pre-encoded: the stock encoding of the packet without its arguments, then\n * the arguments' own text as a JSON array, which is what the stock encoding\n * writes for their values. `undefined` for any other packet, and when the\n * encoder was given a `replacer` (the stock encoding then writes the values).\n */\nfunction splicedText(\n encoder: Encoder,\n packet: Packet,\n encodeAsString: StringEncoding[\"encodeAsString\"],\n): string | undefined {\n const args = preEncodedArguments(packet);\n const { replacer } = encoder as unknown as { readonly replacer?: unknown };\n if (args === undefined || replacer !== undefined) {\n return undefined;\n }\n const head = encodeAsString.call(encoder, { ...packet, data: undefined });\n return `${head}[${args.map((arg) => arg.json()).join(\",\")}]`;\n}\n\nfunction report(onEncoded: EncodedHook, packet: Packet, text: string): void {\n try {\n onEncoded(packet, utf8ByteLength(text));\n } catch {\n // Measuring never stops a packet: a throw here would surface from `emit`.\n }\n}\n\n/**\n * Creates a Socket.IO parser that writes packets as JSON text only: the stock\n * encoder without its search for binary values, and the stock decoder. Pass it\n * as `parser` to the server and to the client; either end may also keep the\n * stock parser, since the two write the same text.\n *\n * An event or acknowledgement whose argument is binary (a `Buffer`, typed\n * array, `ArrayBuffer`, `DataView` or `Blob`) throws a `TypeError` from\n * `emit`. Binary inside an argument is not looked for.\n */\nexport function createJsonParser(options: JsonParserOptions = {}): JsonParser {\n const { onEncoded } = options;\n const encodeAsString = stockEncodeAsString();\n\n class JsonEncoder extends Encoder {\n override encode(packet: Packet): string[] {\n rejectBinaryArguments(packet);\n const text = splicedText(this, packet, encodeAsString) ?? encodeAsString.call(this, packet);\n if (onEncoded !== undefined) {\n report(onEncoded, packet, text);\n }\n return [text];\n }\n }\n\n return Object.freeze({ Encoder: JsonEncoder, Decoder });\n}\n","// A Socket.IO argument whose JSON text is written outside the encoder, so\n// text many packets share is written once (RFC 0003 sections 8.4 and 9): the\n// reply of a shared run goes to every caller in the same text. The JSON\n// parser (`parser.ts`) writes that text as the argument. Anything else that\n// serializes the argument, `JSON.stringify` or the stock parser's encoder,\n// writes its value through `toJSON`. Internal: the server builds these and\n// only its own JSON parser reads them.\n\n/** An argument whose JSON text `json()` writes; see the module comment. */\nexport class PreEncoded {\n /**\n * @param value - What the argument is.\n * @param json - Writes `JSON.stringify(value)`; called once per packet, and\n * may throw what `JSON.stringify` throws.\n */\n constructor(\n readonly value: unknown,\n readonly json: () => string,\n ) {}\n\n toJSON(): unknown {\n return this.value;\n }\n}\n\nexport function isPreEncoded(value: unknown): value is PreEncoded {\n return value instanceof PreEncoded;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
describeError
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RZXJTDH5.js";
|
|
4
4
|
import {
|
|
5
5
|
ANY_FIELD
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-DUBU3BB3.js";
|
|
7
7
|
import {
|
|
8
|
-
consoleLogger
|
|
9
|
-
|
|
8
|
+
consoleLogger,
|
|
9
|
+
errorText
|
|
10
|
+
} from "./chunk-DKE6QO26.js";
|
|
10
11
|
|
|
11
12
|
// src/server/devWarnings.ts
|
|
12
13
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -240,6 +241,22 @@ function sinceLastDelete(history) {
|
|
|
240
241
|
function created(first) {
|
|
241
242
|
return first.op === "create" && first.mayHaveExisted !== true;
|
|
242
243
|
}
|
|
244
|
+
function mergeLinks(history) {
|
|
245
|
+
const deletes = history.filter((record) => record.op === "delete");
|
|
246
|
+
const [first] = deletes;
|
|
247
|
+
if (first?.links === void 0 || deletes.some((record) => record.links === void 0)) {
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
250
|
+
const junctions = Object.keys(first.links).filter(
|
|
251
|
+
(junction) => deletes.every((record) => record.links !== void 0 && Object.hasOwn(record.links, junction))
|
|
252
|
+
);
|
|
253
|
+
return Object.fromEntries(
|
|
254
|
+
junctions.map((junction) => [
|
|
255
|
+
junction,
|
|
256
|
+
deletes.flatMap((record) => record.links?.[junction] ?? [])
|
|
257
|
+
])
|
|
258
|
+
);
|
|
259
|
+
}
|
|
243
260
|
function mergeRow(history) {
|
|
244
261
|
const [first] = history;
|
|
245
262
|
if (created(first) && history[history.length - 1]?.op === "delete") {
|
|
@@ -257,13 +274,15 @@ function mergeRow(history) {
|
|
|
257
274
|
sinceLastDelete(history).map((record) => record.after),
|
|
258
275
|
false
|
|
259
276
|
);
|
|
277
|
+
const links = op === "delete" ? mergeLinks(history) : void 0;
|
|
260
278
|
return {
|
|
261
279
|
model: first.model,
|
|
262
280
|
id: first.id,
|
|
263
281
|
op,
|
|
264
282
|
fields: mergeFields(history),
|
|
265
283
|
...before === void 0 ? {} : { before },
|
|
266
|
-
...after === void 0 ? {} : { after }
|
|
284
|
+
...after === void 0 ? {} : { after },
|
|
285
|
+
...links === void 0 ? {} : { links }
|
|
267
286
|
};
|
|
268
287
|
}
|
|
269
288
|
function mergeRecords(records) {
|
|
@@ -422,6 +441,7 @@ function readsIdList(statement) {
|
|
|
422
441
|
const id = isRecord(where) ? where.id : void 0;
|
|
423
442
|
return isRecord(id) && Array.isArray(id.in);
|
|
424
443
|
}
|
|
444
|
+
var BATCH_INSERTS = /* @__PURE__ */ new Set(["createMany", "createManyAndReturn"]);
|
|
425
445
|
function nPlusOne(statement, keys) {
|
|
426
446
|
const label = `${statement.model}.${statement.operation}${keys.length === 0 ? "" : ` by ${keys.join(", ")}`}`;
|
|
427
447
|
return {
|
|
@@ -443,7 +463,7 @@ function createStatementChecks(raise) {
|
|
|
443
463
|
if (isUnboundedRead(statement)) {
|
|
444
464
|
raise(unboundedRead(statement));
|
|
445
465
|
}
|
|
446
|
-
if (place === "batch" || isRowWriteInTransaction(statement, place) || readsIdList(statement)) {
|
|
466
|
+
if (place === "batch" || BATCH_INSERTS.has(statement.operation) || isRowWriteInTransaction(statement, place) || readsIdList(statement)) {
|
|
447
467
|
return;
|
|
448
468
|
}
|
|
449
469
|
const keys = whereKeys(statement);
|
|
@@ -502,7 +522,7 @@ function notify(state, writes) {
|
|
|
502
522
|
} catch (error) {
|
|
503
523
|
state.logger.error("An onWrite listener threw", {
|
|
504
524
|
category: "quickdraw.writes",
|
|
505
|
-
error:
|
|
525
|
+
error: errorText(error)
|
|
506
526
|
});
|
|
507
527
|
}
|
|
508
528
|
}
|
|
@@ -802,4 +822,4 @@ export {
|
|
|
802
822
|
runBeforeAnyDispatcher,
|
|
803
823
|
createWriteTracker
|
|
804
824
|
};
|
|
805
|
-
//# sourceMappingURL=chunk-
|
|
825
|
+
//# sourceMappingURL=chunk-SQ5VUG77.js.map
|