@agent-native/core-corpus 0.209.2 → 0.209.3-nightly-20261016183441

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 (62) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  3. package/corpus/templates/analytics/actions/update-dashboard.ts +9 -0
  4. package/corpus/templates/analytics/app/components/auth/SqlQuerySnapshotSessionBridge.tsx +25 -0
  5. package/corpus/templates/analytics/app/components/dashboard/PanelFreshnessClock.tsx +140 -0
  6. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +104 -48
  7. package/corpus/templates/analytics/app/components/dashboard/panel-freshness.ts +171 -0
  8. package/corpus/templates/analytics/app/global.css +59 -0
  9. package/corpus/templates/analytics/app/i18n/en-US.ts +8 -0
  10. package/corpus/templates/analytics/app/lib/cost-tracker.ts +122 -16
  11. package/corpus/templates/analytics/app/lib/query-metrics.ts +50 -13
  12. package/corpus/templates/analytics/app/lib/sql-query-cache.ts +697 -0
  13. package/corpus/templates/analytics/app/lib/sql-query-freshness.ts +85 -0
  14. package/corpus/templates/analytics/app/lib/sql-query-snapshot-session.ts +145 -0
  15. package/corpus/templates/analytics/app/lib/sql-query.ts +366 -36
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +73 -18
  17. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +45 -12
  18. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +5 -0
  19. package/corpus/templates/analytics/app/root.tsx +2 -0
  20. package/corpus/templates/analytics/changelog/2026-10-14-dashboard-charts-now-reappear-instantly-after-a-reload-from-.md +6 -0
  21. package/corpus/templates/analytics/changelog/2026-10-14-dashboard-panels-show-fetch-time-in-a-clock-and-label-only-st.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-10-15-dashboard-query-costs-and-refresh-confirmations-reflect-actu.md +6 -0
  23. package/corpus/templates/analytics/changelog/2026-10-15-dashboard-results-disappear-when-access-is-withdrawn-and-exp.md +6 -0
  24. package/corpus/templates/analytics/changelog/2026-10-16-dashboard-freshness-stays-in-the-title-row-with-compact-upda.md +6 -0
  25. package/corpus/templates/analytics/changelog/2026-10-16-dashboard-update-status-remains-visible-for-sources-without-.md +6 -0
  26. package/corpus/templates/analytics/server/lib/bigquery.ts +35 -4
  27. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +2 -3
  28. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +9 -6
  29. package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +2 -2
  30. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +4 -2
  31. package/corpus/templates/analytics/shared/sql-query-contract.ts +49 -0
  32. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  33. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  34. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  35. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  36. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  37. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  38. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  39. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  40. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +24 -0
  41. package/corpus/templates/design/app/components/design/types.ts +1 -0
  42. package/corpus/templates/design/app/lib/design-import.ts +35 -0
  43. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +11 -0
  44. package/corpus/templates/design/app/pages/design-editor/command-types.ts +2 -0
  45. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +3 -0
  46. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +19 -0
  47. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +118 -20
  48. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-layer-actions.ts +15 -3
  49. package/corpus/templates/design/app/pages/design-editor/relative-position-offset.ts +109 -0
  50. package/corpus/templates/design/changelog/2026-10-15-dragged-layers-use-the-final-release-position.md +6 -0
  51. package/corpus/templates/design/changelog/2026-10-15-repeated-pastes-preserve-relative-layer-offsets.md +6 -0
  52. package/corpus/templates/design/changelog/2026-10-15-text-wrapping-hug-widths.md +6 -0
  53. package/corpus/templates/design/shared/code-layer.ts +2 -1
  54. package/corpus/templates/design/shared/relative-position-containing-block.ts +40 -0
  55. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  56. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  57. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  58. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  59. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  60. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +137 -118
  61. package/corpus/templates/slides/changelog/2026-10-16-search-and-preview-reference-decks-before-choosing-one.md +6 -0
  62. package/package.json +1 -1
@@ -0,0 +1,697 @@
1
+ /**
2
+ * Bounded sessionStorage copies of successful SQL query results, so a page
3
+ * reload can show the last rows immediately while ONE normal query refreshes
4
+ * them. sessionStorage is a cache, never a source of truth: every read is
5
+ * validated, every failure is a typed status, and the caller falls back to a
6
+ * normal fetch on anything but `hit`.
7
+ *
8
+ * The caller passes the fully serialized query key (it already carries the
9
+ * source, resolved SQL, dashboard and auth scope) plus the validated account
10
+ * scope. Entries are found only by that exact pair; nothing is read across
11
+ * scopes and nothing is stored for a loading or anonymous session.
12
+ *
13
+ * Storage keys are `an-sql-snapshot/v1/<storedAt>/<scope>/<key>` where scope
14
+ * and key are 106-bit fingerprints, so listing and eviction never parse a
15
+ * value and the SQL text is never written to storage. The account scope is
16
+ * also stored verbatim and compared exactly on read; the fingerprints only
17
+ * pick candidates.
18
+ */
19
+
20
+ export const SQL_QUERY_SNAPSHOT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
21
+ export const SQL_QUERY_SNAPSHOT_MAX_ENTRIES = 48;
22
+ /** Serialized length in string code units, not UTF-8 bytes. */
23
+ export const SQL_QUERY_SNAPSHOT_MAX_ENTRY_LENGTH = 128 * 1024;
24
+ export const SQL_QUERY_SNAPSHOT_MAX_TOTAL_LENGTH = 2 * 1024 * 1024;
25
+
26
+ const KEY_PREFIX = "an-sql-snapshot/";
27
+ const KEY_PATTERN =
28
+ /^an-sql-snapshot\/v1\/([0-9a-z]{1,12})\/([0-9a-f]{28})\/([0-9a-f]{28})$/;
29
+ const ENVELOPE_VERSION = 1;
30
+ const FUTURE_SKEW_MS = 5 * 60 * 1000;
31
+ const MAX_QUERY_KEY_LENGTH = 100_000;
32
+ // Every JSON value costs at least two serialized characters, so a result with
33
+ // more nodes than this cannot fit the per-entry bound; stop walking early.
34
+ const MAX_RESULT_NODES = SQL_QUERY_SNAPSHOT_MAX_ENTRY_LENGTH / 2;
35
+ const MAX_RESULT_DEPTH = 32;
36
+ const ACCOUNT_SCOPE_PATTERN = /^[^:\s]{1,128}:[^:\s]{1,128}$/;
37
+ const RESERVED_SCOPES = new Set(["session-loading"]);
38
+
39
+ export interface SqlQuerySnapshotResult {
40
+ rows: Record<string, unknown>[];
41
+ schema?: { name: string; type: string }[];
42
+ cached?: boolean;
43
+ providerCacheHit?: boolean;
44
+ /**
45
+ * `true` marks rows cut short by a server cap; such a result is never
46
+ * stored, so a restored snapshot is always the complete result.
47
+ */
48
+ truncated?: boolean;
49
+ /** ISO timestamp of the original fetch, independent of when it was stored. */
50
+ fetchedAt?: string;
51
+ }
52
+
53
+ /** `SqlQueryResult` fits here; only the fields above are ever persisted. */
54
+ export type SqlQuerySnapshotInput = SqlQuerySnapshotResult & { error?: string };
55
+
56
+ export interface SqlQuerySnapshot {
57
+ result: SqlQuerySnapshotResult;
58
+ /** Epoch ms when this copy was written; separate from `result.fetchedAt`. */
59
+ storedAt: number;
60
+ }
61
+
62
+ export type SqlQuerySnapshotStorage = Pick<
63
+ Storage,
64
+ "getItem" | "setItem" | "removeItem" | "key" | "length"
65
+ >;
66
+
67
+ export type SqlQuerySnapshotUnavailableReason =
68
+ | "no-storage"
69
+ | "storage-error"
70
+ | "quota-exceeded"
71
+ | "invalid-account-scope"
72
+ | "invalid-query-key";
73
+
74
+ export type SqlQuerySnapshotMissReason =
75
+ | "absent"
76
+ | "expired"
77
+ | "future-dated"
78
+ | "corrupt"
79
+ | "oversized"
80
+ | "truncated"
81
+ | "unsupported-version"
82
+ | "scope-mismatch";
83
+
84
+ export type SqlQuerySnapshotSkipReason =
85
+ | "error-result"
86
+ | "invalid-result"
87
+ | "invalid-fetched-at"
88
+ | "stale"
89
+ | "truncated"
90
+ | "too-large";
91
+
92
+ export type SqlQuerySnapshotRead =
93
+ | { status: "hit"; snapshot: SqlQuerySnapshot }
94
+ | { status: "miss"; reason: SqlQuerySnapshotMissReason }
95
+ | {
96
+ status: "unavailable";
97
+ reason: SqlQuerySnapshotUnavailableReason;
98
+ errorName?: string;
99
+ };
100
+
101
+ export type SqlQuerySnapshotWrite =
102
+ | { status: "written"; storedAt: number; length: number; evicted: number }
103
+ | { status: "skipped"; reason: SqlQuerySnapshotSkipReason }
104
+ | {
105
+ status: "unavailable";
106
+ reason: SqlQuerySnapshotUnavailableReason;
107
+ errorName?: string;
108
+ };
109
+
110
+ export type SqlQuerySnapshotClear =
111
+ | { status: "cleared"; removed: number }
112
+ | {
113
+ status: "unavailable";
114
+ reason: SqlQuerySnapshotUnavailableReason;
115
+ errorName?: string;
116
+ };
117
+
118
+ type StorageOptions = {
119
+ /** Test seam; defaults to `window.sessionStorage`. */
120
+ storage?: SqlQuerySnapshotStorage;
121
+ now?: number;
122
+ };
123
+
124
+ type Identity = { accountScope: string; scopeId: string; keyId: string };
125
+
126
+ type EntryName = {
127
+ name: string;
128
+ storedAt: number;
129
+ scopeId: string;
130
+ keyId: string;
131
+ };
132
+
133
+ type Unavailable = {
134
+ status: "unavailable";
135
+ reason: SqlQuerySnapshotUnavailableReason;
136
+ errorName?: string;
137
+ };
138
+
139
+ export function readSqlQuerySnapshot(
140
+ options: StorageOptions & { accountScope: string; queryKey: string },
141
+ ): SqlQuerySnapshotRead {
142
+ const identity = resolveIdentity(options.accountScope, options.queryKey);
143
+ if ("status" in identity) return identity;
144
+ const storage = resolveStorage(options.storage);
145
+ if ("status" in storage) return storage;
146
+ const now = options.now ?? Date.now();
147
+
148
+ try {
149
+ const candidates = listEntries(storage).entries.filter(
150
+ (entry) =>
151
+ entry.scopeId === identity.scopeId && entry.keyId === identity.keyId,
152
+ );
153
+ if (candidates.length === 0) return { status: "miss", reason: "absent" };
154
+ const newest = candidates.reduce((a, b) =>
155
+ b.storedAt > a.storedAt ? b : a,
156
+ );
157
+
158
+ const drop = (reason: SqlQuerySnapshotMissReason): SqlQuerySnapshotRead => {
159
+ storage.removeItem(newest.name);
160
+ return { status: "miss", reason };
161
+ };
162
+ const nameAge = now - newest.storedAt;
163
+ if (nameAge < -FUTURE_SKEW_MS) return drop("future-dated");
164
+ if (nameAge > SQL_QUERY_SNAPSHOT_MAX_AGE_MS) return drop("expired");
165
+
166
+ const raw = storage.getItem(newest.name);
167
+ if (raw === null) return { status: "miss", reason: "absent" };
168
+ // Checked before parsing: a tampered entry must not cost a huge parse.
169
+ if (raw.length > SQL_QUERY_SNAPSHOT_MAX_ENTRY_LENGTH) {
170
+ return drop("oversized");
171
+ }
172
+ const parsed = parseEnvelope(raw);
173
+ if (parsed === null) return drop("corrupt");
174
+ if (parsed === "truncated") return drop(parsed);
175
+ if (parsed === "unsupported-version") return drop(parsed);
176
+ if (parsed.accountScope !== identity.accountScope) {
177
+ return { status: "miss", reason: "scope-mismatch" };
178
+ }
179
+ if (
180
+ parsed.storedAt !== newest.storedAt ||
181
+ parsed.keyLength !== options.queryKey.length
182
+ ) {
183
+ return drop("corrupt");
184
+ }
185
+ const fetchedAtMs =
186
+ parsed.result.fetchedAt === undefined
187
+ ? undefined
188
+ : Date.parse(parsed.result.fetchedAt);
189
+ if (fetchedAtMs !== undefined) {
190
+ if (now - fetchedAtMs < -FUTURE_SKEW_MS) return drop("future-dated");
191
+ if (now - fetchedAtMs > SQL_QUERY_SNAPSHOT_MAX_AGE_MS) {
192
+ return drop("expired");
193
+ }
194
+ }
195
+ return {
196
+ status: "hit",
197
+ snapshot: { result: parsed.result, storedAt: parsed.storedAt },
198
+ };
199
+ } catch (error) {
200
+ return storageFailure(error);
201
+ }
202
+ }
203
+
204
+ export function writeSqlQuerySnapshot(
205
+ options: StorageOptions & {
206
+ accountScope: string;
207
+ queryKey: string;
208
+ result: SqlQuerySnapshotInput;
209
+ },
210
+ ): SqlQuerySnapshotWrite {
211
+ const identity = resolveIdentity(options.accountScope, options.queryKey);
212
+ if ("status" in identity) return identity;
213
+ const storage = resolveStorage(options.storage);
214
+ if ("status" in storage) return storage;
215
+ const now = options.now ?? Date.now();
216
+
217
+ try {
218
+ const { entries, malformed } = listEntries(storage);
219
+ for (const name of malformed) {
220
+ storage.removeItem(name);
221
+ }
222
+ const survivors: EntryName[] = [];
223
+ for (const entry of entries) {
224
+ const sameIdentity =
225
+ entry.scopeId === identity.scopeId && entry.keyId === identity.keyId;
226
+ const age = now - entry.storedAt;
227
+ if (
228
+ sameIdentity ||
229
+ age > SQL_QUERY_SNAPSHOT_MAX_AGE_MS ||
230
+ age < -FUTURE_SKEW_MS
231
+ ) {
232
+ storage.removeItem(entry.name);
233
+ } else {
234
+ survivors.push(entry);
235
+ }
236
+ }
237
+
238
+ // Any write attempt supersedes the previous copy, including a skipped
239
+ // one: after a reload it would show rows older than the latest fetch.
240
+ const prepared = prepareResult(options.result, now);
241
+ if ("skipped" in prepared) {
242
+ return { status: "skipped", reason: prepared.skipped };
243
+ }
244
+ const value = JSON.stringify({
245
+ v: ENVELOPE_VERSION,
246
+ scope: identity.accountScope,
247
+ keyLength: options.queryKey.length,
248
+ storedAt: now,
249
+ result: prepared.result,
250
+ });
251
+ if (value.length > SQL_QUERY_SNAPSHOT_MAX_ENTRY_LENGTH) {
252
+ return { status: "skipped", reason: "too-large" };
253
+ }
254
+
255
+ const name = entryName(now, identity);
256
+ const sized = survivors
257
+ .map((entry) => ({
258
+ entry,
259
+ length: entry.name.length + (storage.getItem(entry.name)?.length ?? 0),
260
+ }))
261
+ .sort(
262
+ (a, b) =>
263
+ a.entry.storedAt - b.entry.storedAt ||
264
+ a.entry.name.localeCompare(b.entry.name),
265
+ );
266
+ let total = sized.reduce((sum, item) => sum + item.length, 0);
267
+ const length = name.length + value.length;
268
+ let evicted = 0;
269
+ const evictOldest = (): boolean => {
270
+ const oldest = sized.shift();
271
+ if (!oldest) return false;
272
+ storage.removeItem(oldest.entry.name);
273
+ total -= oldest.length;
274
+ evicted += 1;
275
+ return true;
276
+ };
277
+ while (
278
+ sized.length >= SQL_QUERY_SNAPSHOT_MAX_ENTRIES ||
279
+ total + length > SQL_QUERY_SNAPSHOT_MAX_TOTAL_LENGTH
280
+ ) {
281
+ if (!evictOldest()) break;
282
+ }
283
+
284
+ for (;;) {
285
+ try {
286
+ storage.setItem(name, value);
287
+ return { status: "written", storedAt: now, length, evicted };
288
+ } catch (error) {
289
+ if (!isQuotaError(error)) throw error;
290
+ if (!evictOldest()) {
291
+ return {
292
+ status: "unavailable",
293
+ reason: "quota-exceeded",
294
+ errorName: errorName(error),
295
+ };
296
+ }
297
+ }
298
+ }
299
+ } catch (error) {
300
+ return storageFailure(error);
301
+ }
302
+ }
303
+
304
+ export function removeSqlQuerySnapshot(
305
+ options: Pick<StorageOptions, "storage"> & {
306
+ accountScope: string;
307
+ queryKey: string;
308
+ },
309
+ ): SqlQuerySnapshotClear {
310
+ const identity = resolveIdentity(options.accountScope, options.queryKey);
311
+ if ("status" in identity) return identity;
312
+ const storage = resolveStorage(options.storage);
313
+ if ("status" in storage) return storage;
314
+ try {
315
+ const matches = listEntries(storage).entries.filter(
316
+ (entry) =>
317
+ entry.scopeId === identity.scopeId && entry.keyId === identity.keyId,
318
+ );
319
+ for (const entry of matches) storage.removeItem(entry.name);
320
+ return { status: "cleared", removed: matches.length };
321
+ } catch (error) {
322
+ return storageFailure(error);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Drops snapshots for logout or an account/org switch. With
328
+ * `exceptAccountScope` it keeps only that scope's entries; without a valid
329
+ * scope to keep it clears everything.
330
+ */
331
+ export function clearSqlQuerySnapshots(
332
+ options: Pick<StorageOptions, "storage"> & {
333
+ exceptAccountScope?: string;
334
+ } = {},
335
+ ): SqlQuerySnapshotClear {
336
+ const storage = resolveStorage(options.storage);
337
+ if ("status" in storage) return storage;
338
+ const keepScopeId =
339
+ options.exceptAccountScope !== undefined &&
340
+ isValidAccountScope(options.exceptAccountScope)
341
+ ? fingerprint(options.exceptAccountScope)
342
+ : null;
343
+
344
+ try {
345
+ const { entries, malformed } = listEntries(storage);
346
+ let removed = 0;
347
+ for (const name of malformed) {
348
+ storage.removeItem(name);
349
+ removed += 1;
350
+ }
351
+ for (const entry of entries) {
352
+ if (entry.scopeId === keepScopeId) continue;
353
+ storage.removeItem(entry.name);
354
+ removed += 1;
355
+ }
356
+ return { status: "cleared", removed };
357
+ } catch (error) {
358
+ return storageFailure(error);
359
+ }
360
+ }
361
+
362
+ function resolveIdentity(
363
+ accountScope: string,
364
+ queryKey: string,
365
+ ): Identity | Unavailable {
366
+ if (!isValidAccountScope(accountScope)) {
367
+ return { status: "unavailable", reason: "invalid-account-scope" };
368
+ }
369
+ if (
370
+ typeof queryKey !== "string" ||
371
+ queryKey.length === 0 ||
372
+ queryKey.length > MAX_QUERY_KEY_LENGTH
373
+ ) {
374
+ return { status: "unavailable", reason: "invalid-query-key" };
375
+ }
376
+ return {
377
+ accountScope,
378
+ scopeId: fingerprint(accountScope),
379
+ keyId: fingerprint(queryKey),
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Accepts only the `user:org` shape `dashboardCacheScope` produces for an
385
+ * authenticated session. The loading placeholder and the anonymous principal
386
+ * are rejected so they can never name a snapshot.
387
+ */
388
+ function isValidAccountScope(accountScope: unknown): accountScope is string {
389
+ if (typeof accountScope !== "string") return false;
390
+ if (RESERVED_SCOPES.has(accountScope)) return false;
391
+ if (!ACCOUNT_SCOPE_PATTERN.test(accountScope)) return false;
392
+ return !accountScope.startsWith("anonymous:");
393
+ }
394
+
395
+ function resolveStorage(
396
+ storage: SqlQuerySnapshotStorage | undefined,
397
+ ): SqlQuerySnapshotStorage | Unavailable {
398
+ if (storage) return storage;
399
+ try {
400
+ if (typeof window === "undefined") {
401
+ return { status: "unavailable", reason: "no-storage" };
402
+ }
403
+ // Reading `sessionStorage` itself throws when storage is blocked.
404
+ const sessionStorage = window.sessionStorage;
405
+ return sessionStorage
406
+ ? sessionStorage
407
+ : { status: "unavailable", reason: "no-storage" };
408
+ } catch (error) {
409
+ return storageFailure(error);
410
+ }
411
+ }
412
+
413
+ function storageFailure(error: unknown): Unavailable {
414
+ return {
415
+ status: "unavailable",
416
+ reason: "storage-error",
417
+ errorName: errorName(error),
418
+ };
419
+ }
420
+
421
+ function errorName(error: unknown): string | undefined {
422
+ return error instanceof Error ? error.name : undefined;
423
+ }
424
+
425
+ function isQuotaError(error: unknown): boolean {
426
+ if (!error || typeof error !== "object") return false;
427
+ const { name, code } = error as { name?: unknown; code?: unknown };
428
+ return (
429
+ name === "QuotaExceededError" ||
430
+ name === "NS_ERROR_DOM_QUOTA_REACHED" ||
431
+ code === 22 ||
432
+ code === 1014
433
+ );
434
+ }
435
+
436
+ function listEntries(storage: SqlQuerySnapshotStorage): {
437
+ entries: EntryName[];
438
+ malformed: string[];
439
+ } {
440
+ const names: string[] = [];
441
+ for (let index = 0; index < storage.length; index += 1) {
442
+ const name = storage.key(index);
443
+ if (name !== null && name.startsWith(KEY_PREFIX)) names.push(name);
444
+ }
445
+ const entries: EntryName[] = [];
446
+ const malformed: string[] = [];
447
+ for (const name of names) {
448
+ const match = KEY_PATTERN.exec(name);
449
+ const storedAt = match ? Number.parseInt(match[1], 36) : Number.NaN;
450
+ if (!match || !Number.isSafeInteger(storedAt)) {
451
+ malformed.push(name);
452
+ continue;
453
+ }
454
+ entries.push({ name, storedAt, scopeId: match[2], keyId: match[3] });
455
+ }
456
+ return { entries, malformed };
457
+ }
458
+
459
+ function entryName(storedAt: number, identity: Identity): string {
460
+ return `${KEY_PREFIX}v1/${storedAt.toString(36)}/${identity.scopeId}/${identity.keyId}`;
461
+ }
462
+
463
+ /** Two independent 53-bit hashes; collisions are not an attack surface here. */
464
+ function fingerprint(input: string): string {
465
+ return `${cyrb53(input, 0)}${cyrb53(input, 0x9e3779b9)}`;
466
+ }
467
+
468
+ function cyrb53(input: string, seed: number): string {
469
+ let h1 = 0xdeadbeef ^ seed;
470
+ let h2 = 0x41c6ce57 ^ seed;
471
+ for (let index = 0; index < input.length; index += 1) {
472
+ const code = input.charCodeAt(index);
473
+ h1 = Math.imul(h1 ^ code, 2654435761);
474
+ h2 = Math.imul(h2 ^ code, 1597334677);
475
+ }
476
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
477
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
478
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
479
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
480
+ const value = 4294967296 * (2097151 & h2) + (h1 >>> 0);
481
+ return value.toString(16).padStart(14, "0");
482
+ }
483
+
484
+ function prepareResult(
485
+ input: SqlQuerySnapshotInput,
486
+ now: number,
487
+ ):
488
+ | { result: SqlQuerySnapshotResult }
489
+ | { skipped: SqlQuerySnapshotSkipReason } {
490
+ if (!isPlainObject(input)) return { skipped: "invalid-result" };
491
+ // Each field is read once so the checked value is the stored value.
492
+ const {
493
+ error,
494
+ rows,
495
+ schema,
496
+ cached,
497
+ providerCacheHit,
498
+ truncated,
499
+ fetchedAt,
500
+ } = input;
501
+ if (error !== undefined && error !== "") return { skipped: "error-result" };
502
+ if (truncated !== undefined && typeof truncated !== "boolean") {
503
+ return { skipped: "invalid-result" };
504
+ }
505
+ if (truncated === true) return { skipped: "truncated" };
506
+ const budget = { nodes: 0 };
507
+ const rowsVerdict = checkJson(rows, budget, 0);
508
+ if (rowsVerdict === "too-large") return { skipped: "too-large" };
509
+ if (rowsVerdict === "invalid" || !isRowArray(rows)) {
510
+ return { skipped: "invalid-result" };
511
+ }
512
+ if (schema !== undefined) {
513
+ const schemaVerdict = checkJson(schema, budget, 0);
514
+ if (schemaVerdict === "too-large") return { skipped: "too-large" };
515
+ if (schemaVerdict === "invalid" || !isSchema(schema)) {
516
+ return { skipped: "invalid-result" };
517
+ }
518
+ }
519
+ if (
520
+ (cached !== undefined && typeof cached !== "boolean") ||
521
+ (providerCacheHit !== undefined && typeof providerCacheHit !== "boolean")
522
+ ) {
523
+ return { skipped: "invalid-result" };
524
+ }
525
+ if (fetchedAt !== undefined) {
526
+ const fetchedAtMs =
527
+ typeof fetchedAt === "string" ? Date.parse(fetchedAt) : NaN;
528
+ if (!Number.isFinite(fetchedAtMs) || fetchedAtMs - now > FUTURE_SKEW_MS) {
529
+ return { skipped: "invalid-fetched-at" };
530
+ }
531
+ if (now - fetchedAtMs > SQL_QUERY_SNAPSHOT_MAX_AGE_MS) {
532
+ return { skipped: "stale" };
533
+ }
534
+ }
535
+ const result: SqlQuerySnapshotResult = { rows };
536
+ if (schema !== undefined) result.schema = schema;
537
+ if (cached !== undefined) result.cached = cached;
538
+ if (providerCacheHit !== undefined)
539
+ result.providerCacheHit = providerCacheHit;
540
+ if (truncated !== undefined) result.truncated = truncated;
541
+ if (fetchedAt !== undefined) result.fetchedAt = fetchedAt;
542
+ return { result };
543
+ }
544
+
545
+ /**
546
+ * Walks a value that must survive `JSON.stringify` unchanged. `undefined`,
547
+ * non-finite numbers, bigint, dates, class instances, sparse holes and
548
+ * accessors would be rewritten, dropped or re-evaluated by serialization,
549
+ * which would restore different rows than were shown.
550
+ *
551
+ * The walk is in place and charges the node budget before each member is
552
+ * touched, so a wide or hostile value fails after at most `MAX_RESULT_NODES`
553
+ * reads and never has a getter invoked.
554
+ */
555
+ function checkJson(
556
+ value: unknown,
557
+ budget: { nodes: number },
558
+ depth: number,
559
+ ): "ok" | "invalid" | "too-large" {
560
+ budget.nodes += 1;
561
+ if (budget.nodes > MAX_RESULT_NODES) return "too-large";
562
+ if (depth > MAX_RESULT_DEPTH) return "invalid";
563
+ if (value === null) return "ok";
564
+ switch (typeof value) {
565
+ case "string":
566
+ case "boolean":
567
+ return "ok";
568
+ case "number":
569
+ return Number.isFinite(value) ? "ok" : "invalid";
570
+ case "object":
571
+ break;
572
+ default:
573
+ return "invalid";
574
+ }
575
+ if (Array.isArray(value)) {
576
+ if (value.length > MAX_RESULT_NODES - budget.nodes) return "too-large";
577
+ for (let index = 0; index < value.length; index += 1) {
578
+ const verdict = checkMember(value, String(index), true, budget, depth);
579
+ if (verdict !== "ok") return verdict;
580
+ }
581
+ return "ok";
582
+ }
583
+ if (!isPlainObject(value)) return "invalid";
584
+ for (const key in value) {
585
+ if (budget.nodes >= MAX_RESULT_NODES) return "too-large";
586
+ const verdict = checkMember(value, key, false, budget, depth);
587
+ if (verdict !== "ok") return verdict;
588
+ }
589
+ return "ok";
590
+ }
591
+
592
+ function checkMember(
593
+ owner: object,
594
+ key: string,
595
+ required: boolean,
596
+ budget: { nodes: number },
597
+ depth: number,
598
+ ): "ok" | "invalid" | "too-large" {
599
+ const descriptor = Object.getOwnPropertyDescriptor(owner, key);
600
+ // An inherited enumerable key is not serialized; a hole or accessor is
601
+ // invalid because serialization would not reproduce it.
602
+ if (!descriptor) return required ? "invalid" : "ok";
603
+ if (!("value" in descriptor)) return "invalid";
604
+ return checkJson(descriptor.value, budget, depth + 1);
605
+ }
606
+
607
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
608
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
609
+ return false;
610
+ }
611
+ const prototype = Object.getPrototypeOf(value);
612
+ return prototype === Object.prototype || prototype === null;
613
+ }
614
+
615
+ function isRowArray(value: unknown): value is Record<string, unknown>[] {
616
+ return Array.isArray(value) && value.every(isPlainObject);
617
+ }
618
+
619
+ function isSchema(value: unknown): value is { name: string; type: string }[] {
620
+ return (
621
+ Array.isArray(value) &&
622
+ value.every(
623
+ (column) =>
624
+ isPlainObject(column) &&
625
+ typeof column.name === "string" &&
626
+ typeof column.type === "string",
627
+ )
628
+ );
629
+ }
630
+
631
+ function parseEnvelope(raw: string):
632
+ | {
633
+ accountScope: string;
634
+ keyLength: number;
635
+ storedAt: number;
636
+ result: SqlQuerySnapshotResult;
637
+ }
638
+ | "unsupported-version"
639
+ | "truncated"
640
+ | null {
641
+ let parsed: unknown;
642
+ try {
643
+ parsed = JSON.parse(raw);
644
+ } catch {
645
+ // coercion-ok: null is the typed "unreadable" value; the caller drops the entry as corrupt, never serves it
646
+ return null;
647
+ }
648
+ if (!isPlainObject(parsed)) return null;
649
+ const { v, scope, keyLength, storedAt, result } = parsed;
650
+ if (typeof v !== "number") return null;
651
+ // Other versions may change every other field, so read nothing else.
652
+ if (v !== ENVELOPE_VERSION) return "unsupported-version";
653
+ if (
654
+ typeof scope !== "string" ||
655
+ typeof keyLength !== "number" ||
656
+ typeof storedAt !== "number" ||
657
+ !Number.isSafeInteger(storedAt) ||
658
+ !isPlainObject(result) ||
659
+ !isRowArray(result.rows)
660
+ ) {
661
+ return null;
662
+ }
663
+ const clean: SqlQuerySnapshotResult = { rows: result.rows };
664
+ if (result.schema !== undefined) {
665
+ if (!isSchema(result.schema)) return null;
666
+ clean.schema = result.schema;
667
+ }
668
+ // Truncated rows are never written; one found here was altered or came
669
+ // from code that did not know about the flag, and is not a complete result.
670
+ if (result.truncated !== undefined) {
671
+ if (typeof result.truncated !== "boolean") return null;
672
+ if (result.truncated) return "truncated";
673
+ clean.truncated = false;
674
+ }
675
+ for (const flag of ["cached", "providerCacheHit"] as const) {
676
+ if (result[flag] !== undefined) {
677
+ if (typeof result[flag] !== "boolean") return null;
678
+ clean[flag] = result[flag];
679
+ }
680
+ }
681
+ if (result.fetchedAt !== undefined) {
682
+ if (
683
+ typeof result.fetchedAt !== "string" ||
684
+ !Number.isFinite(Date.parse(result.fetchedAt))
685
+ ) {
686
+ return null;
687
+ }
688
+ clean.fetchedAt = result.fetchedAt;
689
+ }
690
+ if (result.error !== undefined) return null;
691
+ return {
692
+ accountScope: scope,
693
+ keyLength,
694
+ storedAt,
695
+ result: clean,
696
+ };
697
+ }