@dev.fast/whiteboard 0.5.1 → 0.5.2-preview.20261104.147

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 (120) hide show
  1. package/dist/{account-alias-Bw3PT3Qo.js → account-alias-DXpd0Px8.js} +5 -5
  2. package/dist/{agent-cli-kHRrwO4n.js → agent-cli-COrZtFks.js} +9 -9
  3. package/dist/agent-cli-x6-Eh1S-.js +2 -0
  4. package/dist/{agent-client-CMacGbMp.js → agent-client-BFtYm3Ng.js} +4 -4
  5. package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-D1avYp8-.js} +12 -8
  6. package/dist/{background-server-eZWXoVcI.js → background-server-BiRSqjxG.js} +4 -4
  7. package/dist/{background-server-BLNHbksB.js → background-server-HOJozY99.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-Bxmiqutp.js → cli-CSlwGRvH.js} +1 -1
  10. package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-CuffXA0Q.js} +22 -22
  11. package/dist/cli.js +2 -2
  12. package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-DEo941za.js} +3 -3
  13. package/dist/{headless-host-B4r0s1ks.js → headless-host-BCkXVVpU.js} +1 -1
  14. package/dist/{headless-host-Dl2KZ9IT.js → headless-host-CIuJJjmM.js} +561 -415
  15. package/dist/{import-YAIU6rB0.js → import-CjcW7EIQ.js} +235 -63
  16. package/dist/{local-data-C58vzLZR.js → local-data-B5PBZ0Ge.js} +101 -17
  17. package/dist/{mcp-COYWiGVn.js → mcp-Ba3WvhUh.js} +9 -6
  18. package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
  19. package/dist/profile-BstQkIY0.js +2 -0
  20. package/dist/{profile--BYIjPkc.js → profile-CNMhLUhf.js} +2 -2
  21. package/dist/{remote-attach-C1prLjmj.js → remote-attach-DkZoYBHV.js} +4 -4
  22. package/dist/{remote-extensions-Bw029iIS.js → remote-extensions-BvV29_gk.js} +1 -1
  23. package/dist/{remote-extensions-MUUgbLJn.js → remote-extensions-L95HNwIH.js} +2 -2
  24. package/dist/{remote-language-server-z3rdHcLL.js → remote-language-server-l4ConTfj.js} +4 -4
  25. package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-BAQM7cU8.js} +5 -5
  26. package/dist/{request-origin-B3_XI2VA.js → request-origin-DZexKlc4.js} +28 -5
  27. package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-B5F10y_4.js} +2 -2
  28. package/dist/{review-home-paths-LdGS4SpR.js → review-home-paths-BmZX2qiz.js} +1 -1
  29. package/dist/{review-telemetry-x8u-9EZb.js → review-telemetry-kQ7esDRE.js} +4 -4
  30. package/dist/server/desktop-host.js +26 -38
  31. package/dist/{server-discovery-WmuFl2S3.js → server-discovery-DYY_xeG9.js} +2 -2
  32. package/dist/sharing/index.d.ts +137 -14
  33. package/dist/sharing/index.js +1 -1
  34. package/dist/{src-DLWol7GZ.js → src-B-91Rd2d.js} +17 -1
  35. package/dist/{src-B2b8cCoI.js → src-DirPN_Qh.js} +82 -6
  36. package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-vg0NAzXG.js} +4 -89
  37. package/dist/{thread-state-B7L1mpN_.js → thread-state-Jxoh1hde.js} +1 -1
  38. package/dist/{tool-failure-RLR8m9hq.js → tool-failure-BLBwcl3H.js} +319 -55
  39. package/instructions/animation.md +69 -5
  40. package/instructions/animations/before-after-structure/README.md +27 -0
  41. package/instructions/animations/before-after-structure/animation.html +89 -0
  42. package/instructions/animations/before-after-structure/block.json +10 -0
  43. package/instructions/animations/before-after-structure/frames.json +313 -0
  44. package/instructions/animations/cache-fill-evict/README.md +24 -0
  45. package/instructions/animations/cache-fill-evict/animation.html +81 -0
  46. package/instructions/animations/cache-fill-evict/block.json +13 -0
  47. package/instructions/animations/cache-fill-evict/frames.json +79 -0
  48. package/instructions/animations/cache-fill-evict/scenarios.json +17 -0
  49. package/instructions/animations/log-replay/README.md +25 -0
  50. package/instructions/animations/log-replay/animation.html +86 -0
  51. package/instructions/animations/log-replay/block.json +10 -0
  52. package/instructions/animations/log-replay/frames.json +143 -0
  53. package/instructions/animations/log-replay/scenarios.json +18 -0
  54. package/instructions/animations/queue-workers/README.md +23 -0
  55. package/instructions/animations/queue-workers/animation.html +107 -0
  56. package/instructions/animations/queue-workers/block.json +13 -0
  57. package/instructions/animations/queue-workers/frames.json +119 -0
  58. package/instructions/animations/queue-workers/scenarios.json +18 -0
  59. package/instructions/animations/race-two-lanes/README.md +22 -0
  60. package/instructions/animations/race-two-lanes/animation.html +97 -0
  61. package/instructions/animations/race-two-lanes/block.json +1 -0
  62. package/instructions/animations/race-two-lanes/frames.json +234 -0
  63. package/instructions/animations/race-two-lanes/scenarios.json +19 -0
  64. package/instructions/animations/request-across-layers/README.md +24 -0
  65. package/instructions/animations/request-across-layers/animation.html +85 -0
  66. package/instructions/animations/request-across-layers/block.json +17 -0
  67. package/instructions/animations/request-across-layers/frames.json +58 -0
  68. package/instructions/animations/request-across-layers/scenarios.json +12 -0
  69. package/instructions/animations/request-timing/README.md +22 -0
  70. package/instructions/animations/request-timing/animation.html +120 -0
  71. package/instructions/animations/request-timing/block.json +21 -0
  72. package/instructions/animations/request-timing/markers.json +32 -0
  73. package/instructions/animations/request-timing/scenarios.json +4 -0
  74. package/instructions/animations/retry-backoff/README.md +22 -0
  75. package/instructions/animations/retry-backoff/animation.html +122 -0
  76. package/instructions/animations/retry-backoff/block.json +10 -0
  77. package/instructions/animations/retry-backoff/frames.json +65 -0
  78. package/instructions/animations/retry-backoff/scenarios.json +12 -0
  79. package/instructions/animations/state-machine/README.md +24 -0
  80. package/instructions/animations/state-machine/animation.html +95 -0
  81. package/instructions/animations/state-machine/block.json +9 -0
  82. package/instructions/animations/state-machine/frames.json +79 -0
  83. package/instructions/animations/state-machine/scenarios.json +12 -0
  84. package/instructions/animations/video-platform-architecture/README.md +24 -0
  85. package/instructions/animations/video-platform-architecture/animation.html +76 -0
  86. package/instructions/animations/video-platform-architecture/block.json +17 -0
  87. package/instructions/animations/video-platform-architecture/frames.json +97 -0
  88. package/instructions/animations/video-platform-architecture/scenarios.json +28 -0
  89. package/instructions/whiteboard.d.ts +168 -14
  90. package/package.json +5 -5
  91. package/src/animation-stage-kit.ts +150 -10
  92. package/src/cli-runner.ts +4 -1
  93. package/src/fixtures/blocks/clip.json +19 -0
  94. package/src/review-api/README.md +1 -0
  95. package/src/review-api/agent-cli.ts +3 -2
  96. package/src/review-api/animation-drafts.ts +151 -47
  97. package/src/review-api/animation-references.ts +92 -0
  98. package/src/review-api/animation-render.ts +106 -0
  99. package/src/review-api/authoring-tools.ts +10 -3
  100. package/src/review-api/comparison-coverage.ts +6 -7
  101. package/src/review-api/document-text.ts +27 -12
  102. package/src/review-api/http.ts +52 -6
  103. package/src/review-api/instructions.ts +1 -1
  104. package/src/review-api/mcp.ts +10 -5
  105. package/src/review-api/public-tools.ts +71 -2
  106. package/src/review-api/review-progress.ts +2 -3
  107. package/src/review-api/store.ts +89 -12
  108. package/src/server/desktop-server.ts +17 -5
  109. package/src/server/headless-host.ts +4 -0
  110. package/src/server/review-gateway.ts +1 -0
  111. package/src/server/review-server-core.ts +71 -12
  112. package/src/sharing/client.ts +129 -32
  113. package/src/sharing/export.ts +42 -5
  114. package/src/sharing/file.ts +8 -0
  115. package/src/sharing/host.ts +202 -73
  116. package/src/sharing/import.ts +125 -33
  117. package/src/ui-telemetry-events.ts +26 -1
  118. package/dist/agent-cli-B8KBHuh_.js +0 -2
  119. package/dist/profile-BjcXi-oH.js +0 -2
  120. package/src/review-api/lens-alignment.ts +0 -52
@@ -1,8 +1,8 @@
1
1
  import { f as gitAt } from "./src-Y30gweiK.js";
2
2
  import { u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { $ as comparisonKey, A as parseMarkdown, B as lensSchema, E as sourceReferences, L as UNCATEGORIZED_LENS_ID, M as checkReferences, O as markdownNodes, Q as anchorSelection, T as selectionReferences, V as lensSelections, Z as SelectionUnavailableError, _ as readsAtBoardPins, at as unionIntervals, c as documentSchema, h as pinsSchema, it as sourceAnchors, n as anchorPins, nt as resolveDiffSelection, o as boardRepositoryId, ot as ReviewInputError, q as fileLineRangeSchema, rt as selectionKey, u as elements, v as resourceReference, y as resourceReferences } from "./src-B2b8cCoI.js";
4
- import { s as safeStorageSegment } from "./review-home-paths-LdGS4SpR.js";
5
- import { D as scopedCoverage, O as sourcesForFile, T as emptyCoverage, b as textIncludesQuote, c as traceSchema, k as subtractIntervals, n as migrateStoredDocument, r as migrateStoredLenses, s as softwareModelDataSchema, t as liftFileLenses, u as pullRequestUrl, w as coverageSources, x as textualRows } from "./stored-document-migration-iwIQ4l5G.js";
3
+ import { $ as comparisonKey, A as parseMarkdown, B as lensSchema, E as sourceReferences, L as UNCATEGORIZED_LENS_ID, M as checkReferences, O as markdownNodes, Q as anchorSelection, T as selectionReferences, V as lensSelections, Z as SelectionUnavailableError, _ as readsAtBoardPins, at as sourceAnchors, c as documentSchema, h as pinsSchema, it as selectionKey, n as anchorPins, nt as patchRows, o as boardRepositoryId, ot as unionIntervals, q as fileLineRangeSchema, rt as resolveDiffSelection, st as ReviewInputError, u as elements, v as resourceReference, y as resourceReferences } from "./src-DirPN_Qh.js";
4
+ import { s as safeStorageSegment } from "./review-home-paths-BmZX2qiz.js";
5
+ import { C as emptyCoverage, D as subtractIntervals, E as sourcesForFile, S as coverageSources, T as scopedCoverage, b as textIncludesQuote, c as traceSchema, n as migrateStoredDocument, r as migrateStoredLenses, s as softwareModelDataSchema, t as liftFileLenses, u as pullRequestUrl } from "./stored-document-migration-vg0NAzXG.js";
6
6
  import { N as devReviewHome } from "./s3-config-CjcLyFV5.js";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
@@ -36,51 +36,93 @@ const gitHubRepositoryUrlSchema = z.string().refine((value) => {
36
36
  const commitSchema = z.string().regex(/^[a-f0-9]{40}([a-f0-9]{24})?$/);
37
37
  const sideSchema = z.enum(["base", "head"]);
38
38
  /** An excerpt is the lines a board references. */
39
- const shareSourceSchema = z.discriminatedUnion("kind", [z.strictObject({
39
+ const shareSourceSchema = z.discriminatedUnion("kind", [z.object({
40
40
  kind: z.literal("excerpt"),
41
41
  file: sharePathSchema,
42
42
  commit: commitSchema,
43
43
  fromLine: z.number().int().positive(),
44
44
  lines: z.array(z.string()).min(1)
45
45
  })]);
46
- const lineRangeSchema = z.strictObject({
46
+ const lineRangeSchema = z.object({
47
47
  side: sideSchema,
48
48
  file: sharePathSchema,
49
49
  fromLine: z.number().int().positive(),
50
50
  toLine: z.number().int().positive(),
51
- pins: z.strictObject({
51
+ pins: z.object({
52
52
  repositoryId: z.string(),
53
53
  head: z.string(),
54
54
  base: z.string().optional()
55
55
  }).optional()
56
56
  });
57
- const shareBundleSchema = z.strictObject({
57
+ /** 1-based base and head lines; null when a side lacks the line. */
58
+ const alignmentRowSchema = z.tuple([z.number().int().positive().nullable(), z.number().int().positive().nullable()]);
59
+ const shareAlignmentSchema = z.object({
60
+ file: sharePathSchema,
61
+ previousFile: sharePathSchema.optional(),
62
+ head: z.string(),
63
+ base: z.string().optional(),
64
+ rows: z.array(alignmentRowSchema).min(1)
65
+ });
66
+ /** Unknown fields drop, so a reader opens bundles from newer sharers. */
67
+ const shareBundleSchema = z.object({
58
68
  format: z.literal(SHARE_FORMAT),
59
69
  reviewId: z.string().min(1).max(256),
60
70
  version: z.number().int().nonnegative(),
61
71
  title: z.string().max(4096),
62
72
  /** Validated against the document schema by the reader. */
63
73
  snapshot: z.record(z.string(), z.unknown()),
64
- images: z.record(z.string(), z.strictObject({ png: z.base64() })),
74
+ images: z.record(z.string(), z.object({ png: z.base64() })),
65
75
  traces: z.record(z.string(), z.unknown()),
66
76
  maps: z.record(z.string(), z.unknown()),
67
77
  commits: z.array(z.unknown()),
68
78
  /** Each reference's lines, resolved where the board was shared. */
69
79
  selections: z.record(z.string(), z.array(lineRangeSchema)),
70
80
  sources: z.array(shareSourceSchema),
71
- repository: z.strictObject({ cloneUrl: gitHubRepositoryUrlSchema }).optional()
81
+ /** The sharer's diff over the selections, since 0.4. */
82
+ alignments: z.array(shareAlignmentSchema).optional(),
83
+ repository: z.object({ cloneUrl: gitHubRepositoryUrlSchema }).optional(),
84
+ /** Quotes uncommitted changes: view only, never imported. Since 0.6. */
85
+ frozen: z.literal(true).optional()
72
86
  });
73
- z.strictObject({
87
+ z.object({
74
88
  requestId: z.uuid(),
75
89
  bundle: shareBundleSchema
76
90
  });
91
+ const shareCreatedSchema = z.strictObject({
92
+ shareId: shareIdSchema,
93
+ url: z.url()
94
+ });
95
+ const shareSizeSchema = z.number().int().positive().max(MAX_SHARE_BYTES);
96
+ const shareRevisionRefSchema = z.object({
97
+ id: shareIdSchema,
98
+ version: z.number().int().nonnegative(),
99
+ createdAt: z.number()
100
+ });
101
+ const shareRevisionSchema = shareRevisionRefSchema.extend({
102
+ size: shareSizeSchema,
103
+ url: z.url()
104
+ });
105
+ const shareRevisionsSchema = z.object({
106
+ latest: shareIdSchema,
107
+ items: z.array(shareRevisionSchema)
108
+ });
109
+ const sharedMetaSchema = z.object({
110
+ sender: z.strictObject({ login: z.string().min(1).max(256) }),
111
+ sharedAt: z.number(),
112
+ size: shareSizeSchema,
113
+ revision: shareRevisionRefSchema,
114
+ latest: shareIdSchema
115
+ });
77
116
  function parseShareLink(value) {
78
117
  const url = new URL(value);
79
- if (url.protocol !== "https:" || url.username || url.password || url.hash || [...url.searchParams.keys()].some((key) => key !== "app")) throw new Error("Use an HTTPS Whiteboard share link.");
80
- return {
118
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || [...url.searchParams.keys()].some((key) => key !== "app" && key !== "rev")) throw new Error("Use an HTTPS Whiteboard share link.");
119
+ const link = {
81
120
  origin: url.origin,
82
121
  capability: capabilitySchema.parse(/^\/s\/([^/]+)$/.exec(url.pathname)?.[1])
83
122
  };
123
+ const revision = url.searchParams.get("rev");
124
+ if (revision !== null) link.revision = shareIdSchema.parse(revision);
125
+ return link;
84
126
  }
85
127
  //#endregion
86
128
  //#region src/server/bounded-stream.ts
@@ -257,7 +299,7 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
257
299
  })));
258
300
  if (base === null && head === null) throw new SelectionUnavailableError("missing_file", "File is unavailable at these pins.");
259
301
  if (base !== null && head !== null && base !== head) throw new SelectionUnavailableError("no_alignment", "Changed context file has no alignment.");
260
- rows = textualRows(source.file, "", base?.split("\n").length ?? 0, head?.split("\n").length ?? 0);
302
+ rows = patchRows("", base?.split("\n").length ?? 0, head?.split("\n").length ?? 0);
261
303
  }
262
304
  if (!rows) throw new SelectionUnavailableError("no_alignment", "File has no text alignment.");
263
305
  const result = resolveDiffSelection(source, rows, file ?? { path: source.file });
@@ -425,7 +467,11 @@ async function exportShare(input) {
425
467
  const stored = structuredClone(input.store.read(input.reviewId, input.version));
426
468
  const repositoryId = boardRepositoryId(stored);
427
469
  const { target, staleSources: _staleSources, sourceUnavailable: _sourceUnavailable, lastEdit: _lastEdit, trashedAt: _trashedAt, personEdit: _personEdit, ...snapshot } = stored;
428
- if (target?.kind === "worktree") throw new ReviewInputError("Pin this review to commits before sharing it.");
470
+ const live = target?.kind === "worktree" ? await input.data.sourcePins(stored) : void 0;
471
+ if (live) {
472
+ const { worktreeRevision: _worktreeRevision, ...committed } = live;
473
+ snapshot.pins = committed;
474
+ }
429
475
  const pullRequestUrl = snapshot.origin?.pullRequestUrl;
430
476
  const baseBranch = pullRequestUrl && !snapshot.origin?.baseBranch && (snapshot.origin?.pullRequestBaseBranch ?? input.data.pullRequestBaseBranch(pullRequestUrl));
431
477
  if (baseBranch) snapshot.origin = {
@@ -439,7 +485,7 @@ async function exportShare(input) {
439
485
  if (block.type !== "markdown") continue;
440
486
  for (const node of markdownNodes(parseMarkdown(block.markdown))) if (node.type === "image") throw new ReviewInputError("Convert Markdown images to managed image blocks before sharing.");
441
487
  }
442
- const pins = snapshot.pins;
488
+ const pins = live ?? snapshot.pins;
443
489
  const references = [...sourceReferences(snapshot.document, { repositoryId }), ...lensSelections(snapshot.lenses ?? []).flatMap((selection) => sourceAnchors(selection.source).map((source) => ({ source })))];
444
490
  const traces = /* @__PURE__ */ new Map();
445
491
  const maps = /* @__PURE__ */ new Map();
@@ -480,7 +526,16 @@ async function exportShare(input) {
480
526
  fromLine,
481
527
  toLine
482
528
  }));
483
- return {
529
+ let frozen = false;
530
+ const sources = await cutExcerpts({ async quote(at, range) {
531
+ const quote = await input.data.quote(at, range);
532
+ if (at.worktreeRevision && range.side === "head" && !frozen) {
533
+ const { worktreeRevision: _worktreeRevision, ...committed } = at;
534
+ if (await input.data.quote(committed, range).then((head) => head.text !== quote.text, () => true)) frozen = true;
535
+ }
536
+ return quote;
537
+ } }, excerpts);
538
+ const bundle = {
484
539
  format: SHARE_FORMAT,
485
540
  reviewId: snapshot.reviewId,
486
541
  version: snapshot.version,
@@ -491,9 +546,49 @@ async function exportShare(input) {
491
546
  maps: Object.fromEntries(maps),
492
547
  commits: pins ? await input.data.commits((await input.data.resolveSource(stored)).pins) : [],
493
548
  selections,
494
- sources: await cutExcerpts(input.data, excerpts),
549
+ sources,
495
550
  ...input.repository && { repository: input.repository }
496
551
  };
552
+ if (frozen) bundle.frozen = true;
553
+ return bundle;
554
+ }
555
+ //#endregion
556
+ //#region src/slug.ts
557
+ /** Lowercase ASCII slug: runs of anything but `a-z0-9` become one `-`, outer
558
+ * dashes go. Empty input, or input with no letters or digits, yields `""`;
559
+ * callers choose their own fallback (`"section"`, `"diagram"`, `"database"`).
560
+ * This is the rule sequence and lens ids were already published with, so it
561
+ * must not change. */
562
+ function slugify(text) {
563
+ return text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
564
+ }
565
+ //#endregion
566
+ //#region src/sharing/file.ts
567
+ const WHITEBOARD_FILE_EXTENSION = ".whiteboard";
568
+ const WHITEBOARD_UPGRADE_REQUIRED_MESSAGE = "This action requires a newer version of Whiteboard. Use Check for Updates, then try again.";
569
+ const headerSchema = z.looseObject({ format: z.string() });
570
+ function whiteboardFileName(title) {
571
+ return `${slugify(title) || "untitled"}${WHITEBOARD_FILE_EXTENSION}`;
572
+ }
573
+ function whiteboardFileText(bundle) {
574
+ return JSON.stringify(bundle, null, 2) + "\n";
575
+ }
576
+ function parseWhiteboardFile(text) {
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(text);
580
+ } catch {
581
+ parsed = void 0;
582
+ }
583
+ const header = headerSchema.safeParse(parsed);
584
+ if (!header.success) throw new ReviewInputError("This is not a Whiteboard file.", 400);
585
+ const version = /^review-share\/([1-9]\d*)$/.exec(header.data.format);
586
+ if (version && Number(version[1]) > Number("review-share/2".split("/")[1])) throw new ReviewInputError(WHITEBOARD_UPGRADE_REQUIRED_MESSAGE, 400);
587
+ if (header.data.format !== "review-share/2") throw new ReviewInputError("This file needs a different Whiteboard version.", 400);
588
+ return shareBundleSchema.parse(parsed);
589
+ }
590
+ function whiteboardFileKey(text) {
591
+ return createHash("sha256").update("whiteboard-file\0").update(text).digest("hex");
497
592
  }
498
593
  //#endregion
499
594
  //#region src/sharing/client.ts
@@ -512,15 +607,6 @@ const pooledFetch = (input, init) => {
512
607
  const jwtSchema = z.object({ token: z.string().min(1) });
513
608
  const jwtPayloadSchema = z.object({ exp: z.number() });
514
609
  const bearers = /* @__PURE__ */ new Map();
515
- const linkSchema = z.strictObject({
516
- shareId: z.uuid(),
517
- url: z.url()
518
- });
519
- const receivedSchema = z.object({
520
- sender: z.strictObject({ login: z.string().min(1).max(256) }),
521
- sharedAt: z.number(),
522
- size: z.number().int().positive().max(MAX_SHARE_BYTES)
523
- });
524
610
  /** Size is checked while reading, even if Content-Length is absent or false. */
525
611
  async function readBoundedBytes(response, limit) {
526
612
  if (!response.ok || !response.body) throw new Error(`Download failed (${response.status}).`);
@@ -543,6 +629,44 @@ var ShareAuthError = class extends Error {
543
629
  this.name = "ShareAuthError";
544
630
  }
545
631
  };
632
+ var ShareRequestError = class extends Error {
633
+ status;
634
+ constructor(status, message) {
635
+ super(message);
636
+ this.status = status;
637
+ this.name = "ShareRequestError";
638
+ }
639
+ };
640
+ const errorSchema = z.object({ error: z.object({ code: z.string() }) });
641
+ async function errorCode(response) {
642
+ if (!response.body) return void 0;
643
+ if (!response.headers.get("content-type")?.includes("application/json")) {
644
+ await response.body.cancel();
645
+ return;
646
+ }
647
+ try {
648
+ const bytes = await readBoundedStream(response.body, 16384);
649
+ return errorSchema.safeParse(parseJsonText(bytes.toString())).data?.error.code;
650
+ } catch {
651
+ return;
652
+ }
653
+ }
654
+ function requestErrorMessage(response, code) {
655
+ const { status, headers } = response;
656
+ if (status === 426 || code === "upgrade_required") return WHITEBOARD_UPGRADE_REQUIRED_MESSAGE;
657
+ if (status === 403 && headers.get("content-type")?.includes("text/html") && headers.get("server")?.toLowerCase() === "cloudflare") return "The sharing service's security filter blocked this request. Please report this issue.";
658
+ if (status === 413) return "This review is too large to share. Remove large resources, then retry.";
659
+ if (status === 429) return code === "share_quota_exceeded" ? "Your sharing storage limit has been reached. Please contact support." : "Too many sharing requests. Wait a moment, then retry.";
660
+ if (status === 400 && code === "invalid_request") return "The sharing service could not accept this review. Update Whiteboard or report this issue.";
661
+ if (status >= 500) return `The sharing service is temporarily unavailable (HTTP ${status}). Try again shortly.`;
662
+ return `The sharing service refused this request (HTTP ${status}). Please report this issue.`;
663
+ }
664
+ async function requestError(response) {
665
+ const message = requestErrorMessage(response, await errorCode(response));
666
+ const ray = response.headers.get("cf-ray");
667
+ const reference = ray && /^[a-f0-9]{16}(?:-[a-z]{3})?$/i.test(ray) ? ` Reference: ${ray}.` : "";
668
+ return new ShareRequestError(response.status, message + reference);
669
+ }
546
670
  /** Account tokens go only to the configured API origin. */
547
671
  var ShareClient = class {
548
672
  token;
@@ -570,9 +694,11 @@ var ShareClient = class {
570
694
  if (body !== void 0) init.body = body;
571
695
  const response = await this.send(this.origin + route, init);
572
696
  if (!response.ok) {
573
- await response.body?.cancel();
574
- if (response.status === 401 && !anonymous) throw new ShareAuthError();
575
- throw new Error(`Share request failed (${response.status}).`);
697
+ if (response.status === 401 && !anonymous) {
698
+ await response.body?.cancel();
699
+ throw new ShareAuthError();
700
+ }
701
+ throw await requestError(response);
576
702
  }
577
703
  return readBoundedBytes(response, maxBytes);
578
704
  }
@@ -595,13 +721,18 @@ var ShareClient = class {
595
721
  headers: { authorization: `Bearer ${token}` }
596
722
  });
597
723
  if (!response.ok) {
598
- await response.body?.cancel();
599
- if (response.status === 404 || response.status === 405) return {
600
- bearer: token,
601
- until: Date.now() + 6e5
602
- };
603
- if (response.status === 401) throw new ShareAuthError();
604
- throw new Error(`Share request failed (${response.status}).`);
724
+ if (response.status === 404 || response.status === 405) {
725
+ await response.body?.cancel();
726
+ return {
727
+ bearer: token,
728
+ until: Date.now() + 6e5
729
+ };
730
+ }
731
+ if (response.status === 401) {
732
+ await response.body?.cancel();
733
+ throw new ShareAuthError();
734
+ }
735
+ throw await requestError(response);
605
736
  }
606
737
  const { token: jwt } = jwtSchema.parse(parseJsonText(Buffer.from(await readBoundedBytes(response, 65536)).toString()));
607
738
  const { exp } = jwtPayloadSchema.parse(parseJsonText(Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString()));
@@ -618,29 +749,38 @@ var ShareClient = class {
618
749
  })).body?.cancel();
619
750
  }
620
751
  async create(bundle, requestId = randomUUID()) {
621
- return linkSchema.parse(await this.api("/api/v2/shares", "POST", shareRequestBody(bundle, requestId)));
752
+ return shareCreatedSchema.parse(await this.api("/api/v2/shares", "POST", shareRequestBody(bundle, requestId)));
622
753
  }
623
754
  async list(cursor) {
624
755
  const query = cursor ? `?cursor=${z.uuid().parse(cursor)}` : "";
625
756
  return this.api(`/api/v2/shares${query}`);
626
757
  }
627
758
  async recoverLink(shareId) {
628
- return linkSchema.parse(await this.api(`/api/v2/shares/${z.uuid().parse(shareId)}/link`));
759
+ return shareCreatedSchema.parse(await this.api(`/api/v2/shares/${z.uuid().parse(shareId)}/link`));
629
760
  }
630
761
  async revoke(shareId) {
631
762
  return this.api(`/api/v2/shares/${z.uuid().parse(shareId)}`, "DELETE");
632
763
  }
633
- /** The bundle and who shared it, read with the link's capability. */
634
- async download(capability) {
635
- const route = `/api/v2/shared/${capabilitySchema.parse(capability)}`;
636
- const received = receivedSchema.parse(await this.api(route, "GET", void 0, true));
637
- const bytes = await this.request(`${route}/bundle`, "GET", void 0, true, MAX_SHARE_BYTES);
764
+ async revisions(shareId) {
765
+ return shareRevisionsSchema.parse(await this.api(`/api/v2/shares/${shareIdSchema.parse(shareId)}/revisions`));
766
+ }
767
+ /** Who shared a link's revision, and which revision is its latest. */
768
+ async shared(capability, revision) {
769
+ const query = revision === void 0 ? "" : `?rev=${shareIdSchema.parse(revision)}`;
770
+ return sharedMetaSchema.parse(await this.api(`/api/v2/shared/${capabilitySchema.parse(capability)}${query}`, "GET", void 0, true));
771
+ }
772
+ /** The bundle of the revision the meta names, and who shared it. */
773
+ async download(capability, revision) {
774
+ const meta = await this.shared(capability, revision);
775
+ const bytes = await this.request(`/api/v2/shared/${capability}/bundle?rev=${meta.revision.id}`, "GET", void 0, true, MAX_SHARE_BYTES);
638
776
  return {
639
- bundle: shareBundleSchema.parse(parseJsonText(new TextDecoder().decode(bytes))),
777
+ bundle: parseWhiteboardFile(new TextDecoder().decode(bytes)),
640
778
  attribution: {
641
- login: received.sender.login,
642
- sharedAt: received.sharedAt
643
- }
779
+ login: meta.sender.login,
780
+ sharedAt: meta.sharedAt
781
+ },
782
+ revision: meta.revision,
783
+ latest: meta.latest
644
784
  };
645
785
  }
646
786
  };
@@ -847,7 +987,7 @@ async function readShareRepository(root) {
847
987
  }
848
988
  //#endregion
849
989
  //#region src/sharing/import.ts
850
- const sharedSnapshotSchema = z.strictObject({
990
+ const sharedSnapshotSchema = z.object({
851
991
  reviewId: z.string().min(1),
852
992
  version: z.number().int().nonnegative(),
853
993
  /** Empty for a board never named; it reads "Untitled". */
@@ -859,7 +999,7 @@ const sharedSnapshotSchema = z.strictObject({
859
999
  document: documentSchema,
860
1000
  lenses: z.array(lensSchema).optional(),
861
1001
  createdAt: z.string(),
862
- origin: z.strictObject({
1002
+ origin: z.object({
863
1003
  branch: z.string().optional(),
864
1004
  baseRef: z.string().optional(),
865
1005
  baseBranch: z.string().optional(),
@@ -871,7 +1011,7 @@ const sharedSnapshotSchema = z.strictObject({
871
1011
  }).optional()
872
1012
  });
873
1013
  const count = z.number().int().nonnegative();
874
- const counts = z.strictObject({
1014
+ const counts = z.object({
875
1015
  additions: count,
876
1016
  deletions: count
877
1017
  });
@@ -881,9 +1021,9 @@ const mapSchema = softwareModelDataSchema.extend({
881
1021
  countsByElementPath: z.record(z.string(), counts),
882
1022
  unmappedByElementPath: z.record(z.string(), counts.extend({ files: z.array(counts.extend({
883
1023
  file: z.string(),
884
- hunks: z.array(z.strictObject({
1024
+ hunks: z.array(z.object({
885
1025
  startLine: count,
886
- lines: z.array(z.strictObject({
1026
+ lines: z.array(z.object({
887
1027
  kind: z.enum(["add", "remove"]),
888
1028
  oldLine: count.nullable(),
889
1029
  newLine: count.nullable(),
@@ -892,13 +1032,14 @@ const mapSchema = softwareModelDataSchema.extend({
892
1032
  }))
893
1033
  })) }))
894
1034
  });
895
- z.strictObject({ maps: z.record(z.string(), mapSchema) });
1035
+ z.object({ maps: z.record(z.string(), mapSchema) });
896
1036
  /** Commits and traces live in a checkout; a document alone needs none. */
897
1037
  function needsRepository(bundle, snapshot) {
898
1038
  return Boolean(snapshot.pins || referenceCommits(snapshot).length || Object.keys(bundle.traces).length);
899
1039
  }
900
1040
  function validateShareBundle(input) {
901
1041
  const bundle = shareBundleSchema.parse(input);
1042
+ if (bundle.frozen) throw new ReviewInputError("This whiteboard is a read-only snapshot of uncommitted changes. View it in the browser.", 409);
902
1043
  const stored = bundle.snapshot;
903
1044
  const snapshot = sharedSnapshotSchema.parse(stored.document === void 0 ? stored : liftSharedLenses(stored, migrateStoredDocument(stored.document)));
904
1045
  if (snapshot.reviewId !== bundle.reviewId || snapshot.version !== bundle.version || snapshot.title !== bundle.title) throw new Error("Shared snapshot does not match its bundle.");
@@ -948,12 +1089,41 @@ function shareKey(origin, capability) {
948
1089
  capabilitySchema.parse(capability);
949
1090
  return createHash("sha256").update(JSON.stringify([url.origin, capability])).digest("hex");
950
1091
  }
951
- /** Imports a share as an ordinary board, once per link: the link's copy
952
- * comes back while it exists, even from Trash. */
953
- async function importShare(input, key, received, attribution) {
1092
+ /** Which copy a served revision opens or refreshes in place; otherwise a new copy. */
1093
+ function planShareCopy(copies, served, pinned = false) {
1094
+ const same = copies.find(({ shared }) => shared.revisionId === void 0 ? shared.sharedAt === served.revision.createdAt : shared.revisionId === served.revision.id);
1095
+ if (same) return {
1096
+ kind: "open",
1097
+ reviewId: same.reviewId
1098
+ };
1099
+ const unedited = !pinned && served.revision.id === served.latest && copies.find((copy) => !copy.shared.pinned && copy.version === (copy.shared.importedVersion ?? 0));
1100
+ return unedited ? {
1101
+ kind: "refresh",
1102
+ reviewId: unedited.reviewId,
1103
+ version: unedited.version
1104
+ } : { kind: "import" };
1105
+ }
1106
+ /** Opens, refreshes or copies a link's board by the revision it serves. */
1107
+ async function receiveShare(input, key, served, download, pinned = false) {
1108
+ const plan = planShareCopy(input.store.copiesOf(key), served, pinned);
1109
+ if (plan.kind === "open") return plan.reviewId;
1110
+ const { bundle, attribution } = await download();
1111
+ const provenance = {
1112
+ ...attribution,
1113
+ revisionId: served.revision.id,
1114
+ ...pinned && { pinned }
1115
+ };
1116
+ if (plan.kind === "import") return importShare(input, key, bundle, provenance);
1117
+ try {
1118
+ return await importShare(input, key, bundle, provenance, plan);
1119
+ } catch (error) {
1120
+ if (error instanceof ReviewInputError && (error.status === 409 || error.status === 404)) return importShare(input, key, bundle, provenance);
1121
+ throw error;
1122
+ }
1123
+ }
1124
+ /** Imports a share as an ordinary board, or into a copy of its link. */
1125
+ async function importShare(input, key, received, provenance, into) {
954
1126
  const { store, data } = input;
955
- const existing = store.copyOf(key);
956
- if (existing) return existing;
957
1127
  const { bundle, snapshot, maps } = validateShareBundle(received);
958
1128
  const cloneUrl = bundle.repository?.cloneUrl;
959
1129
  let repositoryId;
@@ -988,7 +1158,7 @@ async function importShare(input, key, received, attribution) {
988
1158
  }
989
1159
  let document = JSON.stringify(snapshot.document);
990
1160
  for (const [id, copy] of ids) document = document.replaceAll(id, copy);
991
- const { reviewId } = await store.execute({ operation: {
1161
+ const create = { operation: {
992
1162
  type: "create",
993
1163
  ...snapshot.title && { title: snapshot.title },
994
1164
  ...pins ? { target: {
@@ -998,17 +1168,19 @@ async function importShare(input, key, received, attribution) {
998
1168
  kind: "project",
999
1169
  repositoryId
1000
1170
  } }
1001
- } }, {
1171
+ } };
1172
+ const seed = {
1002
1173
  document: JSON.parse(document),
1003
1174
  lenses: snapshot.lenses,
1004
1175
  origin: snapshot.origin,
1005
1176
  shared: {
1006
- ...attribution,
1177
+ ...provenance,
1007
1178
  cloneUrl,
1008
1179
  key
1009
1180
  },
1010
1181
  copiedFrom: snapshot.reviewId
1011
- }, { kind: "person" });
1182
+ };
1183
+ const { reviewId } = into ? await store.refreshCopy(into.reviewId, into.version, create, seed) : await store.execute(create, seed, { kind: "person" });
1012
1184
  return reviewId;
1013
1185
  }
1014
1186
  /** A checkout of the share's repository with every commit it reads at: the
@@ -1050,4 +1222,4 @@ async function shareRepository({ store, data, ...git }, cloneUrl, pins, commits,
1050
1222
  return repositoryId;
1051
1223
  }
1052
1224
  //#endregion
1053
- export { readBoundedStream as _, forgetSharedCommits as a, shareBundleSchema as b, ShareClient as c, legacyModeSchema as d, lensReport as f, StreamLimitError as g, uncategorizedReport as h, validateShareBundle as i, exportShare as l, reviewProgress as m, needsRepository as n, readShareRepository as o, progressUpdateSchema as p, shareKey as r, ShareAuthError as s, importShare as t, validateShareSources as u, SHARE_FORMAT as v, shareIdSchema as x, parseShareLink as y };
1225
+ export { StreamLimitError as C, shareIdSchema as D, shareBundleSchema as E, uncategorizedReport as S, parseShareLink as T, validateShareSources as _, shareKey as a, progressUpdateSchema as b, readShareRepository as c, ShareRequestError as d, parseWhiteboardFile as f, exportShare as g, whiteboardFileText as h, receiveShare as i, ShareAuthError as l, whiteboardFileName as m, needsRepository as n, validateShareBundle as o, whiteboardFileKey as p, planShareCopy as r, forgetSharedCommits as s, importShare as t, ShareClient as u, legacyModeSchema as v, readBoundedStream as w, reviewProgress as x, lensReport as y };