experimental-a2 0.2.0 → 0.4.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/ai-B4YhEnfw.d.ts +333 -0
  3. package/dist/ai-server.d.ts +48 -9
  4. package/dist/ai-server.js +121 -49
  5. package/dist/ai.d.ts +2 -303
  6. package/dist/ai.js +231 -86
  7. package/dist/cli-B3VuxoDe.js +597 -0
  8. package/dist/cli-bin.d.ts +1 -0
  9. package/dist/cli-bin.js +5 -0
  10. package/dist/cli.d.ts +19 -0
  11. package/dist/cli.js +2 -0
  12. package/dist/client-BrfDXQ8A.d.ts +155 -0
  13. package/dist/client-Bt4tAKi9.js +798 -0
  14. package/dist/client.d.ts +2 -90
  15. package/dist/client.js +1 -409
  16. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
  17. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
  18. package/dist/devtools-J_jZ2vQf.d.ts +151 -0
  19. package/dist/devtools-kJJaORn-.js +338 -0
  20. package/dist/devtools-server.browser.js +1 -1
  21. package/dist/devtools-server.d.ts +2 -2
  22. package/dist/devtools-server.js +224 -43
  23. package/dist/devtools.d.ts +2 -0
  24. package/dist/devtools.js +2 -0
  25. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
  26. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
  27. package/dist/http.d.ts +71 -13
  28. package/dist/http.js +302 -41
  29. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +2 -2
  32. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
  33. package/dist/internal-aEotMzu_.js +209 -0
  34. package/dist/otel.d.ts +1 -1
  35. package/dist/platform-B4TnJtWu.js +32 -0
  36. package/dist/react.d.ts +37 -14
  37. package/dist/react.js +26 -15
  38. package/dist/scheduler-qstash.d.ts +78 -0
  39. package/dist/scheduler-qstash.js +499 -0
  40. package/dist/scheduler-task-BpzhPnRS.js +54 -0
  41. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
  42. package/dist/scheduler-vercel.js +226 -0
  43. package/dist/server-CcNnFnoW.js +1405 -0
  44. package/dist/server-YtPq7hjw.d.ts +260 -0
  45. package/dist/server.d.ts +4 -155
  46. package/dist/server.js +2 -2
  47. package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
  48. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
  49. package/dist/store-codec-DTG0Ftek.js +8 -0
  50. package/dist/store-memory.d.ts +11 -0
  51. package/dist/{log-memory.js → store-memory.js} +127 -24
  52. package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
  53. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
  54. package/dist/{log-postgres.js → store-postgres.js} +158 -24
  55. package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
  56. package/dist/store-redis-http.d.ts +21 -0
  57. package/dist/store-redis-http.js +70 -0
  58. package/dist/store-redis.d.ts +37 -0
  59. package/dist/store-redis.js +298 -0
  60. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
  61. package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
  62. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
  63. package/dist/testing.browser.d.ts +1 -0
  64. package/dist/testing.browser.js +4 -0
  65. package/dist/testing.d.ts +31 -0
  66. package/dist/testing.js +101 -0
  67. package/dist/wire-DCUZBUlT.js +222 -0
  68. package/docs/01-quickstart.mdx +4 -5
  69. package/docs/concepts/01-contracts.mdx +21 -17
  70. package/docs/concepts/02-handlers.mdx +7 -7
  71. package/docs/concepts/03-durability.mdx +26 -29
  72. package/docs/concepts/04-state.mdx +18 -21
  73. package/docs/guides/01-timers.mdx +154 -54
  74. package/docs/guides/02-cancellation.mdx +30 -4
  75. package/docs/guides/03-react.mdx +20 -21
  76. package/docs/guides/04-local-first.mdx +1 -1
  77. package/docs/guides/05-production.mdx +321 -60
  78. package/docs/guides/06-ai-agents.mdx +249 -49
  79. package/docs/guides/07-devtools.mdx +137 -12
  80. package/docs/guides/08-application-data.mdx +1 -1
  81. package/docs/guides/09-presence.mdx +284 -0
  82. package/docs/guides/10-transports.mdx +131 -0
  83. package/docs/index.mdx +22 -46
  84. package/docs/reference/01-api.mdx +751 -102
  85. package/docs/reference/02-errors.mdx +10 -5
  86. package/package.json +23 -6
  87. package/dist/internal-D6wNxTck.js +0 -36
  88. package/dist/log-memory.d.ts +0 -11
  89. package/dist/log-redis.d.ts +0 -31
  90. package/dist/recovery-vercel.js +0 -119
  91. package/dist/server-DJgD2YWP.js +0 -877
  92. package/dist/wire-BVsgR8o9.js +0 -62
@@ -0,0 +1,151 @@
1
+ //#region src/devtools.d.ts
2
+ /**
3
+ * Shared wire protocol for A2's read-only devtools surfaces.
4
+ *
5
+ * The HTTP dashboard and non-interactive consumers use the same response
6
+ * shapes. Captures are newline-delimited JSON so they can be streamed,
7
+ * inspected with ordinary command-line tools, and verified without access to
8
+ * an A2 server.
9
+ */
10
+ declare const DEVTOOLS_PROTOCOL_VERSION: 1;
11
+ declare const DEVTOOLS_CAPTURE_VERSION: 1;
12
+ declare const DEVTOOLS_CAPTURE_MEDIA_TYPE: "application/x-ndjson";
13
+ declare const DEVTOOLS_SESSION_PAGE_LIMIT: 250;
14
+ declare const DEVTOOLS_SESSION_PAGE_MAX_LIMIT: 1_000;
15
+ type DevtoolsWireSessionSummary = {
16
+ sessionId: string;
17
+ eventCount: number;
18
+ pendingCount: number;
19
+ failedCount: number;
20
+ attemptCount: number;
21
+ failureCount: number;
22
+ firstEventAt: string;
23
+ updatedAt: string;
24
+ };
25
+ type DevtoolsWireEvent = {
26
+ id: string;
27
+ type: string;
28
+ payload: unknown;
29
+ index: number;
30
+ sessionId: string;
31
+ createdAt: string;
32
+ cause: {
33
+ index: number;
34
+ attempt: number;
35
+ batchSize?: number;
36
+ } | null;
37
+ lane: string | null;
38
+ processedAt: string | null;
39
+ processedByAttempt: number | null;
40
+ returnedEventIds: string[] | null;
41
+ firstClaimedAt: string | null;
42
+ lastClaimedAt: string | null;
43
+ attemptCount: number;
44
+ claimHolder: string | null;
45
+ claimExpiresAt: string | null;
46
+ failureCount: number;
47
+ lastFailedAt: string | null;
48
+ lastFailedAttempt: number | null;
49
+ lastError: string | null;
50
+ failedAt: string | null;
51
+ };
52
+ type DevtoolsWireSnapshot = {
53
+ reducerName: string;
54
+ index: number;
55
+ updatedAt: string | null;
56
+ };
57
+ type DevtoolsContractsResponse = {
58
+ protocolVersion: typeof DEVTOOLS_PROTOCOL_VERSION;
59
+ contracts: Array<{
60
+ name: string;
61
+ }>;
62
+ };
63
+ type DevtoolsSessionsResponse = {
64
+ protocolVersion: typeof DEVTOOLS_PROTOCOL_VERSION;
65
+ cursor: string | null;
66
+ sessions: DevtoolsWireSessionSummary[];
67
+ };
68
+ type DevtoolsSessionDetail = {
69
+ protocolVersion: typeof DEVTOOLS_PROTOCOL_VERSION;
70
+ contract: string;
71
+ sessionId: string;
72
+ /** An invalidation token, not a content-integrity digest. */
73
+ revision: string;
74
+ events: DevtoolsWireEvent[];
75
+ snapshots: DevtoolsWireSnapshot[];
76
+ };
77
+ type DevtoolsSessionPage = {
78
+ protocolVersion: typeof DEVTOOLS_PROTOCOL_VERSION;
79
+ contract: string;
80
+ sessionId: string;
81
+ /** Inclusive log frontier frozen by the first page. */
82
+ throughIndex: number;
83
+ /** Opaque continuation token; null means this frozen read is complete. */
84
+ cursor: string | null;
85
+ events: DevtoolsWireEvent[];
86
+ /** Snapshot metadata is present only on the first page. */
87
+ snapshots: DevtoolsWireSnapshot[];
88
+ };
89
+ type DevtoolsCapabilities = {
90
+ protocolVersion: typeof DEVTOOLS_PROTOCOL_VERSION;
91
+ resources: Array<"contracts" | "sessions" | "session" | "watch" | "export">;
92
+ sessionPagination: {
93
+ defaultLimit: typeof DEVTOOLS_SESSION_PAGE_LIMIT;
94
+ maxLimit: typeof DEVTOOLS_SESSION_PAGE_MAX_LIMIT;
95
+ };
96
+ capture: {
97
+ version: typeof DEVTOOLS_CAPTURE_VERSION;
98
+ mediaType: typeof DEVTOOLS_CAPTURE_MEDIA_TYPE;
99
+ };
100
+ };
101
+ declare const DEVTOOLS_CAPABILITIES: DevtoolsCapabilities;
102
+ type DevtoolsCaptureManifest = {
103
+ kind: "a2.capture";
104
+ captureVersion: typeof DEVTOOLS_CAPTURE_VERSION;
105
+ protocolVersion: typeof DEVTOOLS_PROTOCOL_VERSION;
106
+ contract: string;
107
+ sessionId: string;
108
+ revision: string;
109
+ capturedAt: string;
110
+ fidelity: "exact";
111
+ };
112
+ /** One exact durable event, with the record discriminator added. */
113
+ type DevtoolsCaptureEvent = DevtoolsWireEvent & {
114
+ kind: "event";
115
+ };
116
+ /** Snapshot metadata only. Cached reducer state is never exported. */
117
+ type DevtoolsCaptureSnapshot = DevtoolsWireSnapshot & {
118
+ kind: "snapshot";
119
+ };
120
+ type DevtoolsCaptureFooter = {
121
+ kind: "end";
122
+ captureVersion: typeof DEVTOOLS_CAPTURE_VERSION;
123
+ eventCount: number;
124
+ snapshotCount: number;
125
+ maxIndex: number | null;
126
+ sha256: string;
127
+ };
128
+ type DevtoolsCaptureRecord = DevtoolsCaptureManifest | DevtoolsCaptureEvent | DevtoolsCaptureSnapshot | DevtoolsCaptureFooter;
129
+ type DevtoolsCapture = {
130
+ contract: string;
131
+ sessionId: string;
132
+ revision: string;
133
+ capturedAt: string;
134
+ manifest: DevtoolsCaptureManifest;
135
+ events: DevtoolsCaptureEvent[];
136
+ snapshots: DevtoolsCaptureSnapshot[];
137
+ footer: DevtoolsCaptureFooter;
138
+ records: DevtoolsCaptureRecord[];
139
+ };
140
+ /** Compute the server-compatible lifecycle invalidation token. */
141
+ declare const devtoolsSessionRevision: (detail: {
142
+ events: readonly DevtoolsWireEvent[];
143
+ snapshots: readonly DevtoolsWireSnapshot[];
144
+ }) => Promise<string>;
145
+ declare function encodeDevtoolsCapture(detail: DevtoolsSessionDetail, options?: {
146
+ capturedAt?: string | Date;
147
+ }): Promise<string>;
148
+ declare function parseDevtoolsCapture(value: unknown): DevtoolsCapture;
149
+ declare function verifyDevtoolsCapture(capture: DevtoolsCapture): Promise<void>;
150
+ //#endregion
151
+ export { parseDevtoolsCapture as C, encodeDevtoolsCapture as S, DevtoolsSessionsResponse as _, DEVTOOLS_SESSION_PAGE_LIMIT as a, DevtoolsWireSnapshot as b, DevtoolsCapture as c, DevtoolsCaptureManifest as d, DevtoolsCaptureRecord as f, DevtoolsSessionPage as g, DevtoolsSessionDetail as h, DEVTOOLS_PROTOCOL_VERSION as i, DevtoolsCaptureEvent as l, DevtoolsContractsResponse as m, DEVTOOLS_CAPTURE_MEDIA_TYPE as n, DEVTOOLS_SESSION_PAGE_MAX_LIMIT as o, DevtoolsCaptureSnapshot as p, DEVTOOLS_CAPTURE_VERSION as r, DevtoolsCapabilities as s, DEVTOOLS_CAPABILITIES as t, DevtoolsCaptureFooter as u, DevtoolsWireEvent as v, verifyDevtoolsCapture as w, devtoolsSessionRevision as x, DevtoolsWireSessionSummary as y };
@@ -0,0 +1,338 @@
1
+ //#region src/devtools.ts
2
+ /**
3
+ * Shared wire protocol for A2's read-only devtools surfaces.
4
+ *
5
+ * The HTTP dashboard and non-interactive consumers use the same response
6
+ * shapes. Captures are newline-delimited JSON so they can be streamed,
7
+ * inspected with ordinary command-line tools, and verified without access to
8
+ * an A2 server.
9
+ */
10
+ const DEVTOOLS_PROTOCOL_VERSION = 1;
11
+ const DEVTOOLS_CAPTURE_VERSION = 1;
12
+ const DEVTOOLS_CAPTURE_MEDIA_TYPE = "application/x-ndjson";
13
+ const DEVTOOLS_SESSION_PAGE_LIMIT = 250;
14
+ const DEVTOOLS_SESSION_PAGE_MAX_LIMIT = 1e3;
15
+ const DEVTOOLS_CAPABILITIES = {
16
+ protocolVersion: 1,
17
+ resources: [
18
+ "contracts",
19
+ "sessions",
20
+ "session",
21
+ "watch",
22
+ "export"
23
+ ],
24
+ sessionPagination: {
25
+ defaultLimit: 250,
26
+ maxLimit: DEVTOOLS_SESSION_PAGE_MAX_LIMIT
27
+ },
28
+ capture: {
29
+ version: 1,
30
+ mediaType: DEVTOOLS_CAPTURE_MEDIA_TYPE
31
+ }
32
+ };
33
+ const encoder = new TextEncoder();
34
+ const decoder = new TextDecoder("utf-8", { fatal: true });
35
+ const objectValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
36
+ var DevtoolsCaptureError = class extends TypeError {};
37
+ const captureError = (message) => new DevtoolsCaptureError(`invalid A2 devtools capture: ${message}`);
38
+ const sha256 = async (value) => {
39
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
40
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
41
+ };
42
+ const timestampMs = (value) => value === null ? null : new Date(value).getTime();
43
+ /** Compute the server-compatible lifecycle invalidation token. */
44
+ const devtoolsSessionRevision = (detail) => sha256(JSON.stringify({
45
+ events: detail.events.map((event) => [
46
+ event.id,
47
+ event.index,
48
+ event.cause?.index ?? null,
49
+ event.cause?.attempt ?? null,
50
+ timestampMs(event.processedAt),
51
+ event.processedByAttempt,
52
+ timestampMs(event.firstClaimedAt),
53
+ timestampMs(event.lastClaimedAt),
54
+ event.attemptCount,
55
+ event.failureCount,
56
+ timestampMs(event.lastFailedAt),
57
+ event.lastFailedAttempt,
58
+ event.lastError,
59
+ timestampMs(event.failedAt)
60
+ ]),
61
+ snapshots: detail.snapshots.map((snapshot) => [
62
+ snapshot.reducerName,
63
+ snapshot.index,
64
+ snapshot.updatedAt === null ? 0 : new Date(snapshot.updatedAt).getTime()
65
+ ])
66
+ }));
67
+ const encodeRecords = (records) => records.map((record) => JSON.stringify(record)).join("\n") + "\n";
68
+ const asCapturedAt = (value) => {
69
+ if (value instanceof Date) {
70
+ if (Number.isNaN(value.getTime())) throw new TypeError("capturedAt must be a canonical ISO date");
71
+ return value.toISOString();
72
+ }
73
+ if (value === void 0) return (/* @__PURE__ */ new Date()).toISOString();
74
+ const parsed = new Date(value);
75
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString() !== value) throw new TypeError("capturedAt must be a canonical ISO date string or Date");
76
+ return value;
77
+ };
78
+ async function encodeDevtoolsCapture(detail, options = {}) {
79
+ const manifest = {
80
+ kind: "a2.capture",
81
+ captureVersion: 1,
82
+ protocolVersion: 1,
83
+ contract: detail.contract,
84
+ sessionId: detail.sessionId,
85
+ revision: detail.revision,
86
+ capturedAt: asCapturedAt(options.capturedAt),
87
+ fidelity: "exact"
88
+ };
89
+ const events = detail.events.map((event) => ({
90
+ ...event,
91
+ kind: "event"
92
+ }));
93
+ const snapshots = detail.snapshots.map((snapshot) => ({
94
+ ...snapshot,
95
+ kind: "snapshot"
96
+ }));
97
+ const signedRecords = [
98
+ manifest,
99
+ ...events,
100
+ ...snapshots
101
+ ];
102
+ validateCanonicalJsonValue(signedRecords, "capture");
103
+ const signed = encodeRecords(signedRecords);
104
+ const footer = {
105
+ kind: "end",
106
+ captureVersion: 1,
107
+ eventCount: events.length,
108
+ snapshotCount: snapshots.length,
109
+ maxIndex: events.at(-1)?.index ?? null,
110
+ sha256: `sha256:${await sha256(signed)}`
111
+ };
112
+ return `${signed}${JSON.stringify(footer)}\n`;
113
+ }
114
+ const recordsFromText = (text) => {
115
+ if (text.length === 0) throw captureError("capture is empty");
116
+ const lines = text.split("\n");
117
+ if (lines.pop() !== "") throw captureError("capture must end with a newline");
118
+ const records = lines.map((line, index) => {
119
+ if (line.length === 0) throw captureError(`record ${index + 1} is empty`);
120
+ try {
121
+ return JSON.parse(line);
122
+ } catch {
123
+ throw captureError(`record ${index + 1} is not valid JSON`);
124
+ }
125
+ });
126
+ if (encodeRecords(records) !== text) throw captureError("capture must use canonical UTF-8 NDJSON encoding");
127
+ return records;
128
+ };
129
+ const recordsFromValues = (records) => {
130
+ try {
131
+ validateCanonicalJsonValue(records, "capture.records");
132
+ return recordsFromText(encodeRecords(records));
133
+ } catch (error) {
134
+ if (error instanceof DevtoolsCaptureError) throw error;
135
+ throw captureError("capture records are not JSON-serializable");
136
+ }
137
+ };
138
+ const captureRecordsFrom = (value) => {
139
+ if (typeof value === "string") return recordsFromText(value);
140
+ if (value instanceof Uint8Array) try {
141
+ return recordsFromText(decoder.decode(value));
142
+ } catch (error) {
143
+ if (error instanceof DevtoolsCaptureError) throw error;
144
+ throw captureError("capture is not valid UTF-8");
145
+ }
146
+ if (Array.isArray(value)) return recordsFromValues(value);
147
+ const capture = objectValue(value);
148
+ if (capture) {
149
+ if (Array.isArray(capture["records"])) return recordsFromValues(capture["records"]);
150
+ const manifest = capture["manifest"];
151
+ const events = capture["events"];
152
+ const snapshots = capture["snapshots"];
153
+ const footer = capture["footer"];
154
+ if (Array.isArray(events) && Array.isArray(snapshots)) return recordsFromValues([
155
+ manifest,
156
+ ...events,
157
+ ...snapshots,
158
+ footer
159
+ ]);
160
+ }
161
+ throw captureError("expected NDJSON text, bytes, records, or a capture");
162
+ };
163
+ const stringField = (value, field, record) => {
164
+ const candidate = value[field];
165
+ if (typeof candidate !== "string" || candidate.length === 0) throw captureError(`${record}.${field} must be a non-empty string`);
166
+ return candidate;
167
+ };
168
+ const integerField = (value, field, record) => {
169
+ const candidate = value[field];
170
+ if (!Number.isSafeInteger(candidate) || candidate < 0) throw captureError(`${record}.${field} must be a non-negative integer`);
171
+ return candidate;
172
+ };
173
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
174
+ const nonNegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
175
+ const canonicalDate = (value) => {
176
+ if (typeof value !== "string") return false;
177
+ const parsed = new Date(value);
178
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
179
+ };
180
+ const optionalDate = (value) => value === null || canonicalDate(value);
181
+ const nullableString = (value) => value === null || typeof value === "string";
182
+ const nullablePositiveInteger = (value) => value === null || positiveInteger(value);
183
+ const validateDate = (value, field, record, nullable) => {
184
+ if (nullable ? optionalDate(value) : canonicalDate(value)) return;
185
+ throw captureError(`${record}.${field} has an invalid timestamp; expected a canonical ISO date${nullable ? " or null" : ""}`);
186
+ };
187
+ const validateCause = (value, record) => {
188
+ if (value === null) return;
189
+ const cause = objectValue(value);
190
+ if (cause === null || !positiveInteger(cause["index"]) || !positiveInteger(cause["attempt"]) || cause["batchSize"] !== void 0 && !positiveInteger(cause["batchSize"])) throw captureError(`${record}.cause is invalid`);
191
+ };
192
+ const validateEventRecord = (record, label) => {
193
+ stringField(record, "id", label);
194
+ stringField(record, "type", label);
195
+ stringField(record, "sessionId", label);
196
+ if (!Object.hasOwn(record, "payload")) throw captureError(`${label}.payload is required`);
197
+ if (!positiveInteger(record["index"])) throw captureError(`${label}.index must be a positive integer`);
198
+ validateDate(record["createdAt"], "createdAt", label, false);
199
+ validateCause(record["cause"], label);
200
+ if (!nullableString(record["lane"])) throw captureError(`${label}.lane must be a string or null`);
201
+ validateDate(record["processedAt"], "processedAt", label, true);
202
+ if (!nullablePositiveInteger(record["processedByAttempt"])) throw captureError(`${label}.processedByAttempt must be a positive integer or null`);
203
+ validateDate(record["firstClaimedAt"], "firstClaimedAt", label, true);
204
+ validateDate(record["lastClaimedAt"], "lastClaimedAt", label, true);
205
+ if (!nonNegativeInteger(record["attemptCount"])) throw captureError(`${label}.attemptCount must be a non-negative integer`);
206
+ if (!nullableString(record["claimHolder"])) throw captureError(`${label}.claimHolder must be a string or null`);
207
+ validateDate(record["claimExpiresAt"], "claimExpiresAt", label, true);
208
+ if (!nonNegativeInteger(record["failureCount"])) throw captureError(`${label}.failureCount must be a non-negative integer`);
209
+ validateDate(record["lastFailedAt"], "lastFailedAt", label, true);
210
+ if (!nullablePositiveInteger(record["lastFailedAttempt"])) throw captureError(`${label}.lastFailedAttempt must be a positive integer or null`);
211
+ if (!nullableString(record["lastError"])) throw captureError(`${label}.lastError must be a string or null`);
212
+ validateDate(record["failedAt"], "failedAt", label, true);
213
+ const returned = record["returnedEventIds"];
214
+ if (returned !== null && (!Array.isArray(returned) || returned.some((id) => typeof id !== "string" || id.length === 0))) throw captureError(`${label}.returnedEventIds is invalid`);
215
+ };
216
+ const validateSnapshotRecord = (record, label) => {
217
+ stringField(record, "reducerName", label);
218
+ if (!nonNegativeInteger(record["index"])) throw captureError(`${label}.index must be a non-negative integer`);
219
+ validateDate(record["updatedAt"], "updatedAt", label, true);
220
+ };
221
+ function validateCanonicalJsonValue(value, path, ancestors = /* @__PURE__ */ new WeakSet()) {
222
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
223
+ if (typeof value === "number") {
224
+ if (!Number.isFinite(value) || Object.is(value, -0)) throw captureError(`${path} is not a canonical JSON number`);
225
+ return;
226
+ }
227
+ if (Array.isArray(value)) {
228
+ if (ancestors.has(value)) throw captureError(`${path} is not a canonical JSON tree`);
229
+ ancestors.add(value);
230
+ try {
231
+ const keys = Object.keys(value);
232
+ const ownKeys = Reflect.ownKeys(value);
233
+ if (keys.length !== value.length || ownKeys.length !== keys.length + 1 || !ownKeys.includes("length") || keys.some((key, index) => key !== String(index))) throw captureError(`${path} is not a canonical JSON array`);
234
+ value.forEach((entry, index) => validateCanonicalJsonValue(entry, `${path}[${index}]`, ancestors));
235
+ } finally {
236
+ ancestors.delete(value);
237
+ }
238
+ return;
239
+ }
240
+ const object = objectValue(value);
241
+ if (object === null || Object.getPrototypeOf(object) !== Object.prototype && Object.getPrototypeOf(object) !== null) throw captureError(`${path} is not a canonical JSON value`);
242
+ if (ancestors.has(object)) throw captureError(`${path} is not a canonical JSON tree`);
243
+ ancestors.add(object);
244
+ try {
245
+ const keys = Object.keys(object);
246
+ if (Reflect.ownKeys(object).length !== keys.length || keys.some((key) => !Object.prototype.propertyIsEnumerable.call(object, key))) throw captureError(`${path} is not a canonical JSON object`);
247
+ for (const key of keys) validateCanonicalJsonValue(object[key], `${path}.${key}`, ancestors);
248
+ } finally {
249
+ ancestors.delete(object);
250
+ }
251
+ }
252
+ function parseDevtoolsCapture(value) {
253
+ const records = captureRecordsFrom(value);
254
+ if (records.length < 2) throw captureError("capture needs a manifest and footer");
255
+ const manifestValue = objectValue(records[0]);
256
+ if (manifestValue?.["kind"] !== "a2.capture") throw captureError("first record must have kind 'a2.capture'");
257
+ if (manifestValue["captureVersion"] !== 1) throw captureError(`unsupported capture version '${String(manifestValue["captureVersion"])}'`);
258
+ if (manifestValue["protocolVersion"] !== 1) throw captureError(`unsupported protocol version '${String(manifestValue["protocolVersion"])}'`);
259
+ if (manifestValue["fidelity"] !== "exact") throw captureError("manifest.fidelity must be 'exact'");
260
+ const manifest = manifestValue;
261
+ const contract = stringField(manifestValue, "contract", "manifest");
262
+ const sessionId = stringField(manifestValue, "sessionId", "manifest");
263
+ const revision = stringField(manifestValue, "revision", "manifest");
264
+ const capturedAt = stringField(manifestValue, "capturedAt", "manifest");
265
+ if (!canonicalDate(capturedAt)) throw captureError("manifest.capturedAt must be a canonical ISO date");
266
+ const footerValue = objectValue(records.at(-1));
267
+ if (footerValue?.["kind"] !== "end") throw captureError("last record must have kind 'end'");
268
+ if (footerValue["captureVersion"] !== 1) throw captureError("footer capture version does not match");
269
+ const footer = footerValue;
270
+ integerField(footerValue, "eventCount", "footer");
271
+ integerField(footerValue, "snapshotCount", "footer");
272
+ stringField(footerValue, "sha256", "footer");
273
+ if (footerValue["maxIndex"] !== null && (!Number.isSafeInteger(footerValue["maxIndex"]) || footerValue["maxIndex"] < 1)) throw captureError("footer.maxIndex must be null or a positive integer");
274
+ const events = [];
275
+ const snapshots = [];
276
+ let sawSnapshot = false;
277
+ for (let index = 1; index < records.length - 1; index += 1) {
278
+ const record = objectValue(records[index]);
279
+ if (record?.["kind"] === "event") {
280
+ if (sawSnapshot) throw captureError("event records must precede snapshot records");
281
+ validateEventRecord(record, `record ${index + 1}`);
282
+ events.push(record);
283
+ } else if (record?.["kind"] === "snapshot") {
284
+ sawSnapshot = true;
285
+ validateSnapshotRecord(record, `record ${index + 1}`);
286
+ snapshots.push(record);
287
+ } else throw captureError(`record ${index + 1} has an unknown kind`);
288
+ }
289
+ return {
290
+ contract,
291
+ sessionId,
292
+ revision,
293
+ capturedAt,
294
+ manifest,
295
+ events,
296
+ snapshots,
297
+ footer,
298
+ records
299
+ };
300
+ }
301
+ async function verifyDevtoolsCapture(capture) {
302
+ validateCanonicalJsonValue(capture.records, "capture.records");
303
+ parseDevtoolsCapture(capture.records);
304
+ const expectedRecords = [
305
+ capture.manifest,
306
+ ...capture.events,
307
+ ...capture.snapshots,
308
+ capture.footer
309
+ ];
310
+ if (capture.records.length !== expectedRecords.length || capture.records.some((record, index) => record !== expectedRecords[index])) throw captureError("record sequence does not match the capture");
311
+ if (capture.contract !== capture.manifest.contract || capture.sessionId !== capture.manifest.sessionId || capture.revision !== capture.manifest.revision || capture.capturedAt !== capture.manifest.capturedAt) throw captureError("capture identity does not match its manifest");
312
+ if (!canonicalDate(capture.capturedAt)) throw captureError("manifest.capturedAt must be a canonical ISO date");
313
+ if (capture.footer.eventCount !== capture.events.length) throw captureError("footer event count does not match");
314
+ if (capture.footer.snapshotCount !== capture.snapshots.length) throw captureError("footer snapshot count does not match");
315
+ let previousIndex = 0;
316
+ const ids = /* @__PURE__ */ new Set();
317
+ for (const event of capture.events) {
318
+ if (event.kind !== "event") throw captureError("invalid event record kind");
319
+ validateEventRecord(event, `event #${event.index}`);
320
+ if (!Number.isSafeInteger(event.index) || event.index !== previousIndex + 1) throw captureError("event indexes must be contiguous from 1");
321
+ previousIndex = event.index;
322
+ if (event.sessionId !== capture.sessionId) throw captureError(`event #${event.index} belongs to another session`);
323
+ if (ids.has(event.id)) throw captureError(`event #${event.index} repeats id '${event.id}'`);
324
+ ids.add(event.id);
325
+ }
326
+ for (const snapshot of capture.snapshots) {
327
+ if (snapshot.kind !== "snapshot") throw captureError("invalid snapshot record kind");
328
+ validateSnapshotRecord(snapshot, `snapshot '${snapshot.reducerName}'`);
329
+ if (!Number.isSafeInteger(snapshot.index) || snapshot.index > previousIndex) throw captureError(`snapshot '${snapshot.reducerName}' is invalid`);
330
+ }
331
+ const maxIndex = capture.events.at(-1)?.index ?? null;
332
+ if (capture.footer.maxIndex !== maxIndex) throw captureError("footer max index does not match");
333
+ if (!/^sha256:[\da-f]{64}$/.test(capture.footer.sha256)) throw captureError("footer.sha256 is malformed");
334
+ const expected = `sha256:${await sha256(encodeRecords(capture.records.slice(0, -1)))}`;
335
+ if (capture.footer.sha256 !== expected) throw captureError("capture digest does not match");
336
+ }
337
+ //#endregion
338
+ export { DEVTOOLS_SESSION_PAGE_LIMIT as a, encodeDevtoolsCapture as c, DEVTOOLS_PROTOCOL_VERSION as i, parseDevtoolsCapture as l, DEVTOOLS_CAPTURE_MEDIA_TYPE as n, DEVTOOLS_SESSION_PAGE_MAX_LIMIT as o, DEVTOOLS_CAPTURE_VERSION as r, devtoolsSessionRevision as s, DEVTOOLS_CAPABILITIES as t, verifyDevtoolsCapture as u };
@@ -1,4 +1,4 @@
1
1
  //#region src/devtools-server.browser.ts
2
- /** experimental-a2/devtools/server owns log access and is server-only. */
2
+ /** experimental-a2/devtools/server owns store access and is server-only. */
3
3
  throw new Error("experimental-a2/devtools/server is server-only — mount its handler in a server route and open that URL in the browser.");
4
4
  //#endregion
@@ -9,8 +9,8 @@ type DevtoolsOptions = {
9
9
  servers: readonly DevtoolsServer[];
10
10
  /**
11
11
  * Application-owned access control. Returning a Response short-circuits
12
- * the request. Without this option, devtools is available outside
13
- * production and returns 404 in production.
12
+ * the request. Without this option, devtools is available only when
13
+ * NODE_ENV is exactly "development" and otherwise returns 404.
14
14
  */
15
15
  authorize?: DevtoolsAuthorization;
16
16
  };