@dev.fast/whiteboard 0.5.0 → 0.5.2-preview.20261104.146
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-BlYamXAG.js} +5 -5
- package/dist/{agent-cli-kHRrwO4n.js → agent-cli-D7fF5xmm.js} +9 -9
- package/dist/agent-cli-DLibAL4u.js +2 -0
- package/dist/{agent-client-CMacGbMp.js → agent-client-BCEbSPWU.js} +4 -4
- package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-CkfxTVJm.js} +12 -8
- package/dist/{background-server-eZWXoVcI.js → background-server-BraP8pJ9.js} +4 -4
- package/dist/{background-server-BLNHbksB.js → background-server-CwToVN1g.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-Bxmiqutp.js → cli-BpadBaGU.js} +1 -1
- package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-BebYF50a.js} +22 -22
- package/dist/cli.js +2 -2
- package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-CxcsLQGx.js} +3 -3
- package/dist/{headless-host-Dl2KZ9IT.js → headless-host-FAlbekFD.js} +561 -415
- package/dist/{headless-host-B4r0s1ks.js → headless-host-cLipIufH.js} +1 -1
- package/dist/{import-YAIU6rB0.js → import-CZ1958L4.js} +234 -62
- package/dist/{local-data-C58vzLZR.js → local-data-DdFkxFK_.js} +41 -16
- package/dist/{mcp-COYWiGVn.js → mcp-Claiy9xE.js} +9 -6
- package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
- package/dist/{profile--BYIjPkc.js → profile-Bsgzi9PA.js} +2 -2
- package/dist/profile-BxdzBxHW.js +2 -0
- package/dist/{remote-attach-C1prLjmj.js → remote-attach-LGTpwpUv.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-BAznkpeu.js} +4 -4
- package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-CLP79Cuk.js} +5 -5
- package/dist/{request-origin-B3_XI2VA.js → request-origin-D6B-zrL8.js} +27 -5
- package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-tTe8bTwg.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-DACDMfqq.js} +4 -4
- package/dist/server/desktop-host.js +26 -38
- package/dist/{server-discovery-WmuFl2S3.js → server-discovery-CV5qf1J8.js} +2 -2
- package/dist/sharing/index.d.ts +137 -14
- package/dist/sharing/index.js +1 -1
- package/dist/{src-B2b8cCoI.js → src-BQDlJXkG.js} +44 -5
- package/dist/{src-DLWol7GZ.js → src-CrrW7lcY.js} +16 -1
- package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-D1wK4yID.js} +3 -3
- package/dist/{thread-state-B7L1mpN_.js → thread-state-WTqSborE.js} +1 -1
- package/dist/{tool-failure-RLR8m9hq.js → tool-failure-YOFGHJCZ.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 +6 -6
- 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/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/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 +25 -1
- package/dist/agent-cli-B8KBHuh_.js +0 -2
- package/dist/profile-BjcXi-oH.js +0 -2
package/src/review-api/store.ts
CHANGED
|
@@ -362,6 +362,11 @@ export interface SharedProvenance {
|
|
|
362
362
|
sharedAt?: number;
|
|
363
363
|
cloneUrl?: string;
|
|
364
364
|
key?: string;
|
|
365
|
+
revisionId?: string;
|
|
366
|
+
/** The version import wrote; anything later is the receiver's edit. */
|
|
367
|
+
importedVersion?: number;
|
|
368
|
+
/** Opened from a link naming a revision: never refreshed. */
|
|
369
|
+
pinned?: boolean;
|
|
365
370
|
}
|
|
366
371
|
|
|
367
372
|
/** Content a new board starts with, as another board left it. */
|
|
@@ -1110,16 +1115,24 @@ export class ReviewStore {
|
|
|
1110
1115
|
|
|
1111
1116
|
return run;
|
|
1112
1117
|
}
|
|
1113
|
-
/** The
|
|
1114
|
-
|
|
1115
|
-
|
|
1118
|
+
/** The boards copied from a share link, newest first, until each is deleted. */
|
|
1119
|
+
copiesOf(
|
|
1120
|
+
key: string,
|
|
1121
|
+
): { reviewId: string; version: number; shared: SharedProvenance }[] {
|
|
1122
|
+
return this.db
|
|
1116
1123
|
.prepare(
|
|
1117
|
-
`SELECT reviews.id
|
|
1118
|
-
|
|
1124
|
+
`SELECT reviews.id, reviews.version, json_extract(versions.snapshot,'$.shared') AS shared
|
|
1125
|
+
FROM reviews JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
|
|
1126
|
+
WHERE CASE WHEN json_valid(versions.snapshot) THEN json_extract(versions.snapshot,'$.shared.key') END=?
|
|
1127
|
+
ORDER BY reviews.rowid DESC`,
|
|
1119
1128
|
)
|
|
1120
|
-
.
|
|
1121
|
-
|
|
1122
|
-
|
|
1129
|
+
.all(key)
|
|
1130
|
+
.map((row) => ({
|
|
1131
|
+
reviewId: String(row.id),
|
|
1132
|
+
version: Number(row.version),
|
|
1133
|
+
// SAFETY: the key matched this row's seed.
|
|
1134
|
+
shared: JSON.parse(String(row.shared)) as SharedProvenance,
|
|
1135
|
+
}));
|
|
1123
1136
|
}
|
|
1124
1137
|
/** Checkouts whose remote is cloneUrl, most recently used first. */
|
|
1125
1138
|
checkoutsOf(cloneUrl: string): string[] {
|
|
@@ -1600,6 +1613,50 @@ export class ReviewStore {
|
|
|
1600
1613
|
author,
|
|
1601
1614
|
).then((source) => this.applyPreparedCommand(source, initial, author));
|
|
1602
1615
|
|
|
1616
|
+
return this.track(prepared);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** Replaces a share copy's content under the same board id; 409 once it moved past `baseVersion`. */
|
|
1620
|
+
refreshCopy(
|
|
1621
|
+
reviewId: string,
|
|
1622
|
+
baseVersion: number,
|
|
1623
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Command boundary: commandSchema.parse below rejects malformed input before mutation.
|
|
1624
|
+
create: unknown,
|
|
1625
|
+
seed: Seed,
|
|
1626
|
+
): Promise<Result> {
|
|
1627
|
+
if (this.closing)
|
|
1628
|
+
return Promise.reject(new Error("Review store is closing."));
|
|
1629
|
+
const command = commandSchema.parse(create);
|
|
1630
|
+
const op = command.operation;
|
|
1631
|
+
|
|
1632
|
+
if (
|
|
1633
|
+
op.type !== "create" ||
|
|
1634
|
+
op.pullRequestUrl ||
|
|
1635
|
+
op.parentId ||
|
|
1636
|
+
op.dependsOn ||
|
|
1637
|
+
op.projectId
|
|
1638
|
+
)
|
|
1639
|
+
throw new ReviewInputError(
|
|
1640
|
+
"A refresh takes a create command with a title and target alone.",
|
|
1641
|
+
);
|
|
1642
|
+
const person: Author = { kind: "person" };
|
|
1643
|
+
|
|
1644
|
+
return this.track(
|
|
1645
|
+
this.prepareCommand(
|
|
1646
|
+
command,
|
|
1647
|
+
undefined,
|
|
1648
|
+
Promise.resolve(undefined),
|
|
1649
|
+
person,
|
|
1650
|
+
).then((source) =>
|
|
1651
|
+
this.applyPreparedCommand(source, seed, person, {
|
|
1652
|
+
reviewId,
|
|
1653
|
+
baseVersion,
|
|
1654
|
+
}),
|
|
1655
|
+
),
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
private track(prepared: Promise<Result>) {
|
|
1603
1660
|
this.preparing.add(prepared);
|
|
1604
1661
|
void prepared
|
|
1605
1662
|
.finally(() => this.preparing.delete(prepared))
|
|
@@ -1690,6 +1747,7 @@ export class ReviewStore {
|
|
|
1690
1747
|
prepared: PreparedCommand,
|
|
1691
1748
|
initial: Seed | undefined,
|
|
1692
1749
|
author: Author,
|
|
1750
|
+
into?: { reviewId: string; baseVersion: number },
|
|
1693
1751
|
) {
|
|
1694
1752
|
const {
|
|
1695
1753
|
op,
|
|
@@ -1703,6 +1761,7 @@ export class ReviewStore {
|
|
|
1703
1761
|
|
|
1704
1762
|
return this.enqueue(async () => {
|
|
1705
1763
|
if (
|
|
1764
|
+
!into &&
|
|
1706
1765
|
op.type === "create" &&
|
|
1707
1766
|
(op.pullRequestUrl
|
|
1708
1767
|
? op.reuseExisting !== false
|
|
@@ -1774,9 +1833,19 @@ export class ReviewStore {
|
|
|
1774
1833
|
fromTrash: true,
|
|
1775
1834
|
});
|
|
1776
1835
|
|
|
1777
|
-
const id =
|
|
1836
|
+
const id =
|
|
1837
|
+
into?.reviewId ?? (op.type === "create" ? randomUUID() : op.reviewId);
|
|
1778
1838
|
|
|
1779
|
-
const previous =
|
|
1839
|
+
const previous =
|
|
1840
|
+
op.type === "create" && !into ? undefined : this.read(id);
|
|
1841
|
+
|
|
1842
|
+
if (
|
|
1843
|
+
into &&
|
|
1844
|
+
(!previous?.shared?.key || previous.shared.key !== initial?.shared?.key)
|
|
1845
|
+
)
|
|
1846
|
+
throw new ReviewInputError(
|
|
1847
|
+
"Only a copy of the same share link refreshes in place.",
|
|
1848
|
+
);
|
|
1780
1849
|
|
|
1781
1850
|
let snapshot: Snapshot =
|
|
1782
1851
|
op.type === "create"
|
|
@@ -1994,9 +2063,11 @@ export class ReviewStore {
|
|
|
1994
2063
|
restore: op.type === "restore",
|
|
1995
2064
|
repin: op.type === "set_target",
|
|
1996
2065
|
copied: initial !== undefined,
|
|
2066
|
+
inPlace: into !== undefined,
|
|
2067
|
+
baseVersion: into?.baseVersion,
|
|
1997
2068
|
projectLive: op.type !== "restore" && !resolvedTarget,
|
|
1998
2069
|
result: {
|
|
1999
|
-
...(op.type === "create" && { created: true }),
|
|
2070
|
+
...(op.type === "create" && !into && { created: true }),
|
|
2000
2071
|
targetId: applied?.targetId ?? lensTarget?.targetId,
|
|
2001
2072
|
...(applied && { type: applied.type }),
|
|
2002
2073
|
...(lensTarget && { type: lensTarget.type }),
|
|
@@ -2039,6 +2110,8 @@ export class ReviewStore {
|
|
|
2039
2110
|
repin?: boolean;
|
|
2040
2111
|
/** A board made from another's content, whose images stayed behind. */
|
|
2041
2112
|
copied?: boolean;
|
|
2113
|
+
/** A share copy refreshed in place, in Trash or not. */
|
|
2114
|
+
inPlace?: boolean;
|
|
2042
2115
|
projectLive: boolean;
|
|
2043
2116
|
/** The ids a person's save gave new blocks, by client key. */
|
|
2044
2117
|
personIds?: Record<string, string>;
|
|
@@ -2104,6 +2177,9 @@ export class ReviewStore {
|
|
|
2104
2177
|
}));
|
|
2105
2178
|
warnings = [...(change.warnings ?? []), ...warnings];
|
|
2106
2179
|
|
|
2180
|
+
if (change.copied && snapshot.shared)
|
|
2181
|
+
snapshot.shared.importedVersion = snapshot.version;
|
|
2182
|
+
|
|
2107
2183
|
const result: Result = {
|
|
2108
2184
|
...change.result,
|
|
2109
2185
|
reviewId: id,
|
|
@@ -2156,7 +2232,8 @@ export class ReviewStore {
|
|
|
2156
2232
|
previous
|
|
2157
2233
|
? () => {
|
|
2158
2234
|
// A board in Trash is read-only, whoever writes.
|
|
2159
|
-
this.
|
|
2235
|
+
if (change.inPlace) this.assertExists(id);
|
|
2236
|
+
else this.assertWritable(id);
|
|
2160
2237
|
|
|
2161
2238
|
if (change.baseVersion !== undefined)
|
|
2162
2239
|
assertBaseVersion(change.baseVersion, this.currentVersion(id));
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
ReviewTelemetry,
|
|
55
55
|
type ReviewTelemetryContext,
|
|
56
56
|
} from "@review/review-telemetry";
|
|
57
|
+
import { remoteShare } from "@review/sharing/host.js";
|
|
57
58
|
import { Hono } from "hono";
|
|
58
59
|
import { z } from "zod";
|
|
59
60
|
|
|
@@ -204,6 +205,12 @@ export function createGlobalReviewServer(
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
const hooks = reviewLifecycleTelemetry(
|
|
209
|
+
telemetry,
|
|
210
|
+
(reviewId) => reviewStore.summary(reviewId)?.firstCreatedAt,
|
|
211
|
+
() => aliasInstallationToAccount(telemetry),
|
|
212
|
+
);
|
|
213
|
+
|
|
207
214
|
const core = createWhiteboardCore({
|
|
208
215
|
profile: {
|
|
209
216
|
store: reviewStore,
|
|
@@ -229,11 +236,16 @@ export function createGlobalReviewServer(
|
|
|
229
236
|
home: devReviewHome(),
|
|
230
237
|
};
|
|
231
238
|
},
|
|
232
|
-
hooks:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
hooks: {
|
|
240
|
+
...hooks,
|
|
241
|
+
sharing: {
|
|
242
|
+
...hooks.sharing,
|
|
243
|
+
remoteShare: remoteShare(
|
|
244
|
+
(reviewId) => gateway.isRemote(reviewId),
|
|
245
|
+
(request) => gateway.fetch(request),
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
237
249
|
ask: { tools: askTools },
|
|
238
250
|
});
|
|
239
251
|
|
|
@@ -18,6 +18,7 @@ import { cliAskTools } from "@review/ask/threads.js";
|
|
|
18
18
|
import { desktopUsesHome } from "@review/desktop-discovery.js";
|
|
19
19
|
import { findReviewPackageRoot } from "@review/package-paths.js";
|
|
20
20
|
import { openReviewProfile } from "@review/review-api/profile.js";
|
|
21
|
+
import { readAnimationsEnabled } from "@review/review-preferences.js";
|
|
21
22
|
import {
|
|
22
23
|
DESKTOP_TAKEOVER_SIGNAL,
|
|
23
24
|
type ReviewServerDiscovery,
|
|
@@ -198,6 +199,8 @@ async function serve(input: HeadlessServerInput) {
|
|
|
198
199
|
manageWorkspaces: true,
|
|
199
200
|
});
|
|
200
201
|
|
|
202
|
+
const animationsEnabled = await readAnimationsEnabled(input.stateDir);
|
|
203
|
+
|
|
201
204
|
const discovery: ReviewServerDiscovery = {
|
|
202
205
|
version: 1,
|
|
203
206
|
instanceId: randomUUID(),
|
|
@@ -221,6 +224,7 @@ async function serve(input: HeadlessServerInput) {
|
|
|
221
224
|
token: discovery.token,
|
|
222
225
|
instanceId: discovery.instanceId,
|
|
223
226
|
softwareMapEnabled: input.softwareMapEnabled,
|
|
227
|
+
animations: () => animationsEnabled,
|
|
224
228
|
status: () => ({ key: "headless", home: input.stateDir }),
|
|
225
229
|
ask: {
|
|
226
230
|
tools: cliAskTools(() => (existsSync(cliPath) ? cliPath : undefined), {
|
|
@@ -69,6 +69,7 @@ const REMOTE_ROUTES: readonly RemoteRoute[] = [
|
|
|
69
69
|
{ method: "GET", path: "maps/:name" },
|
|
70
70
|
{ method: "POST", path: "navigator" },
|
|
71
71
|
{ method: "POST", path: "copy-context" },
|
|
72
|
+
{ method: "POST", path: "share-bundle", slow: true },
|
|
72
73
|
{ method: "GET", path: "language-context", slow: true },
|
|
73
74
|
{ method: "GET", path: "ask/agents" },
|
|
74
75
|
{ method: "GET", path: "ask/agents/:agent/offer", slow: true },
|
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
readBuildCommit,
|
|
17
17
|
readReviewPackageVersion,
|
|
18
18
|
} from "@review/package-paths.js";
|
|
19
|
+
import {
|
|
20
|
+
type RenderRequest,
|
|
21
|
+
type RenderedFrame,
|
|
22
|
+
renderedFrameSchema,
|
|
23
|
+
} from "@review/review-api/animation-render.js";
|
|
19
24
|
import {
|
|
20
25
|
type AuthoringCapabilities,
|
|
21
26
|
type ReviewApiHooks,
|
|
@@ -95,7 +100,8 @@ export function createReviewServerApp(input: {
|
|
|
95
100
|
mountFeedback(app);
|
|
96
101
|
app.post("/control/result", async (context) => {
|
|
97
102
|
const accepted = input.relay.acceptResult(
|
|
98
|
-
|
|
103
|
+
// A rendered frame's PNG rides back in a result.
|
|
104
|
+
await readBoundedRequestJson(context.req.raw, VERB_RESULT_MAX_BYTES),
|
|
99
105
|
);
|
|
100
106
|
|
|
101
107
|
return serverJson(accepted ? 200 : 404, { ok: accepted });
|
|
@@ -144,6 +150,10 @@ export interface WhiteboardCoreInput {
|
|
|
144
150
|
};
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
/** A verb result may carry a captured frame: a PNG of up to 1600 pixels on
|
|
154
|
+
* its longest edge, base64. */
|
|
155
|
+
const VERB_RESULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
156
|
+
|
|
147
157
|
export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
148
158
|
const { store, data } = input.profile;
|
|
149
159
|
|
|
@@ -154,7 +164,11 @@ export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
|
154
164
|
relay: input.relay,
|
|
155
165
|
});
|
|
156
166
|
|
|
157
|
-
const callbacks = relayReviewCallbacks(
|
|
167
|
+
const callbacks = relayReviewCallbacks(
|
|
168
|
+
input.relay,
|
|
169
|
+
input.softwareMapEnabled,
|
|
170
|
+
input.animations,
|
|
171
|
+
);
|
|
158
172
|
|
|
159
173
|
const { ask } = input;
|
|
160
174
|
|
|
@@ -195,7 +209,8 @@ export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
|
195
209
|
threads: askThreads,
|
|
196
210
|
agents: async () => detectAskAgents(await ask?.env),
|
|
197
211
|
},
|
|
198
|
-
|
|
212
|
+
callbacks.animations,
|
|
213
|
+
callbacks.render,
|
|
199
214
|
);
|
|
200
215
|
|
|
201
216
|
// Desktop's sharing host mounts the publisher with the rest of sharing.
|
|
@@ -205,6 +220,7 @@ export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
|
205
220
|
app,
|
|
206
221
|
api,
|
|
207
222
|
close: () => {
|
|
223
|
+
callbacks.close();
|
|
208
224
|
stopLocal?.();
|
|
209
225
|
closeDeletedThreads();
|
|
210
226
|
askThreads?.closeAll();
|
|
@@ -212,12 +228,53 @@ export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
|
212
228
|
};
|
|
213
229
|
}
|
|
214
230
|
|
|
215
|
-
|
|
231
|
+
const capabilitiesAnswerSchema = z.object({
|
|
232
|
+
softwareMapEnabled: z.boolean(),
|
|
233
|
+
// Older Desktops omit it.
|
|
234
|
+
animationsEnabled: z.boolean().optional(),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The Desktop callbacks `createReviewApi` takes, answered over the relay.
|
|
239
|
+
* `animations` never asks Desktop: it is the last answer while one is
|
|
240
|
+
* attached, else the host's own.
|
|
241
|
+
*/
|
|
216
242
|
export function relayReviewCallbacks(
|
|
217
243
|
relay: ReviewDesktopVerbRelay,
|
|
218
244
|
softwareMapEnabled = false,
|
|
245
|
+
animations: () => boolean = () => false,
|
|
219
246
|
) {
|
|
247
|
+
let desktopAnimations: boolean | undefined;
|
|
248
|
+
|
|
249
|
+
const capabilities = async (): Promise<AuthoringCapabilities> => {
|
|
250
|
+
if (!relay.attached) return { desktopAvailable: false, softwareMapEnabled };
|
|
251
|
+
|
|
252
|
+
const result = await relay.dispatch({
|
|
253
|
+
name: "authoringCapabilities",
|
|
254
|
+
args: {},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!result.ok) throw new ReviewInputError(result.error, 409);
|
|
258
|
+
|
|
259
|
+
const answer = capabilitiesAnswerSchema.parse(result.result);
|
|
260
|
+
desktopAnimations = answer.animationsEnabled;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
desktopAvailable: true,
|
|
264
|
+
softwareMapEnabled: answer.softwareMapEnabled,
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const stop = relay.onAttachedChange(() => {
|
|
269
|
+
desktopAnimations = undefined;
|
|
270
|
+
|
|
271
|
+
if (relay.attached) capabilities().catch(() => {});
|
|
272
|
+
});
|
|
273
|
+
|
|
220
274
|
return {
|
|
275
|
+
capabilities,
|
|
276
|
+
animations: () => desktopAnimations ?? animations(),
|
|
277
|
+
close: stop,
|
|
221
278
|
async open(review: {
|
|
222
279
|
reviewId: string;
|
|
223
280
|
title: string;
|
|
@@ -231,21 +288,23 @@ export function relayReviewCallbacks(
|
|
|
231
288
|
|
|
232
289
|
return z.object({ softwareMapEnabled: z.boolean() }).parse(result.result);
|
|
233
290
|
},
|
|
234
|
-
async
|
|
291
|
+
async render(request: RenderRequest): Promise<RenderedFrame> {
|
|
235
292
|
if (!relay.attached)
|
|
236
|
-
|
|
293
|
+
throw new ReviewInputError(
|
|
294
|
+
"No Whiteboard Desktop is attached, so nothing can show the animation. Open the whiteboard in Desktop and render again.",
|
|
295
|
+
409,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const { reviewId, targetId, frame } = request;
|
|
237
299
|
|
|
238
300
|
const result = await relay.dispatch({
|
|
239
|
-
name: "
|
|
240
|
-
args: {},
|
|
301
|
+
name: "renderAnimation",
|
|
302
|
+
args: { reviewId, targetId, frame },
|
|
241
303
|
});
|
|
242
304
|
|
|
243
305
|
if (!result.ok) throw new ReviewInputError(result.error, 409);
|
|
244
306
|
|
|
245
|
-
return
|
|
246
|
-
desktopAvailable: true,
|
|
247
|
-
...z.object({ softwareMapEnabled: z.boolean() }).parse(result.result),
|
|
248
|
-
};
|
|
307
|
+
return renderedFrameSchema.parse(result.result);
|
|
249
308
|
},
|
|
250
309
|
};
|
|
251
310
|
}
|
package/src/sharing/client.ts
CHANGED
|
@@ -4,8 +4,13 @@ import { type JsonValue, parseJsonText } from "@dev.fast/json";
|
|
|
4
4
|
import {
|
|
5
5
|
MAX_SHARE_BYTES,
|
|
6
6
|
type ShareBundle,
|
|
7
|
+
type ShareRevisions,
|
|
8
|
+
type SharedMeta,
|
|
7
9
|
capabilitySchema,
|
|
8
|
-
|
|
10
|
+
shareCreatedSchema,
|
|
11
|
+
shareIdSchema,
|
|
12
|
+
shareRevisionsSchema,
|
|
13
|
+
sharedMetaSchema,
|
|
9
14
|
} from "@dev.fast/review-share-protocol";
|
|
10
15
|
import {
|
|
11
16
|
StreamLimitError,
|
|
@@ -15,6 +20,10 @@ import { Agent } from "undici";
|
|
|
15
20
|
import { z } from "zod";
|
|
16
21
|
|
|
17
22
|
import { shareRequestBody } from "./export.js";
|
|
23
|
+
import {
|
|
24
|
+
WHITEBOARD_UPGRADE_REQUIRED_MESSAGE,
|
|
25
|
+
parseWhiteboardFile,
|
|
26
|
+
} from "./file.js";
|
|
18
27
|
|
|
19
28
|
const MAX_RESPONSE_BYTES = MAX_SHARE_BYTES;
|
|
20
29
|
|
|
@@ -37,14 +46,6 @@ const jwtPayloadSchema = z.object({ exp: z.number() });
|
|
|
37
46
|
|
|
38
47
|
const bearers = new Map<string, Promise<{ bearer: string; until: number }>>();
|
|
39
48
|
|
|
40
|
-
const linkSchema = z.strictObject({ shareId: z.uuid(), url: z.url() });
|
|
41
|
-
|
|
42
|
-
const receivedSchema = z.object({
|
|
43
|
-
sender: z.strictObject({ login: z.string().min(1).max(256) }),
|
|
44
|
-
sharedAt: z.number(),
|
|
45
|
-
size: z.number().int().positive().max(MAX_SHARE_BYTES),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
49
|
/** Size is checked while reading, even if Content-Length is absent or false. */
|
|
49
50
|
export async function readBoundedBytes(
|
|
50
51
|
response: Response,
|
|
@@ -76,6 +77,79 @@ export class ShareAuthError extends Error {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
export class ShareRequestError extends Error {
|
|
81
|
+
constructor(
|
|
82
|
+
readonly status: number,
|
|
83
|
+
message: string,
|
|
84
|
+
) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "ShareRequestError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const errorSchema = z.object({ error: z.object({ code: z.string() }) });
|
|
91
|
+
|
|
92
|
+
async function errorCode(response: Response) {
|
|
93
|
+
if (!response.body) return undefined;
|
|
94
|
+
|
|
95
|
+
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
96
|
+
await response.body.cancel();
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const bytes = await readBoundedStream(response.body, 16 * 1024);
|
|
103
|
+
|
|
104
|
+
return errorSchema.safeParse(parseJsonText(bytes.toString())).data?.error
|
|
105
|
+
.code;
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requestErrorMessage(response: Response, code: string | undefined) {
|
|
112
|
+
const { status, headers } = response;
|
|
113
|
+
|
|
114
|
+
if (status === 426 || code === "upgrade_required")
|
|
115
|
+
return WHITEBOARD_UPGRADE_REQUIRED_MESSAGE;
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
status === 403 &&
|
|
119
|
+
headers.get("content-type")?.includes("text/html") &&
|
|
120
|
+
headers.get("server")?.toLowerCase() === "cloudflare"
|
|
121
|
+
)
|
|
122
|
+
return "The sharing service's security filter blocked this request. Please report this issue.";
|
|
123
|
+
|
|
124
|
+
if (status === 413)
|
|
125
|
+
return "This review is too large to share. Remove large resources, then retry.";
|
|
126
|
+
|
|
127
|
+
if (status === 429)
|
|
128
|
+
return code === "share_quota_exceeded"
|
|
129
|
+
? "Your sharing storage limit has been reached. Please contact support."
|
|
130
|
+
: "Too many sharing requests. Wait a moment, then retry.";
|
|
131
|
+
|
|
132
|
+
if (status === 400 && code === "invalid_request")
|
|
133
|
+
return "The sharing service could not accept this review. Update Whiteboard or report this issue.";
|
|
134
|
+
|
|
135
|
+
if (status >= 500)
|
|
136
|
+
return `The sharing service is temporarily unavailable (HTTP ${status}). Try again shortly.`;
|
|
137
|
+
|
|
138
|
+
return `The sharing service refused this request (HTTP ${status}). Please report this issue.`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function requestError(response: Response) {
|
|
142
|
+
const message = requestErrorMessage(response, await errorCode(response));
|
|
143
|
+
const ray = response.headers.get("cf-ray");
|
|
144
|
+
|
|
145
|
+
const reference =
|
|
146
|
+
ray && /^[a-f0-9]{16}(?:-[a-z]{3})?$/i.test(ray)
|
|
147
|
+
? ` Reference: ${ray}.`
|
|
148
|
+
: "";
|
|
149
|
+
|
|
150
|
+
return new ShareRequestError(response.status, message + reference);
|
|
151
|
+
}
|
|
152
|
+
|
|
79
153
|
/** Account tokens go only to the configured API origin. */
|
|
80
154
|
export class ShareClient {
|
|
81
155
|
private readonly origin: string;
|
|
@@ -132,10 +206,12 @@ export class ShareClient {
|
|
|
132
206
|
const response = await this.send(this.origin + route, init);
|
|
133
207
|
|
|
134
208
|
if (!response.ok) {
|
|
135
|
-
|
|
209
|
+
if (response.status === 401 && !anonymous) {
|
|
210
|
+
await response.body?.cancel();
|
|
211
|
+
throw new ShareAuthError();
|
|
212
|
+
}
|
|
136
213
|
|
|
137
|
-
|
|
138
|
-
throw new Error(`Share request failed (${response.status}).`);
|
|
214
|
+
throw await requestError(response);
|
|
139
215
|
}
|
|
140
216
|
|
|
141
217
|
return readBoundedBytes(response, maxBytes);
|
|
@@ -169,14 +245,19 @@ export class ShareClient {
|
|
|
169
245
|
});
|
|
170
246
|
|
|
171
247
|
if (!response.ok) {
|
|
172
|
-
await response.body?.cancel();
|
|
173
|
-
|
|
174
248
|
// A service without the JWT plugin still takes the device token.
|
|
175
|
-
if (response.status === 404 || response.status === 405)
|
|
249
|
+
if (response.status === 404 || response.status === 405) {
|
|
250
|
+
await response.body?.cancel();
|
|
251
|
+
|
|
176
252
|
return { bearer: token, until: Date.now() + 10 * 60_000 };
|
|
253
|
+
}
|
|
177
254
|
|
|
178
|
-
if (response.status === 401)
|
|
179
|
-
|
|
255
|
+
if (response.status === 401) {
|
|
256
|
+
await response.body?.cancel();
|
|
257
|
+
throw new ShareAuthError();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw await requestError(response);
|
|
180
261
|
}
|
|
181
262
|
|
|
182
263
|
const { token: jwt } = jwtSchema.parse(
|
|
@@ -206,7 +287,7 @@ export class ShareClient {
|
|
|
206
287
|
}
|
|
207
288
|
|
|
208
289
|
async create(bundle: ShareBundle, requestId: string = randomUUID()) {
|
|
209
|
-
return
|
|
290
|
+
return shareCreatedSchema.parse(
|
|
210
291
|
await this.api(
|
|
211
292
|
"/api/v2/shares",
|
|
212
293
|
"POST",
|
|
@@ -222,7 +303,7 @@ export class ShareClient {
|
|
|
222
303
|
}
|
|
223
304
|
|
|
224
305
|
async recoverLink(shareId: string) {
|
|
225
|
-
return
|
|
306
|
+
return shareCreatedSchema.parse(
|
|
226
307
|
await this.api(`/api/v2/shares/${z.uuid().parse(shareId)}/link`),
|
|
227
308
|
);
|
|
228
309
|
}
|
|
@@ -231,16 +312,35 @@ export class ShareClient {
|
|
|
231
312
|
return this.api(`/api/v2/shares/${z.uuid().parse(shareId)}`, "DELETE");
|
|
232
313
|
}
|
|
233
314
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
315
|
+
async revisions(shareId: string): Promise<ShareRevisions> {
|
|
316
|
+
return shareRevisionsSchema.parse(
|
|
317
|
+
await this.api(
|
|
318
|
+
`/api/v2/shares/${shareIdSchema.parse(shareId)}/revisions`,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Who shared a link's revision, and which revision is its latest. */
|
|
324
|
+
async shared(capability: string, revision?: string): Promise<SharedMeta> {
|
|
325
|
+
const query =
|
|
326
|
+
revision === undefined ? "" : `?rev=${shareIdSchema.parse(revision)}`;
|
|
237
327
|
|
|
238
|
-
|
|
239
|
-
await this.api(
|
|
328
|
+
return sharedMetaSchema.parse(
|
|
329
|
+
await this.api(
|
|
330
|
+
`/api/v2/shared/${capabilitySchema.parse(capability)}${query}`,
|
|
331
|
+
"GET",
|
|
332
|
+
undefined,
|
|
333
|
+
true,
|
|
334
|
+
),
|
|
240
335
|
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** The bundle of the revision the meta names, and who shared it. */
|
|
339
|
+
async download(capability: string, revision?: string) {
|
|
340
|
+
const meta = await this.shared(capability, revision);
|
|
241
341
|
|
|
242
342
|
const bytes = await this.request(
|
|
243
|
-
|
|
343
|
+
`/api/v2/shared/${capability}/bundle?rev=${meta.revision.id}`,
|
|
244
344
|
"GET",
|
|
245
345
|
undefined,
|
|
246
346
|
true,
|
|
@@ -248,13 +348,10 @@ export class ShareClient {
|
|
|
248
348
|
);
|
|
249
349
|
|
|
250
350
|
return {
|
|
251
|
-
bundle:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
login: received.sender.login,
|
|
256
|
-
sharedAt: received.sharedAt,
|
|
257
|
-
},
|
|
351
|
+
bundle: parseWhiteboardFile(new TextDecoder().decode(bytes)),
|
|
352
|
+
attribution: { login: meta.sender.login, sharedAt: meta.sharedAt },
|
|
353
|
+
revision: meta.revision,
|
|
354
|
+
latest: meta.latest,
|
|
258
355
|
};
|
|
259
356
|
}
|
|
260
357
|
}
|