@fitzzero/quickdraw-core 5.1.0 → 5.1.2
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 +533 -180
- package/dist/{admin-DKwkIh5i.d.ts → admin-DWWrT1h6.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-D4bFKsrb.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DXiSJZaf.d.ts} +42 -16
- package/dist/{caller-CXTFswBO.d.ts → caller-BT-Mv4bD.d.ts} +117 -22
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/chunk-5CGO426C.js +15 -0
- package/dist/chunk-5CGO426C.js.map +1 -0
- package/dist/{chunk-A6DJBGOG.js → chunk-6K5BY47X.js} +4594 -3481
- package/dist/chunk-6K5BY47X.js.map +1 -0
- package/dist/{chunk-VJTFXHOK.js → chunk-753VJIUP.js} +13 -15
- package/dist/chunk-753VJIUP.js.map +1 -0
- package/dist/{chunk-JZBCLNDU.js → chunk-7QCUX4TK.js} +4 -4
- package/dist/chunk-7QCUX4TK.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/{chunk-3674EIRT.js → chunk-AONAFOYU.js} +14 -5
- package/dist/{chunk-3674EIRT.js.map → chunk-AONAFOYU.js.map} +1 -1
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.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-CDKG4TFA.js → chunk-GKH3LJ7N.js} +1 -1
- package/dist/chunk-GKH3LJ7N.js.map +1 -0
- package/dist/{chunk-KNDDCVQJ.js → chunk-GSIPUNJL.js} +49 -9
- package/dist/chunk-GSIPUNJL.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-LCC7HNFP.js} +12 -1
- package/dist/chunk-LCC7HNFP.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-MBH6IDSM.js} +480 -12
- package/dist/chunk-MBH6IDSM.js.map +1 -0
- package/dist/{chunk-2UELNHDD.js → chunk-NMXYENH4.js} +10 -10
- package/dist/{chunk-2UELNHDD.js.map → chunk-NMXYENH4.js.map} +1 -1
- 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-REX5YPHX.js → chunk-RJ5I2VO3.js} +74 -19
- package/dist/chunk-RJ5I2VO3.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-RL4VKLBX.js} +1 -1
- package/dist/{chunk-QEKVYG5F.js.map → chunk-RL4VKLBX.js.map} +1 -1
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-Z5ILULF2.js} +26 -7
- package/dist/chunk-Z5ILULF2.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +5 -3
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +30 -18
- package/dist/client/index.js +8 -6
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-hWcPUVS9.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-CgijdGpT.d.ts} +8 -4
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Cp-Wk500.d.ts} +36 -14
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-BB_PkqQ7.d.ts} +16 -10
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-DzWyIXjj.d.ts} +18 -4
- package/dist/{http-Djj2Inuu.d.ts → http-DotQ5bj8.d.ts} +68 -17
- package/dist/index.d.ts +7 -7
- package/dist/index.js +37 -13
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-BmoZsL9a.d.ts} +43 -10
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-DW80Exve.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/prisma/index.d.ts +5 -5
- package/dist/prisma/index.js +130 -67
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +273 -268
- 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 +147 -67
- package/dist/server/index.js +520 -381
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +63 -17
- package/dist/server/mcp/index.js +256 -54
- 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-UDLn1049.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +9 -7
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +12 -12
- package/dist/testing/index.js +21 -12
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +8 -6
- package/dist/{types-Cu0vmx7c.d.ts → types-CYliTP65.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-A6DJBGOG.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-JZBCLNDU.js.map +0 -1
- package/dist/chunk-KNDDCVQJ.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VJTFXHOK.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → rateLimit-5RKBFLUR.js.map} +0 -0
|
@@ -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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/express/rateLimit.ts"],"sourcesContent":["/**\n * Express rate-limit presets built on express-rate-limit.\n *\n * Lives in the dedicated `/server/express` subpath so the core `/server`\n * entry point carries no express-rate-limit dependency. All presets are\n * factories (not module-level singletons) so env/config is read at call time.\n *\n * Note: distinct from the Socket.IO `createRateLimiter` exported from\n * `/server` — these protect REST routes (OAuth, webhooks, public APIs).\n */\n\nimport type { NextFunction, Request, Response } from \"express\";\nimport {\n ipKeyGenerator,\n rateLimit,\n type Options,\n type RateLimitRequestHandler,\n} from \"express-rate-limit\";\n\ntype PartialOptions = Partial<Options>;\n\nfunction handler(_req: Request, res: Response, _next: NextFunction, options: Options): void {\n const retryAfterSec = Math.max(1, Math.ceil((options.windowMs ?? 60_000) / 1000));\n if (!res.getHeader(\"Retry-After\")) {\n res.setHeader(\"Retry-After\", String(retryAfterSec));\n }\n res.status(options.statusCode ?? 429).json({ error: \"Rate limit exceeded\" });\n}\n\nconst jsonLimiter: PartialOptions = {\n standardHeaders: true,\n legacyHeaders: false,\n handler,\n};\n\n/** Normalize IPv4-mapped IPv6 addresses (::ffff:1.2.3.4 → 1.2.3.4). */\nexport function stripIp(raw: string | undefined): string {\n const ip = raw ?? \"unknown\";\n return ip.startsWith(\"::ffff:\") ? ip.slice(7) : ip;\n}\n\n/**\n * Key by Bearer token when present, falling back to client IP. Lets\n * token-authenticated clients get their own bucket instead of sharing\n * a NAT'd IP.\n */\nexport function bearerOrIp(req: Request): string {\n const auth = req.headers.authorization ?? \"\";\n if (auth.startsWith(\"Bearer \")) return auth.slice(7);\n return ipKeyGenerator(stripIp(req.ip));\n}\n\n/**\n * Base factory: JSON 429 body + Retry-After header + standard headers.\n */\nexport function createJsonRateLimiter(options: PartialOptions): RateLimitRequestHandler {\n return rateLimit({ ...jsonLimiter, ...options });\n}\n\n/** OAuth flows: 20 requests / 15 min per IP (status sub-routes excluded). */\nexport function createAuthLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 15 * 60 * 1000,\n max: 20,\n skip: (req) => req.path.endsWith(\"/status\"),\n ...overrides,\n });\n}\n\n/** Auth status polling: 120 requests / 15 min per IP. */\nexport function createAuthStatusLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 15 * 60 * 1000,\n max: 120,\n ...overrides,\n });\n}\n\n/** Inbound webhooks: 300 requests / min per IP. */\nexport function createWebhookLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 60 * 1000,\n max: 300,\n ...overrides,\n });\n}\n\n/** Generic public API: 60 requests / min per IP. */\nexport function createPublicApiLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 60 * 1000,\n max: 60,\n ...overrides,\n });\n}\n\n/**\n * Answers a refused call in the HTTP transport's own reply shape, so a\n * server caller (`createServerCaller`) reports `RATE_LIMITED` with\n * `retryAfterMs` instead of an unreadable reply.\n */\nfunction callHandler(req: Request, res: Response, _next: NextFunction, options: Options): void {\n const info = (req as Request & Partial<Record<string, { resetTime?: Date }>>)[\n options.requestPropertyName\n ];\n const untilReset =\n info?.resetTime === undefined ? options.windowMs : info.resetTime.getTime() - Date.now();\n const retryAfterMs = Math.max(1000, untilReset);\n if (!res.getHeader(\"Retry-After\")) {\n res.setHeader(\"Retry-After\", String(Math.ceil(retryAfterMs / 1000)));\n }\n res.status(options.statusCode).json({\n ok: false,\n e: { code: \"RATE_LIMITED\", message: \"Rate limit exceeded\", data: { retryAfterMs } },\n });\n}\n\n/**\n * Calls through the HTTP transport (RFC 0003 section 10), mounted with\n * `createServer({ http: { rateLimit: createCallLimiter() } })`: 300 calls per\n * minute per IP, refused with `429 { ok: false, e: { code: \"RATE_LIMITED\",\n * message, data: { retryAfterMs } } }`. Server-side prefetch reaches the API\n * from the web server's address, so key it some other way (`keyGenerator`)\n * or raise `max` when the web server prefetches for many users.\n */\nexport function createCallLimiter(overrides: PartialOptions = {}): RateLimitRequestHandler {\n return createJsonRateLimiter({\n windowMs: 60 * 1000,\n max: 300,\n handler: callHandler,\n ...overrides,\n });\n}\n"],"mappings":";AAYA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAIP,SAAS,QAAQ,MAAe,KAAe,OAAqB,SAAwB;AAC1F,QAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,YAAY,OAAU,GAAI,CAAC;AAChF,MAAI,CAAC,IAAI,UAAU,aAAa,GAAG;AACjC,QAAI,UAAU,eAAe,OAAO,aAAa,CAAC;AAAA,EACpD;AACA,MAAI,OAAO,QAAQ,cAAc,GAAG,EAAE,KAAK,EAAE,OAAO,sBAAsB,CAAC;AAC7E;AAEA,IAAM,cAA8B;AAAA,EAClC,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf;AACF;AAGO,SAAS,QAAQ,KAAiC;AACvD,QAAM,KAAK,OAAO;AAClB,SAAO,GAAG,WAAW,SAAS,IAAI,GAAG,MAAM,CAAC,IAAI;AAClD;AAOO,SAAS,WAAW,KAAsB;AAC/C,QAAM,OAAO,IAAI,QAAQ,iBAAiB;AAC1C,MAAI,KAAK,WAAW,SAAS,EAAG,QAAO,KAAK,MAAM,CAAC;AACnD,SAAO,eAAe,QAAQ,IAAI,EAAE,CAAC;AACvC;AAKO,SAAS,sBAAsB,SAAkD;AACtF,SAAO,UAAU,EAAE,GAAG,aAAa,GAAG,QAAQ,CAAC;AACjD;AAGO,SAAS,kBAAkB,YAA4B,CAAC,GAA4B;AACzF,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK,KAAK;AAAA,IACpB,KAAK;AAAA,IACL,MAAM,CAAC,QAAQ,IAAI,KAAK,SAAS,SAAS;AAAA,IAC1C,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,wBAAwB,YAA4B,CAAC,GAA4B;AAC/F,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK,KAAK;AAAA,IACpB,KAAK;AAAA,IACL,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,qBAAqB,YAA4B,CAAC,GAA4B;AAC5F,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK;AAAA,IACf,KAAK;AAAA,IACL,GAAG;AAAA,EACL,CAAC;AACH;AAGO,SAAS,uBAAuB,YAA4B,CAAC,GAA4B;AAC9F,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK;AAAA,IACf,KAAK;AAAA,IACL,GAAG;AAAA,EACL,CAAC;AACH;AAOA,SAAS,YAAY,KAAc,KAAe,OAAqB,SAAwB;AAC7F,QAAM,OAAQ,IACZ,QAAQ,mBACV;AACA,QAAM,aACJ,MAAM,cAAc,SAAY,QAAQ,WAAW,KAAK,UAAU,QAAQ,IAAI,KAAK,IAAI;AACzF,QAAM,eAAe,KAAK,IAAI,KAAM,UAAU;AAC9C,MAAI,CAAC,IAAI,UAAU,aAAa,GAAG;AACjC,QAAI,UAAU,eAAe,OAAO,KAAK,KAAK,eAAe,GAAI,CAAC,CAAC;AAAA,EACrE;AACA,MAAI,OAAO,QAAQ,UAAU,EAAE,KAAK;AAAA,IAClC,IAAI;AAAA,IACJ,GAAG,EAAE,MAAM,gBAAgB,SAAS,uBAAuB,MAAM,EAAE,aAAa,EAAE;AAAA,EACpF,CAAC;AACH;AAUO,SAAS,kBAAkB,YAA4B,CAAC,GAA4B;AACzF,SAAO,sBAAsB;AAAA,IAC3B,UAAU,KAAK;AAAA,IACf,KAAK;AAAA,IACL,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts","../src/contract/access.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.1.0\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (4.1 `src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /**\n * This server's id: random, new each time a server starts. A client that\n * reconnects to the same id came back after a blip, to another id to a\n * restarted server (a game's world and clock start over) or, behind a\n * cluster, to another node, each of which has its own.\n */\n readonly serverId: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n","// Access levels and access-control lists, unchanged from 4.1\n// (4.1 `src/shared/types.ts:7-14`). The level names are stored in app data\n// (`User.serviceAccess`, JSON access lists), so they never change.\n\nexport type AccessLevel = \"Public\" | \"Read\" | \"Moderate\" | \"Admin\";\n\n/** Every access level, lowest first. */\nexport const ACCESS_LEVELS: readonly AccessLevel[] = Object.freeze([\n \"Public\",\n \"Read\",\n \"Moderate\",\n \"Admin\",\n]);\n\n/** True when `value` is one of the four access level names. */\nexport function isAccessLevel(value: unknown): value is AccessLevel {\n return typeof value === \"string\" && (ACCESS_LEVELS as readonly string[]).includes(value);\n}\n\nexport type ACE = {\n userId: string;\n level: AccessLevel;\n};\n\nexport type ACL = ACE[];\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;;;ACpEzB,IAAM,gBAAwC,OAAO,OAAO;AAAA,EACjE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,cAAc,OAAsC;AAClE,SAAO,OAAO,UAAU,YAAa,cAAoC,SAAS,KAAK;AACzF;","names":[]}
|
package/dist/chunk-YSWH776Y.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// src/contract/logger.ts
|
|
2
|
-
var consoleLogger = {
|
|
3
|
-
info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? ""),
|
|
4
|
-
warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? ""),
|
|
5
|
-
error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? ""),
|
|
6
|
-
debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? ""),
|
|
7
|
-
child: () => consoleLogger
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
consoleLogger
|
|
12
|
-
};
|
|
13
|
-
//# sourceMappingURL=chunk-YSWH776Y.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/contract/logger.ts"],"sourcesContent":["// The logger contract and its console default, unchanged from 4.1\n// (4.1 `src/shared/types.ts:525-545`).\n\n/**\n * Logger interface compatible with Winston and other loggers\n */\nexport interface Logger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n error(message: string, meta?: Record<string, unknown>): void;\n debug(message: string, meta?: Record<string, unknown>): void;\n child(options: Record<string, unknown>): Logger;\n}\n\n/**\n * Default console logger implementation\n */\nexport const consoleLogger: Logger = {\n info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? \"\"),\n warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? \"\"),\n error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? \"\"),\n debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? \"\"),\n child: () => consoleLogger,\n};\n"],"mappings":";AAiBO,IAAM,gBAAwB;AAAA,EACnC,MAAM,CAAC,SAAS,SAAS,QAAQ,IAAI,UAAU,OAAO,IAAI,QAAQ,EAAE;AAAA,EACpE,MAAM,CAAC,SAAS,SAAS,QAAQ,KAAK,UAAU,OAAO,IAAI,QAAQ,EAAE;AAAA,EACrE,OAAO,CAAC,SAAS,SAAS,QAAQ,MAAM,WAAW,OAAO,IAAI,QAAQ,EAAE;AAAA,EACxE,OAAO,CAAC,SAAS,SAAS,QAAQ,MAAM,WAAW,OAAO,IAAI,QAAQ,EAAE;AAAA,EACxE,OAAO,MAAM;AACf;","names":[]}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logger interface compatible with Winston and other loggers
|
|
3
|
-
*/
|
|
4
|
-
interface Logger {
|
|
5
|
-
info(message: string, meta?: Record<string, unknown>): void;
|
|
6
|
-
warn(message: string, meta?: Record<string, unknown>): void;
|
|
7
|
-
error(message: string, meta?: Record<string, unknown>): void;
|
|
8
|
-
debug(message: string, meta?: Record<string, unknown>): void;
|
|
9
|
-
child(options: Record<string, unknown>): Logger;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Default console logger implementation
|
|
13
|
-
*/
|
|
14
|
-
declare const consoleLogger: Logger;
|
|
15
|
-
|
|
16
|
-
export { type Logger as L, consoleLogger as c };
|
|
File without changes
|