@argentic/chest-sdk 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +508 -80
- package/client/index.ts +11 -7
- package/client/src/api.ts +19 -18
- package/client/src/database.ts +6 -1
- package/client/src/errors.ts +50 -0
- package/client/src/eventrules.ts +117 -0
- package/client/src/events.ts +181 -39
- package/client/src/files.ts +37 -7
- package/client/src/member.ts +15 -9
- package/client/src/members.ts +34 -16
- package/client/src/notifications.ts +84 -27
- package/client/src/realtime-client.ts +516 -0
- package/client/src/realtime.ts +118 -0
- package/client/src/sealed.ts +158 -0
- package/client/src/signed.ts +8 -4
- package/client/src/testing-realtime.ts +361 -0
- package/client/src/testing.ts +391 -94
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/src/api.d.ts +2 -1
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +18 -16
- package/dist/src/api.js.map +1 -1
- package/dist/src/database.d.ts.map +1 -1
- package/dist/src/database.js +5 -1
- package/dist/src/database.js.map +1 -1
- package/dist/src/errors.d.ts +18 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +44 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/eventrules.d.ts +23 -0
- package/dist/src/eventrules.d.ts.map +1 -0
- package/dist/src/eventrules.js +107 -0
- package/dist/src/eventrules.js.map +1 -0
- package/dist/src/events.d.ts +31 -6
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +130 -26
- package/dist/src/events.js.map +1 -1
- package/dist/src/files.d.ts +1 -0
- package/dist/src/files.d.ts.map +1 -1
- package/dist/src/files.js +34 -3
- package/dist/src/files.js.map +1 -1
- package/dist/src/member.d.ts +1 -1
- package/dist/src/member.d.ts.map +1 -1
- package/dist/src/member.js +9 -6
- package/dist/src/member.js.map +1 -1
- package/dist/src/members.d.ts +9 -2
- package/dist/src/members.d.ts.map +1 -1
- package/dist/src/members.js +29 -13
- package/dist/src/members.js.map +1 -1
- package/dist/src/notifications.d.ts +12 -1
- package/dist/src/notifications.d.ts.map +1 -1
- package/dist/src/notifications.js +60 -19
- package/dist/src/notifications.js.map +1 -1
- package/dist/src/realtime-client.d.ts +45 -0
- package/dist/src/realtime-client.d.ts.map +1 -0
- package/dist/src/realtime-client.js +453 -0
- package/dist/src/realtime-client.js.map +1 -0
- package/dist/src/realtime.d.ts +22 -0
- package/dist/src/realtime.d.ts.map +1 -0
- package/dist/src/realtime.js +99 -0
- package/dist/src/realtime.js.map +1 -0
- package/dist/src/sealed.d.ts +20 -0
- package/dist/src/sealed.d.ts.map +1 -0
- package/dist/src/sealed.js +121 -0
- package/dist/src/sealed.js.map +1 -0
- package/dist/src/signed.d.ts.map +1 -1
- package/dist/src/signed.js +6 -2
- package/dist/src/signed.js.map +1 -1
- package/dist/src/testing-realtime.d.ts +54 -0
- package/dist/src/testing-realtime.d.ts.map +1 -0
- package/dist/src/testing-realtime.js +376 -0
- package/dist/src/testing-realtime.js.map +1 -0
- package/dist/src/testing.d.ts +40 -3
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +368 -81
- package/dist/src/testing.js.map +1 -1
- package/package.json +23 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ask as chest, errorCode, json, refusal } from "./api.js";
|
|
2
|
+
import { CapabilityNotGranted, ChestError, MemberRequired, NotAllowed, SealedInvalid, SealedLocked, SealedLost, Unavailable } from "./errors.js";
|
|
3
|
+
// The bounds of a Chest: one value, a context, the roles a tool declares.
|
|
4
|
+
// No count bounds a call: a page of values goes in one, 4 MiB at most.
|
|
5
|
+
const maxValue = 512 << 10, maxContext = 256, maxRoles = 16;
|
|
6
|
+
const rolePattern = /^[a-z][a-z0-9-]{0,47}$/u;
|
|
7
|
+
// A sealed value: its format, the roles it was sealed for, its sealed bytes.
|
|
8
|
+
const sealedPattern = /^chest:sealed:1:(?:[a-z][a-z0-9-]{0,47}(?:,[a-z][a-z0-9-]{0,47}){0,15})?:[A-Za-z0-9_-]+$/u;
|
|
9
|
+
const maxSealed = 1 << 20;
|
|
10
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
11
|
+
// isSealed says text is a sealed value of a Chest (its shape only: whether
|
|
12
|
+
// it opens is the Chest's to say).
|
|
13
|
+
export function isSealed(text) {
|
|
14
|
+
return typeof text === "string" && text.length <= maxSealed && sealedPattern.test(text);
|
|
15
|
+
}
|
|
16
|
+
// The ticket of the member of a request, as the Chest's front sends it
|
|
17
|
+
// (Chest-Opener); null without one.
|
|
18
|
+
function ticketOf(request) {
|
|
19
|
+
const headers = request.headers;
|
|
20
|
+
const value = typeof headers.get === "function" ? headers.get("chest-opener") : headers["chest-opener"];
|
|
21
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 ? value : null;
|
|
22
|
+
}
|
|
23
|
+
function checkContext(context) {
|
|
24
|
+
if (context === undefined)
|
|
25
|
+
return "";
|
|
26
|
+
if (typeof context !== "string" || bytes(context) > maxContext)
|
|
27
|
+
throw new ChestError("invalid_body", 400, `a context is text of ${maxContext} bytes at most`);
|
|
28
|
+
return context;
|
|
29
|
+
}
|
|
30
|
+
function checkItem(item) {
|
|
31
|
+
if (typeof item?.value !== "string" || bytes(item.value) > maxValue)
|
|
32
|
+
throw new ChestError("invalid_body", 400, `a sealed value is text of ${maxValue >> 10} KiB at most`);
|
|
33
|
+
const context = checkContext(item.context);
|
|
34
|
+
if (item.roles !== undefined && (!Array.isArray(item.roles) || item.roles.length === 0 || item.roles.length > maxRoles || !item.roles.every(r => typeof r === "string" && rolePattern.test(r)) || new Set(item.roles).size !== item.roles.length))
|
|
35
|
+
throw new ChestError("invalid_role", 400, "roles are 1 to 16 distinct roles the tool declares");
|
|
36
|
+
return { value: item.value, ...(item.roles ? { roles: [...item.roles] } : {}), ...(context ? { context } : {}) };
|
|
37
|
+
}
|
|
38
|
+
// failed turns an answer that is not a success into what the tool tests.
|
|
39
|
+
async function failed(response) {
|
|
40
|
+
if (response.status === 401)
|
|
41
|
+
return new MemberRequired();
|
|
42
|
+
if (response.status !== 403 && response.status !== 503)
|
|
43
|
+
return refusal(response, "sealed");
|
|
44
|
+
switch (await errorCode(response)) {
|
|
45
|
+
case "access_removed": return new NotAllowed();
|
|
46
|
+
case "capability_not_granted": return new CapabilityNotGranted("sealed");
|
|
47
|
+
case "sealed_locked": return new SealedLocked();
|
|
48
|
+
case "sealed_lost": return new SealedLost();
|
|
49
|
+
default: return new Unavailable();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function call(path, ticket, body) {
|
|
53
|
+
const response = await chest("sealed", "POST", path, { body: JSON.stringify(body), type: "application/json", ...(ticket === null ? {} : { headers: { "Chest-Opener": ticket } }) });
|
|
54
|
+
if (response.status === 200)
|
|
55
|
+
return json(response);
|
|
56
|
+
if (response.status < 400) {
|
|
57
|
+
await response.body?.cancel();
|
|
58
|
+
throw new Unavailable();
|
|
59
|
+
}
|
|
60
|
+
throw await failed(response);
|
|
61
|
+
}
|
|
62
|
+
// sealMany seals values, each with its options, in one call: their sealed
|
|
63
|
+
// texts, in the order given.
|
|
64
|
+
export async function sealMany(items) {
|
|
65
|
+
const checked = items.map(checkItem);
|
|
66
|
+
const answer = await call("/sealed/seal", null, { items: checked });
|
|
67
|
+
if (!Array.isArray(answer?.sealed) || answer.sealed.length !== checked.length || !answer.sealed.every(isSealed))
|
|
68
|
+
throw new Unavailable();
|
|
69
|
+
return answer.sealed;
|
|
70
|
+
}
|
|
71
|
+
// seal seals one value: its sealed text, to store.
|
|
72
|
+
export async function seal(value, options = {}) {
|
|
73
|
+
return (await sealMany([{ value, ...options }]))[0];
|
|
74
|
+
}
|
|
75
|
+
// openMany opens values for the member of request, in one call: each
|
|
76
|
+
// value's text, or null for one this member may not open (sealed for roles
|
|
77
|
+
// they do not hold), or that is not a value of this tool in that context.
|
|
78
|
+
export async function openMany(request, items) {
|
|
79
|
+
const ticket = ticketOf(request);
|
|
80
|
+
if (ticket === null)
|
|
81
|
+
throw new MemberRequired();
|
|
82
|
+
const checked = items.map(item => {
|
|
83
|
+
if (typeof item?.sealed !== "string" || item.sealed.length > maxSealed)
|
|
84
|
+
throw new ChestError("invalid_body", 400, "a sealed value is the text seal returned");
|
|
85
|
+
const context = checkContext(item.context);
|
|
86
|
+
return { sealed: item.sealed, ...(context ? { context } : {}) };
|
|
87
|
+
});
|
|
88
|
+
const answer = await call("/sealed/open", ticket, { items: checked });
|
|
89
|
+
const values = answer?.values;
|
|
90
|
+
if (!Array.isArray(values) || values.length !== checked.length)
|
|
91
|
+
throw new Unavailable();
|
|
92
|
+
return values.map(v => {
|
|
93
|
+
const item = v;
|
|
94
|
+
if (typeof item?.value === "string")
|
|
95
|
+
return item.value;
|
|
96
|
+
if (item?.refused === "role" || item?.refused === "invalid")
|
|
97
|
+
return null;
|
|
98
|
+
throw new Unavailable();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// open opens one value for the member of request: its text. A value sealed
|
|
102
|
+
// for roles the member does not hold is NotAllowed; one altered, of another
|
|
103
|
+
// tool or another context, SealedInvalid.
|
|
104
|
+
export async function open(request, sealed, options = {}) {
|
|
105
|
+
const ticket = ticketOf(request);
|
|
106
|
+
if (ticket === null)
|
|
107
|
+
throw new MemberRequired();
|
|
108
|
+
const context = checkContext(options.context);
|
|
109
|
+
if (typeof sealed !== "string" || sealed.length > maxSealed)
|
|
110
|
+
throw new ChestError("invalid_body", 400, "a sealed value is the text seal returned");
|
|
111
|
+
const answer = await call("/sealed/open", ticket, { items: [{ sealed, ...(context ? { context } : {}) }] });
|
|
112
|
+
const item = (Array.isArray(answer?.values) && answer.values.length === 1 ? answer.values[0] : null);
|
|
113
|
+
if (typeof item?.value === "string")
|
|
114
|
+
return item.value;
|
|
115
|
+
if (item?.refused === "role")
|
|
116
|
+
throw new NotAllowed();
|
|
117
|
+
if (item?.refused === "invalid")
|
|
118
|
+
throw new SealedInvalid();
|
|
119
|
+
throw new Unavailable();
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=sealed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealed.js","sourceRoot":"","sources":["../../client/src/sealed.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,IAAI,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA8CjJ,0EAA0E;AAC1E,uEAAuE;AACvE,MAAM,QAAQ,GAAG,GAAG,IAAI,EAAE,EAAE,UAAU,GAAG,GAAG,EAAE,QAAQ,GAAG,EAAE,CAAC;AAC5D,MAAM,WAAW,GAAG,yBAAyB,CAAC;AAC9C,6EAA6E;AAC7E,MAAM,aAAa,GAAG,2FAA2F,CAAC;AAClH,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,CAAC;AAC1B,MAAM,KAAK,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAElE,2EAA2E;AAC3E,mCAAmC;AACnC,MAAM,UAAU,QAAQ,CAAC,IAAa;IACpC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1F,CAAC;AAED,uEAAuE;AACvE,oCAAoC;AACpC,SAAS,QAAQ,CAAC,OAAkC;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,OAA+C,CAAC;IACxE,MAAM,KAAK,GAAG,OAAQ,OAAmB,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,CAAE,OAAmB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAE,OAAsC,CAAC,cAAc,CAAC,CAAC;IAClK,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7F,CAAC;AAED,SAAS,YAAY,CAAC,OAAgB;IACpC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,UAAU;QAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,GAAG,EAAE,wBAAwB,UAAU,gBAAgB,CAAC,CAAC;IAC9J,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,IAAI,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,QAAQ;QAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,GAAG,EAAE,6BAA6B,QAAQ,IAAI,EAAE,cAAc,CAAC,CAAC;IAC1K,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,QAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,GAAG,EAAE,oDAAoD,CAAC,CAAC;IACnV,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACnH,CAAC;AAED,yEAAyE;AACzE,KAAK,UAAU,MAAM,CAAC,QAAkB;IACtC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,cAAc,EAAE,CAAC;IACzD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3F,QAAQ,MAAM,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,KAAK,gBAAgB,EAAE,OAAO,IAAI,UAAU,EAAE,CAAC;QAC/C,KAAK,wBAAwB,EAAE,OAAO,IAAI,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QACzE,KAAK,eAAe,EAAE,OAAO,IAAI,YAAY,EAAE,CAAC;QAChD,KAAK,aAAa,EAAE,OAAO,IAAI,UAAU,EAAE,CAAC;QAC5C,SAAS,OAAO,IAAI,WAAW,EAAE,CAAC;IACpC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,MAAqB,EAAE,IAAa;IACpE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IACpL,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC1B,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,WAAW,EAAE,CAAC;IAC1B,CAAC;IACD,MAAM,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,0EAA0E;AAC1E,6BAA6B;AAC7B,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAiB;IAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAyB,CAAC;IAC5F,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,WAAW,EAAE,CAAC;IACzI,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,KAAa,EAAE,OAAO,GAAgB,EAAE;IACjE,OAAO,CAAC,MAAM,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC;AACvD,CAAC;AAED,qEAAqE;AACrE,2EAA2E;AAC3E,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAkC,EAAE,KAAiB;IAClF,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,MAAM,KAAK,IAAI;QAAE,MAAM,IAAI,cAAc,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAC/B,IAAI,OAAO,IAAI,EAAE,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS;YAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,GAAG,EAAE,0CAA0C,CAAC,CAAC;QAC9J,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAClE,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAyB,CAAC;IAC9F,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,WAAW,EAAE,CAAC;IACxF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACpB,MAAM,IAAI,GAAG,CAAkD,CAAC;QAChE,IAAI,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC;QACvD,IAAI,IAAI,EAAE,OAAO,KAAK,MAAM,IAAI,IAAI,EAAE,OAAO,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACzE,MAAM,IAAI,WAAW,EAAE,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2EAA2E;AAC3E,4EAA4E;AAC5E,0CAA0C;AAC1C,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAkC,EAAE,MAAc,EAAE,OAAO,GAAyB,EAAE;IAC/G,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,MAAM,KAAK,IAAI;QAAE,MAAM,IAAI,cAAc,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,SAAS;QAAE,MAAM,IAAI,UAAU,CAAC,cAAc,EAAE,GAAG,EAAE,0CAA0C,CAAC,CAAC;IACnJ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAyB,CAAC;IACpI,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAkD,CAAC;IACtJ,IAAI,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IACvD,IAAI,IAAI,EAAE,OAAO,KAAK,MAAM;QAAE,MAAM,IAAI,UAAU,EAAE,CAAC;IACrD,IAAI,IAAI,EAAE,OAAO,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,EAAE,CAAC;IAC3D,MAAM,IAAI,WAAW,EAAE,CAAC;AAC1B,CAAC"}
|
package/dist/src/signed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signed.d.ts","sourceRoot":"","sources":["../../client/src/signed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAqBjD,MAAM,MAAM,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"signed.d.ts","sourceRoot":"","sources":["../../client/src/signed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAqBjD,MAAM,MAAM,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAOrF,eAAO,MAAM,YAAY,EAAE,OAAyH,CAAC;AAErJ,eAAO,MAAM,eAAe,EAAE,OAA4G,CAAC;AAQ3I,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAErE;AACD,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMzC;AAED,eAAO,MAAM,OAAO,MAAO,OAAO,KAAG,CAAC,IAAI,MAAiF,CAAC;AAK5H,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9G;AAOD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAmBhG;AAKD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhG;AA6BD,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAYjI;AAID,wBAAgB,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGjH;AAYD,MAAM,MAAM,IAAI,GAAG;IAAE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAI1G,wBAAgB,UAAU,CAAC,KAAK,SAAQ,GAAG,IAAI,CAU9C"}
|
package/dist/src/signed.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
// The events of the members (events): POST /chest-events.
|
|
3
|
-
|
|
2
|
+
// The events of the members and of other tools (events): POST /chest-events.
|
|
3
|
+
// A tool event names the members who may see it, up to a whole team: its
|
|
4
|
+
// body is bounded by the team's capacity — a full team's policy, 32 MiB —
|
|
5
|
+
// and its 64 KiB of envelope and data. Only the Chest makes a tool read that
|
|
6
|
+
// much: delivery reads no body before its signature holds.
|
|
7
|
+
export const eventChannel = { header: "Chest-Event", label: "Chest-Event v1", id: /^evt_[a-z2-7]{26}$/u, maxBody: (32 << 20) + (64 << 10) };
|
|
4
8
|
// The runs of the schedules (schedules): POST /chest-schedules.
|
|
5
9
|
export const scheduleChannel = { header: "Chest-Schedule", label: "Chest-Schedule v1", id: /^run_[a-z2-7]{26}$/u, maxBody: 1024 };
|
|
6
10
|
const claims = ["aud", "iat", "exp", "jti", "digest"];
|
package/dist/src/signed.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signed.js","sourceRoot":"","sources":["../../client/src/signed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAwBtE,
|
|
1
|
+
{"version":3,"file":"signed.js","sourceRoot":"","sources":["../../client/src/signed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAwBtE,6EAA6E;AAC7E,yEAAyE;AACzE,0EAA0E;AAC1E,6EAA6E;AAC7E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,YAAY,GAAY,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,qBAAqB,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACrJ,gEAAgE;AAChE,MAAM,CAAC,MAAM,eAAe,GAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAE3I,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAU,CAAC;AAC/D,gFAAgF;AAChF,MAAM,IAAI,GAAG,CAAC,CAAC;AACf,MAAM,YAAY,GAAG,IAAI,CAAC;AAC1B,MAAM,OAAO,GAAG,yDAAyD,CAAC;AAE1E,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAgC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxH,CAAC;AACD,MAAM,UAAU,IAAI,CAAC,GAAW;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AACD,gDAAgD;AAChD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAU,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAE5H,0EAA0E;AAC1E,4EAA4E;AAC5E,iEAAiE;AACjE,MAAM,UAAU,WAAW,CAAC,OAAkC,EAAE,IAAY,EAAE,SAAiB;IAC7F,MAAM,OAAO,GAAG,OAAO,CAAC,OAA+C,CAAC;IACxE,MAAM,KAAK,GAAG,OAAQ,OAAmB,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,CAAE,OAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,OAAsC,CAAC,IAAI,CAAC,CAAC;IAC9I,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/E,CAAC;AAED,4EAA4E;AAC5E,0EAA0E;AAC1E,2EAA2E;AAC3E,2EAA2E;AAC3E,uEAAuE;AACvE,MAAM,UAAU,YAAY,CAAC,KAAoB,EAAE,KAAa;IAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5E,MAAM,KAAK,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,aAAa,GAAG,EAAE,EAAE,cAAc,GAAG,EAAE,EAAE,gBAAgB,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC;IACjF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtF,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACrH,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;IACpF,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,aAAa,GAAG,GAAG,GAAG,cAAc,CAAC,CAAC,MAAM,EAAE,CAAC;IACjG,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;IACzD,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAC7B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IAC9I,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,OAAO,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,GAAG,IAAI,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;AAChE,CAAC;AAED,yEAAyE;AACzE,4EAA4E;AAC5E,4BAA4B;AAC5B,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,MAA+B,EAAE,KAAa;IACtF,MAAM,MAAM,GAAG,CAAC,KAAc,EAAU,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACpG,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3E,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;IACpF,OAAO,MAAM,GAAG,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACrF,CAAC;AAED,0EAA0E;AAC1E,gBAAgB;AAChB,KAAK,UAAU,MAAM,CAAC,OAAkC,EAAE,KAAa;IACrE,IAAI,CAAC;QACH,IAAI,OAAO,YAAY,OAAO,EAAE,CAAC;YAC/B,IAAI,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,GAAG,KAAK;gBAAE,OAAO,IAAI,CAAC;YAClG,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;YACrD,OAAO,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1C,CAAC;QACD,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAClC,IAAI,IAAK,KAAgB,CAAC,MAAM,CAAC;YACjC,IAAI,IAAI,GAAG,KAAK;gBAAE,OAAO,IAAI,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,yEAAyE;AACzE,uEAAuE;AACvE,wEAAwE;AACxE,oDAAoD;AACpD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAkC,EAAE,OAAgB;IACjF,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,OAAO,GAAG,YAAY,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9G,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAClI,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAChC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAChG,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC9C,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,yEAAyE;AACzE,wDAAwD;AACxD,MAAM,UAAU,IAAI,CAAC,OAAgB,EAAE,EAAU,EAAE,IAAY,EAAE,OAAwC;IACvG,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,OAAO,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACrK,CAAC;AAcD,0EAA0E;AAC1E,0DAA0D;AAC1D,MAAM,UAAU,UAAU,CAAC,KAAK,GAAG,KAAK;IACtC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,OAAO;QACL,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,GAAG,EAAE,EAAE,CAAC,EAAE;YACR,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACf,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACZ,OAAO,GAAG,CAAC,IAAI,GAAG,KAAK;gBAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAe,CAAC,CAAC;QAC3E,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
export type FakeChannelRule = {
|
|
4
|
+
name: string;
|
|
5
|
+
join?: string[] | {
|
|
6
|
+
table: string;
|
|
7
|
+
key: string;
|
|
8
|
+
member: string;
|
|
9
|
+
};
|
|
10
|
+
send?: boolean;
|
|
11
|
+
presence?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type FakeFeed = {
|
|
14
|
+
table: string;
|
|
15
|
+
channel: string;
|
|
16
|
+
columns: string[];
|
|
17
|
+
};
|
|
18
|
+
export type FakeRealtimeOptions = {
|
|
19
|
+
channels?: FakeChannelRule[];
|
|
20
|
+
feeds?: FakeFeed[];
|
|
21
|
+
membership?: (table: string, key: string, member: string) => boolean;
|
|
22
|
+
};
|
|
23
|
+
export type FakePublished = {
|
|
24
|
+
channel: string;
|
|
25
|
+
event: string;
|
|
26
|
+
payload: unknown;
|
|
27
|
+
seq: number;
|
|
28
|
+
};
|
|
29
|
+
export type FakeSent = {
|
|
30
|
+
members: string[];
|
|
31
|
+
event: string;
|
|
32
|
+
payload: unknown;
|
|
33
|
+
};
|
|
34
|
+
export type FakeRealtime = {
|
|
35
|
+
published: FakePublished[];
|
|
36
|
+
sent: FakeSent[];
|
|
37
|
+
renewals: number;
|
|
38
|
+
url(memberId: string): string;
|
|
39
|
+
commit(table: string, op: "insert" | "update" | "delete", row: Record<string, unknown>): number[];
|
|
40
|
+
removed(table: string, key: string, member: string): void;
|
|
41
|
+
drop(memberId: string, code?: number, reason?: string): void;
|
|
42
|
+
revoke(memberId: string): void;
|
|
43
|
+
signOut(memberId: string): void;
|
|
44
|
+
full(seconds: number, what?: "connections" | "joins"): void;
|
|
45
|
+
advance(ms: number): void;
|
|
46
|
+
};
|
|
47
|
+
export declare function fakeRealtime(options: FakeRealtimeOptions, origin: () => string, roleOf: (member: string) => string | null | undefined): {
|
|
48
|
+
realtime: FakeRealtime;
|
|
49
|
+
api: (request: IncomingMessage, response: ServerResponse, url: URL, read: (request: IncomingMessage, limit: number) => Promise<Buffer | null>, send: (response: ServerResponse, status: number, value?: unknown) => void) => Promise<void>;
|
|
50
|
+
probe: (response: ServerResponse, url: URL, send: (response: ServerResponse, status: number, value?: unknown, headers?: Record<string, string>) => void) => void;
|
|
51
|
+
upgrade: (request: IncomingMessage, socket: Duplex, url: URL) => void;
|
|
52
|
+
close: () => void;
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=testing-realtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing-realtime.d.ts","sourceRoot":"","sources":["../../client/src/testing-realtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { STATUS_CODES } from "node:http";
|
|
3
|
+
import { memberIdPattern } from "./member.js";
|
|
4
|
+
import { channelPattern, eventPattern } from "./realtime.js";
|
|
5
|
+
import { peerEventPattern } from "./realtime-client.js";
|
|
6
|
+
const protocol = "chest-realtime.v1", maxPayload = 64 << 10, maxSend = 4 << 10, maxState = 1 << 10;
|
|
7
|
+
// How long the Chest keeps a channel's frames in memory, and the tool's
|
|
8
|
+
// committed rows in its change log.
|
|
9
|
+
const memoryFor = 2 * 60 * 1000, logFor = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
// prefixOf splits a pattern into its fixed part and its variable last
|
|
11
|
+
// segment ("" for an exact name).
|
|
12
|
+
function prefixOf(name) {
|
|
13
|
+
const at = name.lastIndexOf(":");
|
|
14
|
+
const last = name.slice(at + 1);
|
|
15
|
+
return last === "*" || /^\{[a-z_][a-z0-9_]*\}$/u.test(last) ? [name.slice(0, at + 1), last] : [name, ""];
|
|
16
|
+
}
|
|
17
|
+
function matches(rule, name, member) {
|
|
18
|
+
const [prefix, variable] = prefixOf(rule.name);
|
|
19
|
+
if (!variable)
|
|
20
|
+
return name === prefix;
|
|
21
|
+
const last = name.startsWith(prefix) ? name.slice(prefix.length) : "";
|
|
22
|
+
return /^[a-z0-9_-]{1,64}$/u.test(last) && (variable !== "{member}" || last === member);
|
|
23
|
+
}
|
|
24
|
+
export function fakeRealtime(options, origin, roleOf) {
|
|
25
|
+
const channels = new Map();
|
|
26
|
+
const pages = new Set();
|
|
27
|
+
const epoch = randomBytes(9).toString("base64url");
|
|
28
|
+
// The change log: its rows, the last position given, the last one no
|
|
29
|
+
// longer kept. The clock: the test's advance on top of the real one.
|
|
30
|
+
const log = [], signedOut = new Set();
|
|
31
|
+
let head = 0, logForgotten = 0, skew = 0, fullUntil = 0, joinsFullUntil = 0;
|
|
32
|
+
const now = () => Date.now() + skew;
|
|
33
|
+
const realtime = {
|
|
34
|
+
published: [], sent: [], renewals: 0,
|
|
35
|
+
url: member => origin().replace(/^http/u, "ws") + "/_chest/realtime?member=" + encodeURIComponent(member),
|
|
36
|
+
commit(table, op, row) {
|
|
37
|
+
const positions = [];
|
|
38
|
+
for (const feed of options.feeds ?? []) {
|
|
39
|
+
if (feed.table !== table)
|
|
40
|
+
continue;
|
|
41
|
+
const [prefix, variable] = prefixOf(feed.channel);
|
|
42
|
+
const suffix = variable ? row[variable.slice(1, -1)] : "";
|
|
43
|
+
if (suffix === null || suffix === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
const channel = prefix + String(suffix), event = table + "." + op, payload = Object.fromEntries(feed.columns.map(c => [c, row[c] ?? null]));
|
|
46
|
+
const pos = ++head;
|
|
47
|
+
log.push({ pos, at: now(), channel, frame: { op: "msg", ch: channel, event, payload, pos } });
|
|
48
|
+
publish(channel, event, payload, pos);
|
|
49
|
+
positions.push(pos);
|
|
50
|
+
}
|
|
51
|
+
return positions;
|
|
52
|
+
},
|
|
53
|
+
removed(table, key, member) {
|
|
54
|
+
for (const rule of options.channels ?? []) {
|
|
55
|
+
if (typeof rule.join !== "object" || Array.isArray(rule.join) || rule.join.table !== table)
|
|
56
|
+
continue;
|
|
57
|
+
const name = prefixOf(rule.name)[0] + key;
|
|
58
|
+
for (const page of pages)
|
|
59
|
+
if (page.member === member && page.joined.has(name))
|
|
60
|
+
kick(page, name);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
drop(member, code, reason = "") { for (const page of [...pages])
|
|
64
|
+
if (page.member === member)
|
|
65
|
+
code === undefined ? page.cut() : page.close(code, reason); },
|
|
66
|
+
revoke(member) { for (const page of [...pages])
|
|
67
|
+
if (page.member === member)
|
|
68
|
+
page.close(1008, "access_removed"); },
|
|
69
|
+
signOut(member) { signedOut.add(member); },
|
|
70
|
+
full(seconds, what = "connections") {
|
|
71
|
+
if (what === "joins")
|
|
72
|
+
joinsFullUntil = now() + seconds * 1000;
|
|
73
|
+
else
|
|
74
|
+
fullUntil = now() + seconds * 1000;
|
|
75
|
+
},
|
|
76
|
+
advance(ms) { skew += ms; },
|
|
77
|
+
};
|
|
78
|
+
const rule = (name, member) => name.length <= 128 && channelPattern.test(name) ? (options.channels ?? []).find(r => matches(r, name, member)) : undefined;
|
|
79
|
+
const declared = (name) => rule(name, name.slice(name.lastIndexOf(":") + 1)) !== undefined;
|
|
80
|
+
const allows = (r, member) => !Array.isArray(r.join) || r.join.includes(roleOf(member) ?? "");
|
|
81
|
+
// fed says whether a feed's rows go to the channel.
|
|
82
|
+
const fed = (name) => (options.feeds ?? []).some(f => {
|
|
83
|
+
const [prefix, variable] = prefixOf(f.channel);
|
|
84
|
+
return variable ? name.startsWith(prefix) && !name.slice(prefix.length).includes(":") : name === prefix;
|
|
85
|
+
});
|
|
86
|
+
// state is a channel, its memory and the change log aged to now.
|
|
87
|
+
const state = (name) => {
|
|
88
|
+
let ch = channels.get(name);
|
|
89
|
+
if (!ch)
|
|
90
|
+
channels.set(name, ch = { seq: 0, kept: [], forgotten: 0, present: new Map() });
|
|
91
|
+
while (ch.kept[0] && ch.kept[0].at <= now() - memoryFor)
|
|
92
|
+
ch.forgotten = ch.kept.shift().seq;
|
|
93
|
+
while (log[0] && log[0].at <= now() - logFor)
|
|
94
|
+
logForgotten = log.shift().pos;
|
|
95
|
+
return ch;
|
|
96
|
+
};
|
|
97
|
+
function publish(channel, event, payload, pos) {
|
|
98
|
+
const ch = state(channel);
|
|
99
|
+
const frame = { op: "msg", ch: channel, event, payload, seq: ++ch.seq, ...(pos === undefined ? {} : { pos }) };
|
|
100
|
+
ch.kept.push({ seq: ch.seq, at: now(), frame });
|
|
101
|
+
realtime.published.push({ channel, event, payload, seq: ch.seq });
|
|
102
|
+
for (const page of pages)
|
|
103
|
+
if (page.joined.has(channel))
|
|
104
|
+
page.write(frame);
|
|
105
|
+
return ch.seq;
|
|
106
|
+
}
|
|
107
|
+
function untrack(page, name) {
|
|
108
|
+
const ch = channels.get(name), p = ch?.present.get(page.member);
|
|
109
|
+
if (!ch || !p || !page.tracked.delete(name) || --p.tracked > 0)
|
|
110
|
+
return;
|
|
111
|
+
ch.present.delete(page.member);
|
|
112
|
+
for (const other of pages)
|
|
113
|
+
if (other.joined.has(name) && other.member !== page.member)
|
|
114
|
+
other.write({ op: "presence", ch: name, joins: [], leaves: [page.member] });
|
|
115
|
+
}
|
|
116
|
+
// part takes a page out of a channel, and forgets its focus there.
|
|
117
|
+
function part(page, name) {
|
|
118
|
+
untrack(page, name);
|
|
119
|
+
page.joined.delete(name);
|
|
120
|
+
if (page.focus === name)
|
|
121
|
+
page.focus = "";
|
|
122
|
+
}
|
|
123
|
+
function kick(page, name) {
|
|
124
|
+
part(page, name);
|
|
125
|
+
page.write({ op: "kicked", ch: name });
|
|
126
|
+
}
|
|
127
|
+
// The tool's API: what the Chest's answers, with its errors.
|
|
128
|
+
async function api(request, response, url, read, send) {
|
|
129
|
+
if (request.method === "GET" && url.pathname === "/realtime/presence") {
|
|
130
|
+
const name = url.searchParams.get("channel") ?? "";
|
|
131
|
+
if (!declared(name))
|
|
132
|
+
return send(response, 400, { error: "invalid_channel" });
|
|
133
|
+
return send(response, 200, { members: [...(channels.get(name)?.present ?? [])].map(([id, p]) => ({ id, state: p.state })) });
|
|
134
|
+
}
|
|
135
|
+
if (request.method !== "POST" || !["/realtime/publish", "/realtime/send", "/realtime/online"].includes(url.pathname))
|
|
136
|
+
return send(response, 404, { error: "not_found" });
|
|
137
|
+
const raw = await read(request, maxPayload + 1024 + 64 * 1000);
|
|
138
|
+
let command;
|
|
139
|
+
try {
|
|
140
|
+
command = JSON.parse(raw?.toString() ?? "");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return send(response, 400, { error: "invalid_body" });
|
|
144
|
+
}
|
|
145
|
+
if (command === null || typeof command !== "object")
|
|
146
|
+
return send(response, 400, { error: "invalid_body" });
|
|
147
|
+
const payload = command["payload"] ?? null;
|
|
148
|
+
if (Buffer.byteLength(JSON.stringify(payload)) > maxPayload)
|
|
149
|
+
return send(response, 413, { error: "too_large" });
|
|
150
|
+
const members = command["members"];
|
|
151
|
+
if (url.pathname !== "/realtime/publish" && (!Array.isArray(members) || members.length === 0 || !members.every(m => typeof m === "string" && memberIdPattern.test(m))))
|
|
152
|
+
return send(response, 400, { error: Array.isArray(members) && members.length > 0 ? "invalid_id" : "invalid_body" });
|
|
153
|
+
const online = (ids, focus) => [...new Set(ids)].filter(id => [...pages].some(p => p.member === id && (focus === undefined || p.focus === focus)));
|
|
154
|
+
if (url.pathname === "/realtime/online") {
|
|
155
|
+
const channel = command["channel"];
|
|
156
|
+
if (channel !== undefined && (typeof channel !== "string" || channel.length > 128 || !channelPattern.test(channel)))
|
|
157
|
+
return send(response, 400, { error: "invalid_channel" });
|
|
158
|
+
return send(response, 200, { online: online(members), watching: channel === undefined ? [] : online(members, channel) });
|
|
159
|
+
}
|
|
160
|
+
const event = command["event"];
|
|
161
|
+
if (typeof event !== "string" || !eventPattern.test(event))
|
|
162
|
+
return send(response, 400, { error: "invalid_event" });
|
|
163
|
+
if (url.pathname === "/realtime/send") {
|
|
164
|
+
realtime.sent.push({ members: [...members], event, payload });
|
|
165
|
+
for (const page of pages)
|
|
166
|
+
if (members.includes(page.member))
|
|
167
|
+
page.write({ op: "direct", event, payload });
|
|
168
|
+
return send(response, 200, { reached: online(members) });
|
|
169
|
+
}
|
|
170
|
+
const channel = command["channel"];
|
|
171
|
+
if (typeof channel !== "string" || !declared(channel))
|
|
172
|
+
return send(response, 400, { error: "invalid_channel" });
|
|
173
|
+
send(response, 200, { seq: publish(channel, event, payload) });
|
|
174
|
+
}
|
|
175
|
+
// refusal is why a member's page may not connect now: no access (403),
|
|
176
|
+
// a session ended (401), no room (503, and in how many seconds).
|
|
177
|
+
const refusal = (member) => {
|
|
178
|
+
if (roleOf(member) === undefined)
|
|
179
|
+
return { status: 403 };
|
|
180
|
+
if (signedOut.has(member))
|
|
181
|
+
return { status: 401 };
|
|
182
|
+
if (fullUntil > now())
|
|
183
|
+
return { status: 503, retryAfter: Math.ceil((fullUntil - now()) / 1000) };
|
|
184
|
+
return undefined;
|
|
185
|
+
};
|
|
186
|
+
// A page's question without upgrading, which renews its member's session:
|
|
187
|
+
// may they connect now?
|
|
188
|
+
function probe(response, url, send) {
|
|
189
|
+
realtime.renewals++;
|
|
190
|
+
const refused = refusal(url.searchParams.get("member") ?? "");
|
|
191
|
+
send(response, refused?.status ?? 204, undefined, refused?.retryAfter ? { "Retry-After": String(refused.retryAfter) } : {});
|
|
192
|
+
}
|
|
193
|
+
// A page's connection: the handshake, then the protocol's frames.
|
|
194
|
+
function upgrade(request, socket, url) {
|
|
195
|
+
const member = url.searchParams.get("member") ?? "";
|
|
196
|
+
const key = request.headers["sec-websocket-key"];
|
|
197
|
+
const offered = String(request.headers["sec-websocket-protocol"] ?? "").split(",").map(s => s.trim());
|
|
198
|
+
const status = typeof key !== "string" || !offered.includes(protocol) ? 403 : refusal(member)?.status;
|
|
199
|
+
if (status !== undefined) {
|
|
200
|
+
socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status]}\r\nContent-Length: 0\r\n\r\n`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const accept = createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
|
|
204
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\nSec-WebSocket-Protocol: ${protocol}\r\n\r\n`);
|
|
205
|
+
const frame = (op, data) => {
|
|
206
|
+
const head = data.length < 126 ? Buffer.from([0x80 | op, data.length]) : data.length <= 0xffff ? Buffer.from([0x80 | op, 126, data.length >> 8, data.length & 0xff]) : Buffer.concat([Buffer.from([0x80 | op, 127]), (() => { const b = Buffer.alloc(8); b.writeBigUInt64BE(BigInt(data.length)); return b; })()]);
|
|
207
|
+
return Buffer.concat([head, data]);
|
|
208
|
+
};
|
|
209
|
+
const page = {
|
|
210
|
+
member, joined: new Set(), tracked: new Set(), focus: "",
|
|
211
|
+
write: value => { if (!socket.destroyed)
|
|
212
|
+
socket.write(frame(0x1, Buffer.from(JSON.stringify(value)))); },
|
|
213
|
+
close: (code, reason) => {
|
|
214
|
+
const data = Buffer.alloc(2 + Buffer.byteLength(reason));
|
|
215
|
+
data.writeUInt16BE(code);
|
|
216
|
+
data.write(reason, 2);
|
|
217
|
+
if (!socket.destroyed)
|
|
218
|
+
socket.end(frame(0x8, data));
|
|
219
|
+
},
|
|
220
|
+
cut: () => socket.destroy(),
|
|
221
|
+
};
|
|
222
|
+
pages.add(page);
|
|
223
|
+
const forget = () => {
|
|
224
|
+
if (!pages.delete(page))
|
|
225
|
+
return;
|
|
226
|
+
for (const name of [...page.tracked])
|
|
227
|
+
untrack(page, name);
|
|
228
|
+
};
|
|
229
|
+
socket.on("close", forget);
|
|
230
|
+
socket.on("error", forget);
|
|
231
|
+
page.write({ op: "hello", epoch, member });
|
|
232
|
+
let pending = Buffer.alloc(0), message = [];
|
|
233
|
+
socket.on("data", (chunk) => {
|
|
234
|
+
pending = Buffer.concat([pending, chunk]);
|
|
235
|
+
for (;;) {
|
|
236
|
+
if (pending.length < 2)
|
|
237
|
+
return;
|
|
238
|
+
const fin = (pending[0] & 0x80) !== 0, op = pending[0] & 0x0f;
|
|
239
|
+
let length = pending[1] & 0x7f, at = 2;
|
|
240
|
+
if (length === 126) {
|
|
241
|
+
if (pending.length < 4)
|
|
242
|
+
return;
|
|
243
|
+
length = pending.readUInt16BE(2);
|
|
244
|
+
at = 4;
|
|
245
|
+
}
|
|
246
|
+
else if (length === 127) {
|
|
247
|
+
if (pending.length < 10)
|
|
248
|
+
return;
|
|
249
|
+
length = Number(pending.readBigUInt64BE(2));
|
|
250
|
+
at = 10;
|
|
251
|
+
}
|
|
252
|
+
if (pending.length < at + 4 + length)
|
|
253
|
+
return;
|
|
254
|
+
const mask = pending.subarray(at, at + 4), data = Buffer.from(pending.subarray(at + 4, at + 4 + length));
|
|
255
|
+
for (let i = 0; i < data.length; i++)
|
|
256
|
+
data[i] ^= mask[i & 3];
|
|
257
|
+
pending = pending.subarray(at + 4 + length);
|
|
258
|
+
if (op === 0x8)
|
|
259
|
+
return page.close(1000, "");
|
|
260
|
+
if (op === 0x9) {
|
|
261
|
+
socket.write(frame(0xa, data));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (op !== 0x1 && op !== 0x0)
|
|
265
|
+
continue;
|
|
266
|
+
message.push(data);
|
|
267
|
+
if (!fin)
|
|
268
|
+
continue;
|
|
269
|
+
const text = Buffer.concat(message).toString();
|
|
270
|
+
message = [];
|
|
271
|
+
let q;
|
|
272
|
+
try {
|
|
273
|
+
q = JSON.parse(text);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
page.write({ op: "error", ref: 0, code: "invalid_request" });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
handle(page, q);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function handle(page, q) {
|
|
284
|
+
const ref = typeof q["ref"] === "number" ? q["ref"] : 0, name = typeof q["ch"] === "string" ? q["ch"] : "";
|
|
285
|
+
const answer = (code, extra = {}) => { if (ref !== 0 || code === "invalid_request")
|
|
286
|
+
page.write(code ? { op: "error", ref, code } : { op: "ok", ref, ...extra }); };
|
|
287
|
+
const r = rule(name, page.member);
|
|
288
|
+
switch (q["op"]) {
|
|
289
|
+
case "ping":
|
|
290
|
+
return answer();
|
|
291
|
+
case "join": {
|
|
292
|
+
if (!r)
|
|
293
|
+
return answer("invalid_channel");
|
|
294
|
+
if (!allows(r, page.member))
|
|
295
|
+
return answer("forbidden");
|
|
296
|
+
const table = typeof r.join === "object" && !Array.isArray(r.join) ? r.join : undefined;
|
|
297
|
+
if (table && !(options.membership?.(table.table, name.slice(prefixOf(r.name)[0].length), page.member) ?? false))
|
|
298
|
+
return answer("forbidden");
|
|
299
|
+
if (joinsFullUntil > now())
|
|
300
|
+
return answer("full");
|
|
301
|
+
page.joined.add(name);
|
|
302
|
+
// A re-join is given what it missed: from the memory when it keeps
|
|
303
|
+
// all of it (the tool's events too), else from the change log (the
|
|
304
|
+
// feeds' rows), else told resync. A channel fed by a feed says the
|
|
305
|
+
// position the page is at: the one it gave, or the head.
|
|
306
|
+
const ch = state(name), since = q["since"];
|
|
307
|
+
let missed = [], resync = false, pos = fed(name) ? head : undefined;
|
|
308
|
+
if (since !== undefined) {
|
|
309
|
+
const seq = Number(since.seq), given = Number(since.pos ?? 0);
|
|
310
|
+
if (since.epoch === epoch && seq >= ch.forgotten && seq <= ch.seq)
|
|
311
|
+
missed = ch.kept.filter(k => k.seq > seq).map(k => k.frame);
|
|
312
|
+
else if (pos !== undefined && given > 0 && given >= logForgotten && given <= head)
|
|
313
|
+
missed = log.filter(l => l.channel === name && l.pos > given).map(l => l.frame);
|
|
314
|
+
else
|
|
315
|
+
resync = true;
|
|
316
|
+
if (!resync && pos !== undefined)
|
|
317
|
+
pos = given;
|
|
318
|
+
}
|
|
319
|
+
answer(undefined, { seq: ch.seq, ...(pos === undefined ? {} : { pos }), ...(r.presence ? { presence: [...ch.present].map(([id, p]) => ({ id, state: p.state })) } : {}), ...(resync ? { resync } : {}) });
|
|
320
|
+
for (const frame of missed)
|
|
321
|
+
page.write(frame);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
case "leave":
|
|
325
|
+
part(page, name);
|
|
326
|
+
return answer();
|
|
327
|
+
case "focus":
|
|
328
|
+
if (name !== "" && !page.joined.has(name))
|
|
329
|
+
return answer("forbidden");
|
|
330
|
+
page.focus = name;
|
|
331
|
+
return answer();
|
|
332
|
+
// A member's message is a peer frame, its name never dotted: never
|
|
333
|
+
// taken for the feeds' rows or the tool's events.
|
|
334
|
+
case "send": {
|
|
335
|
+
const payload = q["payload"] ?? null;
|
|
336
|
+
if (typeof q["event"] !== "string" || !peerEventPattern.test(q["event"]))
|
|
337
|
+
return answer("invalid_event");
|
|
338
|
+
if (Buffer.byteLength(JSON.stringify(payload)) > maxSend)
|
|
339
|
+
return answer("invalid_body");
|
|
340
|
+
if (!r || !page.joined.has(name) || !r.send || !allows(r, page.member))
|
|
341
|
+
return answer("forbidden");
|
|
342
|
+
for (const other of pages)
|
|
343
|
+
if (other.member !== page.member && other.joined.has(name))
|
|
344
|
+
other.write({ op: "peer", ch: name, event: q["event"], payload, from: page.member });
|
|
345
|
+
return answer();
|
|
346
|
+
}
|
|
347
|
+
case "track": {
|
|
348
|
+
const value = q["state"];
|
|
349
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Buffer.byteLength(JSON.stringify(value)) > maxState)
|
|
350
|
+
return answer("invalid_body");
|
|
351
|
+
if (!r || !page.joined.has(name) || !r.presence)
|
|
352
|
+
return answer("forbidden");
|
|
353
|
+
const ch = state(name);
|
|
354
|
+
let p = ch.present.get(page.member);
|
|
355
|
+
if (!p)
|
|
356
|
+
ch.present.set(page.member, p = { state: value, tracked: 0 });
|
|
357
|
+
if (!page.tracked.has(name)) {
|
|
358
|
+
page.tracked.add(name);
|
|
359
|
+
p.tracked++;
|
|
360
|
+
}
|
|
361
|
+
p.state = value;
|
|
362
|
+
for (const other of pages)
|
|
363
|
+
if (other.member !== page.member && other.joined.has(name))
|
|
364
|
+
other.write({ op: "presence", ch: name, joins: [{ id: page.member, state: value }], leaves: [] });
|
|
365
|
+
return answer();
|
|
366
|
+
}
|
|
367
|
+
default:
|
|
368
|
+
return answer("invalid_request");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// close closes every page, as the Chest stopping would.
|
|
372
|
+
const close = () => { for (const page of pages)
|
|
373
|
+
page.close(1001, ""); };
|
|
374
|
+
return { realtime, api, probe, upgrade, close };
|
|
375
|
+
}
|
|
376
|
+
//# sourceMappingURL=testing-realtime.js.map
|