@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,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ANY_FIELD,
|
|
3
3
|
modelKey
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DUBU3BB3.js";
|
|
5
5
|
import {
|
|
6
6
|
PreEncoded,
|
|
7
7
|
createJsonParser
|
|
8
8
|
} from "./chunk-Z2LZ7IZ7.js";
|
|
9
|
+
import {
|
|
10
|
+
consoleLogger,
|
|
11
|
+
errorText
|
|
12
|
+
} from "./chunk-DKE6QO26.js";
|
|
9
13
|
import {
|
|
10
14
|
INTERNAL_MESSAGE,
|
|
11
15
|
QuickdrawError,
|
|
@@ -29,11 +33,25 @@ function callReplyJson(reply, dataJson) {
|
|
|
29
33
|
|
|
30
34
|
// src/server/transports/auth.ts
|
|
31
35
|
var SOCKET_SESSIONS = /* @__PURE__ */ new WeakMap();
|
|
32
|
-
function recordSocketSession(socket, sessionId) {
|
|
33
|
-
|
|
36
|
+
function recordSocketSession(socket, sessionId, expiresAt) {
|
|
37
|
+
const time = expiresAt === void 0 ? Number.NaN : new Date(expiresAt).getTime();
|
|
38
|
+
SOCKET_SESSIONS.set(socket, {
|
|
39
|
+
id: sessionId,
|
|
40
|
+
expiresAt: Number.isNaN(time) ? void 0 : time
|
|
41
|
+
});
|
|
34
42
|
}
|
|
35
43
|
function socketSessionOf(socket) {
|
|
36
|
-
return SOCKET_SESSIONS.get(socket);
|
|
44
|
+
return SOCKET_SESSIONS.get(socket)?.id;
|
|
45
|
+
}
|
|
46
|
+
function socketSessionExpiryOf(socket) {
|
|
47
|
+
return SOCKET_SESSIONS.get(socket)?.expiresAt;
|
|
48
|
+
}
|
|
49
|
+
var SOCKET_AUTHS = /* @__PURE__ */ new WeakMap();
|
|
50
|
+
function markSocketAuth(authenticate, info) {
|
|
51
|
+
SOCKET_AUTHS.set(authenticate, Object.freeze({ ...info }));
|
|
52
|
+
}
|
|
53
|
+
function socketAuthOf(authenticate) {
|
|
54
|
+
return typeof authenticate === "function" ? SOCKET_AUTHS.get(authenticate) : void 0;
|
|
37
55
|
}
|
|
38
56
|
function isRefusal(error) {
|
|
39
57
|
return error instanceof QuickdrawError && error.code === "UNAUTHENTICATED";
|
|
@@ -109,7 +127,7 @@ function createGrantsSink(auth, refresh, logger) {
|
|
|
109
127
|
logger.error("Refreshing a user's written grants failed", {
|
|
110
128
|
category: "quickdraw.access",
|
|
111
129
|
userId,
|
|
112
|
-
error:
|
|
130
|
+
error: errorText(error)
|
|
113
131
|
});
|
|
114
132
|
}
|
|
115
133
|
})
|
|
@@ -126,7 +144,7 @@ async function createJWT(payload, secret, expiresIn = DEFAULT_EXPIRATION) {
|
|
|
126
144
|
const jwt = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(secretKey);
|
|
127
145
|
return jwt;
|
|
128
146
|
}
|
|
129
|
-
async function
|
|
147
|
+
async function readVerifiedJWT(token, secret) {
|
|
130
148
|
try {
|
|
131
149
|
const secretKey = new TextEncoder().encode(secret);
|
|
132
150
|
const { payload } = await jose.jwtVerify(token, secretKey);
|
|
@@ -141,6 +159,17 @@ async function verifyJWT(token, secret) {
|
|
|
141
159
|
return null;
|
|
142
160
|
}
|
|
143
161
|
}
|
|
162
|
+
var warnedSessionToken = false;
|
|
163
|
+
async function verifyJWT(token, secret) {
|
|
164
|
+
const payload = await readVerifiedJWT(token, secret);
|
|
165
|
+
if (payload?.sid !== void 0 && process.env.NODE_ENV !== "production" && !warnedSessionToken) {
|
|
166
|
+
warnedSessionToken = true;
|
|
167
|
+
consoleLogger.warn(
|
|
168
|
+
"[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(...)."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return payload;
|
|
172
|
+
}
|
|
144
173
|
|
|
145
174
|
// src/server/auth/sessionCookie.ts
|
|
146
175
|
var SESSION_COOKIE = "session";
|
|
@@ -181,8 +210,8 @@ function sessionCookieNameFor(req, naming = {}) {
|
|
|
181
210
|
}
|
|
182
211
|
var SECURE_SESSION_COOKIES = Object.freeze([HOST_SESSION_COOKIE]);
|
|
183
212
|
var PLAIN_SESSION_COOKIES = Object.freeze([
|
|
184
|
-
|
|
185
|
-
|
|
213
|
+
HOST_SESSION_COOKIE,
|
|
214
|
+
SESSION_COOKIE
|
|
186
215
|
]);
|
|
187
216
|
function sessionCookieNamesFor(req, naming = {}) {
|
|
188
217
|
if (naming.cookieName !== void 0) {
|
|
@@ -219,13 +248,33 @@ function sessionCookie(res, options) {
|
|
|
219
248
|
}
|
|
220
249
|
};
|
|
221
250
|
}
|
|
251
|
+
function carriesHostSession(req) {
|
|
252
|
+
const header = req.headers.cookie;
|
|
253
|
+
if (typeof header === "string") {
|
|
254
|
+
return header.split(";").some((pair) => pair.trimStart().startsWith(`${HOST_SESSION_COOKIE}=`));
|
|
255
|
+
}
|
|
256
|
+
const cookies = Reflect.get(req, "cookies");
|
|
257
|
+
return typeof cookies === "object" && cookies !== null && Object.hasOwn(cookies, HOST_SESSION_COOKIE);
|
|
258
|
+
}
|
|
259
|
+
function clearStaleHostSession(res, req, written, sameSite) {
|
|
260
|
+
if (written === HOST_SESSION_COOKIE || !carriesHostSession(req)) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
res.clearCookie(HOST_SESSION_COOKIE, { httpOnly: true, secure: true, sameSite, path: "/" });
|
|
264
|
+
}
|
|
222
265
|
function setSessionCookie(res, jwt, options = {}) {
|
|
223
266
|
const { name, settings } = sessionCookie(res, options);
|
|
224
267
|
res.cookie(name, jwt, { ...settings, maxAge: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS });
|
|
268
|
+
if (options.cookieName === void 0) {
|
|
269
|
+
clearStaleHostSession(res, requestOf(res), name, settings.sameSite);
|
|
270
|
+
}
|
|
225
271
|
}
|
|
226
272
|
function clearSessionCookie(res, options = {}) {
|
|
227
273
|
const { name, settings } = sessionCookie(res, options);
|
|
228
274
|
res.clearCookie(name, settings);
|
|
275
|
+
if (options.cookieName === void 0) {
|
|
276
|
+
clearStaleHostSession(res, requestOf(res), name, settings.sameSite);
|
|
277
|
+
}
|
|
229
278
|
}
|
|
230
279
|
|
|
231
280
|
// src/server/auth/restMiddleware.ts
|
|
@@ -251,7 +300,7 @@ function createRequireAuth(options) {
|
|
|
251
300
|
res.status(401).json({ error: "Authentication is not configured" });
|
|
252
301
|
return;
|
|
253
302
|
}
|
|
254
|
-
const payload = await
|
|
303
|
+
const payload = await readVerifiedJWT(token, secret);
|
|
255
304
|
if (!payload?.userId) {
|
|
256
305
|
res.status(401).json({ error: "Invalid or expired token" });
|
|
257
306
|
return;
|
|
@@ -437,18 +486,19 @@ function cookieToken(cookies, names) {
|
|
|
437
486
|
function transportCookieNaming(cookieName) {
|
|
438
487
|
return { cookieName, domain: cookieDomainFromEnv() };
|
|
439
488
|
}
|
|
440
|
-
function
|
|
489
|
+
function httpCredentialsOf(req, naming) {
|
|
441
490
|
const cookie = cookieToken(cookiesOf(req), sessionCookieNamesFor(req, naming));
|
|
442
|
-
if (cookie !== null) {
|
|
443
|
-
return { token: cookie, from: "cookie" };
|
|
444
|
-
}
|
|
445
491
|
const bearer = extractBearerOrCookieToken({
|
|
446
492
|
headers: { authorization: req.headers.authorization }
|
|
447
493
|
});
|
|
448
|
-
return
|
|
494
|
+
return { cookie, bearer: bearer === "" ? null : bearer };
|
|
449
495
|
}
|
|
450
|
-
function
|
|
451
|
-
|
|
496
|
+
function httpCredentialOf(req, naming) {
|
|
497
|
+
const { cookie, bearer } = httpCredentialsOf(req, naming);
|
|
498
|
+
if (cookie !== null) {
|
|
499
|
+
return { token: cookie, from: "cookie" };
|
|
500
|
+
}
|
|
501
|
+
return bearer === null ? null : { token: bearer, from: "bearer" };
|
|
452
502
|
}
|
|
453
503
|
|
|
454
504
|
export {
|
|
@@ -460,6 +510,9 @@ export {
|
|
|
460
510
|
acknowledge,
|
|
461
511
|
recordSocketSession,
|
|
462
512
|
socketSessionOf,
|
|
513
|
+
socketSessionExpiryOf,
|
|
514
|
+
markSocketAuth,
|
|
515
|
+
socketAuthOf,
|
|
463
516
|
isRefusal,
|
|
464
517
|
isForbiddenCredential,
|
|
465
518
|
isPrincipal,
|
|
@@ -467,6 +520,7 @@ export {
|
|
|
467
520
|
createPrincipalResolver,
|
|
468
521
|
createGrantsSink,
|
|
469
522
|
createJWT,
|
|
523
|
+
readVerifiedJWT,
|
|
470
524
|
verifyJWT,
|
|
471
525
|
SESSION_COOKIE,
|
|
472
526
|
HOST_SESSION_COOKIE,
|
|
@@ -474,6 +528,7 @@ export {
|
|
|
474
528
|
cookieDomainFromEnv,
|
|
475
529
|
sessionCookieNameFor,
|
|
476
530
|
sessionCookieNamesFor,
|
|
531
|
+
clearStaleHostSession,
|
|
477
532
|
setSessionCookie,
|
|
478
533
|
clearSessionCookie,
|
|
479
534
|
extractBearerOrCookieToken,
|
|
@@ -483,7 +538,7 @@ export {
|
|
|
483
538
|
cookiesOf,
|
|
484
539
|
cookieToken,
|
|
485
540
|
transportCookieNaming,
|
|
486
|
-
|
|
487
|
-
|
|
541
|
+
httpCredentialsOf,
|
|
542
|
+
httpCredentialOf
|
|
488
543
|
};
|
|
489
|
-
//# sourceMappingURL=chunk-
|
|
544
|
+
//# sourceMappingURL=chunk-RJ5I2VO3.js.map
|
|
@@ -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`. 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 { 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\", 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). `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 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: 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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/protocol/envelope.ts"],"sourcesContent":["// Protocol v5 frames (RFC 0003 sections 8.2 and 8.3): what each `qd:*` event\n// carries and what its acknowledgement receives. `ClientToServerEvents` and\n// `ServerToClientEvents` key every frame by its event name from\n// `contract/names.ts`, in the event-map form Socket.IO's `Server` and the\n// client's `Socket` take as type parameters.\n//\n// Section 8.2 spells out the `qd:call`, `qd:cancel`, `qd:watch` and `qd:ch`\n// payloads, and sections 6, 7, 11.3 and 12.5 describe the rest. Fields the RFC\n// does not name follow its style: `s` service, `m` method, `c` collection,\n// `i` input, `d` data, `e` error, `v` version, `rev` revision.\n//\n// 4.1 used one event per service method and a `{ success, data }` reply\n// (4.1 `src/shared/types.ts:88-90`). Here every call travels as one\n// `qd:call` envelope and is answered `{ ok: true, d }` or `{ ok: false, e }`.\n//\n// Runtime guards cover the frames the dispatcher reads from an untrusted\n// client (`qd:call`, `qd:cancel`). They are written by hand so the package\n// root stays free of schema libraries.\n//\n// One rule for every frame, both ways: a receiver ignores the object fields\n// it does not know and the array elements after the last one it reads, and\n// a later revision of protocol 5 may only add fields to objects and append\n// elements to arrays (`qd:event`, `qd:stream`, `qd:ch`), never remove,\n// rename, retype or reorder what is here. So a client that reads only what\n// it knows keeps working against a newer server, and a server against a\n// newer client. Anything else needs a new protocol number (`qd:hello`'s\n// `protocol`), which the handshake refuses with `PROTOCOL_MISMATCH`.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport type { CLIENT_EVENTS, SERVER_EVENTS } from \"../contract/names\";\nimport type { WireError } from \"./errors\";\nimport { isName, isRecord } from \"./guards\";\nimport type { HelloFrame } from \"./version\";\n\n/**\n * A revision: a whole number of microseconds since the epoch that every\n * flush raises (RFC 0003 section 5.3), `max(Date.now() * 1000, last + 1)` on\n * one process and the shared counter's next value (Valkey's clock in\n * microseconds) behind a cluster; a safe integer, about 1.8e15. Compare\n * revisions as numbers; do not read them as times.\n */\nexport type Revision = number;\n\n/**\n * The version of a result the caller holds, sent back as a call's `v` so the\n * server can answer \"not modified\" (RFC 0003 section 9, step 5): a row's\n * revision, or what the method's `version(input, ctx)` returns. Opaque to the\n * client.\n */\nexport type Version = number | string;\n\n/** A call's number among its socket's calls in flight: a non-negative safe integer. */\nexport type CallId = number;\n\n/** An acknowledgement that succeeded with nothing to return. */\nexport interface Ok {\n readonly ok: true;\n}\n\n/** An acknowledgement that failed, and why. */\nexport interface Failure {\n readonly ok: false;\n readonly e: WireError;\n}\n\n// ---------------------------------------------------------------------------\n// Calls (section 8.2)\n// ---------------------------------------------------------------------------\n\n/** `qd:call`: one method call, answered through the acknowledgement. */\nexport interface CallEnvelope {\n /** Names the call for `qd:cancel`; unique among the socket's calls in flight. */\n readonly id: CallId;\n /** The service name. */\n readonly s: string;\n /** The method name. */\n readonly m: string;\n /** The input, not yet validated. Absent when the input is `undefined`. */\n readonly i?: unknown;\n /** The version of this call's result the caller already holds. */\n readonly v?: Version;\n}\n\n/** A call that returned data, with the version of that data when the method has one. */\nexport interface CallSuccess<Output = unknown> {\n readonly ok: true;\n readonly d: Output;\n readonly v?: Version;\n readonly nm?: undefined;\n}\n\n/** A call whose result is still the version the caller sent: keep the cached copy. */\nexport interface CallNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly v: Version;\n}\n\n/** The acknowledgement of `qd:call`. */\nexport type CallReply<Output = unknown> = CallSuccess<Output> | CallNotModified | Failure;\n\n/** `qd:cancel`: abort a call in flight. The call's acknowledgement still arrives. */\nexport interface CancelFrame {\n readonly id: CallId;\n}\n\n// ---------------------------------------------------------------------------\n// Entity subscriptions (section 6)\n// ---------------------------------------------------------------------------\n\n/** `qd:sub`: subscribe to rows of one service, sending the revision held for each. */\nexport interface EntitySubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n /** The revision held for each id, by position in `ids`; `null` where none is cached. */\n readonly revs?: readonly (Revision | null)[];\n}\n\n/** One id's row, as `qd:sub` answers it. */\nexport interface EntityRow<Row = unknown> {\n readonly ok: true;\n readonly d: Row;\n readonly rev: Revision;\n readonly nm?: undefined;\n}\n\n/** The revision the client sent for this id is current. */\nexport interface EntityNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly rev: Revision;\n}\n\n/** One id's answer to `qd:sub`: the row, \"not modified\", or why it was refused. */\nexport type EntityResult<Row = unknown> = EntityRow<Row> | EntityNotModified | Failure;\n\n/** The acknowledgement of `qd:sub`: one result per requested id, in request order. */\nexport type EntitySubscribeReply<Row = unknown> =\n | { readonly ok: true; readonly r: readonly EntityResult<Row>[] }\n | Failure;\n\n/** `qd:unsub`: stop receiving frames for these rows. */\nexport interface EntityUnsubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n}\n\n/** `qd:e` `u`: the full row. Replace the cached row when `rev` is newer. */\nexport interface EntityUpdate<Row = unknown> {\n readonly t: \"u\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Row;\n}\n\n/** `qd:e` `p`: changed fields only. Merge when `rev` is newer; fetch the row when it is not cached. */\nexport interface EntityPatch<Row = unknown> {\n readonly t: \"p\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Partial<Row>;\n}\n\n/** `qd:e` `r`: the row is gone. Keep a tombstone until a newer `u`. */\nexport interface EntityRemove {\n readonly t: \"r\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n}\n\n/** `qd:e`: a change to one row, sent to the room of each access tier that has subscribers. */\nexport type EntityFrame<Row = unknown> = EntityUpdate<Row> | EntityPatch<Row> | EntityRemove;\n\n// ---------------------------------------------------------------------------\n// Collections (section 7)\n// ---------------------------------------------------------------------------\n\n/** One scope of one collection of a service. */\nexport interface CollectionScopeRef {\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n}\n\n/** `qd:col:sub`: the first page of a scope, a later page by `cursor`, or a resume from `since`. */\nexport interface CollectionSubscribe extends CollectionScopeRef {\n /** The revision the client holds the scope at; the server resumes from it when it can. */\n readonly since?: Revision;\n readonly limit?: number;\n readonly cursor?: string;\n}\n\n/**\n * A scope member in the collection's index (RFC 0003 section 7.4): its id,\n * its revision, then its index field values in the order `index` declares.\n * The revision is the time in the service's `versionColumn` when it declares\n * one (in microseconds, as revisions are), else the revision the snapshot\n * (or, in an `added` delta, the flush) was read at.\n */\nexport type WireIndexRow = readonly [id: string, rev: Revision, ...fields: unknown[]];\n\n/** A page of a scope: the answer to `qd:col:sub` without `since`, or when `since` is too old. */\nexport interface CollectionSnapshot<Item = unknown> {\n readonly ok: true;\n readonly resumed?: undefined;\n /** The revision the page was read at; resume from it with `since`. */\n readonly rev: Revision;\n readonly items: readonly Item[];\n /** How many members the scope has. */\n readonly total: number;\n /** The next page's cursor; `null` on the last page. */\n readonly cursor: string | null;\n /** The page size used: the request's, or `maxLimit` when the request asked for more. */\n readonly limit: number;\n /** Present when the requested limit was above `maxLimit` and was lowered to it. */\n readonly clamped?: true;\n /**\n * One row per member in `order`, on the first page of a collection that\n * declares `index` (not on a page read with `cursor`). Absent when the\n * scope has more members than the index holds: see `indexTruncated`.\n */\n readonly index?: readonly WireIndexRow[];\n /** Present instead of `index` when the scope has more members than the index holds (50,000). */\n readonly indexTruncated?: true;\n}\n\n/** The answer to `qd:col:sub` when the server still holds every change since `since`. */\nexport interface CollectionResumed<Item = unknown> {\n readonly ok: true;\n readonly resumed: true;\n /** The scope's revision after these deltas. */\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n/** The acknowledgement of `qd:col:sub`. */\nexport type CollectionSubscribeReply<Item = unknown> =\n | CollectionSnapshot<Item>\n | CollectionResumed<Item>\n | Failure;\n\n/** `qd:col:items`: load full items of a scope by id. */\nexport interface CollectionItemsRequest extends CollectionScopeRef {\n readonly ids: readonly string[];\n}\n\n/**\n * The acknowledgement of `qd:col:items`: the items found, in request order,\n * and the revision they were read at, taken before the read as a\n * snapshot's is: a client drops an item older than what a delta brought it.\n */\nexport type CollectionItemsReply<Item = unknown> =\n | { readonly ok: true; readonly rev: Revision; readonly items: readonly Item[] }\n | Failure;\n\n/**\n * One change to a scope (RFC 0003 section 7.2). In a collection that declares\n * `index`, `added` carries the member's index row, built from the same row as\n * its item, and `patched` and `updated` carry every changed index field\n * (index fields are item fields): a client keeping the index updates the\n * row's fields from them, and its `rev` to the frame's `rev`.\n */\nexport type CollectionDelta<Item = unknown> =\n | { readonly t: \"added\"; readonly item: Item; readonly index?: WireIndexRow }\n | { readonly t: \"updated\"; readonly item: Item }\n | { readonly t: \"patched\"; readonly id: string; readonly d: Partial<Item> }\n | { readonly t: \"removed\"; readonly id: string }\n | { readonly t: \"reset\" };\n\n/** `qd:c`: one flush's changes to one scope, in order. */\nexport interface CollectionFrame<Item = unknown> extends CollectionScopeRef {\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n// ---------------------------------------------------------------------------\n// Topics, streams, channels and events (sections 8.2, 11.3 and 12.5)\n// ---------------------------------------------------------------------------\n\n/**\n * `qd:watch` and `qd:unwatch`: join or leave a change topic of service `s`:\n * `{collection}:{scope}` for one scope of a collection, or `service` for the\n * service-wide topic (RFC 0003 section 11.3).\n */\nexport interface WatchFrame {\n readonly s: string;\n readonly topic: string;\n}\n\n/**\n * `qd:changed`: a watched topic changed in the flush at `rev`; invalidate the\n * queries that watch it. Sent once per flush per topic, and carries no data.\n * On the `service` topic, `models` names the models whose writes changed it\n * (rc.5, additive: the client's model names, first letter lowercased), so a\n * query that watches only some of them (`watch: { service: [models] }`)\n * ignores the rest; a frame without it may have changed any of them.\n */\nexport interface ChangedFrame {\n readonly s: string;\n readonly topic: string;\n readonly rev: Revision;\n readonly models?: readonly string[];\n}\n\n/**\n * `qd:stream:sub` and `qd:stream:unsub`: one stream of a service, and for a\n * scoped stream one scope of it (absent for a global stream).\n */\nexport interface StreamSubscribe {\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n}\n\n/**\n * The acknowledgement of `qd:stream:sub`: the stream's seed, oldest first,\n * read as the socket joined the stream's room, so items pushed after it\n * arrive as `qd:stream` frames (possibly before this acknowledgement). The\n * seed is the latest items the server kept, or what the service computed\n * for this subscriber (the current state the items change); a computed one\n * the server had to wait for may also reflect items that arrived as frames\n * meanwhile. A client replaces what it held with it.\n */\nexport type StreamSubscribeReply<Item = unknown> =\n | { readonly ok: true; readonly seed: readonly Item[] }\n | Failure;\n\n/**\n * `qd:stream`: an item pushed to a stream, as one array argument\n * `[service, stream, scope, item]` (like `qd:event`), so a fast stream sends\n * no key names: `scope` is `null` for a global stream. The positions are\n * fixed; a later revision may append elements after `item`, never insert\n * them, so a client reads the four it knows and ignores the rest. Before\n * `5.0.0-rc.4` it was the object `{ s, stream, scope?, item }`.\n */\nexport type StreamFrame<Item = unknown> = readonly [\n s: string,\n stream: string,\n scope: string | null,\n item: Item,\n];\n\n/**\n * `qd:ch`, sent volatile and never acknowledged: `[service, channel,\n * payload]`. A later revision may append elements after `payload`; the\n * server reads the three it knows and ignores the rest.\n */\nexport type ChannelFrame<Payload = unknown> = readonly [\n s: string,\n channel: string,\n payload: Payload,\n];\n\n/**\n * `qd:event`: a custom room event declared in a contract's `events`:\n * `[service, event, payload]`. A later revision may append elements after\n * `payload`; a client reads the three it knows and ignores the rest.\n */\nexport type EventFrame<Payload = unknown> = readonly [s: string, event: string, payload: Payload];\n\n/**\n * `qd:presence`: who is in an app room (one a method joined with\n * `ctx.rooms.join`) the socket is in, by user id (RFC 0003 section 12.5).\n * Sent to a socket as it joins with `users`, the whole list (its own user\n * included); to the room's other sockets with `joined` when a user's first\n * socket joins, and with `left` when a user's last socket leaves; and to a\n * socket that leaves with `users: []`, since it no longer sees the room. A\n * socket the server took out of the room (`rooms.leave(room, { userId })`)\n * gets that `users: []` unasked: it is out, and its channels requiring the\n * room are dropped until a joining call lets it back. Exactly one of\n * `users`, `joined` and `left` is present. Anonymous sockets are in no list.\n */\nexport interface PresenceFrame {\n readonly room: string;\n /** Every user in the room: replaces what the client holds for it. */\n readonly users?: readonly string[];\n /** A user who joined the room. */\n readonly joined?: string;\n /** A user who left the room: their last socket in it left or disconnected. */\n readonly left?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Connection-level frames (sections 4.4, 7.2 and 8.3)\n// ---------------------------------------------------------------------------\n\n/** Why the server ended a subscription. */\nexport type RevokeReason = \"access\" | \"anchor-deleted\";\n\n/**\n * `qd:revoked`: the server removed this socket from a subscription's room,\n * because the principal's access was lowered or removed (`access`), or the\n * row a collection scope is anchored on was deleted (`anchor-deleted`). A\n * stream feed's subscription is revoked for `access` only; `scope` is absent\n * for a global stream.\n */\nexport type RevokedFrame =\n | {\n readonly kind: \"entity\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly id: string;\n }\n | {\n readonly kind: \"collection\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n }\n | {\n readonly kind: \"stream\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n };\n\n/** `qd:rotate`: stay connected until a random moment within `withinMs`, then reconnect. */\nexport interface RotateFrame {\n readonly withinMs: number;\n}\n\n/** `qd:access`: the user's service grants changed. */\nexport interface AccessFrame {\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n\n// ---------------------------------------------------------------------------\n// Event maps\n// ---------------------------------------------------------------------------\n\n/** The function a listener calls to acknowledge an event. */\ntype Ack<Reply> = (reply: Reply) => void;\n\n/** The listener of each client event, by its key in `CLIENT_EVENTS`. */\ninterface ClientListeners {\n call: (envelope: CallEnvelope, ack: Ack<CallReply>) => void;\n cancel: (frame: CancelFrame) => void;\n sub: (frame: EntitySubscribe, ack: Ack<EntitySubscribeReply>) => void;\n unsub: (frame: EntityUnsubscribe, ack?: Ack<Ok | Failure>) => void;\n collectionSub: (frame: CollectionSubscribe, ack: Ack<CollectionSubscribeReply>) => void;\n collectionUnsub: (frame: CollectionScopeRef, ack?: Ack<Ok | Failure>) => void;\n collectionItems: (frame: CollectionItemsRequest, ack: Ack<CollectionItemsReply>) => void;\n watch: (frame: WatchFrame, ack: Ack<Ok | Failure>) => void;\n unwatch: (frame: WatchFrame, ack?: Ack<Ok | Failure>) => void;\n streamSub: (frame: StreamSubscribe, ack: Ack<StreamSubscribeReply>) => void;\n streamUnsub: (frame: StreamSubscribe, ack?: Ack<Ok | Failure>) => void;\n channel: (frame: ChannelFrame) => void;\n}\n\n/** The listener of each server event, by its key in `SERVER_EVENTS`. */\ninterface ServerListeners {\n hello: (frame: HelloFrame) => void;\n entity: (frame: EntityFrame) => void;\n collection: (frame: CollectionFrame) => void;\n changed: (frame: ChangedFrame) => void;\n revoked: (frame: RevokedFrame) => void;\n stream: (frame: StreamFrame) => void;\n event: (frame: EventFrame) => void;\n presence: (frame: PresenceFrame) => void;\n rotate: (frame: RotateFrame) => void;\n access: (frame: AccessFrame) => void;\n}\n\n/**\n * The events a v5 client sends, keyed by name (`\"qd:call\"`, ...). The server\n * types its sockets with it: `new Server<ClientToServerEvents, ServerToClientEvents>()`.\n */\nexport type ClientToServerEvents = {\n [Key in keyof typeof CLIENT_EVENTS as (typeof CLIENT_EVENTS)[Key]]: ClientListeners[Key];\n};\n\n/**\n * The events a v5 server sends, keyed by name (`\"qd:hello\"`, ...). The client\n * types its socket with it: `Socket<ServerToClientEvents, ClientToServerEvents>`.\n */\nexport type ServerToClientEvents = {\n [Key in keyof typeof SERVER_EVENTS as (typeof SERVER_EVENTS)[Key]]: ServerListeners[Key];\n};\n\n// ---------------------------------------------------------------------------\n// Guards\n// ---------------------------------------------------------------------------\n\nfunction isCallId(value: unknown): value is CallId {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction isVersion(value: unknown): value is Version {\n return typeof value === \"string\" || (typeof value === \"number\" && Number.isFinite(value));\n}\n\n/**\n * True when `value` has the shape of a `qd:call` envelope: a call id, service\n * and method names, and a version when one is present. The input is not\n * looked at (the method's schema validates it), and unknown keys are ignored.\n */\nexport function isCallEnvelope(value: unknown): value is CallEnvelope {\n return (\n isRecord(value) &&\n isCallId(value.id) &&\n isName(value.s) &&\n isName(value.m) &&\n (value.v === undefined || isVersion(value.v))\n );\n}\n\n/** True when `value` has the shape of a `qd:cancel` frame. Unknown keys are ignored. */\nexport function isCancel(value: unknown): value is CancelFrame {\n return isRecord(value) && isCallId(value.id);\n}\n"],"mappings":";;;;;;AAweA,SAAS,SAAS,OAAiC;AACjD,SAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS;AAC9E;AAEA,SAAS,UAAU,OAAkC;AACnD,SAAO,OAAO,UAAU,YAAa,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AACzF;AAOO,SAAS,eAAe,OAAuC;AACpE,SACE,SAAS,KAAK,KACd,SAAS,MAAM,EAAE,KACjB,OAAO,MAAM,CAAC,KACd,OAAO,MAAM,CAAC,MACb,MAAM,MAAM,UAAa,UAAU,MAAM,CAAC;AAE/C;AAGO,SAAS,SAAS,OAAsC;AAC7D,SAAO,SAAS,KAAK,KAAK,SAAS,MAAM,EAAE;AAC7C;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/protocol/envelope.ts"],"sourcesContent":["// Protocol v5 frames (RFC 0003 sections 8.2 and 8.3): what each `qd:*` event\n// carries and what its acknowledgement receives. `ClientToServerEvents` and\n// `ServerToClientEvents` key every frame by its event name from\n// `contract/names.ts`, in the event-map form Socket.IO's `Server` and the\n// client's `Socket` take as type parameters.\n//\n// Section 8.2 spells out the `qd:call`, `qd:cancel`, `qd:watch` and `qd:ch`\n// payloads, and sections 6, 7, 11.3 and 12.5 describe the rest. Fields the RFC\n// does not name follow its style: `s` service, `m` method, `c` collection,\n// `i` input, `d` data, `e` error, `v` version, `rev` revision.\n//\n// 4.1 used one event per service method and a `{ success, data }` reply\n// (4.1 `src/shared/types.ts:88-90`). Here every call travels as one\n// `qd:call` envelope and is answered `{ ok: true, d }` or `{ ok: false, e }`.\n//\n// Runtime guards cover the frames the dispatcher reads from an untrusted\n// client (`qd:call`, `qd:cancel`). They are written by hand so the package\n// root stays free of schema libraries.\n//\n// One rule for every frame, both ways: a receiver ignores the object fields\n// it does not know and the array elements after the last one it reads, and\n// a later revision of protocol 5 may only add fields to objects and append\n// elements to arrays (`qd:event`, `qd:stream`, `qd:ch`), never remove,\n// rename, retype or reorder what is here. So a client that reads only what\n// it knows keeps working against a newer server, and a server against a\n// newer client. Anything else needs a new protocol number (`qd:hello`'s\n// `protocol`), which the handshake refuses with `PROTOCOL_MISMATCH`.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport type { CLIENT_EVENTS, SERVER_EVENTS } from \"../contract/names\";\nimport type { WireError } from \"./errors\";\nimport { isName, isRecord } from \"./guards\";\nimport type { HelloFrame } from \"./version\";\n\n/**\n * A revision: a whole number of microseconds since the epoch that every\n * flush raises (RFC 0003 section 5.3), `max(Date.now() * 1000, last + 1)` on\n * one process and the shared counter's next value (Valkey's clock in\n * microseconds) behind a cluster; a safe integer, about 1.8e15. Compare\n * revisions as numbers; do not read them as times.\n */\nexport type Revision = number;\n\n/**\n * The version of a result the caller holds, sent back as a call's `v` so the\n * server can answer \"not modified\" (RFC 0003 section 9, step 5): a row's\n * revision, or what the method's `version(input, ctx)` returns. Opaque to the\n * client.\n */\nexport type Version = number | string;\n\n/** A call's number among its socket's calls in flight: a non-negative safe integer. */\nexport type CallId = number;\n\n/** An acknowledgement that succeeded with nothing to return. */\nexport interface Ok {\n readonly ok: true;\n}\n\n/** An acknowledgement that failed, and why. */\nexport interface Failure {\n readonly ok: false;\n readonly e: WireError;\n}\n\n// ---------------------------------------------------------------------------\n// Calls (section 8.2)\n// ---------------------------------------------------------------------------\n\n/** `qd:call`: one method call, answered through the acknowledgement. */\nexport interface CallEnvelope {\n /** Names the call for `qd:cancel`; unique among the socket's calls in flight. */\n readonly id: CallId;\n /** The service name. */\n readonly s: string;\n /** The method name. */\n readonly m: string;\n /** The input, not yet validated. Absent when the input is `undefined`. */\n readonly i?: unknown;\n /** The version of this call's result the caller already holds. */\n readonly v?: Version;\n}\n\n/** A call that returned data, with the version of that data when the method has one. */\nexport interface CallSuccess<Output = unknown> {\n readonly ok: true;\n readonly d: Output;\n readonly v?: Version;\n readonly nm?: undefined;\n}\n\n/** A call whose result is still the version the caller sent: keep the cached copy. */\nexport interface CallNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly v: Version;\n}\n\n/** The acknowledgement of `qd:call`. */\nexport type CallReply<Output = unknown> = CallSuccess<Output> | CallNotModified | Failure;\n\n/** `qd:cancel`: abort a call in flight. The call's acknowledgement still arrives. */\nexport interface CancelFrame {\n readonly id: CallId;\n}\n\n// ---------------------------------------------------------------------------\n// Entity subscriptions (section 6)\n// ---------------------------------------------------------------------------\n\n/** `qd:sub`: subscribe to rows of one service, sending the revision held for each. */\nexport interface EntitySubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n /** The revision held for each id, by position in `ids`; `null` where none is cached. */\n readonly revs?: readonly (Revision | null)[];\n}\n\n/** One id's row, as `qd:sub` answers it. */\nexport interface EntityRow<Row = unknown> {\n readonly ok: true;\n readonly d: Row;\n readonly rev: Revision;\n readonly nm?: undefined;\n}\n\n/** The revision the client sent for this id is current. */\nexport interface EntityNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly rev: Revision;\n}\n\n/** One id's answer to `qd:sub`: the row, \"not modified\", or why it was refused. */\nexport type EntityResult<Row = unknown> = EntityRow<Row> | EntityNotModified | Failure;\n\n/** The acknowledgement of `qd:sub`: one result per requested id, in request order. */\nexport type EntitySubscribeReply<Row = unknown> =\n | { readonly ok: true; readonly r: readonly EntityResult<Row>[] }\n | Failure;\n\n/** `qd:unsub`: stop receiving frames for these rows. */\nexport interface EntityUnsubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n}\n\n/** `qd:e` `u`: the full row. Replace the cached row when `rev` is newer. */\nexport interface EntityUpdate<Row = unknown> {\n readonly t: \"u\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Row;\n}\n\n/** `qd:e` `p`: changed fields only. Merge when `rev` is newer; fetch the row when it is not cached. */\nexport interface EntityPatch<Row = unknown> {\n readonly t: \"p\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Partial<Row>;\n}\n\n/** `qd:e` `r`: the row is gone. Keep a tombstone until a newer `u`. */\nexport interface EntityRemove {\n readonly t: \"r\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n}\n\n/** `qd:e`: a change to one row, sent to the room of each access tier that has subscribers. */\nexport type EntityFrame<Row = unknown> = EntityUpdate<Row> | EntityPatch<Row> | EntityRemove;\n\n// ---------------------------------------------------------------------------\n// Collections (section 7)\n// ---------------------------------------------------------------------------\n\n/** One scope of one collection of a service. */\nexport interface CollectionScopeRef {\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n}\n\n/** `qd:col:sub`: the first page of a scope, a later page by `cursor`, or a resume from `since`. */\nexport interface CollectionSubscribe extends CollectionScopeRef {\n /** The revision the client holds the scope at; the server resumes from it when it can. */\n readonly since?: Revision;\n readonly limit?: number;\n readonly cursor?: string;\n}\n\n/**\n * A scope member in the collection's index (RFC 0003 section 7.4): its id,\n * its revision, then its index field values in the order `index` declares.\n * The revision is the time in the service's `versionColumn` when it declares\n * one (in microseconds, as revisions are), else the revision the snapshot\n * (or, in an `added` delta, the flush) was read at.\n */\nexport type WireIndexRow = readonly [id: string, rev: Revision, ...fields: unknown[]];\n\n/** A page of a scope: the answer to `qd:col:sub` without `since`, or when `since` is too old. */\nexport interface CollectionSnapshot<Item = unknown> {\n readonly ok: true;\n readonly resumed?: undefined;\n /** The revision the page was read at; resume from it with `since`. */\n readonly rev: Revision;\n readonly items: readonly Item[];\n /** How many members the scope has. */\n readonly total: number;\n /** The next page's cursor; `null` on the last page. */\n readonly cursor: string | null;\n /** The page size used: the request's, or `maxLimit` when the request asked for more. */\n readonly limit: number;\n /** Present when the requested limit was above `maxLimit` and was lowered to it. */\n readonly clamped?: true;\n /**\n * One row per member in `order`, on the first page of a collection that\n * declares `index` (not on a page read with `cursor`). Absent when the\n * scope has more members than the index holds: see `indexTruncated`.\n */\n readonly index?: readonly WireIndexRow[];\n /** Present instead of `index` when the scope has more members than the index holds (50,000). */\n readonly indexTruncated?: true;\n}\n\n/** The answer to `qd:col:sub` when the server still holds every change since `since`. */\nexport interface CollectionResumed<Item = unknown> {\n readonly ok: true;\n readonly resumed: true;\n /** The scope's revision after these deltas. */\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n/** The acknowledgement of `qd:col:sub`. */\nexport type CollectionSubscribeReply<Item = unknown> =\n | CollectionSnapshot<Item>\n | CollectionResumed<Item>\n | Failure;\n\n/** `qd:col:items`: load full items of a scope by id. */\nexport interface CollectionItemsRequest extends CollectionScopeRef {\n readonly ids: readonly string[];\n}\n\n/**\n * The acknowledgement of `qd:col:items`: the items found, in request order,\n * and the revision they were read at, taken before the read as a\n * snapshot's is: a client drops an item older than what a delta brought it.\n */\nexport type CollectionItemsReply<Item = unknown> =\n | { readonly ok: true; readonly rev: Revision; readonly items: readonly Item[] }\n | Failure;\n\n/**\n * One change to a scope (RFC 0003 section 7.2). In a collection that declares\n * `index`, `added` carries the member's index row, built from the same row as\n * its item, and `patched` and `updated` carry every changed index field\n * (index fields are item fields): a client keeping the index updates the\n * row's fields from them, and its `rev` to the frame's `rev`.\n */\nexport type CollectionDelta<Item = unknown> =\n | { readonly t: \"added\"; readonly item: Item; readonly index?: WireIndexRow }\n | { readonly t: \"updated\"; readonly item: Item }\n | { readonly t: \"patched\"; readonly id: string; readonly d: Partial<Item> }\n | { readonly t: \"removed\"; readonly id: string }\n | { readonly t: \"reset\" };\n\n/** `qd:c`: one flush's changes to one scope, in order. */\nexport interface CollectionFrame<Item = unknown> extends CollectionScopeRef {\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n// ---------------------------------------------------------------------------\n// Topics, streams, channels and events (sections 8.2, 11.3 and 12.5)\n// ---------------------------------------------------------------------------\n\n/**\n * `qd:watch` and `qd:unwatch`: join or leave a change topic of service `s`:\n * `{collection}:{scope}` for one scope of a collection, or `service` for the\n * service-wide topic (RFC 0003 section 11.3).\n */\nexport interface WatchFrame {\n readonly s: string;\n readonly topic: string;\n}\n\n/**\n * `qd:changed`: a watched topic changed in the flush at `rev`; invalidate the\n * queries that watch it. Sent once per flush per topic, and carries no data.\n * On the `service` topic, `models` names the models whose writes changed it\n * (rc.5, additive: the client's model names, first letter lowercased), so a\n * query that watches only some of them (`watch: { service: [models] }`)\n * ignores the rest; a frame without it may have changed any of them.\n */\nexport interface ChangedFrame {\n readonly s: string;\n readonly topic: string;\n readonly rev: Revision;\n readonly models?: readonly string[];\n}\n\n/**\n * `qd:stream:sub` and `qd:stream:unsub`: one stream of a service, and for a\n * scoped stream one scope of it (absent for a global stream).\n */\nexport interface StreamSubscribe {\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n}\n\n/**\n * The acknowledgement of `qd:stream:sub`: the stream's seed, oldest first,\n * read as the socket joined the stream's room, so items pushed after it\n * arrive as `qd:stream` frames (possibly before this acknowledgement). The\n * seed is the latest items the server kept, or what the service computed\n * for this subscriber (the current state the items change); a computed one\n * the server had to wait for may also reflect items that arrived as frames\n * meanwhile. A subscribe an unsubscribe overtook answers an empty seed. A\n * client replaces what it held with it.\n */\nexport type StreamSubscribeReply<Item = unknown> =\n | { readonly ok: true; readonly seed: readonly Item[] }\n | Failure;\n\n/**\n * `qd:stream`: an item pushed to a stream, as one array argument\n * `[service, stream, scope, item]` (like `qd:event`), so a fast stream sends\n * no key names: `scope` is `null` for a global stream. The positions are\n * fixed; a later revision may append elements after `item`, never insert\n * them, so a client reads the four it knows and ignores the rest. Before\n * `5.0.0-rc.4` it was the object `{ s, stream, scope?, item }`.\n */\nexport type StreamFrame<Item = unknown> = readonly [\n s: string,\n stream: string,\n scope: string | null,\n item: Item,\n];\n\n/**\n * `qd:ch`, sent volatile and never acknowledged: `[service, channel,\n * payload]`. A later revision may append elements after `payload`; the\n * server reads the three it knows and ignores the rest.\n */\nexport type ChannelFrame<Payload = unknown> = readonly [\n s: string,\n channel: string,\n payload: Payload,\n];\n\n/**\n * `qd:event`: a custom room event declared in a contract's `events`:\n * `[service, event, payload]`. A later revision may append elements after\n * `payload`; a client reads the three it knows and ignores the rest.\n */\nexport type EventFrame<Payload = unknown> = readonly [s: string, event: string, payload: Payload];\n\n/**\n * `qd:presence`: who is in an app room (one a method joined with\n * `ctx.rooms.join`) the socket is in, by user id (RFC 0003 section 12.5).\n * Sent to a socket as it joins with `users`, the whole list (its own user\n * included); to the room's other sockets with `joined` when a user's first\n * socket joins, and with `left` when a user's last socket leaves; and to a\n * socket that leaves with `users: []`, since it no longer sees the room. A\n * socket the server took out of the room (`rooms.leave(room, { userId })`)\n * gets that `users: []` unasked: it is out, and its channels requiring the\n * room are dropped until a joining call lets it back. Exactly one of\n * `users`, `joined` and `left` is present. Anonymous sockets are in no list.\n */\nexport interface PresenceFrame {\n readonly room: string;\n /** Every user in the room: replaces what the client holds for it. */\n readonly users?: readonly string[];\n /** A user who joined the room. */\n readonly joined?: string;\n /** A user who left the room: their last socket in it left or disconnected. */\n readonly left?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Connection-level frames (sections 4.4, 7.2 and 8.3)\n// ---------------------------------------------------------------------------\n\n/** Why the server ended a subscription. */\nexport type RevokeReason = \"access\" | \"anchor-deleted\";\n\n/**\n * `qd:revoked`: the server removed this socket from a subscription's room,\n * because the principal's access was lowered or removed (`access`), or the\n * row a collection scope is anchored on was deleted (`anchor-deleted`). A\n * stream feed's subscription is revoked for `access` only; `scope` is absent\n * for a global stream.\n */\nexport type RevokedFrame =\n | {\n readonly kind: \"entity\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly id: string;\n }\n | {\n readonly kind: \"collection\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n }\n | {\n readonly kind: \"stream\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n };\n\n/** `qd:rotate`: stay connected until a random moment within `withinMs`, then reconnect. */\nexport interface RotateFrame {\n readonly withinMs: number;\n}\n\n/** `qd:access`: the user's service grants changed. */\nexport interface AccessFrame {\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n\n// ---------------------------------------------------------------------------\n// Event maps\n// ---------------------------------------------------------------------------\n\n/** The function a listener calls to acknowledge an event. */\ntype Ack<Reply> = (reply: Reply) => void;\n\n/** The listener of each client event, by its key in `CLIENT_EVENTS`. */\ninterface ClientListeners {\n call: (envelope: CallEnvelope, ack: Ack<CallReply>) => void;\n cancel: (frame: CancelFrame) => void;\n sub: (frame: EntitySubscribe, ack: Ack<EntitySubscribeReply>) => void;\n unsub: (frame: EntityUnsubscribe, ack?: Ack<Ok | Failure>) => void;\n collectionSub: (frame: CollectionSubscribe, ack: Ack<CollectionSubscribeReply>) => void;\n collectionUnsub: (frame: CollectionScopeRef, ack?: Ack<Ok | Failure>) => void;\n collectionItems: (frame: CollectionItemsRequest, ack: Ack<CollectionItemsReply>) => void;\n watch: (frame: WatchFrame, ack: Ack<Ok | Failure>) => void;\n unwatch: (frame: WatchFrame, ack?: Ack<Ok | Failure>) => void;\n streamSub: (frame: StreamSubscribe, ack: Ack<StreamSubscribeReply>) => void;\n streamUnsub: (frame: StreamSubscribe, ack?: Ack<Ok | Failure>) => void;\n channel: (frame: ChannelFrame) => void;\n}\n\n/** The listener of each server event, by its key in `SERVER_EVENTS`. */\ninterface ServerListeners {\n hello: (frame: HelloFrame) => void;\n entity: (frame: EntityFrame) => void;\n collection: (frame: CollectionFrame) => void;\n changed: (frame: ChangedFrame) => void;\n revoked: (frame: RevokedFrame) => void;\n stream: (frame: StreamFrame) => void;\n event: (frame: EventFrame) => void;\n presence: (frame: PresenceFrame) => void;\n rotate: (frame: RotateFrame) => void;\n access: (frame: AccessFrame) => void;\n}\n\n/**\n * The events a v5 client sends, keyed by name (`\"qd:call\"`, ...). The server\n * types its sockets with it: `new Server<ClientToServerEvents, ServerToClientEvents>()`.\n */\nexport type ClientToServerEvents = {\n [Key in keyof typeof CLIENT_EVENTS as (typeof CLIENT_EVENTS)[Key]]: ClientListeners[Key];\n};\n\n/**\n * The events a v5 server sends, keyed by name (`\"qd:hello\"`, ...). The client\n * types its socket with it: `Socket<ServerToClientEvents, ClientToServerEvents>`.\n */\nexport type ServerToClientEvents = {\n [Key in keyof typeof SERVER_EVENTS as (typeof SERVER_EVENTS)[Key]]: ServerListeners[Key];\n};\n\n// ---------------------------------------------------------------------------\n// Guards\n// ---------------------------------------------------------------------------\n\nfunction isCallId(value: unknown): value is CallId {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction isVersion(value: unknown): value is Version {\n return typeof value === \"string\" || (typeof value === \"number\" && Number.isFinite(value));\n}\n\n/**\n * True when `value` has the shape of a `qd:call` envelope: a call id, service\n * and method names, and a version when one is present. The input is not\n * looked at (the method's schema validates it), and unknown keys are ignored.\n */\nexport function isCallEnvelope(value: unknown): value is CallEnvelope {\n return (\n isRecord(value) &&\n isCallId(value.id) &&\n isName(value.s) &&\n isName(value.m) &&\n (value.v === undefined || isVersion(value.v))\n );\n}\n\n/** True when `value` has the shape of a `qd:cancel` frame. Unknown keys are ignored. */\nexport function isCancel(value: unknown): value is CancelFrame {\n return isRecord(value) && isCallId(value.id);\n}\n"],"mappings":";;;;;;AAyeA,SAAS,SAAS,OAAiC;AACjD,SAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS;AAC9E;AAEA,SAAS,UAAU,OAAkC;AACnD,SAAO,OAAO,UAAU,YAAa,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AACzF;AAOO,SAAS,eAAe,OAAuC;AACpE,SACE,SAAS,KAAK,KACd,SAAS,MAAM,EAAE,KACjB,OAAO,MAAM,CAAC,KACd,OAAO,MAAM,CAAC,MACb,MAAM,MAAM,UAAa,UAAU,MAAM,CAAC;AAE/C;AAGO,SAAS,SAAS,OAAsC;AAC7D,SAAO,SAAS,KAAK,KAAK,SAAS,MAAM,EAAE;AAC7C;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/prisma/nested.ts
|
|
2
|
+
var NESTED_KEYS = [
|
|
3
|
+
"create",
|
|
4
|
+
"createMany",
|
|
5
|
+
"connect",
|
|
6
|
+
"connectOrCreate",
|
|
7
|
+
"disconnect",
|
|
8
|
+
"set",
|
|
9
|
+
"update",
|
|
10
|
+
"updateMany",
|
|
11
|
+
"upsert",
|
|
12
|
+
"delete",
|
|
13
|
+
"deleteMany"
|
|
14
|
+
];
|
|
15
|
+
var FLAG_KEYS = /* @__PURE__ */ new Set(["delete", "disconnect"]);
|
|
16
|
+
function isPlainObject(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const prototype = Object.getPrototypeOf(value);
|
|
21
|
+
return prototype === Object.prototype || prototype === null;
|
|
22
|
+
}
|
|
23
|
+
function isRows(value) {
|
|
24
|
+
return Array.isArray(value) || isPlainObject(value);
|
|
25
|
+
}
|
|
26
|
+
function isRelationSet(value) {
|
|
27
|
+
return Array.isArray(value) ? value.some(isPlainObject) : isPlainObject(value);
|
|
28
|
+
}
|
|
29
|
+
function writesRelation(value, key) {
|
|
30
|
+
if (!Object.hasOwn(value, key)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const operand = value[key];
|
|
34
|
+
if (key === "set") {
|
|
35
|
+
return isRelationSet(operand);
|
|
36
|
+
}
|
|
37
|
+
return isRows(operand) || FLAG_KEYS.has(key) && operand === true;
|
|
38
|
+
}
|
|
39
|
+
function nestedKey(value) {
|
|
40
|
+
if (!isPlainObject(value)) {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
return NESTED_KEYS.find((key) => writesRelation(value, key));
|
|
44
|
+
}
|
|
45
|
+
function findNestedWrites(data) {
|
|
46
|
+
if (!isPlainObject(data)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const found = [];
|
|
50
|
+
for (const [field, value] of Object.entries(data)) {
|
|
51
|
+
const operation = nestedKey(value);
|
|
52
|
+
if (operation !== void 0) {
|
|
53
|
+
found.push({ field, operation });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
findNestedWrites
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=chunk-XH2IZVLT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/prisma/nested.ts"],"sourcesContent":["// Nested writes (RFC 0003 section 5.2): a `data` value that writes a related\n// row, such as `{ labels: { create: [...] } }`, reaches Prisma's query hook\n// only as the parent operation, so the related rows' writes are not tracked.\n// They are found by shape and reported as a development warning; the lint\n// rule `no-nested-write` catches them before they run.\n//\n// The check reads shapes, not the schema, so it also looks at each key's\n// value: a relation operation takes rows (an object or a list), and `delete`\n// and `disconnect` on a to-one relation take `true`. A JSON column's value\n// such as `{ create: true, update: false, delete: false }` is therefore not a\n// nested write; one whose keys hold objects still is.\n\n/** The keys of a relation operation, in the order a warning names them. */\nconst NESTED_KEYS = [\n \"create\",\n \"createMany\",\n \"connect\",\n \"connectOrCreate\",\n \"disconnect\",\n \"set\",\n \"update\",\n \"updateMany\",\n \"upsert\",\n \"delete\",\n \"deleteMany\",\n] as const;\n\n/** Relation operations that also take `true` (a to-one relation); the rest take rows. */\nconst FLAG_KEYS: ReadonlySet<string> = new Set([\"delete\", \"disconnect\"]);\n\nfunction isPlainObject(value: unknown): value is Readonly<Record<string, unknown>> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype: unknown = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction isRows(value: unknown): boolean {\n return Array.isArray(value) || isPlainObject(value);\n}\n\n/**\n * `set` also sets a scalar (`{ title: { set: \"x\" } }`) or a scalar list; on a\n * relation it takes the rows to connect, which are objects.\n */\nfunction isRelationSet(value: unknown): boolean {\n return Array.isArray(value) ? value.some(isPlainObject) : isPlainObject(value);\n}\n\n/**\n * Whether `value[key]` is a relation operation, judged by what the key\n * holds: rows (an object or a list) for every operation, `true` for\n * `delete` and `disconnect`, rows given as objects for `set`.\n */\nfunction writesRelation(value: Readonly<Record<string, unknown>>, key: string): boolean {\n if (!Object.hasOwn(value, key)) {\n return false;\n }\n const operand = value[key];\n if (key === \"set\") {\n return isRelationSet(operand);\n }\n return isRows(operand) || (FLAG_KEYS.has(key) && operand === true);\n}\n\nfunction nestedKey(value: unknown): string | undefined {\n if (!isPlainObject(value)) {\n return undefined;\n }\n return NESTED_KEYS.find((key) => writesRelation(value, key));\n}\n\n/** One nested write found in a write's `data`. */\nexport interface NestedWrite {\n /** The relation field, for example `\"labels\"`. */\n readonly field: string;\n /** The nested operation, for example `\"create\"`. */\n readonly operation: string;\n}\n\n/** The nested writes in `data`, one per relation field that has one. */\nexport function findNestedWrites(data: unknown): NestedWrite[] {\n if (!isPlainObject(data)) {\n return [];\n }\n const found: NestedWrite[] = [];\n for (const [field, value] of Object.entries(data)) {\n const operation = nestedKey(value);\n if (operation !== undefined) {\n found.push({ field, operation });\n }\n }\n return found;\n}\n"],"mappings":";AAaA,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,YAAiC,oBAAI,IAAI,CAAC,UAAU,YAAY,CAAC;AAEvE,SAAS,cAAc,OAA4D;AACjF,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,WAAO;AAAA,EACT;AACA,QAAM,YAAqB,OAAO,eAAe,KAAK;AACtD,SAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAEA,SAAS,OAAO,OAAyB;AACvC,SAAO,MAAM,QAAQ,KAAK,KAAK,cAAc,KAAK;AACpD;AAMA,SAAS,cAAc,OAAyB;AAC9C,SAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,aAAa,IAAI,cAAc,KAAK;AAC/E;AAOA,SAAS,eAAe,OAA0C,KAAsB;AACtF,MAAI,CAAC,OAAO,OAAO,OAAO,GAAG,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,UAAU,MAAM,GAAG;AACzB,MAAI,QAAQ,OAAO;AACjB,WAAO,cAAc,OAAO;AAAA,EAC9B;AACA,SAAO,OAAO,OAAO,KAAM,UAAU,IAAI,GAAG,KAAK,YAAY;AAC/D;AAEA,SAAS,UAAU,OAAoC;AACrD,MAAI,CAAC,cAAc,KAAK,GAAG;AACzB,WAAO;AAAA,EACT;AACA,SAAO,YAAY,KAAK,CAAC,QAAQ,eAAe,OAAO,GAAG,CAAC;AAC7D;AAWO,SAAS,iBAAiB,MAA8B;AAC7D,MAAI,CAAC,cAAc,IAAI,GAAG;AACxB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,QAAuB,CAAC;AAC9B,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AACjD,UAAM,YAAY,UAAU,KAAK;AACjC,QAAI,cAAc,QAAW;AAC3B,YAAM,KAAK,EAAE,OAAO,UAAU,CAAC;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
describeError
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LCC7HNFP.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) {
|
|
@@ -502,7 +521,7 @@ function notify(state, writes) {
|
|
|
502
521
|
} catch (error) {
|
|
503
522
|
state.logger.error("An onWrite listener threw", {
|
|
504
523
|
category: "quickdraw.writes",
|
|
505
|
-
error:
|
|
524
|
+
error: errorText(error)
|
|
506
525
|
});
|
|
507
526
|
}
|
|
508
527
|
}
|
|
@@ -802,4 +821,4 @@ export {
|
|
|
802
821
|
runBeforeAnyDispatcher,
|
|
803
822
|
createWriteTracker
|
|
804
823
|
};
|
|
805
|
-
//# sourceMappingURL=chunk-
|
|
824
|
+
//# sourceMappingURL=chunk-Z5ILULF2.js.map
|