openplanr 2.2640.4 → 2.2640.6

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 (102) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/cli/commands/artifact.d.ts.map +1 -1
  3. package/dist/cli/commands/artifact.js +44 -21
  4. package/dist/cli/commands/artifact.js.map +1 -1
  5. package/dist/dashboard/.vite/manifest.json +1 -1
  6. package/dist/dashboard/assets/{index-CaX9EJp4.js → index-BqFEZO-9.js} +2 -2
  7. package/dist/dashboard/assets/{index-CaX9EJp4.js.map → index-BqFEZO-9.js.map} +1 -1
  8. package/dist/dashboard/dashboard-manifest.json +8 -8
  9. package/dist/dashboard/index.html +1 -1
  10. package/dist/services/diagram-artifact-service.d.ts +28 -1
  11. package/dist/services/diagram-artifact-service.d.ts.map +1 -1
  12. package/dist/services/diagram-artifact-service.js +64 -1
  13. package/dist/services/diagram-artifact-service.js.map +1 -1
  14. package/docs/ARTIFACT_REVIEW.md +38 -2
  15. package/docs/CLI.md +12 -2
  16. package/lib/host-packages/adapter-registry.json +5 -5
  17. package/lib/host-packages/capability-map.json +2 -2
  18. package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
  19. package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
  20. package/lib/host-packages/claude/openplanr/.openplanr-content.json +33 -25
  21. package/lib/host-packages/claude/openplanr/README.md +1 -1
  22. package/lib/host-packages/claude/openplanr/skills/artifact/SKILL.md +14 -4
  23. package/lib/host-packages/claude/openplanr/skills/artifact/references/diagram-sharing.md +51 -0
  24. package/lib/host-packages/claude/openplanr/skills/design/scripts/design-artifact-shell.mjs +127 -123
  25. package/lib/host-packages/claude/openplanr/skills/design/scripts/design-review-handoff.mjs +4 -3
  26. package/lib/host-packages/claude/openplanr/skills/design/scripts/design-share.mjs +805 -574
  27. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  28. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
  29. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-artifact-shell.mjs +127 -123
  30. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-review-handoff.mjs +4 -3
  31. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-share.mjs +805 -574
  32. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  33. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  34. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-artifact-shell.mjs +127 -123
  35. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-review-handoff.mjs +4 -3
  36. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-share.mjs +805 -574
  37. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  38. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
  39. package/lib/host-packages/claude/openplanr/skills/diagram/SKILL.md +13 -0
  40. package/lib/host-packages/claude/openplanr/skills/diagram/references/diagram-sharing.md +51 -0
  41. package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-artifact-shell.mjs +127 -123
  42. package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-review-handoff.mjs +4 -3
  43. package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-share.mjs +805 -574
  44. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  45. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
  46. package/lib/host-packages/cursor/openplanr/.openplanr-content.json +32 -24
  47. package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
  48. package/lib/host-packages/cursor/openplanr/rules/planr-artifact/references/diagram-sharing.md +51 -0
  49. package/lib/host-packages/cursor/openplanr/rules/planr-artifact.mdc +14 -4
  50. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-artifact-shell.mjs +127 -123
  51. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-review-handoff.mjs +4 -3
  52. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-share.mjs +805 -574
  53. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  54. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/package.json +5 -1
  55. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-artifact-shell.mjs +127 -123
  56. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-review-handoff.mjs +4 -3
  57. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-share.mjs +805 -574
  58. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  59. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  60. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-artifact-shell.mjs +127 -123
  61. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-review-handoff.mjs +4 -3
  62. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-share.mjs +805 -574
  63. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  64. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/package.json +5 -1
  65. package/lib/host-packages/cursor/openplanr/rules/planr-diagram/references/diagram-sharing.md +51 -0
  66. package/lib/host-packages/cursor/openplanr/rules/planr-diagram.mdc +13 -0
  67. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-artifact-shell.mjs +127 -123
  68. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-review-handoff.mjs +4 -3
  69. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-share.mjs +805 -574
  70. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  71. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/package.json +5 -1
  72. package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
  73. package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
  74. package/lib/host-packages/openai/openplanr/.openplanr-content.json +35 -27
  75. package/lib/host-packages/openai/openplanr/skills/artifact/SKILL.md +14 -4
  76. package/lib/host-packages/openai/openplanr/skills/artifact/agents/openai.yaml +2 -2
  77. package/lib/host-packages/openai/openplanr/skills/artifact/openplanr.skill.json +11 -0
  78. package/lib/host-packages/openai/openplanr/skills/artifact/references/diagram-sharing.md +51 -0
  79. package/lib/host-packages/openai/openplanr/skills/design/scripts/design-artifact-shell.mjs +127 -123
  80. package/lib/host-packages/openai/openplanr/skills/design/scripts/design-review-handoff.mjs +4 -3
  81. package/lib/host-packages/openai/openplanr/skills/design/scripts/design-share.mjs +805 -574
  82. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  83. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
  84. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-artifact-shell.mjs +127 -123
  85. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-review-handoff.mjs +4 -3
  86. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-share.mjs +805 -574
  87. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  88. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
  89. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-artifact-shell.mjs +127 -123
  90. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-review-handoff.mjs +4 -3
  91. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-share.mjs +805 -574
  92. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  93. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
  94. package/lib/host-packages/openai/openplanr/skills/diagram/SKILL.md +13 -0
  95. package/lib/host-packages/openai/openplanr/skills/diagram/openplanr.skill.json +11 -0
  96. package/lib/host-packages/openai/openplanr/skills/diagram/references/diagram-sharing.md +51 -0
  97. package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-artifact-shell.mjs +127 -123
  98. package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-review-handoff.mjs +4 -3
  99. package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-share.mjs +805 -574
  100. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/share.css +1 -1
  101. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
  102. package/package.json +2 -2
@@ -31,23 +31,9 @@ import {
31
31
  } from "./design-artifact-shell.mjs";
32
32
 
33
33
  // packages/design/lib/design/share.mjs
34
- import { randomBytes as randomBytes2 } from "node:crypto";
35
- import {
36
- chmodSync,
37
- closeSync,
38
- existsSync as existsSync3,
39
- fsyncSync,
40
- lstatSync as lstatSync3,
41
- mkdirSync as mkdirSync3,
42
- openSync,
43
- readFileSync as readFileSync3,
44
- realpathSync as realpathSync2,
45
- renameSync as renameSync3,
46
- unlinkSync,
47
- writeFileSync as writeFileSync3
48
- } from "node:fs";
34
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "node:fs";
49
35
  import { homedir as homedir2 } from "node:os";
50
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
36
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join3, relative as relative3, resolve as resolve3 } from "node:path";
51
37
 
52
38
  // packages/artifact/lib/artifact/import.mjs
53
39
  import {
@@ -733,143 +719,741 @@ function resolveArtifactReviewDestination({
733
719
  });
734
720
  }
735
721
 
736
- // packages/design/lib/design/workspace-client.mjs
737
- var DESIGN_SHARE_BASE_URL = "https://share.openplanr.dev";
738
- var encoder = new TextEncoder();
739
- var decoder = new TextDecoder("utf-8", { fatal: true });
740
- var tokenPattern = /^[A-Za-z0-9_-]{43}$/;
741
- var idPattern = /^[A-Za-z0-9_-]{22,64}$/;
742
- var ec = { name: "ECDSA", namedCurve: "P-256" };
743
- var signatureAlgorithm = { name: "ECDSA", hash: "SHA-256" };
744
- var omitSignature = ({ signature: _signature, ...value }) => value;
745
- function encodeWorkspaceBytes(bytes) {
746
- let binary = "";
747
- for (let offset = 0; offset < bytes.length; offset += 8192)
748
- binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
749
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
750
- }
751
- function decodeWorkspaceBytes(value) {
752
- if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/u.test(value))
753
- throw new TypeError("Invalid encoded workspace value.");
754
- const decoded = Uint8Array.from(
755
- atob(value.replaceAll("-", "+").replaceAll("_", "/")),
756
- (char) => char.charCodeAt(0)
757
- );
758
- if (encodeWorkspaceBytes(decoded) !== value)
759
- throw new TypeError("Noncanonical encoded workspace value.");
760
- return decoded;
761
- }
762
- function newWorkspaceToken() {
763
- return encodeWorkspaceBytes(crypto.getRandomValues(new Uint8Array(32)));
764
- }
765
- function newWorkspaceId() {
766
- return encodeWorkspaceBytes(crypto.getRandomValues(new Uint8Array(18)));
767
- }
768
- function normalizeWorkspaceBase(baseUrl = DESIGN_SHARE_BASE_URL) {
769
- const url = new URL(baseUrl);
770
- if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
771
- throw new TypeError("Design sharing requires an HTTPS origin or a local development server.");
772
- }
773
- return url.origin;
774
- }
775
- function workspaceReviewUrl(access) {
776
- if (!idPattern.test(access.id)) throw new TypeError("Invalid design workspace identity.");
777
- return `${normalizeWorkspaceBase(access.baseUrl)}/d/${access.id}`;
778
- }
779
- async function tokenMaterial(token, id, purpose) {
780
- if (!tokenPattern.test(token) || decodeWorkspaceBytes(token).length !== 32 || !idPattern.test(id))
781
- throw new TypeError("Enter the complete generated access token.");
782
- const key = await crypto.subtle.importKey("raw", decodeWorkspaceBytes(token), "HKDF", false, [
783
- "deriveBits"
784
- ]);
785
- return new Uint8Array(
786
- await crypto.subtle.deriveBits(
787
- {
788
- name: "HKDF",
789
- hash: "SHA-256",
790
- salt: encoder.encode(id),
791
- info: encoder.encode(`openplanr-design-workspace/v1/${purpose}`)
792
- },
793
- key,
794
- 256
795
- )
796
- );
722
+ // packages/artifact/lib/artifact/owner-custody.mjs
723
+ import { randomBytes as randomBytes2 } from "node:crypto";
724
+ import {
725
+ chmodSync,
726
+ closeSync,
727
+ existsSync as existsSync3,
728
+ fsyncSync,
729
+ lstatSync as lstatSync3,
730
+ mkdirSync as mkdirSync3,
731
+ openSync,
732
+ readFileSync as readFileSync3,
733
+ realpathSync as realpathSync2,
734
+ renameSync as renameSync3,
735
+ unlinkSync,
736
+ writeFileSync as writeFileSync3
737
+ } from "node:fs";
738
+ import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
739
+ function custodyError(message, code = "E_OWNER_CUSTODY_LOCATION") {
740
+ return Object.assign(new Error(message), { code, status: 400 });
741
+ }
742
+ function assertDirectoryAncestry(root, label) {
743
+ for (let path = root; dirname3(path) !== path; path = dirname3(path)) {
744
+ if (existsSync3(path) && lstatSync3(path).isSymbolicLink())
745
+ throw custodyError(`${label} custody directory must not contain symbolic links.`);
746
+ }
797
747
  }
798
- async function deriveWorkspaceAuthentication(token, id) {
799
- return encodeWorkspaceBytes(await tokenMaterial(token, id, "reviewer-auth"));
748
+ function assertPrivateFile(path, label) {
749
+ const stat = lstatSync3(path);
750
+ if (!stat.isFile() || stat.isSymbolicLink() || process.platform !== "win32" && stat.mode & 63)
751
+ throw custodyError(
752
+ `${label} owner custody must be a private 0600 file.`,
753
+ "E_OWNER_CUSTODY_INVALID"
754
+ );
800
755
  }
801
- function canonicalWorkspacePublicKey(value) {
802
- if (!value || value.kty !== "EC" || value.crv !== "P-256" || !tokenPattern.test(value.x ?? "") || !tokenPattern.test(value.y ?? "") || "d" in value) {
803
- throw new TypeError("Invalid design workspace public key.");
756
+ function physicalDirectoryPath(directory) {
757
+ let existing = resolve2(directory);
758
+ const missing = [];
759
+ while (!existsSync3(existing) && dirname3(existing) !== existing) {
760
+ missing.unshift(basename(existing));
761
+ existing = dirname3(existing);
804
762
  }
805
- return { kty: "EC", crv: "P-256", x: value.x, y: value.y };
763
+ return join2(realpathSync2(existing), ...missing);
806
764
  }
807
- async function createWorkspaceSigner() {
808
- const pair = await crypto.subtle.generateKey(ec, true, ["sign", "verify"]);
809
- return {
810
- privateKey: encodeWorkspaceBytes(
811
- new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey))
812
- ),
813
- publicKey: canonicalWorkspacePublicKey(await crypto.subtle.exportKey("jwk", pair.publicKey))
814
- };
765
+ function assertPrivateDirectory(root, label, recoveryOutput = false) {
766
+ const stat = lstatSync3(root);
767
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !recoveryOutput && process.platform !== "win32" && stat.mode & 63)
768
+ throw custodyError(`${label} custody must use a private local directory.`);
815
769
  }
816
- async function signWorkspaceValue(value, privateKey) {
817
- const key = await crypto.subtle.importKey("pkcs8", decodeWorkspaceBytes(privateKey), ec, false, [
818
- "sign"
819
- ]);
820
- const signature = await crypto.subtle.sign(
821
- signatureAlgorithm,
822
- key,
823
- encoder.encode(canonicalizeJson(omitSignature(value)))
824
- );
825
- return { ...omitSignature(value), signature: encodeWorkspaceBytes(new Uint8Array(signature)) };
770
+ function ensurePrivateDirectory(root, { label = "Owner", recoveryOutput = false } = {}) {
771
+ if (recoveryOutput) root = physicalDirectoryPath(root);
772
+ assertDirectoryAncestry(root, label);
773
+ mkdirSync3(root, { recursive: true, mode: 448 });
774
+ assertPrivateDirectory(root, label, recoveryOutput);
775
+ }
776
+ function readCustody(path, { label = "Owner", format, recoveryInput = false } = {}) {
777
+ if (recoveryInput && existsSync3(path)) {
778
+ assertPrivateFile(path, label);
779
+ path = realpathSync2(path);
780
+ }
781
+ assertDirectoryAncestry(dirname3(path), label);
782
+ if (!existsSync3(path)) return null;
783
+ assertPrivateDirectory(dirname3(path), label, recoveryInput);
784
+ assertPrivateFile(path, label);
785
+ let record;
786
+ try {
787
+ record = JSON.parse(readFileSync3(path, "utf8"));
788
+ } catch {
789
+ throw custodyError(`${label} owner custody is invalid.`, "E_OWNER_CUSTODY_INVALID");
790
+ }
791
+ if (record?.kind !== format || record.schemaVersion !== "1.0.0" || !record.custody)
792
+ throw custodyError(`${label} owner custody is invalid.`, "E_OWNER_CUSTODY_INVALID");
793
+ return record;
826
794
  }
827
- async function verifyWorkspaceSignature(value, publicKey) {
795
+ function writeCustody(path, record, { label = "Owner" } = {}) {
796
+ assertDirectoryAncestry(dirname3(path), label);
797
+ assertPrivateDirectory(dirname3(path), label);
798
+ if (existsSync3(path)) assertPrivateFile(path, label);
799
+ const temp = `${path}.${randomBytes2(8).toString("hex")}.tmp`;
800
+ const fd = openSync(temp, "wx", 384);
828
801
  try {
829
- const key = await crypto.subtle.importKey("jwk", publicKey, ec, false, ["verify"]);
830
- return await crypto.subtle.verify(
802
+ writeFileSync3(fd, `${JSON.stringify(record)}
803
+ `);
804
+ fsyncSync(fd);
805
+ } finally {
806
+ closeSync(fd);
807
+ }
808
+ try {
809
+ renameSync3(temp, path);
810
+ if (process.platform !== "win32") {
811
+ chmodSync(path, 384);
812
+ const directory = openSync(dirname3(path), "r");
813
+ try {
814
+ fsyncSync(directory);
815
+ } finally {
816
+ closeSync(directory);
817
+ }
818
+ }
819
+ } catch (error) {
820
+ try {
821
+ unlinkSync(temp);
822
+ } catch {
823
+ }
824
+ throw error;
825
+ }
826
+ }
827
+
828
+ // packages/artifact/lib/artifact/encrypted-workspace-client.mjs
829
+ function createEncryptedWorkspaceClient({
830
+ domain,
831
+ apiPath,
832
+ reviewPath,
833
+ label,
834
+ compatibilityMessage,
835
+ compatibilityCode = "E_WORKSPACE_UNSUPPORTED",
836
+ defaultBaseUrl = "https://share.openplanr.dev",
837
+ version = "1.0.0",
838
+ maxBytes = 5 * 1024 * 1024,
839
+ maxEventBytes = 256 * 1024,
840
+ schemas,
841
+ assertContract,
842
+ assertBundle,
843
+ assertFeedback,
844
+ bundleDigest,
845
+ digestInput = (bundle) => bundle,
846
+ packBundle = async (bundle) => bundle,
847
+ unpackBundle = async (bundle) => bundle
848
+ }) {
849
+ if (!domain || !apiPath?.startsWith("/") || !reviewPath?.startsWith("/") || !label)
850
+ throw new TypeError("An encrypted workspace requires an explicit domain and routes.");
851
+ const encoder2 = new TextEncoder();
852
+ const decoder2 = new TextDecoder("utf-8", { fatal: true });
853
+ const tokenPattern = /^[A-Za-z0-9_-]{43}$/;
854
+ const idPattern = /^[A-Za-z0-9_-]{22,64}$/;
855
+ const ec = { name: "ECDSA", namedCurve: "P-256" };
856
+ const signatureAlgorithm = { name: "ECDSA", hash: "SHA-256" };
857
+ const omitSignature = ({ signature: _signature, ...value }) => value;
858
+ const operationError = (message, code, status) => Object.assign(new Error(message), { code, ...status ? { status } : {} });
859
+ function encodeWorkspaceBytes2(bytes) {
860
+ let binary = "";
861
+ for (let offset = 0; offset < bytes.length; offset += 8192)
862
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
863
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
864
+ }
865
+ function decodeWorkspaceBytes2(value) {
866
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/u.test(value))
867
+ throw new TypeError("Invalid encoded workspace value.");
868
+ const decoded = Uint8Array.from(
869
+ atob(value.replaceAll("-", "+").replaceAll("_", "/")),
870
+ (char) => char.charCodeAt(0)
871
+ );
872
+ if (encodeWorkspaceBytes2(decoded) !== value)
873
+ throw new TypeError("Noncanonical encoded workspace value.");
874
+ return decoded;
875
+ }
876
+ function newWorkspaceToken2() {
877
+ return encodeWorkspaceBytes2(crypto.getRandomValues(new Uint8Array(32)));
878
+ }
879
+ function newWorkspaceId2() {
880
+ return encodeWorkspaceBytes2(crypto.getRandomValues(new Uint8Array(18)));
881
+ }
882
+ function normalizeWorkspaceBase2(baseUrl = defaultBaseUrl) {
883
+ const url = new URL(baseUrl);
884
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
885
+ throw new TypeError(
886
+ `${label} sharing requires an HTTPS origin or a local development server.`
887
+ );
888
+ }
889
+ return url.origin;
890
+ }
891
+ function workspaceReviewUrl2(access) {
892
+ if (!idPattern.test(access.id))
893
+ throw new TypeError(`Invalid ${label.toLowerCase()} workspace identity.`);
894
+ return `${normalizeWorkspaceBase2(access.baseUrl)}${reviewPath}/${access.id}`;
895
+ }
896
+ async function tokenMaterial(token, id, purpose) {
897
+ if (!tokenPattern.test(token) || decodeWorkspaceBytes2(token).length !== 32 || !idPattern.test(id))
898
+ throw new TypeError("Enter the complete generated access token.");
899
+ const key = await crypto.subtle.importKey("raw", decodeWorkspaceBytes2(token), "HKDF", false, [
900
+ "deriveBits"
901
+ ]);
902
+ return new Uint8Array(
903
+ await crypto.subtle.deriveBits(
904
+ {
905
+ name: "HKDF",
906
+ hash: "SHA-256",
907
+ salt: encoder2.encode(id),
908
+ info: encoder2.encode(`${domain}/${purpose}`)
909
+ },
910
+ key,
911
+ 256
912
+ )
913
+ );
914
+ }
915
+ async function deriveWorkspaceAuthentication2(token, id) {
916
+ return encodeWorkspaceBytes2(await tokenMaterial(token, id, "reviewer-auth"));
917
+ }
918
+ function canonicalWorkspacePublicKey2(value) {
919
+ if (value?.kty !== "EC" || value.crv !== "P-256" || !tokenPattern.test(value.x ?? "") || !tokenPattern.test(value.y ?? "") || "d" in value) {
920
+ throw new TypeError(`Invalid ${label.toLowerCase()} workspace public key.`);
921
+ }
922
+ return { kty: "EC", crv: "P-256", x: value.x, y: value.y };
923
+ }
924
+ async function createWorkspaceSigner2() {
925
+ const pair = await crypto.subtle.generateKey(ec, true, ["sign", "verify"]);
926
+ return {
927
+ privateKey: encodeWorkspaceBytes2(
928
+ new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey))
929
+ ),
930
+ publicKey: canonicalWorkspacePublicKey2(await crypto.subtle.exportKey("jwk", pair.publicKey))
931
+ };
932
+ }
933
+ async function signWorkspaceValue2(value, privateKey) {
934
+ const key = await crypto.subtle.importKey(
935
+ "pkcs8",
936
+ decodeWorkspaceBytes2(privateKey),
937
+ ec,
938
+ false,
939
+ ["sign"]
940
+ );
941
+ const signature = await crypto.subtle.sign(
831
942
  signatureAlgorithm,
832
943
  key,
833
- decodeWorkspaceBytes(value.signature),
834
- encoder.encode(canonicalizeJson(omitSignature(value)))
944
+ encoder2.encode(canonicalizeJson(omitSignature(value)))
835
945
  );
836
- } catch {
837
- return false;
946
+ return { ...omitSignature(value), signature: encodeWorkspaceBytes2(new Uint8Array(signature)) };
838
947
  }
839
- }
840
- async function seal(value, rawKey, context, limit = DESIGN_WORKSPACE_MAX_BYTES) {
841
- const bytes = encoder.encode(canonicalizeJson(value));
842
- if (bytes.length + 16 > limit)
843
- throw new RangeError(
844
- `Encrypted design data exceeds the ${Math.floor(limit / 1024)} KB upload limit.`
948
+ async function verifyWorkspaceSignature2(value, publicKey) {
949
+ try {
950
+ const key = await crypto.subtle.importKey("jwk", publicKey, ec, false, ["verify"]);
951
+ return await crypto.subtle.verify(
952
+ signatureAlgorithm,
953
+ key,
954
+ decodeWorkspaceBytes2(value.signature),
955
+ encoder2.encode(canonicalizeJson(omitSignature(value)))
956
+ );
957
+ } catch {
958
+ return false;
959
+ }
960
+ }
961
+ async function seal(value, rawKey, context, limit = maxBytes) {
962
+ const bytes = encoder2.encode(canonicalizeJson(value));
963
+ if (bytes.length + 16 > limit)
964
+ throw Object.assign(
965
+ new RangeError(
966
+ `Encrypted ${label.toLowerCase()} data exceeds the ${Math.floor(limit / 1024)} KB upload limit.`
967
+ ),
968
+ { code: "E_WORKSPACE_PAYLOAD_TOO_LARGE", status: 413 }
969
+ );
970
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["encrypt"]);
971
+ const iv = crypto.getRandomValues(new Uint8Array(12));
972
+ const ciphertext = await crypto.subtle.encrypt(
973
+ { name: "AES-GCM", iv, additionalData: encoder2.encode(canonicalizeJson(context)) },
974
+ key,
975
+ bytes
845
976
  );
846
- const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["encrypt"]);
847
- const iv = crypto.getRandomValues(new Uint8Array(12));
848
- const ciphertext = await crypto.subtle.encrypt(
849
- { name: "AES-GCM", iv, additionalData: encoder.encode(canonicalizeJson(context)) },
850
- key,
851
- bytes
852
- );
977
+ return {
978
+ iv: encodeWorkspaceBytes2(iv),
979
+ ciphertext: encodeWorkspaceBytes2(new Uint8Array(ciphertext))
980
+ };
981
+ }
982
+ async function unseal(value, rawKey, context, limit = maxBytes) {
983
+ const bytes = decodeWorkspaceBytes2(value.ciphertext);
984
+ if (bytes.length > limit)
985
+ throw new RangeError(`Shared ${label.toLowerCase()} data exceeds its size limit.`);
986
+ const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["decrypt"]);
987
+ const plaintext = await crypto.subtle.decrypt(
988
+ {
989
+ name: "AES-GCM",
990
+ iv: decodeWorkspaceBytes2(value.iv),
991
+ additionalData: encoder2.encode(canonicalizeJson(context))
992
+ },
993
+ key,
994
+ bytes
995
+ );
996
+ return JSON.parse(decoder2.decode(plaintext));
997
+ }
998
+ const keyringContext = (id, epoch) => ({ type: "keyring", workspaceId: id, epoch });
999
+ const revisionContext = (id, revision) => ({
1000
+ type: "revision",
1001
+ workspaceId: id,
1002
+ id: revision.id,
1003
+ epoch: revision.epoch,
1004
+ reviewOf: revision.reviewOf
1005
+ });
1006
+ const eventContext = (id, event) => ({
1007
+ type: "event",
1008
+ workspaceId: id,
1009
+ id: event.id,
1010
+ epoch: event.epoch,
1011
+ revisionId: event.revisionId,
1012
+ reviewOf: event.reviewOf
1013
+ });
1014
+ function workspaceEnvelopeDigest2(value) {
1015
+ return bundleDigest(value);
1016
+ }
1017
+ async function wrapKeyring(custody, token = custody.token, epoch = custody.epoch, keys = custody.keys) {
1018
+ return seal(
1019
+ { keys, ownerPublicKey: custody.ownerPublicKey },
1020
+ await tokenMaterial(token, custody.id, "key-wrap"),
1021
+ keyringContext(custody.id, epoch)
1022
+ );
1023
+ }
1024
+ async function prepareRevision(custody, bundle) {
1025
+ assertBundle(bundle);
1026
+ const header = {
1027
+ id: newWorkspaceId2(),
1028
+ epoch: custody.epoch,
1029
+ reviewOf: workspaceEnvelopeDigest2(digestInput(bundle)),
1030
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1031
+ };
1032
+ return signWorkspaceValue2(
1033
+ {
1034
+ ...header,
1035
+ ...await seal(
1036
+ await packBundle(bundle),
1037
+ decodeWorkspaceBytes2(custody.keys[custody.epoch]),
1038
+ revisionContext(custody.id, header)
1039
+ )
1040
+ },
1041
+ custody.ownerPrivateKey
1042
+ );
1043
+ }
1044
+ async function prepareWorkspace2(bundle, { baseUrl = defaultBaseUrl } = {}) {
1045
+ const signer = await createWorkspaceSigner2();
1046
+ const custody = {
1047
+ schemaVersion: version,
1048
+ id: newWorkspaceId2(),
1049
+ baseUrl: normalizeWorkspaceBase2(baseUrl),
1050
+ token: newWorkspaceToken2(),
1051
+ ownerAuth: newWorkspaceToken2(),
1052
+ ownerPrivateKey: signer.privateKey,
1053
+ ownerPublicKey: signer.publicKey,
1054
+ epoch: 1,
1055
+ keys: { 1: newWorkspaceToken2() },
1056
+ version: 0
1057
+ };
1058
+ custody.keyring = await wrapKeyring(custody);
1059
+ custody.pendingCreate = await signWorkspaceValue2(
1060
+ {
1061
+ schemaVersion: version,
1062
+ id: custody.id,
1063
+ ownerPublicKey: custody.ownerPublicKey,
1064
+ ownerAuthHash: sha256Hex(custody.ownerAuth),
1065
+ reviewerAuthHash: sha256Hex(await deriveWorkspaceAuthentication2(custody.token, custody.id)),
1066
+ epoch: 1,
1067
+ keyring: custody.keyring,
1068
+ revision: await prepareRevision(custody, bundle),
1069
+ operationId: newWorkspaceId2()
1070
+ },
1071
+ custody.ownerPrivateKey
1072
+ );
1073
+ assertContract(custody.pendingCreate, schemas.create);
1074
+ return custody;
1075
+ }
1076
+ async function request(access, suffix = "", {
1077
+ method = "GET",
1078
+ body,
1079
+ fetchImpl = globalThis.fetch,
1080
+ owner = Boolean(access.ownerAuth),
1081
+ timeoutMs = 2e4
1082
+ } = {}) {
1083
+ if (!idPattern.test(access.id))
1084
+ throw new TypeError(`Invalid ${label.toLowerCase()} workspace identity.`);
1085
+ const authorization = owner ? access.ownerAuth : await deriveWorkspaceAuthentication2(access.token, access.id);
1086
+ const headers = { Authorization: `Bearer ${authorization}`, Accept: "application/json" };
1087
+ if (body) headers["Content-Type"] = "application/json";
1088
+ let response;
1089
+ try {
1090
+ response = await fetchImpl(
1091
+ `${normalizeWorkspaceBase2(access.baseUrl)}${apiPath}/${access.id}${suffix}`,
1092
+ {
1093
+ method,
1094
+ headers,
1095
+ body: body ? JSON.stringify(body) : void 0,
1096
+ redirect: "error",
1097
+ cache: "no-store",
1098
+ credentials: "omit",
1099
+ signal: AbortSignal.timeout(timeoutMs)
1100
+ }
1101
+ );
1102
+ } catch {
1103
+ throw operationError(
1104
+ `${label} sharing is unreachable. Check the connection and retry; the previous review is unchanged.`,
1105
+ "E_WORKSPACE_NETWORK"
1106
+ );
1107
+ }
1108
+ if (!response.ok) await throwResponseError(response, method);
1109
+ return readResponse(response);
1110
+ }
1111
+ async function throwResponseError(response, method) {
1112
+ if (response.status === 404 && method === "PUT" && compatibilityMessage) {
1113
+ const error2 = new Error(compatibilityMessage);
1114
+ error2.code = compatibilityCode;
1115
+ error2.status = 404;
1116
+ throw error2;
1117
+ }
1118
+ const messages = {
1119
+ 401: "Access token is incorrect or has been rotated.",
1120
+ 403: "This review is unavailable or this action requires its owner.",
1121
+ 404: "This shared review could not be found.",
1122
+ 409: "The shared review changed. Refresh its status before retrying.",
1123
+ 410: "This shared review has been revoked or deleted.",
1124
+ 413: `This ${label.toLowerCase()} exceeds the sharing upload limit.`,
1125
+ 429: "Too many requests. Wait a moment and retry.",
1126
+ 503: `${label} sharing is temporarily unavailable. Retry shortly.`
1127
+ };
1128
+ const error = new Error(
1129
+ messages[response.status] ?? `${label} sharing failed (${response.status}). Retry shortly.`
1130
+ );
1131
+ error.status = response.status;
1132
+ error.code = `E_WORKSPACE_HTTP_${response.status}`;
1133
+ try {
1134
+ const reader = response.body?.getReader();
1135
+ const body = reader && JSON.parse(decoder2.decode(await responseBytes(reader, 4096)));
1136
+ if (typeof body?.error === "string" && /^[A-Za-z][A-Za-z0-9_-]{0,127}$/u.test(body.error))
1137
+ error.code = body.error;
1138
+ } catch {
1139
+ }
1140
+ if (response.status === 429) {
1141
+ const header = response.headers.get("retry-after");
1142
+ const seconds = header && /^\d+(?:\.\d+)?$/u.test(header.trim()) ? Number(header) : header ? (Date.parse(header) - Date.now()) / 1e3 : NaN;
1143
+ if (Number.isFinite(seconds) && seconds >= 0)
1144
+ error.retryAfterSeconds = Math.min(3600, Math.max(1, Math.ceil(seconds)));
1145
+ }
1146
+ throw error;
1147
+ }
1148
+ async function responseBytes(reader, limit) {
1149
+ const chunks = [];
1150
+ let length = 0;
1151
+ try {
1152
+ for (; ; ) {
1153
+ const { value, done } = await reader.read();
1154
+ if (done) break;
1155
+ length += value.length;
1156
+ if (length > limit) {
1157
+ await reader.cancel();
1158
+ throw new RangeError(`${label} service returned an oversized response.`);
1159
+ }
1160
+ chunks.push(value);
1161
+ }
1162
+ } catch (error) {
1163
+ if (error instanceof RangeError) throw error;
1164
+ throw new Error("The sharing response was interrupted. Retry the pending operation.");
1165
+ }
1166
+ const bytes = new Uint8Array(length);
1167
+ let offset = 0;
1168
+ for (const chunk of chunks) {
1169
+ bytes.set(chunk, offset);
1170
+ offset += chunk.length;
1171
+ }
1172
+ return bytes;
1173
+ }
1174
+ async function readResponse(response) {
1175
+ if (response.status === 204) return {};
1176
+ const reader = response.body?.getReader();
1177
+ if (!reader) throw new Error(`${label} service returned an empty response.`);
1178
+ try {
1179
+ return JSON.parse(decoder2.decode(await responseBytes(reader, maxBytes * 1.5 + 65536)));
1180
+ } catch (cause) {
1181
+ throw Object.assign(
1182
+ operationError(
1183
+ "The sharing response could not be verified. Retry the pending operation.",
1184
+ "E_WORKSPACE_RESPONSE_INVALID"
1185
+ ),
1186
+ { cause }
1187
+ );
1188
+ }
1189
+ }
1190
+ async function commitWorkspace2(custody, options = {}) {
1191
+ if (!custody.pendingCreate) return getWorkspace2(custody, options);
1192
+ const result = await request(custody, "", {
1193
+ ...options,
1194
+ method: "PUT",
1195
+ body: custody.pendingCreate
1196
+ });
1197
+ assertContract(result, schemas.workspace);
1198
+ if (result.id !== custody.id || result.version !== 1 || result.epoch !== 1 || result.currentRevision !== custody.pendingCreate.revision.id || canonicalizeJson(result.ownerPublicKey) !== canonicalizeJson(custody.ownerPublicKey) || canonicalizeJson(result.keyring) !== canonicalizeJson(custody.keyring))
1199
+ throw new Error("The sharing creation receipt is invalid. Retry the saved operation.");
1200
+ custody.version = result.version;
1201
+ custody.currentRevision = result.currentRevision;
1202
+ delete custody.pendingCreate;
1203
+ return result;
1204
+ }
1205
+ async function getWorkspace2(access, options = {}) {
1206
+ const result = assertContract(await request(access, "", options), schemas.workspace);
1207
+ if (result.id !== access.id) throw new Error("Shared review identity mismatch.");
1208
+ const ring = await unseal(
1209
+ result.keyring,
1210
+ await tokenMaterial(access.token, access.id, "key-wrap"),
1211
+ keyringContext(access.id, result.epoch)
1212
+ );
1213
+ if (canonicalizeJson(ring.ownerPublicKey) !== canonicalizeJson(result.ownerPublicKey) || access.ownerPublicKey && canonicalizeJson(access.ownerPublicKey) !== canonicalizeJson(result.ownerPublicKey))
1214
+ throw new Error("Shared review owner identity changed.");
1215
+ if (!ring.keys?.[result.epoch] || Object.entries(ring.keys).some(
1216
+ ([keyEpoch, key]) => !/^[1-9][0-9]*$/u.test(keyEpoch) || !tokenPattern.test(key)
1217
+ ))
1218
+ throw new Error("Invalid shared review key history.");
1219
+ Object.assign(access, {
1220
+ keys: ring.keys,
1221
+ ownerPublicKey: result.ownerPublicKey,
1222
+ epoch: result.epoch,
1223
+ keyring: result.keyring,
1224
+ version: result.version,
1225
+ currentRevision: result.currentRevision,
1226
+ commentsPaused: result.commentsPaused
1227
+ });
1228
+ return result;
1229
+ }
1230
+ async function listWorkspaceRevisions2(access, { after = "", ...options } = {}) {
1231
+ if (after && !idPattern.test(after)) throw new TypeError("Invalid revision cursor.");
1232
+ return request(
1233
+ access,
1234
+ `/revisions${after ? `?after=${encodeURIComponent(after)}` : ""}`,
1235
+ options
1236
+ );
1237
+ }
1238
+ async function decryptWorkspaceRevision2(access, revisionId = access.currentRevision, options = {}) {
1239
+ if (!access.keys) await getWorkspace2(access, options);
1240
+ if (!idPattern.test(revisionId))
1241
+ throw new TypeError(`Invalid ${label.toLowerCase()} revision.`);
1242
+ const revision = assertContract(
1243
+ await request(access, `/revisions/${revisionId}`, options),
1244
+ schemas.revision
1245
+ );
1246
+ if (revision.id !== revisionId || !await verifyWorkspaceSignature2(revision, access.ownerPublicKey))
1247
+ throw new Error(`The published ${label.toLowerCase()} signature is invalid.`);
1248
+ if (!access.keys[revision.epoch])
1249
+ throw new Error("The access token cannot open this revision.");
1250
+ const bundle = assertBundle(
1251
+ await unpackBundle(
1252
+ await unseal(
1253
+ revision,
1254
+ decodeWorkspaceBytes2(access.keys[revision.epoch]),
1255
+ revisionContext(access.id, revision)
1256
+ )
1257
+ )
1258
+ );
1259
+ if (workspaceEnvelopeDigest2(digestInput(bundle)) !== revision.reviewOf)
1260
+ throw new Error(`Published ${label.toLowerCase()} content does not match its revision.`);
1261
+ return { ...bundle, workspaceRevision: revision.id, reviewOf: revision.reviewOf };
1262
+ }
1263
+ async function prepareWorkspaceMutation2(custody, action, payload = void 0) {
1264
+ if (custody.pendingCreate)
1265
+ throw new Error("Finish creating this shared review before changing it.");
1266
+ if (custody.pendingMutation)
1267
+ throw new Error("A sharing operation is pending. Retry it before making another change.");
1268
+ const body = {
1269
+ operationId: newWorkspaceId2(),
1270
+ expectedVersion: custody.version,
1271
+ epoch: custody.epoch
1272
+ };
1273
+ let next = {};
1274
+ if (action === "publish") body.revision = await prepareRevision(custody, payload);
1275
+ else if (action === "rotate") {
1276
+ const token = newWorkspaceToken2();
1277
+ const epoch = custody.epoch + 1;
1278
+ const keys = { ...custody.keys, [epoch]: newWorkspaceToken2() };
1279
+ const keyring = await wrapKeyring(custody, token, epoch, keys);
1280
+ Object.assign(body, {
1281
+ epoch,
1282
+ reviewerAuthHash: sha256Hex(await deriveWorkspaceAuthentication2(token, custody.id)),
1283
+ keyring
1284
+ });
1285
+ next = { token, epoch, keys, keyring };
1286
+ } else if (["pause", "resume", "revoke", "delete"].includes(action)) body.action = action;
1287
+ else throw new TypeError(`Unknown ${label.toLowerCase()} sharing operation.`);
1288
+ custody.pendingMutation = {
1289
+ action,
1290
+ body: await signWorkspaceValue2(body, custody.ownerPrivateKey),
1291
+ next
1292
+ };
1293
+ return custody.pendingMutation;
1294
+ }
1295
+ function assertDeletionReceipt(custody, result) {
1296
+ if (result.schemaVersion !== version || result.id !== custody.id || result.deleted !== true || Object.keys(result).some((key) => !["schemaVersion", "id", "deleted"].includes(key)))
1297
+ throw new Error("The deletion receipt is invalid. Retry the saved operation.");
1298
+ }
1299
+ function assertMutationReceipt(custody, pending, result) {
1300
+ if (pending.action === "delete") return assertDeletionReceipt(custody, result);
1301
+ assertContract(result, schemas.workspace);
1302
+ const expectedRevision = pending.action === "publish" ? pending.body.revision.id : custody.currentRevision;
1303
+ if (result.id !== custody.id || result.version !== pending.body.expectedVersion + 1 || result.epoch !== pending.body.epoch || result.currentRevision !== expectedRevision || canonicalizeJson(result.ownerPublicKey) !== canonicalizeJson(custody.ownerPublicKey) || canonicalizeJson(result.keyring) !== canonicalizeJson(pending.next.keyring ?? custody.keyring) || ["pause", "resume"].includes(pending.action) && result.commentsPaused !== (pending.action === "pause"))
1304
+ throw new Error("The sharing operation receipt is invalid. Retry the saved operation.");
1305
+ }
1306
+ async function commitWorkspaceMutation2(custody, options = {}) {
1307
+ const pending = custody.pendingMutation;
1308
+ if (!pending) throw new Error("No sharing operation is pending.");
1309
+ const suffix = ["publish", "rotate"].includes(pending.action) ? pending.action : "manage";
1310
+ const result = await request(custody, `/${suffix}`, {
1311
+ ...options,
1312
+ method: "POST",
1313
+ body: pending.body
1314
+ });
1315
+ assertMutationReceipt(custody, pending, result);
1316
+ Object.assign(custody, pending.next, { version: pending.body.expectedVersion + 1 });
1317
+ if (pending.action === "publish") custody.currentRevision = pending.body.revision.id;
1318
+ if (pending.action === "pause" || pending.action === "resume")
1319
+ custody.commentsPaused = pending.action === "pause";
1320
+ if (pending.action === "revoke" || pending.action === "delete")
1321
+ custody.status = pending.action === "revoke" ? "revoked" : "deleted";
1322
+ delete custody.pendingMutation;
1323
+ return result;
1324
+ }
1325
+ async function publishWorkspace2(custody, bundle, options = {}) {
1326
+ await prepareWorkspaceMutation2(custody, "publish", bundle);
1327
+ return commitWorkspaceMutation2(custody, options);
1328
+ }
1329
+ async function rotateWorkspace2(custody, options = {}) {
1330
+ await prepareWorkspaceMutation2(custody, "rotate");
1331
+ return commitWorkspaceMutation2(custody, options);
1332
+ }
1333
+ async function manageWorkspace2(custody, action, options = {}) {
1334
+ await prepareWorkspaceMutation2(custody, action);
1335
+ return commitWorkspaceMutation2(custody, options);
1336
+ }
1337
+ async function prepareWorkspaceEvent2(access, payload, { revisionId = access.currentRevision, reviewOf = payload.reviewOf, signer } = {}) {
1338
+ if (!access.keys) throw new Error("Unlock the shared review before commenting.");
1339
+ assertFeedback(payload);
1340
+ const identity = signer ?? await createWorkspaceSigner2();
1341
+ const publicKey = canonicalWorkspacePublicKey2(identity.publicKey);
1342
+ const header = { id: newWorkspaceId2(), revisionId, reviewOf, epoch: access.epoch };
1343
+ const event = await signWorkspaceValue2(
1344
+ {
1345
+ ...header,
1346
+ ...await seal(
1347
+ payload,
1348
+ decodeWorkspaceBytes2(access.keys[access.epoch]),
1349
+ eventContext(access.id, header),
1350
+ maxEventBytes
1351
+ ),
1352
+ publicKey
1353
+ },
1354
+ identity.privateKey
1355
+ );
1356
+ return assertContract(event, schemas.event);
1357
+ }
1358
+ async function appendWorkspaceEvent2(access, payload, { preparedEvent, signer, revisionId, reviewOf, ...options } = {}) {
1359
+ const event = preparedEvent ?? await prepareWorkspaceEvent2(access, payload, {
1360
+ signer,
1361
+ revisionId,
1362
+ reviewOf: reviewOf ?? payload.reviewOf
1363
+ });
1364
+ const result = await request(access, "/events", { ...options, method: "POST", body: event });
1365
+ if (!result || typeof result !== "object" || Array.isArray(result) || !Number.isSafeInteger(result.sequence) || result.sequence < 1 || !result.event || typeof result.event !== "object" || Array.isArray(result.event))
1366
+ throw new Error("The feedback receipt is invalid. Retry the saved operation.");
1367
+ const { sequence: eventSequence, ...received } = result.event;
1368
+ if (eventSequence !== void 0 && eventSequence !== result.sequence || canonicalizeJson(received) !== canonicalizeJson(event))
1369
+ throw new Error(
1370
+ "The feedback receipt does not match the saved event. Retry the saved operation."
1371
+ );
1372
+ return { event: { ...received, sequence: result.sequence }, sequence: result.sequence };
1373
+ }
1374
+ function assertEventPage(page, after) {
1375
+ if (!Array.isArray(page.events) || page.events.length > 100 || !Number.isSafeInteger(page.cursor) || page.cursor < after || typeof page.hasMore !== "boolean")
1376
+ throw new Error("Invalid shared feedback page.");
1377
+ }
1378
+ async function decryptEvent(access, record, sequence) {
1379
+ assertContract(record, schemas.event);
1380
+ if (!await verifyWorkspaceSignature2(record, record.publicKey))
1381
+ throw new Error("Signature verification failed.");
1382
+ if (!access.keys[record.epoch]) throw new Error("The encryption epoch is unavailable.");
1383
+ const payload = await unseal(
1384
+ record,
1385
+ decodeWorkspaceBytes2(access.keys[record.epoch]),
1386
+ eventContext(access.id, record),
1387
+ maxEventBytes
1388
+ );
1389
+ assertFeedback(payload);
1390
+ if (payload.reviewOf !== record.reviewOf) throw new Error("Feedback revision is invalid.");
1391
+ return { ...record, sequence, payload };
1392
+ }
1393
+ function eventFromPage(item, pageIds, previousSequence, cursor) {
1394
+ const { sequence, ...record } = item?.event ? { ...item.event, sequence: item.sequence } : item ?? {};
1395
+ if (!Number.isSafeInteger(sequence) || sequence <= previousSequence || sequence > cursor)
1396
+ throw new Error("Invalid shared feedback sequence.");
1397
+ if (typeof record.id !== "string" || pageIds.has(record.id))
1398
+ throw new Error("Invalid shared feedback event identity.");
1399
+ pageIds.add(record.id);
1400
+ return { sequence, record };
1401
+ }
1402
+ async function readWorkspaceEvents2(access, { after = 0, ...options } = {}) {
1403
+ if (!Number.isSafeInteger(after) || after < 0) throw new TypeError("Invalid feedback cursor.");
1404
+ if (!access.keys) await getWorkspace2(access, options);
1405
+ const page = await request(access, `/events?after=${after}`, options);
1406
+ assertEventPage(page, after);
1407
+ const events = [], issues = [], pageIds = /* @__PURE__ */ new Set();
1408
+ let previousSequence = after;
1409
+ for (const item of page.events) {
1410
+ const { sequence, record } = eventFromPage(item, pageIds, previousSequence, page.cursor);
1411
+ previousSequence = sequence;
1412
+ try {
1413
+ events.push(await decryptEvent(access, record, sequence));
1414
+ } catch {
1415
+ issues.push({
1416
+ sequence,
1417
+ id: typeof record.id === "string" && idPattern.test(record.id) ? record.id : null,
1418
+ reason: "Invalid encrypted feedback; not imported."
1419
+ });
1420
+ }
1421
+ }
1422
+ return { ...page, events, issues };
1423
+ }
853
1424
  return {
854
- iv: encodeWorkspaceBytes(iv),
855
- ciphertext: encodeWorkspaceBytes(new Uint8Array(ciphertext))
1425
+ encodeWorkspaceBytes: encodeWorkspaceBytes2,
1426
+ decodeWorkspaceBytes: decodeWorkspaceBytes2,
1427
+ newWorkspaceToken: newWorkspaceToken2,
1428
+ newWorkspaceId: newWorkspaceId2,
1429
+ normalizeWorkspaceBase: normalizeWorkspaceBase2,
1430
+ workspaceReviewUrl: workspaceReviewUrl2,
1431
+ deriveWorkspaceAuthentication: deriveWorkspaceAuthentication2,
1432
+ canonicalWorkspacePublicKey: canonicalWorkspacePublicKey2,
1433
+ createWorkspaceSigner: createWorkspaceSigner2,
1434
+ signWorkspaceValue: signWorkspaceValue2,
1435
+ verifyWorkspaceSignature: verifyWorkspaceSignature2,
1436
+ workspaceEnvelopeDigest: workspaceEnvelopeDigest2,
1437
+ prepareWorkspace: prepareWorkspace2,
1438
+ commitWorkspace: commitWorkspace2,
1439
+ getWorkspace: getWorkspace2,
1440
+ listWorkspaceRevisions: listWorkspaceRevisions2,
1441
+ decryptWorkspaceRevision: decryptWorkspaceRevision2,
1442
+ prepareWorkspaceMutation: prepareWorkspaceMutation2,
1443
+ commitWorkspaceMutation: commitWorkspaceMutation2,
1444
+ publishWorkspace: publishWorkspace2,
1445
+ rotateWorkspace: rotateWorkspace2,
1446
+ manageWorkspace: manageWorkspace2,
1447
+ prepareWorkspaceEvent: prepareWorkspaceEvent2,
1448
+ appendWorkspaceEvent: appendWorkspaceEvent2,
1449
+ readWorkspaceEvents: readWorkspaceEvents2
856
1450
  };
857
1451
  }
858
- async function unseal(value, rawKey, context, limit = DESIGN_WORKSPACE_MAX_BYTES) {
859
- const bytes = decodeWorkspaceBytes(value.ciphertext);
860
- if (bytes.length > limit) throw new RangeError("Shared design data exceeds its size limit.");
861
- const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, ["decrypt"]);
862
- const plaintext = await crypto.subtle.decrypt(
863
- {
864
- name: "AES-GCM",
865
- iv: decodeWorkspaceBytes(value.iv),
866
- additionalData: encoder.encode(canonicalizeJson(context))
867
- },
868
- key,
869
- bytes
870
- );
871
- return JSON.parse(decoder.decode(plaintext));
872
- }
1452
+
1453
+ // packages/design/lib/design/workspace-client.mjs
1454
+ var DESIGN_SHARE_BASE_URL = "https://share.openplanr.dev";
1455
+ var encoder = new TextEncoder();
1456
+ var decoder = new TextDecoder("utf-8", { fatal: true });
873
1457
  var PACKED_BUNDLE_KIND = "openplanr-design-review-bundle-packed";
874
1458
  var PACKED_BUNDLE_MAX_BYTES = 128 * 1024 * 1024;
875
1459
  var SHARED_BLOCK_MIN_LENGTH = 2048;
@@ -968,376 +1552,69 @@ async function unpackDesignReviewBundle(value, { maxBytes = PACKED_BUNDLE_MAX_BY
968
1552
  });
969
1553
  return { ...bundle, envelope: { ...bundle.envelope, artifacts } };
970
1554
  }
971
- var keyringContext = (id, epoch) => ({ type: "keyring", workspaceId: id, epoch });
972
- var revisionContext = (id, revision) => ({
973
- type: "revision",
974
- workspaceId: id,
975
- id: revision.id,
976
- epoch: revision.epoch,
977
- reviewOf: revision.reviewOf
978
- });
979
- var eventContext = (id, event) => ({
980
- type: "event",
981
- workspaceId: id,
982
- id: event.id,
983
- epoch: event.epoch,
984
- revisionId: event.revisionId,
985
- reviewOf: event.reviewOf
986
- });
987
- function workspaceEnvelopeDigest(envelope) {
988
- return sha256Hex(
1555
+ var client = createEncryptedWorkspaceClient({
1556
+ domain: "openplanr-design-workspace/v1",
1557
+ apiPath: DESIGN_WORKSPACE_API,
1558
+ reviewPath: "/d",
1559
+ label: "Design",
1560
+ defaultBaseUrl: DESIGN_SHARE_BASE_URL,
1561
+ version: DESIGN_WORKSPACE_VERSION,
1562
+ maxBytes: DESIGN_WORKSPACE_MAX_BYTES,
1563
+ maxEventBytes: DESIGN_WORKSPACE_MAX_EVENT_BYTES,
1564
+ schemas: {
1565
+ create: DESIGN_WORKSPACE_CREATE_SCHEMA,
1566
+ event: DESIGN_WORKSPACE_EVENT_SCHEMA,
1567
+ revision: DESIGN_WORKSPACE_REVISION_SCHEMA,
1568
+ workspace: DESIGN_WORKSPACE_SCHEMA
1569
+ },
1570
+ assertContract: assertWorkspaceContract,
1571
+ assertBundle: assertDesignReviewBundle,
1572
+ assertFeedback(payload) {
1573
+ if (["category", "disposition"].includes(payload.kind)) assertDesignReviewMetadata(payload);
1574
+ if (!["review", "direction", "category", "disposition"].includes(payload.kind))
1575
+ throw new TypeError("Unknown design feedback kind.");
1576
+ if (typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160)
1577
+ throw new TypeError("Enter your name before leaving feedback.");
1578
+ return payload;
1579
+ },
1580
+ digestInput: (bundle) => bundle.envelope,
1581
+ bundleDigest: (envelope) => sha256Hex(
989
1582
  canonicalizeJson({
990
1583
  schemaVersion: envelope.schemaVersion,
991
1584
  artifacts: envelope.artifacts,
992
1585
  viewer: envelope.viewer
993
1586
  })
994
- );
995
- }
996
- async function wrapKeyring(custody, token = custody.token, epoch = custody.epoch, keys = custody.keys) {
997
- return seal(
998
- { keys, ownerPublicKey: custody.ownerPublicKey },
999
- await tokenMaterial(token, custody.id, "key-wrap"),
1000
- keyringContext(custody.id, epoch)
1001
- );
1002
- }
1003
- async function prepareRevision(custody, bundle) {
1004
- assertDesignReviewBundle(bundle);
1005
- const header = {
1006
- id: newWorkspaceId(),
1007
- epoch: custody.epoch,
1008
- reviewOf: workspaceEnvelopeDigest(bundle.envelope),
1009
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1010
- };
1011
- return signWorkspaceValue(
1012
- {
1013
- ...header,
1014
- ...await seal(
1015
- await packDesignReviewBundle(bundle),
1016
- decodeWorkspaceBytes(custody.keys[custody.epoch]),
1017
- revisionContext(custody.id, header)
1018
- )
1019
- },
1020
- custody.ownerPrivateKey
1021
- );
1022
- }
1023
- async function prepareWorkspace(bundle, { baseUrl = DESIGN_SHARE_BASE_URL } = {}) {
1024
- const signer = await createWorkspaceSigner();
1025
- const custody = {
1026
- schemaVersion: DESIGN_WORKSPACE_VERSION,
1027
- id: newWorkspaceId(),
1028
- baseUrl: normalizeWorkspaceBase(baseUrl),
1029
- token: newWorkspaceToken(),
1030
- ownerAuth: newWorkspaceToken(),
1031
- ownerPrivateKey: signer.privateKey,
1032
- ownerPublicKey: signer.publicKey,
1033
- epoch: 1,
1034
- keys: { 1: newWorkspaceToken() },
1035
- version: 0
1036
- };
1037
- custody.keyring = await wrapKeyring(custody);
1038
- custody.pendingCreate = await signWorkspaceValue(
1039
- {
1040
- schemaVersion: DESIGN_WORKSPACE_VERSION,
1041
- id: custody.id,
1042
- ownerPublicKey: custody.ownerPublicKey,
1043
- ownerAuthHash: sha256Hex(custody.ownerAuth),
1044
- reviewerAuthHash: sha256Hex(await deriveWorkspaceAuthentication(custody.token, custody.id)),
1045
- epoch: 1,
1046
- keyring: custody.keyring,
1047
- revision: await prepareRevision(custody, bundle),
1048
- operationId: newWorkspaceId()
1049
- },
1050
- custody.ownerPrivateKey
1051
- );
1052
- assertWorkspaceContract(custody.pendingCreate, DESIGN_WORKSPACE_CREATE_SCHEMA);
1053
- return custody;
1054
- }
1055
- async function request(access, suffix = "", {
1056
- method = "GET",
1057
- body,
1058
- fetchImpl = globalThis.fetch,
1059
- owner = Boolean(access.ownerAuth),
1060
- timeoutMs = 2e4
1061
- } = {}) {
1062
- if (!idPattern.test(access.id)) throw new TypeError("Invalid design workspace identity.");
1063
- const authorization = owner ? access.ownerAuth : await deriveWorkspaceAuthentication(access.token, access.id);
1064
- const headers = { Authorization: `Bearer ${authorization}`, Accept: "application/json" };
1065
- if (body) headers["Content-Type"] = "application/json";
1066
- let response;
1067
- try {
1068
- response = await fetchImpl(
1069
- `${normalizeWorkspaceBase(access.baseUrl)}${DESIGN_WORKSPACE_API}/${access.id}${suffix}`,
1070
- {
1071
- method,
1072
- headers,
1073
- body: body ? JSON.stringify(body) : void 0,
1074
- redirect: "error",
1075
- cache: "no-store",
1076
- credentials: "omit",
1077
- signal: AbortSignal.timeout(timeoutMs)
1078
- }
1079
- );
1080
- } catch {
1081
- throw new Error(
1082
- "Design sharing is unreachable. Check the connection and retry; the previous review is unchanged."
1083
- );
1084
- }
1085
- if (!response.ok) {
1086
- const messages = {
1087
- 401: "Access token is incorrect or has been rotated.",
1088
- 403: "This review is unavailable or this action requires its owner.",
1089
- 404: "This shared review could not be found.",
1090
- 409: "The shared review changed. Refresh its status before retrying.",
1091
- 410: "This shared review has been revoked or deleted.",
1092
- 413: "This design exceeds the sharing upload limit.",
1093
- 429: "Too many requests. Wait a moment and retry.",
1094
- 503: "Design sharing is temporarily unavailable. Retry shortly."
1095
- };
1096
- const error = new Error(
1097
- messages[response.status] ?? `Design sharing failed (${response.status}). Retry shortly.`
1098
- );
1099
- error.status = response.status;
1100
- throw error;
1101
- }
1102
- if (response.status === 204) return {};
1103
- const limit = DESIGN_WORKSPACE_MAX_BYTES * 1.5 + 65536;
1104
- const reader = response.body?.getReader();
1105
- if (!reader) throw new Error("Design service returned an empty response.");
1106
- const chunks = [];
1107
- let length = 0;
1108
- try {
1109
- for (; ; ) {
1110
- const { value, done } = await reader.read();
1111
- if (done) break;
1112
- length += value.length;
1113
- if (length > limit) {
1114
- await reader.cancel();
1115
- throw new RangeError("Design service returned an oversized response.");
1116
- }
1117
- chunks.push(value);
1118
- }
1119
- } catch (error) {
1120
- if (error instanceof RangeError) throw error;
1121
- throw new Error("The sharing response was interrupted. Retry the pending operation.");
1122
- }
1123
- const bytes = new Uint8Array(length);
1124
- let offset = 0;
1125
- for (const chunk of chunks) {
1126
- bytes.set(chunk, offset);
1127
- offset += chunk.length;
1128
- }
1129
- return JSON.parse(decoder.decode(bytes));
1130
- }
1131
- async function commitWorkspace(custody, options = {}) {
1132
- if (!custody.pendingCreate) return getWorkspace(custody, options);
1133
- const result = await request(custody, "", {
1134
- ...options,
1135
- method: "PUT",
1136
- body: custody.pendingCreate
1137
- });
1138
- assertWorkspaceContract(result, DESIGN_WORKSPACE_SCHEMA);
1139
- if (result.id !== custody.id || result.version !== 1 || result.epoch !== 1 || result.currentRevision !== custody.pendingCreate.revision.id || canonicalizeJson(result.ownerPublicKey) !== canonicalizeJson(custody.ownerPublicKey) || canonicalizeJson(result.keyring) !== canonicalizeJson(custody.keyring))
1140
- throw new Error("The sharing creation receipt is invalid. Retry the saved operation.");
1141
- custody.version = result.version;
1142
- custody.currentRevision = result.currentRevision;
1143
- delete custody.pendingCreate;
1144
- return result;
1145
- }
1146
- async function getWorkspace(access, options = {}) {
1147
- const result = assertWorkspaceContract(
1148
- await request(access, "", options),
1149
- DESIGN_WORKSPACE_SCHEMA
1150
- );
1151
- if (result.id !== access.id) throw new Error("Shared review identity mismatch.");
1152
- const ring = await unseal(
1153
- result.keyring,
1154
- await tokenMaterial(access.token, access.id, "key-wrap"),
1155
- keyringContext(access.id, result.epoch)
1156
- );
1157
- if (canonicalizeJson(ring.ownerPublicKey) !== canonicalizeJson(result.ownerPublicKey) || access.ownerPublicKey && canonicalizeJson(access.ownerPublicKey) !== canonicalizeJson(result.ownerPublicKey))
1158
- throw new Error("Shared review owner identity changed.");
1159
- if (!ring.keys || !ring.keys[result.epoch] || Object.entries(ring.keys).some(
1160
- ([keyEpoch, key]) => !/^[1-9][0-9]*$/u.test(keyEpoch) || !tokenPattern.test(key)
1161
- ))
1162
- throw new Error("Invalid shared review key history.");
1163
- Object.assign(access, {
1164
- keys: ring.keys,
1165
- ownerPublicKey: result.ownerPublicKey,
1166
- epoch: result.epoch,
1167
- keyring: result.keyring,
1168
- version: result.version,
1169
- currentRevision: result.currentRevision,
1170
- commentsPaused: result.commentsPaused
1171
- });
1172
- return result;
1173
- }
1174
- async function decryptWorkspaceRevision(access, revisionId = access.currentRevision, options = {}) {
1175
- if (!access.keys) await getWorkspace(access, options);
1176
- if (!idPattern.test(revisionId)) throw new TypeError("Invalid design revision.");
1177
- const revision = assertWorkspaceContract(
1178
- await request(access, `/revisions/${revisionId}`, options),
1179
- DESIGN_WORKSPACE_REVISION_SCHEMA
1180
- );
1181
- if (revision.id !== revisionId || !await verifyWorkspaceSignature(revision, access.ownerPublicKey))
1182
- throw new Error("The published design signature is invalid.");
1183
- if (!access.keys[revision.epoch]) throw new Error("The access token cannot open this revision.");
1184
- const bundle = assertDesignReviewBundle(
1185
- await unpackDesignReviewBundle(
1186
- await unseal(
1187
- revision,
1188
- decodeWorkspaceBytes(access.keys[revision.epoch]),
1189
- revisionContext(access.id, revision)
1190
- )
1191
- )
1192
- );
1193
- if (workspaceEnvelopeDigest(bundle.envelope) !== revision.reviewOf)
1194
- throw new Error("Published design content does not match its revision.");
1195
- return { ...bundle, workspaceRevision: revision.id, reviewOf: revision.reviewOf };
1196
- }
1197
- async function prepareWorkspaceMutation(custody, action, payload = void 0) {
1198
- if (custody.pendingCreate)
1199
- throw new Error("Finish creating this shared review before changing it.");
1200
- if (custody.pendingMutation)
1201
- throw new Error("A sharing operation is pending. Retry it before making another change.");
1202
- const body = {
1203
- operationId: newWorkspaceId(),
1204
- expectedVersion: custody.version,
1205
- epoch: custody.epoch
1206
- };
1207
- let next = {};
1208
- if (action === "publish") body.revision = await prepareRevision(custody, payload);
1209
- else if (action === "rotate") {
1210
- const token = newWorkspaceToken();
1211
- const epoch = custody.epoch + 1;
1212
- const keys = { ...custody.keys, [epoch]: newWorkspaceToken() };
1213
- const keyring = await wrapKeyring(custody, token, epoch, keys);
1214
- Object.assign(body, {
1215
- epoch,
1216
- reviewerAuthHash: sha256Hex(await deriveWorkspaceAuthentication(token, custody.id)),
1217
- keyring
1218
- });
1219
- next = { token, epoch, keys, keyring };
1220
- } else if (["pause", "resume", "revoke", "delete"].includes(action)) body.action = action;
1221
- else throw new TypeError("Unknown design sharing operation.");
1222
- custody.pendingMutation = {
1223
- action,
1224
- body: await signWorkspaceValue(body, custody.ownerPrivateKey),
1225
- next
1226
- };
1227
- return custody.pendingMutation;
1228
- }
1229
- async function commitWorkspaceMutation(custody, options = {}) {
1230
- const pending = custody.pendingMutation;
1231
- if (!pending) throw new Error("No sharing operation is pending.");
1232
- const suffix = ["publish", "rotate"].includes(pending.action) ? pending.action : "manage";
1233
- const result = await request(custody, `/${suffix}`, {
1234
- ...options,
1235
- method: "POST",
1236
- body: pending.body
1237
- });
1238
- if (pending.action === "delete") {
1239
- if (result.schemaVersion !== DESIGN_WORKSPACE_VERSION || result.id !== custody.id || result.deleted !== true || Object.keys(result).some((key) => !["schemaVersion", "id", "deleted"].includes(key)))
1240
- throw new Error("The deletion receipt is invalid. Retry the saved operation.");
1241
- } else {
1242
- assertWorkspaceContract(result, DESIGN_WORKSPACE_SCHEMA);
1243
- const expectedRevision = pending.action === "publish" ? pending.body.revision.id : custody.currentRevision;
1244
- if (result.id !== custody.id || result.version !== pending.body.expectedVersion + 1 || result.epoch !== pending.body.epoch || result.currentRevision !== expectedRevision || canonicalizeJson(result.ownerPublicKey) !== canonicalizeJson(custody.ownerPublicKey) || canonicalizeJson(result.keyring) !== canonicalizeJson(pending.next.keyring ?? custody.keyring) || ["pause", "resume"].includes(pending.action) && result.commentsPaused !== (pending.action === "pause"))
1245
- throw new Error("The sharing operation receipt is invalid. Retry the saved operation.");
1246
- }
1247
- Object.assign(custody, pending.next, { version: pending.body.expectedVersion + 1 });
1248
- if (pending.action === "publish") custody.currentRevision = pending.body.revision.id;
1249
- if (pending.action === "pause" || pending.action === "resume")
1250
- custody.commentsPaused = pending.action === "pause";
1251
- if (pending.action === "revoke" || pending.action === "delete")
1252
- custody.status = pending.action === "revoke" ? "revoked" : "deleted";
1253
- delete custody.pendingMutation;
1254
- return result;
1255
- }
1256
- async function prepareWorkspaceEvent(access, payload, { revisionId = access.currentRevision, reviewOf = payload.reviewOf, signer } = {}) {
1257
- if (!access.keys) throw new Error("Unlock the shared review before commenting.");
1258
- if (["category", "disposition"].includes(payload.kind)) assertDesignReviewMetadata(payload);
1259
- if (!["review", "direction", "category", "disposition"].includes(payload.kind))
1260
- throw new TypeError("Unknown design feedback kind.");
1261
- if (typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160)
1262
- throw new TypeError("Enter your name before leaving feedback.");
1263
- const identity = signer ?? await createWorkspaceSigner();
1264
- const publicKey = canonicalWorkspacePublicKey(identity.publicKey);
1265
- const header = { id: newWorkspaceId(), revisionId, reviewOf, epoch: access.epoch };
1266
- const event = await signWorkspaceValue(
1267
- {
1268
- ...header,
1269
- ...await seal(
1270
- payload,
1271
- decodeWorkspaceBytes(access.keys[access.epoch]),
1272
- eventContext(access.id, header),
1273
- DESIGN_WORKSPACE_MAX_EVENT_BYTES
1274
- ),
1275
- publicKey
1276
- },
1277
- identity.privateKey
1278
- );
1279
- return assertWorkspaceContract(event, DESIGN_WORKSPACE_EVENT_SCHEMA);
1280
- }
1281
- async function appendWorkspaceEvent(access, payload, { preparedEvent, signer, revisionId, reviewOf, ...options } = {}) {
1282
- const event = preparedEvent ?? await prepareWorkspaceEvent(access, payload, {
1283
- signer,
1284
- revisionId,
1285
- reviewOf: reviewOf ?? payload.reviewOf
1286
- });
1287
- const result = await request(access, "/events", { ...options, method: "POST", body: event });
1288
- if (!result || typeof result !== "object" || Array.isArray(result) || !Number.isSafeInteger(result.sequence) || result.sequence < 1 || !result.event || typeof result.event !== "object" || Array.isArray(result.event))
1289
- throw new Error("The feedback receipt is invalid. Retry the saved operation.");
1290
- const { sequence: eventSequence, ...received } = result.event;
1291
- if (eventSequence !== void 0 && eventSequence !== result.sequence || canonicalizeJson(received) !== canonicalizeJson(event))
1292
- throw new Error(
1293
- "The feedback receipt does not match the saved event. Retry the saved operation."
1294
- );
1295
- return { event: { ...received, sequence: result.sequence }, sequence: result.sequence };
1296
- }
1297
- async function readWorkspaceEvents(access, { after = 0, ...options } = {}) {
1298
- if (!Number.isSafeInteger(after) || after < 0) throw new TypeError("Invalid feedback cursor.");
1299
- if (!access.keys) await getWorkspace(access, options);
1300
- const page = await request(access, `/events?after=${after}`, options);
1301
- if (!Array.isArray(page.events) || page.events.length > 100 || !Number.isSafeInteger(page.cursor) || page.cursor < after || typeof page.hasMore !== "boolean")
1302
- throw new Error("Invalid shared feedback page.");
1303
- const events = [];
1304
- const issues = [];
1305
- let previousSequence = after;
1306
- const pageIds = /* @__PURE__ */ new Set();
1307
- for (const item of page.events) {
1308
- const { sequence, ...record } = item?.event ? { ...item.event, sequence: item.sequence } : item ?? {};
1309
- if (!Number.isSafeInteger(sequence) || sequence <= previousSequence || sequence > page.cursor)
1310
- throw new Error("Invalid shared feedback sequence.");
1311
- previousSequence = sequence;
1312
- if (typeof record.id !== "string" || pageIds.has(record.id))
1313
- throw new Error("Invalid shared feedback event identity.");
1314
- pageIds.add(record.id);
1315
- try {
1316
- assertWorkspaceContract(record, DESIGN_WORKSPACE_EVENT_SCHEMA);
1317
- if (!await verifyWorkspaceSignature(record, record.publicKey))
1318
- throw new Error("Signature verification failed.");
1319
- if (!access.keys[record.epoch]) throw new Error("The encryption epoch is unavailable.");
1320
- const payload = await unseal(
1321
- record,
1322
- decodeWorkspaceBytes(access.keys[record.epoch]),
1323
- eventContext(access.id, record),
1324
- DESIGN_WORKSPACE_MAX_EVENT_BYTES
1325
- );
1326
- if (["category", "disposition"].includes(payload.kind)) assertDesignReviewMetadata(payload);
1327
- if (!["review", "direction", "category", "disposition"].includes(payload.kind) || typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160 || payload.reviewOf !== record.reviewOf)
1328
- throw new Error("Feedback author, kind or revision is invalid.");
1329
- events.push({ ...record, sequence, payload });
1330
- } catch {
1331
- issues.push({
1332
- sequence,
1333
- id: typeof record.id === "string" && idPattern.test(record.id) ? record.id : null,
1334
- reason: "Invalid encrypted feedback; not imported."
1335
- });
1336
- }
1337
- }
1338
- if (previousSequence > page.cursor) throw new Error("Invalid shared feedback cursor.");
1339
- return { ...page, events, issues };
1340
- }
1587
+ ),
1588
+ packBundle: packDesignReviewBundle,
1589
+ unpackBundle: unpackDesignReviewBundle
1590
+ });
1591
+ var {
1592
+ encodeWorkspaceBytes,
1593
+ decodeWorkspaceBytes,
1594
+ newWorkspaceToken,
1595
+ newWorkspaceId,
1596
+ normalizeWorkspaceBase,
1597
+ workspaceReviewUrl,
1598
+ deriveWorkspaceAuthentication,
1599
+ canonicalWorkspacePublicKey,
1600
+ createWorkspaceSigner,
1601
+ signWorkspaceValue,
1602
+ verifyWorkspaceSignature,
1603
+ workspaceEnvelopeDigest,
1604
+ prepareWorkspace,
1605
+ commitWorkspace,
1606
+ getWorkspace,
1607
+ listWorkspaceRevisions,
1608
+ decryptWorkspaceRevision,
1609
+ prepareWorkspaceMutation,
1610
+ commitWorkspaceMutation,
1611
+ publishWorkspace,
1612
+ rotateWorkspace,
1613
+ manageWorkspace,
1614
+ prepareWorkspaceEvent,
1615
+ appendWorkspaceEvent,
1616
+ readWorkspaceEvents
1617
+ } = client;
1341
1618
 
1342
1619
  // packages/artifact/lib/artifact/ui/annotations.mjs
1343
1620
  var ARTIFACT_ANNOTATION_EVENTS = Object.freeze({
@@ -1721,97 +1998,47 @@ function mergeWorkspaceFeedback(events, { revisionId, reviewOf, ownerPublicKey }
1721
1998
  var FORMAT = "openplanr-design-owner-custody";
1722
1999
  function prepareDesignShareBundle(file) {
1723
2000
  const current = currentDesign(file);
1724
- const saved = readJson(join2(current.root, ".design/studio-state.json"), { state: {} }).state;
2001
+ const saved = readJson(join3(current.root, ".design/studio-state.json"), { state: {} }).state;
1725
2002
  return bundleDesignRevision(current, saved);
1726
2003
  }
1727
2004
  function custodyLocation(file, options = {}, { allowMissing = false } = {}) {
1728
2005
  const current = currentDesign(file);
1729
2006
  const env = options.env ?? process.env;
1730
- const root = resolve2(
1731
- options.custodyRoot ?? join2(
1732
- configuredPlanrHome(env) ?? join2(realpathSync2(env.HOME || homedir2()), ".openplanr"),
2007
+ const root = resolve3(
2008
+ options.custodyRoot ?? join3(
2009
+ configuredPlanrHome(env) ?? join3(realpathSync3(env.HOME || homedir2()), ".openplanr"),
1733
2010
  "design-shares"
1734
2011
  )
1735
2012
  );
1736
2013
  let project = current.root;
1737
- for (let candidate = current.root; dirname3(candidate) !== candidate; candidate = dirname3(candidate)) {
1738
- if (existsSync3(join2(candidate, ".git")) || existsSync3(join2(candidate, ".planr"))) {
2014
+ for (let candidate = current.root; dirname4(candidate) !== candidate; candidate = dirname4(candidate)) {
2015
+ if (existsSync4(join3(candidate, ".git")) || existsSync4(join3(candidate, ".planr"))) {
1739
2016
  project = candidate;
1740
2017
  break;
1741
2018
  }
1742
2019
  }
1743
2020
  const key = hash(`${current.root}
1744
2021
  ${current.document.id}`);
1745
- const path = join2(root, `${key}.json`);
1746
- const within = relative2(project, root);
1747
- if ((!allowMissing || existsSync3(path)) && (within === "" || !within.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && within !== ".." && !isAbsolute2(within)))
2022
+ const path = join3(root, `${key}.json`);
2023
+ const within = relative3(project, root);
2024
+ if ((!allowMissing || existsSync4(path)) && (within === "" || !within.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && within !== ".." && !isAbsolute3(within)))
1748
2025
  throw new Error(
1749
2026
  "Design owner credentials must be stored outside the project. Set PLANR_HOME to a private user-level directory."
1750
2027
  );
1751
2028
  return { root, path, current };
1752
2029
  }
1753
- function ensurePrivateDirectory(root) {
1754
- for (let path = root; dirname3(path) !== path; path = dirname3(path)) {
1755
- if (existsSync3(path) && lstatSync3(path).isSymbolicLink())
1756
- throw new Error("Design custody directory must not contain symbolic links.");
1757
- }
1758
- mkdirSync3(root, { recursive: true, mode: 448 });
1759
- const stat = lstatSync3(root);
1760
- if (!stat.isDirectory() || stat.isSymbolicLink())
1761
- throw new Error("Design custody must use a private local directory.");
1762
- if (process.platform !== "win32") chmodSync(root, 448);
1763
- }
1764
- function readCustody(path) {
1765
- if (!existsSync3(path)) return null;
1766
- const stat = lstatSync3(path);
1767
- if (!stat.isFile() || stat.isSymbolicLink() || process.platform !== "win32" && stat.mode & 63)
1768
- throw new Error("Design owner custody must be a private 0600 file.");
1769
- const record = JSON.parse(readFileSync3(path, "utf8"));
1770
- if (record.kind !== FORMAT || record.schemaVersion !== "1.0.0" || !record.custody)
1771
- throw new Error("Design owner custody is invalid.");
1772
- return record;
1773
- }
1774
- function writeCustody(path, record) {
1775
- const temp = `${path}.${randomBytes2(8).toString("hex")}.tmp`;
1776
- const fd = openSync(temp, "wx", 384);
1777
- try {
1778
- writeFileSync3(fd, `${JSON.stringify(record)}
1779
- `);
1780
- fsyncSync(fd);
1781
- } finally {
1782
- closeSync(fd);
1783
- }
1784
- try {
1785
- renameSync3(temp, path);
1786
- if (process.platform !== "win32") {
1787
- chmodSync(path, 384);
1788
- const directory = openSync(dirname3(path), "r");
1789
- try {
1790
- fsyncSync(directory);
1791
- } finally {
1792
- closeSync(directory);
1793
- }
1794
- }
1795
- } catch (error) {
1796
- try {
1797
- unlinkSync(temp);
1798
- } catch {
1799
- }
1800
- throw error;
1801
- }
1802
- }
1803
2030
  async function withCustody(file, options, action) {
1804
2031
  const location = custodyLocation(file, options);
1805
- ensurePrivateDirectory(location.root);
2032
+ ensurePrivateDirectory(location.root, { label: "Design" });
1806
2033
  const unlock = await acquireStartLock(`${location.path}.lock`);
1807
2034
  try {
1808
- let record = readCustody(location.path);
2035
+ let record = readCustody(location.path, { label: "Design", format: FORMAT });
1809
2036
  return await action({
1810
2037
  ...location,
1811
2038
  record,
1812
2039
  save(value = record) {
1813
2040
  record = value;
1814
- writeCustody(location.path, value);
2041
+ writeCustody(location.path, value, { label: "Design" });
1815
2042
  }
1816
2043
  });
1817
2044
  } finally {
@@ -1843,7 +2070,7 @@ async function commitMutation(record, save, options) {
1843
2070
  }
1844
2071
  }
1845
2072
  function presentationFingerprint(current) {
1846
- const state = readJson(join2(current.root, ".design/studio-state.json"), { state: {} }).state;
2073
+ const state = readJson(join3(current.root, ".design/studio-state.json"), { state: {} }).state;
1847
2074
  return hash(
1848
2075
  JSON.stringify({
1849
2076
  revision: current.revision,
@@ -1878,7 +2105,7 @@ var safeStatus = (record, current) => ({
1878
2105
  });
1879
2106
  function getDesignShareStatus(file, options = {}) {
1880
2107
  const { path, current } = custodyLocation(file, options);
1881
- return safeStatus(readCustody(path), current);
2108
+ return safeStatus(readCustody(path, { label: "Design", format: FORMAT }), current);
1882
2109
  }
1883
2110
  async function shareDesign(file, options = {}) {
1884
2111
  return withCustody(file, options, async ({ record, current, save }) => {
@@ -1983,7 +2210,7 @@ async function flushOwnerMetadata(record, save, options) {
1983
2210
  }
1984
2211
  async function publishDesignReviewMetadata(file, payload, { revisionId, ...options } = {}) {
1985
2212
  const location = custodyLocation(file, options, { allowMissing: true });
1986
- if (!existsSync3(location.path) || !revisionId) return { shared: false };
2213
+ if (!existsSync4(location.path) || !revisionId) return { shared: false };
1987
2214
  return withCustody(file, options, async ({ record, save }) => {
1988
2215
  if (!record || record.deleted || record.revoked || record.custody.pendingCreate)
1989
2216
  return { shared: false };
@@ -2010,16 +2237,20 @@ async function exportDesignShareRecovery(file, { output, ...options } = {}) {
2010
2237
  if (!output) throw new Error("Recovery export requires a new private output path.");
2011
2238
  return withCustody(file, options, async ({ record }) => {
2012
2239
  if (!record) throw new Error("Create the shared review first.");
2013
- const target = resolve2(output);
2014
- mkdirSync3(dirname3(target), { recursive: true, mode: 448 });
2015
- writeFileSync3(target, `${JSON.stringify(record, null, 2)}
2240
+ const target = resolve3(output);
2241
+ mkdirSync4(dirname4(target), { recursive: true, mode: 448 });
2242
+ writeFileSync4(target, `${JSON.stringify(record, null, 2)}
2016
2243
  `, { flag: "wx", mode: 384 });
2017
2244
  return { ok: true, output: target };
2018
2245
  });
2019
2246
  }
2020
2247
  async function importDesignShareRecovery(file, { input, ...options } = {}) {
2021
2248
  if (!input) throw new Error("Recovery restore requires --input <private recovery file>.");
2022
- const recovered = readCustody(resolve2(input));
2249
+ const recovered = readCustody(resolve3(input), {
2250
+ label: "Design",
2251
+ format: FORMAT,
2252
+ recoveryInput: true
2253
+ });
2023
2254
  if (!recovered) throw new Error("Recovery file could not be found.");
2024
2255
  const custody = recovered.custody;
2025
2256
  workspaceReviewUrl(custody);
@@ -2056,7 +2287,7 @@ async function importDesignShareRecovery(file, { input, ...options } = {}) {
2056
2287
  }
2057
2288
  async function syncDesignShare(file, options = {}) {
2058
2289
  const location = custodyLocation(file, options, { allowMissing: true });
2059
- if (!existsSync3(location.path)) return { ok: true, shared: false, imported: 0 };
2290
+ if (!existsSync4(location.path)) return { ok: true, shared: false, imported: 0 };
2060
2291
  return withCustody(file, options, async ({ record, current, save }) => {
2061
2292
  if (!record || record.custody.pendingCreate || record.deleted)
2062
2293
  return { ok: true, shared: Boolean(record), imported: 0 };
@@ -2069,7 +2300,7 @@ async function syncDesignShare(file, options = {}) {
2069
2300
  }).path;
2070
2301
  const currentDigest = digestArtifactEnvelope(current.envelope);
2071
2302
  let imported = 0, hasMore = true, issues = [];
2072
- const ledgerPath = join2(current.root, ".design/shared-feedback.json");
2303
+ const ledgerPath = join3(current.root, ".design/shared-feedback.json");
2073
2304
  while (hasMore) {
2074
2305
  const page = await readWorkspaceEvents(record.custody, {
2075
2306
  after: record.lastEvent ?? 0,