@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.
- package/dist/{account-alias-Bw3PT3Qo.js → account-alias-DXpd0Px8.js} +5 -5
- package/dist/{agent-cli-kHRrwO4n.js → agent-cli-COrZtFks.js} +9 -9
- package/dist/agent-cli-x6-Eh1S-.js +2 -0
- package/dist/{agent-client-CMacGbMp.js → agent-client-BFtYm3Ng.js} +4 -4
- package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-D1avYp8-.js} +12 -8
- package/dist/{background-server-eZWXoVcI.js → background-server-BiRSqjxG.js} +4 -4
- package/dist/{background-server-BLNHbksB.js → background-server-HOJozY99.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-Bxmiqutp.js → cli-CSlwGRvH.js} +1 -1
- package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-CuffXA0Q.js} +22 -22
- package/dist/cli.js +2 -2
- package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-DEo941za.js} +3 -3
- package/dist/{headless-host-B4r0s1ks.js → headless-host-BCkXVVpU.js} +1 -1
- package/dist/{headless-host-Dl2KZ9IT.js → headless-host-CIuJJjmM.js} +561 -415
- package/dist/{import-YAIU6rB0.js → import-CjcW7EIQ.js} +235 -63
- package/dist/{local-data-C58vzLZR.js → local-data-B5PBZ0Ge.js} +101 -17
- package/dist/{mcp-COYWiGVn.js → mcp-Ba3WvhUh.js} +9 -6
- package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
- package/dist/profile-BstQkIY0.js +2 -0
- package/dist/{profile--BYIjPkc.js → profile-CNMhLUhf.js} +2 -2
- package/dist/{remote-attach-C1prLjmj.js → remote-attach-DkZoYBHV.js} +4 -4
- package/dist/{remote-extensions-Bw029iIS.js → remote-extensions-BvV29_gk.js} +1 -1
- package/dist/{remote-extensions-MUUgbLJn.js → remote-extensions-L95HNwIH.js} +2 -2
- package/dist/{remote-language-server-z3rdHcLL.js → remote-language-server-l4ConTfj.js} +4 -4
- package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-BAQM7cU8.js} +5 -5
- package/dist/{request-origin-B3_XI2VA.js → request-origin-DZexKlc4.js} +28 -5
- package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-B5F10y_4.js} +2 -2
- package/dist/{review-home-paths-LdGS4SpR.js → review-home-paths-BmZX2qiz.js} +1 -1
- package/dist/{review-telemetry-x8u-9EZb.js → review-telemetry-kQ7esDRE.js} +4 -4
- package/dist/server/desktop-host.js +26 -38
- package/dist/{server-discovery-WmuFl2S3.js → server-discovery-DYY_xeG9.js} +2 -2
- package/dist/sharing/index.d.ts +137 -14
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DLWol7GZ.js → src-B-91Rd2d.js} +17 -1
- package/dist/{src-B2b8cCoI.js → src-DirPN_Qh.js} +82 -6
- package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-vg0NAzXG.js} +4 -89
- package/dist/{thread-state-B7L1mpN_.js → thread-state-Jxoh1hde.js} +1 -1
- package/dist/{tool-failure-RLR8m9hq.js → tool-failure-BLBwcl3H.js} +319 -55
- package/instructions/animation.md +69 -5
- package/instructions/animations/before-after-structure/README.md +27 -0
- package/instructions/animations/before-after-structure/animation.html +89 -0
- package/instructions/animations/before-after-structure/block.json +10 -0
- package/instructions/animations/before-after-structure/frames.json +313 -0
- package/instructions/animations/cache-fill-evict/README.md +24 -0
- package/instructions/animations/cache-fill-evict/animation.html +81 -0
- package/instructions/animations/cache-fill-evict/block.json +13 -0
- package/instructions/animations/cache-fill-evict/frames.json +79 -0
- package/instructions/animations/cache-fill-evict/scenarios.json +17 -0
- package/instructions/animations/log-replay/README.md +25 -0
- package/instructions/animations/log-replay/animation.html +86 -0
- package/instructions/animations/log-replay/block.json +10 -0
- package/instructions/animations/log-replay/frames.json +143 -0
- package/instructions/animations/log-replay/scenarios.json +18 -0
- package/instructions/animations/queue-workers/README.md +23 -0
- package/instructions/animations/queue-workers/animation.html +107 -0
- package/instructions/animations/queue-workers/block.json +13 -0
- package/instructions/animations/queue-workers/frames.json +119 -0
- package/instructions/animations/queue-workers/scenarios.json +18 -0
- package/instructions/animations/race-two-lanes/README.md +22 -0
- package/instructions/animations/race-two-lanes/animation.html +97 -0
- package/instructions/animations/race-two-lanes/block.json +1 -0
- package/instructions/animations/race-two-lanes/frames.json +234 -0
- package/instructions/animations/race-two-lanes/scenarios.json +19 -0
- package/instructions/animations/request-across-layers/README.md +24 -0
- package/instructions/animations/request-across-layers/animation.html +85 -0
- package/instructions/animations/request-across-layers/block.json +17 -0
- package/instructions/animations/request-across-layers/frames.json +58 -0
- package/instructions/animations/request-across-layers/scenarios.json +12 -0
- package/instructions/animations/request-timing/README.md +22 -0
- package/instructions/animations/request-timing/animation.html +120 -0
- package/instructions/animations/request-timing/block.json +21 -0
- package/instructions/animations/request-timing/markers.json +32 -0
- package/instructions/animations/request-timing/scenarios.json +4 -0
- package/instructions/animations/retry-backoff/README.md +22 -0
- package/instructions/animations/retry-backoff/animation.html +122 -0
- package/instructions/animations/retry-backoff/block.json +10 -0
- package/instructions/animations/retry-backoff/frames.json +65 -0
- package/instructions/animations/retry-backoff/scenarios.json +12 -0
- package/instructions/animations/state-machine/README.md +24 -0
- package/instructions/animations/state-machine/animation.html +95 -0
- package/instructions/animations/state-machine/block.json +9 -0
- package/instructions/animations/state-machine/frames.json +79 -0
- package/instructions/animations/state-machine/scenarios.json +12 -0
- package/instructions/animations/video-platform-architecture/README.md +24 -0
- package/instructions/animations/video-platform-architecture/animation.html +76 -0
- package/instructions/animations/video-platform-architecture/block.json +17 -0
- package/instructions/animations/video-platform-architecture/frames.json +97 -0
- package/instructions/animations/video-platform-architecture/scenarios.json +28 -0
- package/instructions/whiteboard.d.ts +168 -14
- package/package.json +5 -5
- package/src/animation-stage-kit.ts +150 -10
- package/src/cli-runner.ts +4 -1
- package/src/fixtures/blocks/clip.json +19 -0
- package/src/review-api/README.md +1 -0
- package/src/review-api/agent-cli.ts +3 -2
- package/src/review-api/animation-drafts.ts +151 -47
- package/src/review-api/animation-references.ts +92 -0
- package/src/review-api/animation-render.ts +106 -0
- package/src/review-api/authoring-tools.ts +10 -3
- package/src/review-api/comparison-coverage.ts +6 -7
- package/src/review-api/document-text.ts +27 -12
- package/src/review-api/http.ts +52 -6
- package/src/review-api/instructions.ts +1 -1
- package/src/review-api/mcp.ts +10 -5
- package/src/review-api/public-tools.ts +71 -2
- package/src/review-api/review-progress.ts +2 -3
- package/src/review-api/store.ts +89 -12
- package/src/server/desktop-server.ts +17 -5
- package/src/server/headless-host.ts +4 -0
- package/src/server/review-gateway.ts +1 -0
- package/src/server/review-server-core.ts +71 -12
- package/src/sharing/client.ts +129 -32
- package/src/sharing/export.ts +42 -5
- package/src/sharing/file.ts +8 -0
- package/src/sharing/host.ts +202 -73
- package/src/sharing/import.ts +125 -33
- package/src/ui-telemetry-events.ts +26 -1
- package/dist/agent-cli-B8KBHuh_.js +0 -2
- package/dist/profile-BjcXi-oH.js +0 -2
- package/src/review-api/lens-alignment.ts +0 -52
package/src/sharing/export.ts
CHANGED
|
@@ -156,8 +156,16 @@ export async function exportShare(input: {
|
|
|
156
156
|
...snapshot
|
|
157
157
|
} = stored;
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// A worktree board reads its checkout as it is now, and shares at HEAD.
|
|
160
|
+
const live =
|
|
161
|
+
target?.kind === "worktree"
|
|
162
|
+
? await input.data.sourcePins(stored)
|
|
163
|
+
: undefined;
|
|
164
|
+
|
|
165
|
+
if (live) {
|
|
166
|
+
const { worktreeRevision: _worktreeRevision, ...committed } = live;
|
|
167
|
+
snapshot.pins = committed;
|
|
168
|
+
}
|
|
161
169
|
|
|
162
170
|
// A viewer has no PR state to read the base branch from.
|
|
163
171
|
const pullRequestUrl = snapshot.origin?.pullRequestUrl;
|
|
@@ -184,7 +192,7 @@ export async function exportShare(input: {
|
|
|
184
192
|
);
|
|
185
193
|
}
|
|
186
194
|
|
|
187
|
-
const pins = snapshot.pins;
|
|
195
|
+
const pins = live ?? snapshot.pins;
|
|
188
196
|
|
|
189
197
|
// Lens ranges travel with the snapshot and must resolve at its pins too.
|
|
190
198
|
const references: {
|
|
@@ -274,7 +282,32 @@ export async function exportShare(input: {
|
|
|
274
282
|
toLine,
|
|
275
283
|
}));
|
|
276
284
|
|
|
277
|
-
|
|
285
|
+
// Quoting uncommitted lines makes the share view only.
|
|
286
|
+
let frozen = false;
|
|
287
|
+
|
|
288
|
+
const sources = await cutExcerpts(
|
|
289
|
+
{
|
|
290
|
+
async quote(at, range) {
|
|
291
|
+
const quote = await input.data.quote(at, range);
|
|
292
|
+
|
|
293
|
+
if (at.worktreeRevision && range.side === "head" && !frozen) {
|
|
294
|
+
const { worktreeRevision: _worktreeRevision, ...committed } = at;
|
|
295
|
+
|
|
296
|
+
const changed = await input.data.quote(committed, range).then(
|
|
297
|
+
(head) => head.text !== quote.text,
|
|
298
|
+
() => true,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (changed) frozen = true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return quote;
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
excerpts,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const bundle: ShareBundle = {
|
|
278
311
|
format: SHARE_FORMAT,
|
|
279
312
|
reviewId: snapshot.reviewId,
|
|
280
313
|
version: snapshot.version,
|
|
@@ -287,7 +320,11 @@ export async function exportShare(input: {
|
|
|
287
320
|
? await input.data.commits((await input.data.resolveSource(stored)).pins)
|
|
288
321
|
: [],
|
|
289
322
|
selections,
|
|
290
|
-
sources
|
|
323
|
+
sources,
|
|
291
324
|
...(input.repository && { repository: input.repository }),
|
|
292
325
|
};
|
|
326
|
+
|
|
327
|
+
if (frozen) bundle.frozen = true;
|
|
328
|
+
|
|
329
|
+
return bundle;
|
|
293
330
|
}
|
package/src/sharing/file.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { z } from "zod";
|
|
|
11
11
|
|
|
12
12
|
export const WHITEBOARD_FILE_EXTENSION = ".whiteboard";
|
|
13
13
|
|
|
14
|
+
export const WHITEBOARD_UPGRADE_REQUIRED_MESSAGE =
|
|
15
|
+
"This action requires a newer version of Whiteboard. Use Check for Updates, then try again.";
|
|
16
|
+
|
|
14
17
|
const headerSchema = z.looseObject({ format: z.string() });
|
|
15
18
|
|
|
16
19
|
export function whiteboardFileName(title: string): string {
|
|
@@ -35,6 +38,11 @@ export function parseWhiteboardFile(text: string): ShareBundle {
|
|
|
35
38
|
if (!header.success)
|
|
36
39
|
throw new ReviewInputError("This is not a Whiteboard file.", 400);
|
|
37
40
|
|
|
41
|
+
const version = /^review-share\/([1-9]\d*)$/.exec(header.data.format);
|
|
42
|
+
|
|
43
|
+
if (version && Number(version[1]) > Number(SHARE_FORMAT.split("/")[1]))
|
|
44
|
+
throw new ReviewInputError(WHITEBOARD_UPGRADE_REQUIRED_MESSAGE, 400);
|
|
45
|
+
|
|
38
46
|
if (header.data.format !== SHARE_FORMAT)
|
|
39
47
|
throw new ReviewInputError(
|
|
40
48
|
"This file needs a different Whiteboard version.",
|
package/src/sharing/host.ts
CHANGED
|
@@ -4,8 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { Writable } from "node:stream";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
type
|
|
7
|
+
type SharedMeta,
|
|
8
8
|
parseShareLink,
|
|
9
|
+
shareBundleSchema,
|
|
9
10
|
shareIdSchema,
|
|
10
11
|
} from "@dev.fast/review-share-protocol";
|
|
11
12
|
import {
|
|
@@ -17,13 +18,13 @@ import {
|
|
|
17
18
|
} from "@dev.fast/trace-core";
|
|
18
19
|
import { ReviewInputError } from "@dev.fast/whiteboard-document";
|
|
19
20
|
import type { LocalReviewData } from "@review/review-api/local-data.js";
|
|
20
|
-
import type { ReviewStore
|
|
21
|
+
import type { ReviewStore } from "@review/review-api/store.js";
|
|
21
22
|
import { readBoundedRequestJson } from "@review/server/hono-http.js";
|
|
22
23
|
import type { Hono } from "hono";
|
|
23
24
|
import { z } from "zod";
|
|
24
25
|
|
|
25
26
|
import { readSharingAuth } from "./auth.js";
|
|
26
|
-
import { ShareAuthError, ShareClient } from "./client.js";
|
|
27
|
+
import { ShareAuthError, ShareClient, ShareRequestError } from "./client.js";
|
|
27
28
|
import { exportShare, validateShareSources } from "./export.js";
|
|
28
29
|
import {
|
|
29
30
|
parseWhiteboardFile,
|
|
@@ -32,9 +33,11 @@ import {
|
|
|
32
33
|
whiteboardFileText,
|
|
33
34
|
} from "./file.js";
|
|
34
35
|
import {
|
|
35
|
-
type
|
|
36
|
+
type ShareImport,
|
|
36
37
|
importShare,
|
|
37
38
|
needsRepository,
|
|
39
|
+
planShareCopy,
|
|
40
|
+
receiveShare,
|
|
38
41
|
shareKey,
|
|
39
42
|
} from "./import.js";
|
|
40
43
|
import {
|
|
@@ -73,6 +76,7 @@ interface SharingHostOptions extends SharingHostEvents {
|
|
|
73
76
|
login?: typeof runStoreLogin;
|
|
74
77
|
openUrl?: typeof openUrlInBrowser;
|
|
75
78
|
fetch?: typeof fetch;
|
|
79
|
+
remoteShare?: RemoteShare;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
const importSchema = z.union([
|
|
@@ -181,9 +185,20 @@ export function mountSharingHost(
|
|
|
181
185
|
|
|
182
186
|
const imports = new Map<string, ImportStatus>();
|
|
183
187
|
|
|
184
|
-
// One
|
|
188
|
+
// One download at a time, so two links of one repository clone it once.
|
|
185
189
|
let importing = Promise.resolve();
|
|
186
190
|
|
|
191
|
+
const serially = (task: () => Promise<string>) => {
|
|
192
|
+
const run = importing.then(task);
|
|
193
|
+
|
|
194
|
+
importing = run.then(
|
|
195
|
+
() => {},
|
|
196
|
+
() => {},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return run;
|
|
200
|
+
};
|
|
201
|
+
|
|
187
202
|
app.get("/sharing/import/:id", (context) => {
|
|
188
203
|
const id = context.req.param("id");
|
|
189
204
|
|
|
@@ -199,54 +214,41 @@ export function mountSharingHost(
|
|
|
199
214
|
});
|
|
200
215
|
});
|
|
201
216
|
|
|
217
|
+
const shareInput = (onStage: ShareImport["onStage"]): ShareImport => ({
|
|
218
|
+
store,
|
|
219
|
+
data,
|
|
220
|
+
fetchRepository: options.fetchRepository,
|
|
221
|
+
cloneRepository: options.cloneRepository,
|
|
222
|
+
onStage,
|
|
223
|
+
});
|
|
224
|
+
|
|
202
225
|
const startImport = (
|
|
203
226
|
id: string,
|
|
204
227
|
initial: "downloading" | "reading",
|
|
205
|
-
|
|
206
|
-
bundle: ShareBundle;
|
|
207
|
-
attribution?: ShareAttribution;
|
|
208
|
-
}>,
|
|
228
|
+
receive: (onStage: ShareImport["onStage"]) => Promise<string>,
|
|
209
229
|
) => {
|
|
210
230
|
const known = imports.get(id)?.stage;
|
|
211
231
|
|
|
212
|
-
// A
|
|
232
|
+
// A finished import asks again: its link may serve a newer revision.
|
|
213
233
|
if (!known || known === "ready" || known === "error") {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
const status: ImportStatus = copy
|
|
217
|
-
? { stage: "ready", reviewId: copy }
|
|
218
|
-
: { stage: initial };
|
|
234
|
+
const status: ImportStatus = { stage: initial };
|
|
219
235
|
|
|
220
236
|
imports.set(id, status);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
},
|
|
237
|
-
id,
|
|
238
|
-
bundle,
|
|
239
|
-
attribution,
|
|
240
|
-
);
|
|
241
|
-
status.stage = "ready";
|
|
242
|
-
})
|
|
243
|
-
.catch((error) => {
|
|
244
|
-
status.stage = "error";
|
|
245
|
-
status.error =
|
|
246
|
-
error instanceof ReviewInputError
|
|
247
|
-
? error.message
|
|
248
|
-
: "This share is unavailable, revoked, or needs a newer Whiteboard version.";
|
|
249
|
-
});
|
|
237
|
+
receive((stage) => {
|
|
238
|
+
status.stage = stage;
|
|
239
|
+
})
|
|
240
|
+
.then((reviewId) => {
|
|
241
|
+
status.reviewId = reviewId;
|
|
242
|
+
status.stage = "ready";
|
|
243
|
+
})
|
|
244
|
+
.catch((error) => {
|
|
245
|
+
status.stage = "error";
|
|
246
|
+
status.error =
|
|
247
|
+
error instanceof ReviewInputError ||
|
|
248
|
+
(error instanceof ShareRequestError && error.status !== 404)
|
|
249
|
+
? error.message
|
|
250
|
+
: "This share is unavailable, revoked, or could not be read.";
|
|
251
|
+
});
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
const { reviewId: _reviewId, ...status } = imports.get(id)!;
|
|
@@ -270,10 +272,25 @@ export function mountSharingHost(
|
|
|
270
272
|
);
|
|
271
273
|
});
|
|
272
274
|
|
|
275
|
+
const key = whiteboardFileKey(text);
|
|
276
|
+
const copy = () => store.copiesOf(key)[0]?.reviewId;
|
|
277
|
+
|
|
273
278
|
return context.json(
|
|
274
|
-
startImport(
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
startImport(
|
|
280
|
+
key,
|
|
281
|
+
"reading",
|
|
282
|
+
async (onStage) =>
|
|
283
|
+
copy() ??
|
|
284
|
+
serially(
|
|
285
|
+
async () =>
|
|
286
|
+
copy() ??
|
|
287
|
+
importShare(
|
|
288
|
+
shareInput(onStage),
|
|
289
|
+
key,
|
|
290
|
+
parseWhiteboardFile(text),
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
),
|
|
277
294
|
202,
|
|
278
295
|
);
|
|
279
296
|
}
|
|
@@ -290,17 +307,101 @@ export function mountSharingHost(
|
|
|
290
307
|
400,
|
|
291
308
|
);
|
|
292
309
|
|
|
310
|
+
const { origin, capability, revision } = parsed;
|
|
311
|
+
const key = shareKey(origin, capability);
|
|
312
|
+
const client = new ShareClient(origin, undefined, options.fetch);
|
|
313
|
+
|
|
293
314
|
return context.json(
|
|
294
315
|
startImport(
|
|
295
|
-
|
|
316
|
+
revision ? `${key}-${revision}` : key,
|
|
296
317
|
"downloading",
|
|
297
|
-
() =>
|
|
318
|
+
async (onStage) => {
|
|
319
|
+
let meta: SharedMeta;
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
meta = await client.shared(capability, revision);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
// Offline, the copy the link prefers still opens.
|
|
325
|
+
const copies = store.copiesOf(key);
|
|
326
|
+
|
|
327
|
+
const copy =
|
|
328
|
+
copies.find(({ shared }) =>
|
|
329
|
+
revision ? shared.revisionId === revision : !shared.pinned,
|
|
330
|
+
) ?? copies[0];
|
|
331
|
+
|
|
332
|
+
if (!copy) throw error;
|
|
333
|
+
|
|
334
|
+
return copy.reviewId;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const pinned = revision !== undefined;
|
|
338
|
+
const plan = planShareCopy(store.copiesOf(key), meta, pinned);
|
|
339
|
+
|
|
340
|
+
// receiveShare plans again: the queue may have made a copy since.
|
|
341
|
+
return plan.kind === "open"
|
|
342
|
+
? plan.reviewId
|
|
343
|
+
: serially(() =>
|
|
344
|
+
receiveShare(
|
|
345
|
+
shareInput(onStage),
|
|
346
|
+
key,
|
|
347
|
+
meta,
|
|
348
|
+
() => client.download(capability, meta.revision.id),
|
|
349
|
+
pinned,
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
},
|
|
298
353
|
),
|
|
299
354
|
202,
|
|
300
355
|
);
|
|
301
356
|
});
|
|
302
357
|
}
|
|
303
358
|
|
|
359
|
+
const preparedShareSchema = z.strictObject({
|
|
360
|
+
title: z.string(),
|
|
361
|
+
version: z.number().int().nonnegative(),
|
|
362
|
+
bundle: shareBundleSchema,
|
|
363
|
+
needsRepository: z.boolean(),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
type PreparedShare = z.infer<typeof preparedShareSchema>;
|
|
367
|
+
|
|
368
|
+
/** `undefined` for a board on this machine. */
|
|
369
|
+
export type RemoteShare = (
|
|
370
|
+
input: z.infer<typeof publishSchema>,
|
|
371
|
+
) => Promise<PreparedShare> | undefined;
|
|
372
|
+
|
|
373
|
+
export function remoteShare(
|
|
374
|
+
isRemote: (reviewId: string) => boolean,
|
|
375
|
+
fetch: (request: Request) => Response | Promise<Response>,
|
|
376
|
+
): RemoteShare {
|
|
377
|
+
return ({ reviewId, version }) =>
|
|
378
|
+
isRemote(reviewId)
|
|
379
|
+
? (async () => {
|
|
380
|
+
const response = await fetch(
|
|
381
|
+
new Request(
|
|
382
|
+
`http://whiteboard/reviews-api/${encodeURIComponent(reviewId)}/share-bundle`,
|
|
383
|
+
{
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { "content-type": "application/json" },
|
|
386
|
+
body: JSON.stringify({ version }),
|
|
387
|
+
},
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
392
|
+
|
|
393
|
+
if (!response.ok)
|
|
394
|
+
throw new ReviewInputError(
|
|
395
|
+
z.object({ error: z.string() }).safeParse(body).data?.error ??
|
|
396
|
+
"Could not read the board from its host.",
|
|
397
|
+
response.status === 400 ? 400 : 409,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
return preparedShareSchema.parse(body);
|
|
401
|
+
})()
|
|
402
|
+
: undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
304
405
|
/** Publishing needs only an authored store, never Desktop or recipient workspaces. */
|
|
305
406
|
export function mountSharingPublisher(
|
|
306
407
|
app: Hono,
|
|
@@ -308,23 +409,16 @@ export function mountSharingPublisher(
|
|
|
308
409
|
data: LocalReviewData,
|
|
309
410
|
options: Pick<
|
|
310
411
|
SharingHostOptions,
|
|
311
|
-
"readRepository" | "fetch" | "onPublished" | "onRevoked"
|
|
412
|
+
"readRepository" | "fetch" | "onPublished" | "onRevoked" | "remoteShare"
|
|
312
413
|
> = {},
|
|
313
414
|
) {
|
|
314
|
-
const
|
|
415
|
+
const prepareLocal = async (
|
|
416
|
+
input: z.infer<typeof publishSchema>,
|
|
417
|
+
): Promise<PreparedShare> => {
|
|
315
418
|
const snapshot = store.read(input.reviewId, input.version);
|
|
316
419
|
|
|
317
|
-
if (snapshot.target?.kind
|
|
318
|
-
throw new ReviewInputError(
|
|
319
|
-
"Pin this review to commits before sharing it.",
|
|
320
|
-
);
|
|
420
|
+
if (snapshot.target?.kind !== "worktree") validateShareSources(snapshot);
|
|
321
421
|
|
|
322
|
-
validateShareSources(snapshot);
|
|
323
|
-
|
|
324
|
-
return snapshot;
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
const shareBundle = async (snapshot: Snapshot) => {
|
|
328
422
|
const repositoryId =
|
|
329
423
|
snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
|
|
330
424
|
|
|
@@ -348,24 +442,44 @@ export function mountSharingPublisher(
|
|
|
348
442
|
|
|
349
443
|
if (verified) bundle.repository = verified;
|
|
350
444
|
|
|
351
|
-
return
|
|
445
|
+
return {
|
|
446
|
+
title: snapshot.title,
|
|
447
|
+
version: snapshot.version,
|
|
448
|
+
bundle,
|
|
449
|
+
needsRepository: needsRepository(bundle, snapshot),
|
|
450
|
+
};
|
|
352
451
|
};
|
|
353
452
|
|
|
453
|
+
const prepare = (input: z.infer<typeof publishSchema>) =>
|
|
454
|
+
options.remoteShare?.(input) ?? prepareLocal(input);
|
|
455
|
+
|
|
456
|
+
// Desktop uploads it with its own login.
|
|
457
|
+
app.post("/:id/share-bundle", async (context) => {
|
|
458
|
+
const { version } = publishSchema
|
|
459
|
+
.pick({ version: true })
|
|
460
|
+
.parse(await readBoundedRequestJson(context.req.raw));
|
|
461
|
+
|
|
462
|
+
return context.json(
|
|
463
|
+
await prepareLocal({ reviewId: context.req.param("id"), version }),
|
|
464
|
+
);
|
|
465
|
+
});
|
|
354
466
|
app.post("/sharing/export", async (context) => {
|
|
355
|
-
const
|
|
467
|
+
const { title, bundle, needsRepository } = await prepare(
|
|
356
468
|
publishSchema.parse(await readBoundedRequestJson(context.req.raw)),
|
|
357
469
|
);
|
|
358
470
|
|
|
359
|
-
const bundle = await shareBundle(snapshot);
|
|
360
|
-
|
|
361
471
|
// A link falls back to the browser viewer; a file has no such reader.
|
|
362
|
-
if (
|
|
472
|
+
if (bundle.frozen)
|
|
473
|
+
throw new ReviewInputError(
|
|
474
|
+
"Commit your changes to export this whiteboard as a file.",
|
|
475
|
+
);
|
|
476
|
+
else if (!bundle.repository && needsRepository)
|
|
363
477
|
throw new ReviewInputError(
|
|
364
478
|
"Exporting pinned code needs a GitHub remote the file's reader can fetch from.",
|
|
365
479
|
);
|
|
366
480
|
|
|
367
481
|
return context.json({
|
|
368
|
-
filename: whiteboardFileName(
|
|
482
|
+
filename: whiteboardFileName(title),
|
|
369
483
|
text: whiteboardFileText(bundle),
|
|
370
484
|
});
|
|
371
485
|
});
|
|
@@ -374,7 +488,6 @@ export function mountSharingPublisher(
|
|
|
374
488
|
await readBoundedRequestJson(context.req.raw),
|
|
375
489
|
);
|
|
376
490
|
|
|
377
|
-
const snapshot = shareable(input);
|
|
378
491
|
const account = await readSharingAuth();
|
|
379
492
|
|
|
380
493
|
if (!account)
|
|
@@ -383,7 +496,7 @@ export function mountSharingPublisher(
|
|
|
383
496
|
409,
|
|
384
497
|
);
|
|
385
498
|
|
|
386
|
-
const bundle = await
|
|
499
|
+
const { bundle, version } = await prepare(input);
|
|
387
500
|
|
|
388
501
|
try {
|
|
389
502
|
const result = await new ShareClient(
|
|
@@ -402,17 +515,20 @@ export function mountSharingPublisher(
|
|
|
402
515
|
|
|
403
516
|
options.onPublished?.({
|
|
404
517
|
reviewId: input.reviewId,
|
|
405
|
-
version
|
|
518
|
+
version,
|
|
406
519
|
});
|
|
407
520
|
|
|
408
521
|
return context.json({
|
|
409
522
|
...result,
|
|
410
523
|
url: url.href,
|
|
411
|
-
version
|
|
524
|
+
version,
|
|
412
525
|
});
|
|
413
526
|
} catch (error) {
|
|
414
527
|
if (error instanceof ReviewInputError) throw error;
|
|
415
528
|
|
|
529
|
+
if (error instanceof ShareRequestError)
|
|
530
|
+
throw new ReviewInputError(error.message, 409);
|
|
531
|
+
|
|
416
532
|
if (error instanceof ShareAuthError) {
|
|
417
533
|
// A CI token lives in the environment; only the saved login can go stale.
|
|
418
534
|
if (process.env.DEV_REVIEW_SHARE_TOKEN === undefined)
|
|
@@ -423,8 +539,19 @@ export function mountSharingPublisher(
|
|
|
423
539
|
);
|
|
424
540
|
}
|
|
425
541
|
|
|
542
|
+
if (
|
|
543
|
+
error instanceof DOMException &&
|
|
544
|
+
(error.name === "TimeoutError" || error.name === "AbortError")
|
|
545
|
+
)
|
|
546
|
+
throw new ReviewInputError(
|
|
547
|
+
"The sharing request timed out. Please retry.",
|
|
548
|
+
409,
|
|
549
|
+
);
|
|
550
|
+
|
|
426
551
|
throw new ReviewInputError(
|
|
427
|
-
|
|
552
|
+
error instanceof TypeError
|
|
553
|
+
? "Could not reach the sharing service. Check your connection, then retry."
|
|
554
|
+
: "The sharing service returned an unexpected response. Update Whiteboard or report this issue.",
|
|
428
555
|
409,
|
|
429
556
|
);
|
|
430
557
|
}
|
|
@@ -441,7 +568,9 @@ export function mountSharingPublisher(
|
|
|
441
568
|
"Set DEV_REVIEW_SHARE_TOKEN for CI, or run whiteboard login first.",
|
|
442
569
|
409,
|
|
443
570
|
);
|
|
444
|
-
await new ShareClient(account.origin, account.token).revoke(
|
|
571
|
+
await new ShareClient(account.origin, account.token, options.fetch).revoke(
|
|
572
|
+
shareId,
|
|
573
|
+
);
|
|
445
574
|
options.onRevoked?.({ shareId });
|
|
446
575
|
|
|
447
576
|
return context.json({ shareId, revoked: true });
|