@dev.fast/whiteboard 0.4.0 → 0.4.1-preview.20261026.138

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 (78) hide show
  1. package/dist/{account-alias-2yTAxoBx.js → account-alias-BPlPnzQM.js} +7 -7
  2. package/dist/{agent-cli-CHNDd4Ga.js → agent-cli-BbCf9uid.js} +11 -11
  3. package/dist/agent-cli-ipN_JmzG.js +2 -0
  4. package/dist/{agent-client-Dc0vHFJt.js → agent-client-DfpM8F5N.js} +3 -3
  5. package/dist/{authoring-tools-D8EkIDjm.js → authoring-tools-bm39DRoV.js} +23 -10
  6. package/dist/{background-server-DyWnPJyZ.js → background-server--0EeCCeV.js} +1 -1
  7. package/dist/{background-server-DHWHzpo5.js → background-server-B--5p72o.js} +6 -6
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-qZomwJmK.js → cli-DBTQIroM.js} +2 -2
  10. package/dist/{cli-runner-DFBaVGij.js → cli-runner-CRFCdb7G.js} +25 -25
  11. package/dist/cli.js +3 -3
  12. package/dist/{fuzzy-match-CfSxqzBF.js → fuzzy-match-l3mJZ1Ge.js} +5 -5
  13. package/dist/{headless-host-dwlqhqMY.js → headless-host-5ei3nHS8.js} +1 -1
  14. package/dist/{headless-host-DJhcab6r.js → headless-host-BkJ9OUQY.js} +131 -299
  15. package/dist/{client-p9WB8Sj0.js → import-Bwvf9Wos.js} +505 -390
  16. package/dist/{lens-selection-9byzEgvU.js → lens-selection-EuZdag4Q.js} +1 -1
  17. package/dist/{local-data-DHedfhxw.js → local-data-BCSeTyic.js} +98 -154
  18. package/dist/{mcp-Br-r_wwR.js → mcp-_Xg1gQuA.js} +17 -8
  19. package/dist/{package-paths-BeYHYjeR.js → package-paths-Bg731FHS.js} +2 -2
  20. package/dist/profile-C8zL4u3L.js +2 -0
  21. package/dist/{profile-D9wdlPgw.js → profile-xzrUFgbT.js} +4 -4
  22. package/dist/{remote-attach-DSGpTOJ4.js → remote-attach-DF25Epoa.js} +4 -4
  23. package/dist/{remote-extensions-WtJO7pkb.js → remote-extensions-BJamgsCe.js} +1 -1
  24. package/dist/{remote-extensions-CsI_BJ_f.js → remote-extensions-CeywZL-Z.js} +4 -4
  25. package/dist/{remote-language-server-Dx13Mg2m.js → remote-language-server-CH8sG_CU.js} +6 -6
  26. package/dist/{remote-uninstall-Dp2o3qz0.js → remote-uninstall-Cq-lkBY7.js} +7 -7
  27. package/dist/{request-origin-CNSLVn7x.js → request-origin-CargJFvH.js} +14 -3
  28. package/dist/{review-app-launcher-BZAnR8Vp.js → review-app-launcher-D8ApNDUV.js} +3 -3
  29. package/dist/{review-home-paths-CG-sb9Cw.js → review-home-paths-DcB4xLD-.js} +2 -2
  30. package/dist/{review-telemetry-BM-_KdLi.js → review-telemetry-BW7gJrSo.js} +7 -7
  31. package/dist/{runtime-C6_1K-3Z.js → runtime-DY6Ctyog.js} +1 -1
  32. package/dist/runtime.js +1 -1
  33. package/dist/{s3-CsG2EWED.js → s3-BIWv4h1J.js} +2 -2
  34. package/dist/{s3-config-zz4GWh2T.js → s3-config-Du3RBIvv.js} +1 -1
  35. package/dist/server/desktop-host.js +51 -17
  36. package/dist/{server-discovery-DKLIOgg3.js → server-discovery-aHK_1r3U.js} +5 -5
  37. package/dist/sharing/index.d.ts +209 -98
  38. package/dist/sharing/index.js +1 -1
  39. package/dist/{src-DYHVzX3b.js → src-DEOc8bvC.js} +63 -2
  40. package/dist/{src-Z1x4-Qmb.js → src-DjRaCctk.js} +6 -6
  41. package/dist/{stored-document-migration-DEt8iwem.js → stored-document-migration-CwmTwTb8.js} +139 -13
  42. package/dist/{thread-state-CO97FUqK.js → thread-state-CbLB0jUA.js} +1 -1
  43. package/dist/{tool-failure-C1L1vJyz.js → tool-failure-DZaQzi4f.js} +2 -2
  44. package/instructions/animation.md +345 -0
  45. package/instructions/authoring.md +2 -2
  46. package/package.json +6 -6
  47. package/src/fixtures/blocks/animation.json +19 -0
  48. package/src/review-api/authoring-tools.ts +17 -6
  49. package/src/review-api/blocks/animation.ts +98 -0
  50. package/src/review-api/blocks/index.ts +3 -0
  51. package/src/review-api/blocks/sequence.ts +3 -1
  52. package/src/review-api/comparison-coverage.ts +120 -89
  53. package/src/review-api/component-reference.ts +16 -0
  54. package/src/review-api/document-text.ts +11 -0
  55. package/src/review-api/document.ts +26 -5
  56. package/src/review-api/http.ts +31 -2
  57. package/src/review-api/instructions.ts +11 -2
  58. package/src/review-api/local-data.ts +21 -4
  59. package/src/review-api/mcp.ts +22 -14
  60. package/src/review-api/onboarding.ts +100 -26
  61. package/src/review-preferences.ts +43 -0
  62. package/src/server/desktop-server.ts +24 -0
  63. package/src/server/json-review-reporting.ts +1 -1
  64. package/src/server/review-server-core.ts +2 -0
  65. package/src/server/structural-comparisons.ts +5 -11
  66. package/src/server/structural-diff.ts +8 -2
  67. package/src/sharing/cli.ts +1 -1
  68. package/src/sharing/client.ts +54 -222
  69. package/src/sharing/export.ts +172 -114
  70. package/src/sharing/host.ts +32 -36
  71. package/src/sharing/import.ts +93 -147
  72. package/src/sharing/index.ts +1 -6
  73. package/src/sharing/routes.ts +18 -17
  74. package/src/ui-telemetry-events.ts +11 -0
  75. package/dist/agent-cli-Cn4-16XA.js +0 -2
  76. package/dist/profile-BopGMZUh.js +0 -2
  77. package/dist/{s3-config-CjcLyFV5.js → s3-config-D7Yt0VhS.js} +1 -1
  78. package/dist/{src-X9ZebeOD.js → src-DXnn7RrI.js} +1 -1
@@ -2,9 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
 
3
3
  import { type JsonValue, parseJsonText } from "@dev.fast/json";
4
4
  import {
5
- MAX_SHARE_MANIFEST_BYTES,
6
- type ShareManifest,
7
- shareManifestSchema,
5
+ MAX_SHARE_BYTES,
6
+ type ShareBundle,
7
+ capabilitySchema,
8
+ shareBundleSchema,
8
9
  } from "@dev.fast/review-share-protocol";
9
10
  import {
10
11
  StreamLimitError,
@@ -12,46 +13,18 @@ import {
12
13
  } from "@review/server/bounded-stream.js";
13
14
  import { z } from "zod";
14
15
 
15
- import { type ShareBundle, digestBytes } from "./export.js";
16
- import { validateShareBundle } from "./import.js";
16
+ import { shareRequestBody } from "./export.js";
17
17
 
18
- const uploadSchema = z.strictObject({
19
- url: z.url(),
20
- headers: z.record(z.string(), z.string()),
21
- expiresAt: z.string(),
22
- });
23
-
24
- const createdSchema = z.strictObject({
25
- shareId: z.uuid(),
26
- upload: uploadSchema,
27
- });
28
-
29
- const registeredSchema = z.object({
30
- registered: z.literal(true),
31
- uploads: z.record(z.string(), uploadSchema),
32
- });
33
-
34
- const UPLOAD_CONCURRENCY = 4;
35
-
36
- // Presigned URLs and headers are larger than manifest object descriptors.
37
- const MAX_UPLOAD_RESPONSE_BYTES = 8 * 1024 * 1024;
18
+ const MAX_RESPONSE_BYTES = MAX_SHARE_BYTES;
38
19
 
39
20
  const linkSchema = z.strictObject({ shareId: z.uuid(), url: z.url() });
40
21
 
41
- const receivedSchema = z.strictObject({
42
- manifest: shareManifestSchema,
22
+ const receivedSchema = z.object({
43
23
  sender: z.strictObject({ login: z.string().min(1).max(256) }),
44
24
  sharedAt: z.number(),
25
+ size: z.number().int().positive().max(MAX_SHARE_BYTES),
45
26
  });
46
27
 
47
- const signedUrlSchema = z.url().refine((value) => {
48
- const url = new URL(value);
49
-
50
- return url.protocol === "https:" && !url.username && !url.password;
51
- });
52
-
53
- const downloadSchema = z.strictObject({ url: z.url(), expiresAt: z.string() });
54
-
55
28
  /** Size is checked while reading, even if Content-Length is absent or false. */
56
29
  export async function readBoundedBytes(
57
30
  response: Response,
@@ -83,25 +56,7 @@ export class ShareAuthError extends Error {
83
56
  }
84
57
  }
85
58
 
86
- export class SharePreflightError extends Error {
87
- constructor(cause: unknown) {
88
- super(
89
- cause instanceof Error
90
- ? cause.message
91
- : "Repository verification failed.",
92
- { cause },
93
- );
94
- this.name = "SharePreflightError";
95
- }
96
- }
97
-
98
- class ShareRequestError extends Error {
99
- constructor(readonly status: number) {
100
- super(`Share request failed (${status}).`);
101
- }
102
- }
103
-
104
- /** Account tokens go only to the configured API origin, never to object storage. */
59
+ /** Account tokens go only to the configured API origin. */
105
60
  export class ShareClient {
106
61
  private readonly origin: string;
107
62
  constructor(
@@ -124,221 +79,98 @@ export class ShareClient {
124
79
  private async api(
125
80
  route: string,
126
81
  method = "GET",
127
- body?: JsonValue,
128
- capability?: string,
129
- maxBytes = MAX_SHARE_MANIFEST_BYTES,
82
+ body?: string,
83
+ anonymous = false,
130
84
  ): Promise<JsonValue> {
85
+ return parseJsonText(
86
+ Buffer.from(
87
+ await this.request(route, method, body, anonymous, MAX_RESPONSE_BYTES),
88
+ ).toString(),
89
+ );
90
+ }
91
+
92
+ private async request(
93
+ route: string,
94
+ method: string,
95
+ body: string | undefined,
96
+ anonymous: boolean,
97
+ maxBytes: number,
98
+ ) {
131
99
  const headers = new Headers({ "content-type": "application/json" });
132
100
 
133
- if (capability) headers.set("x-review-share-token", capability);
134
- else if (this.token) headers.set("authorization", `Bearer ${this.token}`);
101
+ if (this.token && !anonymous)
102
+ headers.set("authorization", `Bearer ${this.token}`);
135
103
 
136
104
  const init: RequestInit = {
137
105
  method,
138
106
  redirect: "error",
139
- signal: AbortSignal.timeout(60_000),
107
+ signal: AbortSignal.timeout(300_000),
140
108
  headers,
141
109
  };
142
110
 
143
- if (body !== undefined) init.body = JSON.stringify(body);
111
+ if (body !== undefined) init.body = body;
144
112
  const response = await this.send(this.origin + route, init);
145
113
 
146
114
  if (!response.ok) {
147
115
  await response.body?.cancel();
148
116
 
149
- if (response.status === 401 && !capability) throw new ShareAuthError();
150
- throw new ShareRequestError(response.status);
117
+ if (response.status === 401 && !anonymous) throw new ShareAuthError();
118
+ throw new Error(`Share request failed (${response.status}).`);
151
119
  }
152
120
 
153
- return parseJsonText(
154
- Buffer.from(await readBoundedBytes(response, maxBytes)).toString(),
155
- );
156
- }
157
-
158
- private async upload(
159
- upload: z.infer<typeof uploadSchema>,
160
- bytes: Uint8Array,
161
- ) {
162
- if (!signedUrlSchema.safeParse(upload.url).success)
163
- throw new Error("Invalid object upload URL.");
164
-
165
- const response = await this.send(upload.url, {
166
- method: "PUT",
167
- headers: upload.headers,
168
- body: Buffer.from(bytes),
169
- redirect: "error",
170
- signal: AbortSignal.timeout(120_000),
171
- });
172
-
173
- // Conditional create can return 412 after a successful upload whose response
174
- // was lost. Completion checks the actual stored digest before publication.
175
- await response.body?.cancel();
176
-
177
- if (!response.ok && response.status !== 412)
178
- throw new Error(`Object upload failed (${response.status}).`);
121
+ return readBoundedBytes(response, maxBytes);
179
122
  }
180
123
 
181
- async create(
182
- bundle: ShareBundle,
183
- requestId: string = randomUUID(),
184
- beforeUpload?: () => Promise<void>,
185
- ) {
186
- try {
187
- validateShareBundle(bundle);
188
- } catch (cause) {
189
- throw new SharePreflightError(
190
- new Error(
191
- "Whiteboard could not prepare this review for sharing. Please report this issue.",
192
- { cause },
193
- ),
194
- );
195
- }
196
-
197
- const manifestBytes = Buffer.from(JSON.stringify(bundle.manifest));
198
-
199
- const created = createdSchema.parse(
200
- await this.api("/api/shares", "POST", {
201
- requestId,
202
- manifest: {
203
- size: manifestBytes.byteLength,
204
- sha256: digestBytes(manifestBytes),
205
- },
206
- }),
207
- );
208
-
209
- const route = `/api/shares/${created.shareId}`;
210
- await this.upload(created.upload, manifestBytes);
211
-
212
- const { uploads } = registeredSchema.parse(
124
+ async create(bundle: ShareBundle, requestId: string = randomUUID()) {
125
+ return linkSchema.parse(
213
126
  await this.api(
214
- `${route}/manifest`,
127
+ "/api/v2/shares",
215
128
  "POST",
216
- {},
217
- undefined,
218
- MAX_UPLOAD_RESPONSE_BYTES,
129
+ shareRequestBody(bundle, requestId),
219
130
  ),
220
131
  );
221
-
222
- try {
223
- if (bundle.manifest.objects.some(({ id }) => !uploads[id]))
224
- throw new Error("The share service omitted an object upload URL.");
225
-
226
- await beforeUpload?.();
227
- } catch (error) {
228
- // A retry may recover an already published share after a lost response.
229
- await this.recoverLink(created.shareId)
230
- .catch(async (failure) => {
231
- if (failure instanceof ShareRequestError && failure.status === 409)
232
- await this.revoke(created.shareId);
233
- })
234
- .catch(() => {});
235
- throw new SharePreflightError(error);
236
- }
237
-
238
- for (
239
- let offset = 0;
240
- offset < bundle.manifest.objects.length;
241
- offset += UPLOAD_CONCURRENCY
242
- ) {
243
- const results = await Promise.allSettled(
244
- bundle.manifest.objects
245
- .slice(offset, offset + UPLOAD_CONCURRENCY)
246
- .map(async (object) => {
247
- await this.upload(
248
- uploads[object.id]!,
249
- bundle.objects.get(object.id)!,
250
- );
251
- }),
252
- );
253
-
254
- const failed = results.find((result) => result.status === "rejected");
255
-
256
- if (failed) throw failed.reason;
257
- }
258
-
259
- for (
260
- let batch = 0;
261
- batch <= Math.ceil(bundle.manifest.objects.length / 32);
262
- batch++
263
- ) {
264
- const result = await this.api(`${route}/complete`, "POST", {});
265
-
266
- if (linkSchema.safeParse(result).success) return linkSchema.parse(result);
267
- z.strictObject({ complete: z.literal(false) }).parse(result);
268
- }
269
-
270
- throw new Error("Share verification did not finish.");
271
132
  }
272
133
 
273
134
  async list(cursor?: string) {
274
135
  const query = cursor ? `?cursor=${z.uuid().parse(cursor)}` : "";
275
136
 
276
- return this.api(`/api/shares${query}`);
137
+ return this.api(`/api/v2/shares${query}`);
277
138
  }
278
139
 
279
140
  async recoverLink(shareId: string) {
280
141
  return linkSchema.parse(
281
- await this.api(`/api/shares/${z.uuid().parse(shareId)}/link`),
142
+ await this.api(`/api/v2/shares/${z.uuid().parse(shareId)}/link`),
282
143
  );
283
144
  }
284
145
 
285
146
  async revoke(shareId: string) {
286
- return this.api(`/api/shares/${z.uuid().parse(shareId)}`, "DELETE");
147
+ return this.api(`/api/v2/shares/${z.uuid().parse(shareId)}`, "DELETE");
287
148
  }
288
149
 
289
- async download(shareId: string, capability: string): Promise<ShareBundle> {
290
- const route = `/api/shared/${z.uuid().parse(shareId)}`;
150
+ /** The bundle and who shared it, read with the link's capability. */
151
+ async download(capability: string) {
152
+ const route = `/api/v2/shared/${capabilitySchema.parse(capability)}`;
291
153
 
292
154
  const received = receivedSchema.parse(
293
- await this.api(route, "GET", undefined, capability),
155
+ await this.api(route, "GET", undefined, true),
294
156
  );
295
157
 
296
- const manifest: ShareManifest = received.manifest;
297
- const objects = new Map<string, Uint8Array>();
298
-
299
- const download = async (object: ShareManifest["objects"][number]) => {
300
- const signed = downloadSchema.parse(
301
- await this.api(
302
- `${route}/objects/${object.id}`,
303
- "GET",
304
- undefined,
305
- capability,
306
- ),
307
- );
308
-
309
- if (!signedUrlSchema.safeParse(signed.url).success)
310
- throw new Error("Invalid object download URL.");
311
-
312
- const bytes = await readBoundedBytes(
313
- await this.send(signed.url, {
314
- redirect: "error",
315
- signal: AbortSignal.timeout(120_000),
316
- }),
317
- object.size,
318
- );
319
-
320
- if (
321
- bytes.byteLength !== object.size ||
322
- digestBytes(bytes) !== object.sha256
323
- )
324
- throw new Error("Downloaded share object failed its integrity check.");
325
- objects.set(object.id, bytes);
326
- };
327
-
328
- for (let start = 0; start < manifest.objects.length; start += 4)
329
- await Promise.all(manifest.objects.slice(start, start + 4).map(download));
158
+ const bytes = await this.request(
159
+ `${route}/bundle`,
160
+ "GET",
161
+ undefined,
162
+ true,
163
+ MAX_SHARE_BYTES,
164
+ );
330
165
 
331
- const bundle = {
332
- manifest,
333
- objects,
166
+ return {
167
+ bundle: shareBundleSchema.parse(
168
+ parseJsonText(new TextDecoder().decode(bytes)),
169
+ ),
334
170
  attribution: {
335
171
  login: received.sender.login,
336
172
  sharedAt: received.sharedAt,
337
173
  },
338
174
  };
339
-
340
- // Document validation happens once at the importer boundary.
341
-
342
- return bundle;
343
175
  }
344
176
  }