@dev.fast/whiteboard 0.0.0-stage → 0.2.0

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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/account-alias-DOHN1RSH.js +973 -0
  5. package/dist/agent-cli-ChOq0ZuO.js +117 -0
  6. package/dist/agent-cli-DzWfZr6G.js +2 -0
  7. package/dist/agent-client-CPQo7iTI.js +305 -0
  8. package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
  9. package/dist/build-info.json +1 -0
  10. package/dist/cli-hVgHcdsv.js +47 -0
  11. package/dist/cli-runner-D8y2_luU.js +5095 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +128 -0
  14. package/dist/client-DFoh50W3.js +950 -0
  15. package/dist/desktop-discovery-DBO-PM2V.js +164 -0
  16. package/dist/error-message-OtiDonty.js +6 -0
  17. package/dist/fs-utils-BMPLt0cr.js +22 -0
  18. package/dist/fuzzy-match-BoAmcyak.js +30 -0
  19. package/dist/headless-host-BJxf4dUU.js +94 -0
  20. package/dist/input-error-OLgB_h31.js +11 -0
  21. package/dist/local-data-CxLk75rx.js +4491 -0
  22. package/dist/mcp-QeF8vkdf.js +140 -0
  23. package/dist/package-paths-B6-zxvIO.js +47 -0
  24. package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
  25. package/dist/profile-4ry2f3AM.js +76 -0
  26. package/dist/profile-Bh5FIrmf.js +2 -0
  27. package/dist/request-origin-D_4QIdoN.js +811 -0
  28. package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
  29. package/dist/review-home-paths-6zZH-1c9.js +2560 -0
  30. package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
  31. package/dist/runtime-CKiWhGrf.js +52 -0
  32. package/dist/runtime.d.ts +9 -0
  33. package/dist/runtime.js +2 -0
  34. package/dist/s3-SzLpeCG6.js +345 -0
  35. package/dist/s3-config-BgxkSoOy.js +2 -0
  36. package/dist/s3-config-CAxhO9u_.js +676 -0
  37. package/dist/server/desktop-host.d.ts +4 -0
  38. package/dist/server/desktop-host.js +1702 -0
  39. package/dist/server-discovery-Dbzn3w6Z.js +66 -0
  40. package/dist/sharing/index.d.ts +1977 -0
  41. package/dist/sharing/index.js +2 -0
  42. package/dist/src-CGP5ytbV.js +283 -0
  43. package/dist/src-CmdiBL20.js +3676 -0
  44. package/dist/src-DnwdaQ2r.js +865 -0
  45. package/dist/src-X9phtB2j.js +68 -0
  46. package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
  47. package/dist/tool-failure-C8zB73HV.js +98 -0
  48. package/dist/tutorial-trace-DrBvkaeL.js +43 -0
  49. package/instructions/authoring.md +36 -0
  50. package/instructions/file-lenses.md +13 -0
  51. package/instructions/scratchpad.md +23 -0
  52. package/instructions/trace-archaeology.md +113 -0
  53. package/onboarding.md +8 -0
  54. package/package.json +106 -3
  55. package/src/agent-selection.ts +100 -0
  56. package/src/agent-session-ref.ts +80 -0
  57. package/src/ask/agents.ts +351 -0
  58. package/src/ask/checkout-files.ts +47 -0
  59. package/src/ask/file-refs.ts +111 -0
  60. package/src/ask/pi-mcp.ts +154 -0
  61. package/src/ask/protocol.ts +244 -0
  62. package/src/ask/thread-state.ts +350 -0
  63. package/src/ask/thread.ts +1400 -0
  64. package/src/ask/threads.ts +219 -0
  65. package/src/ask/watch.ts +79 -0
  66. package/src/cli-install.ts +1041 -0
  67. package/src/cli-runner.ts +1461 -0
  68. package/src/cli-runtime-info.ts +35 -0
  69. package/src/cli.ts +222 -0
  70. package/src/connect-prompts.ts +248 -0
  71. package/src/cursor-deeplink.ts +11 -0
  72. package/src/desktop-discovery.ts +355 -0
  73. package/src/diff-selection-migration.ts +63 -0
  74. package/src/embedded-posthog-key.ts +6 -0
  75. package/src/error-telemetry.ts +247 -0
  76. package/src/evidence.ts +14 -0
  77. package/src/exception-telemetry.ts +126 -0
  78. package/src/fixtures/blocks/call_stack_diff.json +27 -0
  79. package/src/fixtures/blocks/callout.json +11 -0
  80. package/src/fixtures/blocks/code.json +9 -0
  81. package/src/fixtures/blocks/code_peek.json +8 -0
  82. package/src/fixtures/blocks/database_lens.json +51 -0
  83. package/src/fixtures/blocks/divider.json +1 -0
  84. package/src/fixtures/blocks/fixtures.ts +33 -0
  85. package/src/fixtures/blocks/flow_diagram.json +36 -0
  86. package/src/fixtures/blocks/ids.ts +8 -0
  87. package/src/fixtures/blocks/image.json +9 -0
  88. package/src/fixtures/blocks/markdown.json +7 -0
  89. package/src/fixtures/blocks/section.json +11 -0
  90. package/src/fixtures/blocks/sequence.json +31 -0
  91. package/src/fixtures/blocks/software_map.json +7 -0
  92. package/src/fixtures/blocks/trace_quote.json +9 -0
  93. package/src/fixtures/blocks/tutorial.json +51 -0
  94. package/src/fs-utils.ts +32 -0
  95. package/src/fuzzy-match.ts +94 -0
  96. package/src/install.ts +29 -0
  97. package/src/legacy-skills.ts +133 -0
  98. package/src/lens-selection.ts +230 -0
  99. package/src/markdown-latex-math.ts +230 -0
  100. package/src/markdown.ts +81 -0
  101. package/src/package-paths.ts +53 -0
  102. package/src/posthog-capture-client.ts +610 -0
  103. package/src/review-api/README.md +295 -0
  104. package/src/review-api/activity.ts +340 -0
  105. package/src/review-api/agent-cli.ts +219 -0
  106. package/src/review-api/agent-client.ts +188 -0
  107. package/src/review-api/anchor-quotes.ts +88 -0
  108. package/src/review-api/ask-history.ts +242 -0
  109. package/src/review-api/authoring-tools.ts +211 -0
  110. package/src/review-api/blocks/call_stack_diff.ts +75 -0
  111. package/src/review-api/blocks/callout.ts +24 -0
  112. package/src/review-api/blocks/code.ts +9 -0
  113. package/src/review-api/blocks/code_peek.ts +13 -0
  114. package/src/review-api/blocks/database_lens.ts +150 -0
  115. package/src/review-api/blocks/definition.ts +40 -0
  116. package/src/review-api/blocks/divider.ts +6 -0
  117. package/src/review-api/blocks/flow_diagram.ts +111 -0
  118. package/src/review-api/blocks/image.ts +10 -0
  119. package/src/review-api/blocks/index.ts +90 -0
  120. package/src/review-api/blocks/markdown.ts +17 -0
  121. package/src/review-api/blocks/section.ts +24 -0
  122. package/src/review-api/blocks/sequence.ts +56 -0
  123. package/src/review-api/blocks/software_map.ts +9 -0
  124. package/src/review-api/blocks/trace_quote.ts +10 -0
  125. package/src/review-api/blocks/tutorial.ts +39 -0
  126. package/src/review-api/checkout-fs.ts +14 -0
  127. package/src/review-api/client.ts +1 -0
  128. package/src/review-api/comparison-coverage.ts +304 -0
  129. package/src/review-api/component-reference.ts +30 -0
  130. package/src/review-api/diff-lenses.ts +175 -0
  131. package/src/review-api/document-headings.ts +51 -0
  132. package/src/review-api/document-text.ts +200 -0
  133. package/src/review-api/document.ts +906 -0
  134. package/src/review-api/file-lenses.ts +100 -0
  135. package/src/review-api/http.ts +1877 -0
  136. package/src/review-api/image-decode.ts +29 -0
  137. package/src/review-api/input-error.ts +9 -0
  138. package/src/review-api/instructions.ts +89 -0
  139. package/src/review-api/lens-alignment.ts +52 -0
  140. package/src/review-api/local-data.ts +1837 -0
  141. package/src/review-api/map-input.ts +154 -0
  142. package/src/review-api/mcp-client-agent.ts +32 -0
  143. package/src/review-api/mcp.ts +240 -0
  144. package/src/review-api/origin.ts +43 -0
  145. package/src/review-api/profile.ts +146 -0
  146. package/src/review-api/public-tools.ts +102 -0
  147. package/src/review-api/pull-request.ts +389 -0
  148. package/src/review-api/read-schemas.ts +85 -0
  149. package/src/review-api/recovery.ts +2 -0
  150. package/src/review-api/request-origin.ts +33 -0
  151. package/src/review-api/review-progress.ts +382 -0
  152. package/src/review-api/status-tool.ts +10 -0
  153. package/src/review-api/store-schema.ts +22 -0
  154. package/src/review-api/store.ts +1647 -0
  155. package/src/review-api/tool-failure.ts +64 -0
  156. package/src/review-api/trace-schema.ts +13 -0
  157. package/src/review-api/traces.ts +119 -0
  158. package/src/review-api/unsupported-files.integration.ts +91 -0
  159. package/src/review-api/workspaces.ts +692 -0
  160. package/src/review-api/worktree-source.ts +170 -0
  161. package/src/review-api/worktree-structural.integration.ts +320 -0
  162. package/src/review-app-launcher.ts +432 -0
  163. package/src/review-app-picker.ts +168 -0
  164. package/src/review-app.ts +134 -0
  165. package/src/review-bundled-tools.ts +249 -0
  166. package/src/review-checkout-paths.ts +37 -0
  167. package/src/review-diff-files.ts +89 -0
  168. package/src/review-head-checkout.ts +300 -0
  169. package/src/review-home-paths.ts +73 -0
  170. package/src/review-info.ts +59 -0
  171. package/src/review-instances.ts +113 -0
  172. package/src/review-logger.ts +183 -0
  173. package/src/review-preferences.ts +88 -0
  174. package/src/review-prepare.ts +291 -0
  175. package/src/review-stack.ts +112 -0
  176. package/src/review-telemetry.ts +1066 -0
  177. package/src/runtime.ts +74 -0
  178. package/src/server/account-alias.ts +30 -0
  179. package/src/server/bounded-stream.ts +34 -0
  180. package/src/server/bug-report.ts +301 -0
  181. package/src/server/client-error-budget.ts +45 -0
  182. package/src/server/crash-report.ts +273 -0
  183. package/src/server/desktop-host-shutdown.ts +64 -0
  184. package/src/server/desktop-host.ts +190 -0
  185. package/src/server/desktop-server.ts +703 -0
  186. package/src/server/diffr-config.ts +509 -0
  187. package/src/server/diffr-languages.ts +97 -0
  188. package/src/server/global-verb-relay.ts +190 -0
  189. package/src/server/headless-host.ts +149 -0
  190. package/src/server/hono-http.ts +162 -0
  191. package/src/server/http-json.ts +24 -0
  192. package/src/server/json-review-reporting.ts +174 -0
  193. package/src/server/process-error-telemetry.ts +174 -0
  194. package/src/server/review-api-parsers.ts +90 -0
  195. package/src/server/review-lifecycle-telemetry.ts +73 -0
  196. package/src/server/review-open-watchdog.ts +46 -0
  197. package/src/server/review-server-core.ts +290 -0
  198. package/src/server/structural-comparisons.ts +153 -0
  199. package/src/server/structural-diff.ts +186 -0
  200. package/src/server/tutorial-service.ts +241 -0
  201. package/src/server/ui-telemetry.ts +198 -0
  202. package/src/server-discovery.ts +95 -0
  203. package/src/session-markers.ts +132 -0
  204. package/src/sharing/auth.ts +41 -0
  205. package/src/sharing/cli.ts +83 -0
  206. package/src/sharing/client.ts +334 -0
  207. package/src/sharing/export.ts +195 -0
  208. package/src/sharing/host.ts +316 -0
  209. package/src/sharing/import.ts +787 -0
  210. package/src/sharing/index.ts +16 -0
  211. package/src/sharing/repository.ts +145 -0
  212. package/src/sharing/routes.ts +34 -0
  213. package/src/slug.ts +23 -0
  214. package/src/software-map-diff-counts.ts +517 -0
  215. package/src/software-map-model.ts +1147 -0
  216. package/src/software-map-topology-diff.ts +260 -0
  217. package/src/source.ts +92 -0
  218. package/src/startup-trace.ts +232 -0
  219. package/src/stored-document-migration.ts +151 -0
  220. package/src/telemetry-clean-text.ts +257 -0
  221. package/src/telemetry-config.ts +298 -0
  222. package/src/telemetry-debug-sink.ts +38 -0
  223. package/src/telemetry.ts +13 -0
  224. package/src/trace-cli.ts +156 -0
  225. package/src/trace-storage-cli.ts +509 -0
  226. package/src/tutorial-conversation.ts +18 -0
  227. package/src/ui-telemetry-events.ts +765 -0
  228. package/src/unified-diff.ts +71 -0
  229. package/src/viewed-coverage.ts +259 -0
  230. package/src/windows-cli.ts +125 -0
  231. package/tutorial/document.json +275 -0
  232. package/tutorial/runtime-manifest.json +12 -0
  233. package/tutorial/sample-service/package.json +9 -0
  234. package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
  235. package/tutorial/sample-service/src/app.ts +26 -0
  236. package/tutorial/sample-service/src/database/schema.ts +10 -0
  237. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
  238. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
  239. package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
  240. package/tutorial/sample-service/src/orders/order-service.ts +34 -0
  241. package/tutorial/sample-service/src/orders/order.ts +21 -0
  242. package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
  243. package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
  244. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
  245. package/tutorial/sample-service/tsconfig.json +12 -0
  246. package/tutorial/software-map.json +139 -0
  247. package/tutorial/trace.json +20 -0
@@ -0,0 +1,950 @@
1
+ import { d as gitAt } from "./src-CmdiBL20.js";
2
+ import { n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
+ import "./src-DnwdaQ2r.js";
4
+ import { B as parseMarkdown, C as resourceReferences, E as sourceReferences, K as sourceAnchors, M as lensSchema, N as lensSelections, O as checkReferences, S as resourceReference, _ as hasCodeReferences, b as pinsSchema, c as traceSchema, h as elements, l as textIncludesQuote, n as migrateStoredDocument, o as normalizedSoftwareElementSchema, p as documentSchema, r as migrateStoredLenses, s as normalizedSoftwareRelationshipSchema, t as liftFileLenses, u as anchorPins, z as markdownNodes } from "./stored-document-migration-DHeMpLMb.js";
5
+ import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { z } from "zod";
8
+ import { existsSync } from "node:fs";
9
+ import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
10
+ import path from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ //#region ../review-share-protocol/src/index.ts
13
+ const SHARE_FORMAT = "review-share/1";
14
+ const MAX_SHARE_MANIFEST_BYTES = 2097152;
15
+ const MAX_SHARE_OBJECT_BYTES = 33554432;
16
+ const MAX_SHARE_OBJECTS = 2048;
17
+ const objectIdSchema = z.string().regex(/^[a-f0-9]{64}$/);
18
+ const shareIdSchema = z.uuid();
19
+ const capabilitySchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
20
+ const shareObjectSchema = z.strictObject({
21
+ id: objectIdSchema,
22
+ size: z.number().int().min(0).max(MAX_SHARE_OBJECT_BYTES),
23
+ sha256: objectIdSchema
24
+ }).refine((object) => object.id === object.sha256, "Object ID must match its digest.");
25
+ z.string().min(1).max(4096).refine((file) => !file.startsWith("/") && !file.includes("\\") && !/[\u0000-\u001f\u007f]/.test(file) && file.split("/").every((part) => part !== "" && part !== "." && part !== ".."), "Use a repository-relative file path.");
26
+ /** The published identity is independent of the author's credential transport. */
27
+ function normalizeGitHubRemote(remote) {
28
+ const url = new URL(remote.trim().replace(/^git@github\.com:/, "https://github.com/"));
29
+ if (!["https:", "ssh:"].includes(url.protocol) || url.hostname !== "github.com" || url.port || url.search || url.hash || url.protocol === "ssh:" && url.username !== "git") throw new Error("Sharing requires a GitHub repository remote.");
30
+ const parts = url.pathname.replace(/\.git$/, "").split("/").slice(1);
31
+ if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9_.-]+$/.test(part) || part === "." || part === "..")) throw new Error("Sharing requires a GitHub repository remote.");
32
+ return `https://github.com/${parts.join("/")}.git`;
33
+ }
34
+ const gitHubRepositoryUrlSchema = z.string().refine((value) => {
35
+ try {
36
+ return normalizeGitHubRemote(value) === value;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }, "Use a canonical GitHub HTTPS repository URL without credentials.");
41
+ const shareManifestSchema = z.strictObject({
42
+ format: z.literal(SHARE_FORMAT),
43
+ reviewId: z.string().min(1).max(256),
44
+ version: z.number().int().nonnegative(),
45
+ title: z.string().min(1).max(4096),
46
+ snapshot: objectIdSchema,
47
+ presentation: objectIdSchema,
48
+ objects: z.array(shareObjectSchema).min(2).max(MAX_SHARE_OBJECTS),
49
+ resources: z.array(z.strictObject({
50
+ id: z.string().min(1).max(256),
51
+ kind: z.enum([
52
+ "image",
53
+ "trace",
54
+ "map"
55
+ ]),
56
+ mimeType: z.enum(["image/png", "application/json"]),
57
+ object: objectIdSchema
58
+ })).max(MAX_SHARE_OBJECTS),
59
+ repository: z.strictObject({ cloneUrl: gitHubRepositoryUrlSchema }).optional()
60
+ }).superRefine((manifest, context) => {
61
+ const ids = new Set(manifest.objects.map((object) => object.id));
62
+ const fail = (message) => context.addIssue({
63
+ code: "custom",
64
+ message
65
+ });
66
+ if (ids.size !== manifest.objects.length) fail("Duplicate object ID.");
67
+ if (manifest.objects.reduce((size, object) => size + object.size, 0) > 134217728) fail("Share exceeds the total byte limit.");
68
+ for (const id of [
69
+ manifest.snapshot,
70
+ manifest.presentation,
71
+ ...manifest.resources.map((resource) => resource.object)
72
+ ]) if (!ids.has(id)) fail("Reference to an undeclared object.");
73
+ if (new Set(manifest.resources.map((resource) => resource.id)).size !== manifest.resources.length) fail("Duplicate resource ID.");
74
+ for (const resource of manifest.resources) if (resource.kind === "image" !== (resource.mimeType === "image/png")) fail("Resource MIME type does not match its kind.");
75
+ });
76
+ z.strictObject({
77
+ requestId: z.uuid(),
78
+ manifest: z.strictObject({
79
+ sha256: objectIdSchema,
80
+ size: z.number().int().positive().max(MAX_SHARE_MANIFEST_BYTES)
81
+ })
82
+ });
83
+ function parseShareLink(value) {
84
+ const url = new URL(value);
85
+ if (url.protocol !== "https:" || url.username || url.password || url.search) throw new Error("Use an HTTPS Review share link.");
86
+ const match = /^\/s\/([^/]+)$/.exec(url.pathname);
87
+ return {
88
+ origin: url.origin,
89
+ shareId: shareIdSchema.parse(match?.[1]),
90
+ capability: capabilitySchema.parse(url.hash.slice(1))
91
+ };
92
+ }
93
+ //#endregion
94
+ //#region src/server/bounded-stream.ts
95
+ var StreamLimitError = class extends Error {
96
+ constructor() {
97
+ super("Stream exceeds its byte limit.");
98
+ }
99
+ };
100
+ async function readBoundedStream(stream, limit) {
101
+ const reader = stream.getReader();
102
+ const parts = [];
103
+ let size = 0;
104
+ try {
105
+ for (;;) {
106
+ const { value, done } = await reader.read();
107
+ if (done) break;
108
+ size += value.byteLength;
109
+ if (size > limit) {
110
+ await reader.cancel().catch(() => {});
111
+ throw new StreamLimitError();
112
+ }
113
+ parts.push(value);
114
+ }
115
+ return Buffer.concat(parts, size);
116
+ } finally {
117
+ reader.releaseLock();
118
+ }
119
+ }
120
+ //#endregion
121
+ //#region src/sharing/export.ts
122
+ function digestBytes(bytes) {
123
+ return createHash("sha256").update(bytes).digest("hex");
124
+ }
125
+ function validateShareSources(snapshot) {
126
+ if (!snapshot.pins && hasCodeReferences(snapshot)) throw new ReviewInputError("Reviews with code references need source pins before sharing.", 409);
127
+ }
128
+ /** Freeze one stored version before any asynchronous source reads. */
129
+ async function exportShare(input) {
130
+ const { target, kind: _kind, staleSources: _staleSources, sourceUnavailable: _sourceUnavailable, lastEdit: _lastEdit, ...snapshot } = structuredClone(input.store.read(input.reviewId, input.version));
131
+ if (target?.kind === "worktree") throw new ReviewInputError("Pin this review to commits before sharing it.");
132
+ documentSchema.parse(snapshot.document);
133
+ checkReferences(snapshot.document);
134
+ const objects = /* @__PURE__ */ new Map();
135
+ let totalBytes = 0;
136
+ const add = (bytes) => {
137
+ if (bytes.byteLength > 33554432) throw new ReviewInputError("A shared object exceeds the upload limit.");
138
+ const id = digestBytes(bytes);
139
+ if (!objects.has(id)) {
140
+ totalBytes += bytes.byteLength;
141
+ if (totalBytes > 134217728) throw new ReviewInputError("Review exceeds the sharing limit.");
142
+ objects.set(id, Uint8Array.from(bytes));
143
+ }
144
+ return id;
145
+ };
146
+ const json = (value) => add(Buffer.from(JSON.stringify(value)));
147
+ const resources = [];
148
+ const maps = {};
149
+ const sources = [...sourceReferences(snapshot.document), ...lensSelections(snapshot.lenses ?? []).flatMap((selection) => sourceAnchors(selection.source).map((source) => ({
150
+ ...selection,
151
+ source,
152
+ peek: false
153
+ })))];
154
+ const pins = snapshot.pins;
155
+ validateShareSources(snapshot);
156
+ if (Boolean(pins) !== Boolean(input.repository)) throw new ReviewInputError("Shared source pins require a repository.", 409);
157
+ for (const block of elements(snapshot.document)) {
158
+ if (block.type !== "markdown") continue;
159
+ for (const node of markdownNodes(parseMarkdown(block.markdown))) if (node.type === "image") throw new ReviewInputError("Convert Markdown images to managed image blocks before sharing.");
160
+ }
161
+ for (const block of resourceReferences(snapshot.document)) {
162
+ await input.data.validateResource(pins, block);
163
+ const reference = resourceReference(block);
164
+ if (!reference || resources.some((resource) => resource.id === reference.id)) continue;
165
+ const { id } = reference;
166
+ const resource = input.store.resource(id);
167
+ resources.push({
168
+ id,
169
+ kind: z.enum([
170
+ "image",
171
+ "trace",
172
+ "map"
173
+ ]).parse(resource.kind),
174
+ mimeType: z.enum(["image/png", "application/json"]).parse(resource.mimeType),
175
+ object: add(resource.data)
176
+ });
177
+ if (block.type === "software_map") {
178
+ const map = await input.data.map(pins, id);
179
+ Object.defineProperty(maps, id, {
180
+ value: map,
181
+ enumerable: true
182
+ });
183
+ for (const element of map.elements) for (const range of element.sourceRanges ?? []) sources.push({
184
+ id: `${id}:${element.id}`,
185
+ source: {
186
+ ...range,
187
+ side: map.side
188
+ }
189
+ });
190
+ }
191
+ }
192
+ for (const reference of sources) await input.data.validateSource(anchorPins(reference.source, pins), reference.source, { peek: reference.peek ?? false });
193
+ const snapshotId = json(snapshot);
194
+ const presentationId = json({ maps });
195
+ const manifest = shareManifestSchema.parse({
196
+ format: SHARE_FORMAT,
197
+ reviewId: snapshot.reviewId,
198
+ version: snapshot.version,
199
+ title: snapshot.title,
200
+ snapshot: snapshotId,
201
+ presentation: presentationId,
202
+ objects: [...objects].map(([id, bytes]) => ({
203
+ id,
204
+ sha256: id,
205
+ size: bytes.byteLength
206
+ })),
207
+ resources,
208
+ repository: input.repository
209
+ });
210
+ if (Buffer.byteLength(JSON.stringify(manifest)) > 2097152) throw new ReviewInputError("The share manifest exceeds the upload limit.");
211
+ return {
212
+ manifest,
213
+ objects
214
+ };
215
+ }
216
+ //#endregion
217
+ //#region src/sharing/repository.ts
218
+ /** Keep credential helpers but prevent the calling shell from redirecting Git's object store. */
219
+ function sharedGit(cwd, args) {
220
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_") || [
221
+ "GIT_SSH",
222
+ "GIT_SSH_COMMAND",
223
+ "GIT_ASKPASS",
224
+ "GIT_SSH_VARIANT"
225
+ ].includes(key)));
226
+ return gitAt(cwd, [
227
+ "-c",
228
+ "core.hooksPath=/dev/null",
229
+ "-c",
230
+ "core.fsmonitor=false",
231
+ ...args
232
+ ], {
233
+ env: {
234
+ ...env,
235
+ GIT_TERMINAL_PROMPT: "0"
236
+ },
237
+ timeout: 3e5
238
+ });
239
+ }
240
+ function fetchError(stderr, publishing) {
241
+ if (/not our ref|couldn't find remote ref|unadvertised object/i.test(stderr)) return new ReviewInputError(publishing ? "Push the reviewed commits to GitHub before sharing." : "The shared commits are no longer available on GitHub. Ask the sender to restore them, then retry.", 409);
242
+ return new ReviewInputError("Could not fetch the GitHub repository. Check your connection and Git credentials for this repository, then retry.", 409);
243
+ }
244
+ /** Recipients need complete history; publication only checks that the commits exist. */
245
+ async function fetchPinnedRepository(root, url, pins, publishing = false) {
246
+ if (![pins.base, pins.head].every((commit) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit))) throw new ReviewInputError("Sharing requires immutable base and head commits.");
247
+ await mkdir(root, {
248
+ recursive: true,
249
+ mode: 448
250
+ });
251
+ await sharedGit(root, ["init", "--quiet"]);
252
+ if (!publishing) {
253
+ await sharedGit(root, [
254
+ "config",
255
+ "core.hooksPath",
256
+ "/dev/null"
257
+ ]);
258
+ await sharedGit(root, [
259
+ "config",
260
+ "core.fsmonitor",
261
+ "false"
262
+ ]);
263
+ await sharedGit(root, [
264
+ "remote",
265
+ "add",
266
+ "origin",
267
+ url
268
+ ]);
269
+ }
270
+ const refs = publishing ? "refs/heads/review" : "refs/review";
271
+ try {
272
+ await sharedGit(root, [
273
+ "fetch",
274
+ "--no-tags",
275
+ "--force",
276
+ ...publishing ? ["--depth=1", "--filter=tree:0"] : [],
277
+ publishing ? url : "origin",
278
+ `${pins.base}:${refs}/base`,
279
+ `${pins.head}:${refs}/head`
280
+ ]);
281
+ } catch (error) {
282
+ throw fetchError(error instanceof Error && "stderr" in error ? String(error.stderr) : "", publishing);
283
+ }
284
+ if (!publishing) for (const commit of [pins.base, pins.head]) await sharedGit(root, [
285
+ "cat-file",
286
+ "-e",
287
+ `${commit}^{commit}`
288
+ ]);
289
+ }
290
+ async function repositoryReady(root, pins) {
291
+ try {
292
+ if ((await sharedGit(root, ["rev-parse", "HEAD"])).stdout.trim() !== pins.head) return false;
293
+ for (const commit of [pins.base, pins.head]) await sharedGit(root, [
294
+ "cat-file",
295
+ "-e",
296
+ `${commit}^{commit}`
297
+ ]);
298
+ return true;
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+ async function readShareRepository(root) {
304
+ let cloneUrl;
305
+ try {
306
+ cloneUrl = normalizeGitHubRemote((await sharedGit(root, [
307
+ "remote",
308
+ "get-url",
309
+ "origin"
310
+ ])).stdout);
311
+ } catch {
312
+ throw new ReviewInputError("Sharing requires a GitHub origin remote.");
313
+ }
314
+ return { cloneUrl };
315
+ }
316
+ async function verifyShareRepository(root, pins, repository) {
317
+ const { cloneUrl } = repository ?? await readShareRepository(root);
318
+ const temporary = await mkdtemp(path.join(tmpdir(), "review-share-check-"));
319
+ try {
320
+ await fetchPinnedRepository(temporary, cloneUrl, pins, true);
321
+ } finally {
322
+ await rm(temporary, {
323
+ recursive: true,
324
+ force: true
325
+ });
326
+ }
327
+ return { cloneUrl };
328
+ }
329
+ //#endregion
330
+ //#region src/sharing/import.ts
331
+ const sharedSnapshotSchema = z.strictObject({
332
+ reviewId: z.string().min(1),
333
+ version: z.number().int().nonnegative(),
334
+ title: z.string().min(1),
335
+ pins: pinsSchema.extend({
336
+ base: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/),
337
+ head: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/)
338
+ }).optional(),
339
+ document: documentSchema,
340
+ lenses: z.array(lensSchema).optional(),
341
+ createdAt: z.string(),
342
+ origin: z.strictObject({
343
+ branch: z.string().optional(),
344
+ baseRef: z.string().optional(),
345
+ pullRequestNumber: z.number().int().positive().optional(),
346
+ pullRequestUrl: z.string().optional(),
347
+ revision: z.string().optional()
348
+ }).optional()
349
+ });
350
+ const count = z.number().int().nonnegative();
351
+ const counts = z.strictObject({
352
+ additions: count,
353
+ deletions: count
354
+ });
355
+ const mapSchema = z.strictObject({
356
+ commit: z.string(),
357
+ side: z.enum(["base", "head"]),
358
+ elements: z.array(normalizedSoftwareElementSchema),
359
+ relationships: z.array(normalizedSoftwareRelationshipSchema),
360
+ countsByElementPath: z.record(z.string(), counts),
361
+ unmappedByElementPath: z.record(z.string(), counts.extend({ files: z.array(counts.extend({
362
+ file: z.string(),
363
+ hunks: z.array(z.strictObject({
364
+ startLine: count,
365
+ lines: z.array(z.strictObject({
366
+ kind: z.enum(["add", "remove"]),
367
+ oldLine: count.nullable(),
368
+ newLine: count.nullable(),
369
+ text: z.string()
370
+ }))
371
+ }))
372
+ })) }))
373
+ });
374
+ const sharePresentationSchema = z.strictObject({ maps: z.record(z.string(), mapSchema) });
375
+ function validateShareBundle(bundle) {
376
+ const manifest = shareManifestSchema.parse(bundle.manifest);
377
+ if (Buffer.byteLength(JSON.stringify(manifest)) > 2097152) throw new Error("Share manifest is too large.");
378
+ if (bundle.objects.size !== manifest.objects.length) throw new Error("Share object count does not match its manifest.");
379
+ for (const object of manifest.objects) {
380
+ const bytes = bundle.objects.get(object.id);
381
+ if (!bytes || bytes.byteLength !== object.size || digestBytes(bytes) !== object.sha256) throw new Error(`Shared object ${object.id} is missing or corrupt.`);
382
+ }
383
+ const json = (id) => parseJsonText(Buffer.from(bundle.objects.get(id)).toString());
384
+ const stored = json(manifest.snapshot);
385
+ const snapshot = sharedSnapshotSchema.parse(isJsonObject(stored) && "document" in stored ? liftSharedLenses(stored, migrateStoredDocument(stored.document)) : stored);
386
+ if (snapshot.reviewId !== manifest.reviewId || snapshot.version !== manifest.version || snapshot.title !== manifest.title) throw new Error("Shared snapshot does not match its manifest.");
387
+ checkReferences(snapshot.document);
388
+ validateShareSources(snapshot);
389
+ if (Boolean(snapshot.pins) !== Boolean(manifest.repository)) throw new Error("Shared source pins require a repository.");
390
+ const presentation = sharePresentationSchema.parse(json(manifest.presentation));
391
+ if (!snapshot.pins && Object.keys(presentation.maps).length) throw new Error("Shared maps require source pins.");
392
+ const traces = /* @__PURE__ */ new Map();
393
+ for (const block of resourceReferences(snapshot.document)) {
394
+ const reference = resourceReference(block);
395
+ const resource = manifest.resources.find((item) => item.id === reference?.id);
396
+ if (!reference || !resource || resource.kind !== reference.kind) throw new Error("Missing or mismatched shared resource.");
397
+ if (block.type === "trace_quote") {
398
+ let events = traces.get(resource.object);
399
+ if (!events) {
400
+ const trace = traceSchema.parse(json(resource.object));
401
+ events = new Map(trace.events.map((event) => [event.id, event]));
402
+ if (events.size !== trace.events.length) throw new Error("Duplicate shared trace event.");
403
+ traces.set(resource.object, events);
404
+ }
405
+ const event = events.get(block.eventId);
406
+ if (!event || !textIncludesQuote(event.text, block.text)) throw new Error("Shared quote does not match its trace.");
407
+ }
408
+ if (block.type === "software_map") {
409
+ const map = presentation.maps[block.mapVersionId];
410
+ if (!map || map.commit !== snapshot.pins?.[map.side]) throw new Error("Shared map pins do not match.");
411
+ }
412
+ }
413
+ return {
414
+ manifest,
415
+ snapshot,
416
+ presentation
417
+ };
418
+ }
419
+ function liftSharedLenses(stored, migrated) {
420
+ const { document, lenses } = liftFileLenses(migrated);
421
+ const kept = Array.isArray(stored.lenses) ? migrateStoredLenses(stored.lenses) : [];
422
+ return {
423
+ ...stored,
424
+ document,
425
+ ...(kept.length || lenses.length) && { lenses: [...kept, ...lenses] }
426
+ };
427
+ }
428
+ function sharedReviewId(origin, shareId) {
429
+ const url = new URL(origin);
430
+ shareIdSchema.parse(shareId);
431
+ return `shared-${digestBytes(Buffer.from(JSON.stringify([url.origin, shareId])))}`;
432
+ }
433
+ /** Separate immutable directory tree; authoring mutation handlers cannot write it. */
434
+ var SharedReviewStore = class {
435
+ root;
436
+ fetchRepository;
437
+ loaded = /* @__PURE__ */ new Map();
438
+ validated = /* @__PURE__ */ new Map();
439
+ attention = /* @__PURE__ */ new Map();
440
+ listeners = /* @__PURE__ */ new Set();
441
+ catalogLoaded = false;
442
+ downloads = /* @__PURE__ */ new Set();
443
+ trackImport(job) {
444
+ this.downloads.add(job);
445
+ job.finally(() => this.downloads.delete(job));
446
+ }
447
+ async close() {
448
+ await Promise.allSettled([...this.downloads, ...this.jobs.values()]);
449
+ }
450
+ local;
451
+ repositories = /* @__PURE__ */ new Map();
452
+ jobs = /* @__PURE__ */ new Map();
453
+ states = /* @__PURE__ */ new Map();
454
+ constructor(root, fetchRepository = fetchPinnedRepository) {
455
+ this.root = root;
456
+ this.fetchRepository = fetchRepository;
457
+ }
458
+ connect(store, data) {
459
+ this.local = {
460
+ store,
461
+ data
462
+ };
463
+ data.workspaces.attachExternalReviews({
464
+ has: (id) => this.has(id) || !this.catalogLoaded && id.startsWith("shared-"),
465
+ subscribe: (listener) => this.subscribe(listener)
466
+ });
467
+ }
468
+ has(id) {
469
+ return this.loaded.has(id);
470
+ }
471
+ status(id) {
472
+ return this.states.get(id) ?? {
473
+ stage: "error",
474
+ error: "Shared review is not available."
475
+ };
476
+ }
477
+ setStatus(id, stage, error) {
478
+ this.states.set(id, {
479
+ stage,
480
+ error
481
+ });
482
+ for (const listener of this.listeners) listener();
483
+ }
484
+ repositoryRoot(id) {
485
+ if (!/^shared-[a-f0-9]{64}$/.test(id)) throw new ReviewInputError("Invalid shared review ID.");
486
+ return path.join(this.root, ".repositories", id);
487
+ }
488
+ async prepare(id) {
489
+ const active = this.jobs.get(id);
490
+ if (active) return active;
491
+ const job = this.prepareRepository(id).finally(() => this.jobs.delete(id));
492
+ this.jobs.set(id, job);
493
+ return job;
494
+ }
495
+ async prepareRepository(id) {
496
+ const local = this.local;
497
+ const saved = this.validated.get(id);
498
+ if (!saved) throw new ReviewInputError("Shared review is not available.", 404);
499
+ if (!saved.snapshot.pins) {
500
+ this.setStatus(id, "ready");
501
+ return;
502
+ }
503
+ if (!local) throw new ReviewInputError("Repository service is unavailable.", 409);
504
+ const root = this.repositoryRoot(id);
505
+ try {
506
+ this.setStatus(id, "fetching");
507
+ if (!await repositoryReady(root, saved.snapshot.pins)) {
508
+ const prior = this.repositories.get(id);
509
+ await local.data.workspaces.remove(id);
510
+ if (prior) await local.data.forgetRepository(prior);
511
+ await rm(root, {
512
+ recursive: true,
513
+ force: true
514
+ });
515
+ await this.fetchRepository(root, saved.manifest.repository.cloneUrl, saved.snapshot.pins);
516
+ await sharedGit(root, [
517
+ "checkout",
518
+ "--detach",
519
+ saved.snapshot.pins.head
520
+ ]);
521
+ }
522
+ const repository = await local.data.register(root);
523
+ this.repositories.set(id, repository.id);
524
+ await writeFile(path.join(this.root, id, "repository.json"), JSON.stringify({
525
+ repositoryId: repository.id,
526
+ ready: false
527
+ }), { mode: 384 });
528
+ const pins = {
529
+ ...saved.snapshot.pins,
530
+ repositoryId: repository.id
531
+ };
532
+ this.setStatus(id, "validating");
533
+ await local.data.validatePins(pins);
534
+ const references = sourceReferences(saved.snapshot.document).map((item) => item.source);
535
+ for (const map of Object.values(saved.presentation.maps)) for (const element of map.elements) for (const range of element.sourceRanges ?? []) references.push({
536
+ ...range,
537
+ side: map.side
538
+ });
539
+ await local.data.validateSources(pins, references);
540
+ await local.data.workspaces.open(id, pins);
541
+ if (local.data.workspaces.list(id).some((workspace) => !workspace.rootPath)) throw new ReviewInputError("Could not prepare the pinned checkout. Retry opening the share.", 409);
542
+ await writeFile(path.join(this.root, id, "repository.json"), JSON.stringify({
543
+ repositoryId: repository.id,
544
+ ready: true
545
+ }), { mode: 384 });
546
+ this.setStatus(id, "ready");
547
+ } catch (error) {
548
+ const message = error instanceof ReviewInputError ? error.message : "Could not prepare the shared repository. Check Git credentials and retry.";
549
+ this.setStatus(id, "error", message);
550
+ throw new ReviewInputError(message, 409);
551
+ }
552
+ }
553
+ async assertReady(id) {
554
+ const saved = this.validated.get(id);
555
+ if (!saved || saved.snapshot.pins && !await repositoryReady(this.repositoryRoot(id), saved.snapshot.pins)) {
556
+ this.setStatus(id, "error", "The managed checkout is missing. Reopen the share link to fetch it again.");
557
+ throw new ReviewInputError(this.status(id).error, 409);
558
+ }
559
+ this.get(id);
560
+ }
561
+ subscribe(listener) {
562
+ this.listeners.add(listener);
563
+ return () => {
564
+ this.listeners.delete(listener);
565
+ };
566
+ }
567
+ async load() {
568
+ await mkdir(this.root, {
569
+ recursive: true,
570
+ mode: 448
571
+ });
572
+ for (const id of await readdir(this.root)) {
573
+ if (!/^shared-[a-f0-9]{64}$/.test(id)) continue;
574
+ let saved;
575
+ try {
576
+ saved = await this.readValidated(id);
577
+ } catch {
578
+ await rename(path.join(this.root, id), path.join(this.root, `.invalid-${id}-${Date.now()}`));
579
+ console.warn("A cached shared review was corrupt. Reopen its share link to download it again.");
580
+ continue;
581
+ }
582
+ const { bundle, validated } = saved;
583
+ this.loaded.set(id, {
584
+ manifest: bundle.manifest,
585
+ attribution: bundle.attribution
586
+ });
587
+ this.validated.set(id, validated);
588
+ let prepared = false;
589
+ try {
590
+ const metadata = z.strictObject({
591
+ repositoryId: z.string(),
592
+ ready: z.boolean()
593
+ }).parse(JSON.parse(await readFile(path.join(this.root, id, "repository.json"), "utf8")));
594
+ if (this.local?.store.repositoryPath(metadata.repositoryId) === path.join(await realpath(this.root), ".repositories", id)) {
595
+ this.repositories.set(id, metadata.repositoryId);
596
+ prepared = metadata.ready;
597
+ }
598
+ } catch {}
599
+ if (!validated.snapshot.pins) this.setStatus(id, "ready");
600
+ else if (prepared && this.local && await repositoryReady(this.repositoryRoot(id), validated.snapshot.pins)) {
601
+ const repository = await this.local.data.register(this.repositoryRoot(id));
602
+ this.repositories.set(id, repository.id);
603
+ this.setStatus(id, "ready");
604
+ } else this.setStatus(id, "error", "Reopen the share link to fetch its repository.");
605
+ try {
606
+ this.attention.set(id, z.strictObject({
607
+ viewedAt: z.string().nullable(),
608
+ dismissedAt: z.string().nullable()
609
+ }).parse(JSON.parse(await readFile(path.join(this.root, id, "attention.json"), "utf8"))));
610
+ } catch {}
611
+ }
612
+ this.catalogLoaded = true;
613
+ for (const listener of this.listeners) listener();
614
+ }
615
+ get(id) {
616
+ const bundle = this.loaded.get(id);
617
+ if (!bundle) throw new ReviewInputError("Shared review is not available.", 404);
618
+ const validated = this.validated.get(id);
619
+ const repositoryId = this.repositories.get(id);
620
+ if (this.status(id).stage !== "ready" || validated.snapshot.pins && (!repositoryId || !existsSync(path.join(this.repositoryRoot(id), ".git")))) throw new ReviewInputError(this.status(id).error ?? "Fetch the shared repository before opening this review.", 409);
621
+ return {
622
+ ...validated,
623
+ snapshot: {
624
+ ...validated.snapshot,
625
+ reviewId: id,
626
+ pins: validated.snapshot.pins ? {
627
+ ...validated.snapshot.pins,
628
+ repositoryId
629
+ } : void 0,
630
+ target: validated.snapshot.pins ? {
631
+ kind: "commits",
632
+ ...validated.snapshot.pins,
633
+ repositoryId
634
+ } : void 0,
635
+ shared: {
636
+ ...bundle.attribution,
637
+ cloneUrl: bundle.manifest.repository?.cloneUrl
638
+ }
639
+ }
640
+ };
641
+ }
642
+ list(mode = "structural") {
643
+ const reviews = this.loaded.keys().filter((id) => this.status(id).stage === "ready" && (!this.validated.get(id)?.snapshot.pins || existsSync(path.join(this.repositoryRoot(id), ".git")))).map((id) => {
644
+ const { document: _document, ...snapshot } = this.get(id).snapshot;
645
+ return {
646
+ ...snapshot,
647
+ firstCreatedAt: snapshot.version === 0 ? snapshot.createdAt : void 0,
648
+ repositoryName: "Shared review",
649
+ viewedAt: this.attention.get(id)?.viewedAt ?? null,
650
+ dismissedAt: this.attention.get(id)?.dismissedAt ?? null
651
+ };
652
+ }).toArray();
653
+ return this.local?.store.withDiffStats(reviews, mode) ?? reviews;
654
+ }
655
+ async readObject(id, objectId) {
656
+ const entry = this.get(id).manifest.objects.find((object) => object.id === objectId);
657
+ if (!entry) throw new ReviewInputError("Shared object is unavailable.", 404);
658
+ const file = path.join(this.root, id, objectId);
659
+ if ((await stat(file)).size !== entry.size) throw new Error("Corrupt cached shared object.");
660
+ const bytes = await readFile(file);
661
+ if (digestBytes(bytes) !== entry.sha256) throw new Error("Corrupt cached shared object.");
662
+ return bytes;
663
+ }
664
+ async removeLocal(id) {
665
+ await this.jobs.get(id)?.catch(() => {});
666
+ if (!this.loaded.has(id)) throw new ReviewInputError("Shared review is not available.", 404);
667
+ await this.local?.data.workspaces.remove(id);
668
+ const repositoryId = this.repositories.get(id);
669
+ if (repositoryId) {
670
+ await this.local?.data.forgetRepository(repositoryId);
671
+ this.local?.store.unregisterRepository(repositoryId);
672
+ }
673
+ await rm(this.repositoryRoot(id), {
674
+ recursive: true,
675
+ force: true
676
+ });
677
+ this.repositories.delete(id);
678
+ this.states.delete(id);
679
+ await rm(path.join(this.root, id), {
680
+ recursive: true,
681
+ force: true
682
+ });
683
+ this.loaded.delete(id);
684
+ this.validated.delete(id);
685
+ this.attention.delete(id);
686
+ for (const listener of this.listeners) listener();
687
+ }
688
+ async setAttention(id, action) {
689
+ this.get(id);
690
+ const state = this.attention.get(id) ?? {
691
+ viewedAt: null,
692
+ dismissedAt: null
693
+ };
694
+ if (action === "view") state.viewedAt = (/* @__PURE__ */ new Date()).toISOString();
695
+ else state.dismissedAt = action === "dismiss" ? (/* @__PURE__ */ new Date()).toISOString() : null;
696
+ await writeFile(path.join(this.root, id, "attention.json"), JSON.stringify(state), { mode: 384 });
697
+ this.attention.set(id, state);
698
+ for (const listener of this.listeners) listener();
699
+ }
700
+ async import(origin, shareId, bundle) {
701
+ const validated = validateShareBundle(bundle);
702
+ const id = sharedReviewId(origin, shareId);
703
+ await mkdir(this.root, {
704
+ recursive: true,
705
+ mode: 448
706
+ });
707
+ const staging = await mkdtemp(path.join(this.root, ".import-"));
708
+ try {
709
+ await writeFile(path.join(staging, "manifest.json"), JSON.stringify(bundle.manifest), { mode: 384 });
710
+ if (bundle.attribution) await writeFile(path.join(staging, "attribution.json"), JSON.stringify(bundle.attribution), { mode: 384 });
711
+ for (const [objectId, bytes] of bundle.objects) await writeFile(path.join(staging, objectId), bytes, { mode: 384 });
712
+ try {
713
+ await rename(staging, path.join(this.root, id));
714
+ } catch (error) {
715
+ if (!(error instanceof Error && "code" in error && ["ENOTEMPTY", "EEXIST"].includes(String(error.code)))) throw error;
716
+ const existing = await this.read(id);
717
+ if (JSON.stringify(existing.manifest) !== JSON.stringify(bundle.manifest)) throw new Error("An immutable share changed its manifest.");
718
+ }
719
+ } finally {
720
+ await rm(staging, {
721
+ recursive: true,
722
+ force: true
723
+ });
724
+ }
725
+ this.loaded.set(id, {
726
+ manifest: bundle.manifest,
727
+ attribution: bundle.attribution
728
+ });
729
+ this.validated.set(id, validated);
730
+ for (const listener of this.listeners) listener();
731
+ await this.prepare(id);
732
+ return id;
733
+ }
734
+ async read(id) {
735
+ return (await this.readValidated(id)).bundle;
736
+ }
737
+ async readValidated(id) {
738
+ if (!/^shared-[a-f0-9]{64}$/.test(id)) throw new Error("Invalid shared review ID.");
739
+ const dir = path.join(this.root, id);
740
+ if ((await stat(path.join(dir, "manifest.json"))).size > 2097152) throw new Error("Shared manifest is too large.");
741
+ const manifest = shareManifestSchema.parse(JSON.parse(await readFile(path.join(dir, "manifest.json"), "utf8")));
742
+ const objects = /* @__PURE__ */ new Map();
743
+ for (const object of manifest.objects) {
744
+ const file = path.join(dir, object.id);
745
+ if ((await stat(file)).size !== object.size) throw new Error("Corrupt cached shared object.");
746
+ objects.set(object.id, await readFile(file));
747
+ }
748
+ let attribution;
749
+ try {
750
+ attribution = z.strictObject({
751
+ login: z.string(),
752
+ sharedAt: z.number()
753
+ }).parse(JSON.parse(await readFile(path.join(dir, "attribution.json"), "utf8")));
754
+ } catch {}
755
+ const bundle = {
756
+ manifest,
757
+ objects,
758
+ attribution
759
+ };
760
+ return {
761
+ bundle,
762
+ validated: validateShareBundle(bundle)
763
+ };
764
+ }
765
+ };
766
+ //#endregion
767
+ //#region src/sharing/client.ts
768
+ const uploadSchema = z.strictObject({
769
+ url: z.url(),
770
+ headers: z.record(z.string(), z.string()),
771
+ expiresAt: z.string()
772
+ });
773
+ const createdSchema = z.strictObject({
774
+ shareId: z.uuid(),
775
+ upload: uploadSchema
776
+ });
777
+ const registeredSchema = z.object({
778
+ registered: z.literal(true),
779
+ uploads: z.record(z.string(), uploadSchema)
780
+ });
781
+ const UPLOAD_CONCURRENCY = 4;
782
+ const MAX_UPLOAD_RESPONSE_BYTES = 8388608;
783
+ const linkSchema = z.strictObject({
784
+ shareId: z.uuid(),
785
+ url: z.url()
786
+ });
787
+ const receivedSchema = z.strictObject({
788
+ manifest: shareManifestSchema,
789
+ sender: z.strictObject({ login: z.string().min(1).max(256) }),
790
+ sharedAt: z.number()
791
+ });
792
+ const signedUrlSchema = z.url().refine((value) => {
793
+ const url = new URL(value);
794
+ return url.protocol === "https:" && !url.username && !url.password;
795
+ });
796
+ const downloadSchema = z.strictObject({
797
+ url: z.url(),
798
+ expiresAt: z.string()
799
+ });
800
+ /** Size is checked while reading, even if Content-Length is absent or false. */
801
+ async function readBoundedBytes(response, limit) {
802
+ if (!response.ok || !response.body) throw new Error(`Download failed (${response.status}).`);
803
+ const declared = response.headers.get("content-length");
804
+ if (declared && Number(declared) > limit) {
805
+ await response.body.cancel().catch(() => {});
806
+ throw new Error("Download exceeds its declared limit.");
807
+ }
808
+ try {
809
+ return await readBoundedStream(response.body, limit);
810
+ } catch (error) {
811
+ if (error instanceof StreamLimitError) throw new Error("Download exceeds its declared limit.");
812
+ throw error;
813
+ }
814
+ }
815
+ /** The share service rejected the account token; the stored login is stale. */
816
+ var ShareAuthError = class extends Error {
817
+ constructor() {
818
+ super("The share service rejected the stored login.");
819
+ this.name = "ShareAuthError";
820
+ }
821
+ };
822
+ var SharePreflightError = class extends Error {
823
+ constructor(cause) {
824
+ super(cause instanceof Error ? cause.message : "Repository verification failed.", { cause });
825
+ this.name = "SharePreflightError";
826
+ }
827
+ };
828
+ var ShareRequestError = class extends Error {
829
+ status;
830
+ constructor(status) {
831
+ super(`Share request failed (${status}).`);
832
+ this.status = status;
833
+ }
834
+ };
835
+ /** Account tokens go only to the configured API origin, never to object storage. */
836
+ var ShareClient = class {
837
+ token;
838
+ send;
839
+ origin;
840
+ constructor(origin, token, send = fetch) {
841
+ this.token = token;
842
+ this.send = send;
843
+ const url = new URL(origin);
844
+ if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && url.hostname === "localhost")) throw new Error("Use a secure share service origin.");
845
+ this.origin = url.origin;
846
+ }
847
+ async api(route, method = "GET", body, capability, maxBytes = MAX_SHARE_MANIFEST_BYTES) {
848
+ const headers = new Headers({ "content-type": "application/json" });
849
+ if (capability) headers.set("x-review-share-token", capability);
850
+ else if (this.token) headers.set("authorization", `Bearer ${this.token}`);
851
+ const init = {
852
+ method,
853
+ redirect: "error",
854
+ signal: AbortSignal.timeout(6e4),
855
+ headers
856
+ };
857
+ if (body !== void 0) init.body = JSON.stringify(body);
858
+ const response = await this.send(this.origin + route, init);
859
+ if (!response.ok) {
860
+ await response.body?.cancel();
861
+ if (response.status === 401 && !capability) throw new ShareAuthError();
862
+ throw new ShareRequestError(response.status);
863
+ }
864
+ return parseJsonText(Buffer.from(await readBoundedBytes(response, maxBytes)).toString());
865
+ }
866
+ async upload(upload, bytes) {
867
+ if (!signedUrlSchema.safeParse(upload.url).success) throw new Error("Invalid object upload URL.");
868
+ const response = await this.send(upload.url, {
869
+ method: "PUT",
870
+ headers: upload.headers,
871
+ body: Buffer.from(bytes),
872
+ redirect: "error",
873
+ signal: AbortSignal.timeout(12e4)
874
+ });
875
+ await response.body?.cancel();
876
+ if (!response.ok && response.status !== 412) throw new Error(`Object upload failed (${response.status}).`);
877
+ }
878
+ async create(bundle, requestId = randomUUID(), beforeUpload) {
879
+ validateShareBundle(bundle);
880
+ const manifestBytes = Buffer.from(JSON.stringify(bundle.manifest));
881
+ const created = createdSchema.parse(await this.api("/api/shares", "POST", {
882
+ requestId,
883
+ manifest: {
884
+ size: manifestBytes.byteLength,
885
+ sha256: digestBytes(manifestBytes)
886
+ }
887
+ }));
888
+ const route = `/api/shares/${created.shareId}`;
889
+ await this.upload(created.upload, manifestBytes);
890
+ const { uploads } = registeredSchema.parse(await this.api(`${route}/manifest`, "POST", {}, void 0, MAX_UPLOAD_RESPONSE_BYTES));
891
+ try {
892
+ if (bundle.manifest.objects.some(({ id }) => !uploads[id])) throw new Error("The share service omitted an object upload URL.");
893
+ await beforeUpload?.();
894
+ } catch (error) {
895
+ await this.recoverLink(created.shareId).catch(async (failure) => {
896
+ if (failure instanceof ShareRequestError && failure.status === 409) await this.revoke(created.shareId);
897
+ }).catch(() => {});
898
+ throw new SharePreflightError(error);
899
+ }
900
+ for (let offset = 0; offset < bundle.manifest.objects.length; offset += UPLOAD_CONCURRENCY) {
901
+ const failed = (await Promise.allSettled(bundle.manifest.objects.slice(offset, offset + UPLOAD_CONCURRENCY).map(async (object) => {
902
+ await this.upload(uploads[object.id], bundle.objects.get(object.id));
903
+ }))).find((result) => result.status === "rejected");
904
+ if (failed) throw failed.reason;
905
+ }
906
+ for (let batch = 0; batch <= Math.ceil(bundle.manifest.objects.length / 32); batch++) {
907
+ const result = await this.api(`${route}/complete`, "POST", {});
908
+ if (linkSchema.safeParse(result).success) return linkSchema.parse(result);
909
+ z.strictObject({ complete: z.literal(false) }).parse(result);
910
+ }
911
+ throw new Error("Share verification did not finish.");
912
+ }
913
+ async list(cursor) {
914
+ const query = cursor ? `?cursor=${z.uuid().parse(cursor)}` : "";
915
+ return this.api(`/api/shares${query}`);
916
+ }
917
+ async recoverLink(shareId) {
918
+ return linkSchema.parse(await this.api(`/api/shares/${z.uuid().parse(shareId)}/link`));
919
+ }
920
+ async revoke(shareId) {
921
+ return this.api(`/api/shares/${z.uuid().parse(shareId)}`, "DELETE");
922
+ }
923
+ async download(shareId, capability) {
924
+ const route = `/api/shared/${z.uuid().parse(shareId)}`;
925
+ const received = receivedSchema.parse(await this.api(route, "GET", void 0, capability));
926
+ const manifest = received.manifest;
927
+ const objects = /* @__PURE__ */ new Map();
928
+ const download = async (object) => {
929
+ const signed = downloadSchema.parse(await this.api(`${route}/objects/${object.id}`, "GET", void 0, capability));
930
+ if (!signedUrlSchema.safeParse(signed.url).success) throw new Error("Invalid object download URL.");
931
+ const bytes = await readBoundedBytes(await this.send(signed.url, {
932
+ redirect: "error",
933
+ signal: AbortSignal.timeout(12e4)
934
+ }), object.size);
935
+ if (bytes.byteLength !== object.size || digestBytes(bytes) !== object.sha256) throw new Error("Downloaded share object failed its integrity check.");
936
+ objects.set(object.id, bytes);
937
+ };
938
+ for (let start = 0; start < manifest.objects.length; start += 4) await Promise.all(manifest.objects.slice(start, start + 4).map(download));
939
+ return {
940
+ manifest,
941
+ objects,
942
+ attribution: {
943
+ login: received.sender.login,
944
+ sharedAt: received.sharedAt
945
+ }
946
+ };
947
+ }
948
+ };
949
+ //#endregion
950
+ export { sharedReviewId as a, verifyShareRepository as c, StreamLimitError as d, readBoundedStream as f, SharedReviewStore as i, exportShare as l, shareIdSchema as m, ShareClient as n, validateShareBundle as o, parseShareLink as p, SharePreflightError as r, readShareRepository as s, ShareAuthError as t, validateShareSources as u };