openplanr 2.2640.3 → 2.2640.5

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 +48 -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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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 +875 -544
  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,511 +719,902 @@ 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 });
797
741
  }
798
- async function deriveWorkspaceAuthentication(token, id) {
799
- return encodeWorkspaceBytes(await tokenMaterial(token, id, "reviewer-auth"));
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
+ }
747
+ }
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);
826
775
  }
827
- async function verifyWorkspaceSignature(value, publicKey) {
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;
828
786
  try {
829
- const key = await crypto.subtle.importKey("jwk", publicKey, ec, false, ["verify"]);
830
- return await crypto.subtle.verify(
831
- signatureAlgorithm,
832
- key,
833
- decodeWorkspaceBytes(value.signature),
834
- encoder.encode(canonicalizeJson(omitSignature(value)))
835
- );
787
+ record = JSON.parse(readFileSync3(path, "utf8"));
836
788
  } catch {
837
- return false;
789
+ throw custodyError(`${label} owner custody is invalid.`, "E_OWNER_CUSTODY_INVALID");
838
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;
839
794
  }
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.`
845
- );
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
- );
853
- return {
854
- iv: encodeWorkspaceBytes(iv),
855
- ciphertext: encodeWorkspaceBytes(new Uint8Array(ciphertext))
856
- };
857
- }
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
- }
873
- var keyringContext = (id, epoch) => ({ type: "keyring", workspaceId: id, epoch });
874
- var revisionContext = (id, revision) => ({
875
- type: "revision",
876
- workspaceId: id,
877
- id: revision.id,
878
- epoch: revision.epoch,
879
- reviewOf: revision.reviewOf
880
- });
881
- var eventContext = (id, event) => ({
882
- type: "event",
883
- workspaceId: id,
884
- id: event.id,
885
- epoch: event.epoch,
886
- revisionId: event.revisionId,
887
- reviewOf: event.reviewOf
888
- });
889
- function workspaceEnvelopeDigest(envelope) {
890
- return sha256Hex(
891
- canonicalizeJson({
892
- schemaVersion: envelope.schemaVersion,
893
- artifacts: envelope.artifacts,
894
- viewer: envelope.viewer
895
- })
896
- );
897
- }
898
- async function wrapKeyring(custody, token = custody.token, epoch = custody.epoch, keys = custody.keys) {
899
- return seal(
900
- { keys, ownerPublicKey: custody.ownerPublicKey },
901
- await tokenMaterial(token, custody.id, "key-wrap"),
902
- keyringContext(custody.id, epoch)
903
- );
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);
801
+ try {
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
+ }
904
826
  }
905
- async function prepareRevision(custody, bundle) {
906
- assertDesignReviewBundle(bundle);
907
- const header = {
908
- id: newWorkspaceId(),
909
- epoch: custody.epoch,
910
- reviewOf: workspaceEnvelopeDigest(bundle.envelope),
911
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
912
- };
913
- return signWorkspaceValue(
914
- {
915
- ...header,
916
- ...await seal(
917
- bundle,
918
- decodeWorkspaceBytes(custody.keys[custody.epoch]),
919
- revisionContext(custody.id, header)
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
920
912
  )
921
- },
922
- custody.ownerPrivateKey
923
- );
924
- }
925
- async function prepareWorkspace(bundle, { baseUrl = DESIGN_SHARE_BASE_URL } = {}) {
926
- const signer = await createWorkspaceSigner();
927
- const custody = {
928
- schemaVersion: DESIGN_WORKSPACE_VERSION,
929
- id: newWorkspaceId(),
930
- baseUrl: normalizeWorkspaceBase(baseUrl),
931
- token: newWorkspaceToken(),
932
- ownerAuth: newWorkspaceToken(),
933
- ownerPrivateKey: signer.privateKey,
934
- ownerPublicKey: signer.publicKey,
935
- epoch: 1,
936
- keys: { 1: newWorkspaceToken() },
937
- version: 0
938
- };
939
- custody.keyring = await wrapKeyring(custody);
940
- custody.pendingCreate = await signWorkspaceValue(
941
- {
942
- schemaVersion: DESIGN_WORKSPACE_VERSION,
943
- id: custody.id,
944
- ownerPublicKey: custody.ownerPublicKey,
945
- ownerAuthHash: sha256Hex(custody.ownerAuth),
946
- reviewerAuthHash: sha256Hex(await deriveWorkspaceAuthentication(custody.token, custody.id)),
947
- epoch: 1,
948
- keyring: custody.keyring,
949
- revision: await prepareRevision(custody, bundle),
950
- operationId: newWorkspaceId()
951
- },
952
- custody.ownerPrivateKey
953
- );
954
- assertWorkspaceContract(custody.pendingCreate, DESIGN_WORKSPACE_CREATE_SCHEMA);
955
- return custody;
956
- }
957
- async function request(access, suffix = "", {
958
- method = "GET",
959
- body,
960
- fetchImpl = globalThis.fetch,
961
- owner = Boolean(access.ownerAuth),
962
- timeoutMs = 2e4
963
- } = {}) {
964
- if (!idPattern.test(access.id)) throw new TypeError("Invalid design workspace identity.");
965
- const authorization = owner ? access.ownerAuth : await deriveWorkspaceAuthentication(access.token, access.id);
966
- const headers = { Authorization: `Bearer ${authorization}`, Accept: "application/json" };
967
- if (body) headers["Content-Type"] = "application/json";
968
- let response;
969
- try {
970
- response = await fetchImpl(
971
- `${normalizeWorkspaceBase(access.baseUrl)}${DESIGN_WORKSPACE_API}/${access.id}${suffix}`,
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(
942
+ signatureAlgorithm,
943
+ key,
944
+ encoder2.encode(canonicalizeJson(omitSignature(value)))
945
+ );
946
+ return { ...omitSignature(value), signature: encodeWorkspaceBytes2(new Uint8Array(signature)) };
947
+ }
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
976
+ );
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(
972
988
  {
973
- method,
974
- headers,
975
- body: body ? JSON.stringify(body) : void 0,
976
- redirect: "error",
977
- cache: "no-store",
978
- credentials: "omit",
979
- signal: AbortSignal.timeout(timeoutMs)
980
- }
989
+ name: "AES-GCM",
990
+ iv: decodeWorkspaceBytes2(value.iv),
991
+ additionalData: encoder2.encode(canonicalizeJson(context))
992
+ },
993
+ key,
994
+ bytes
981
995
  );
982
- } catch {
983
- throw new Error(
984
- "Design sharing is unreachable. Check the connection and retry; the previous review is unchanged."
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
985
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);
986
1110
  }
987
- if (!response.ok) {
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
+ }
988
1118
  const messages = {
989
1119
  401: "Access token is incorrect or has been rotated.",
990
1120
  403: "This review is unavailable or this action requires its owner.",
991
1121
  404: "This shared review could not be found.",
992
1122
  409: "The shared review changed. Refresh its status before retrying.",
993
1123
  410: "This shared review has been revoked or deleted.",
994
- 413: "This design exceeds the sharing upload limit.",
1124
+ 413: `This ${label.toLowerCase()} exceeds the sharing upload limit.`,
995
1125
  429: "Too many requests. Wait a moment and retry.",
996
- 503: "Design sharing is temporarily unavailable. Retry shortly."
1126
+ 503: `${label} sharing is temporarily unavailable. Retry shortly.`
997
1127
  };
998
1128
  const error = new Error(
999
- messages[response.status] ?? `Design sharing failed (${response.status}). Retry shortly.`
1129
+ messages[response.status] ?? `${label} sharing failed (${response.status}). Retry shortly.`
1000
1130
  );
1001
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
+ }
1002
1146
  throw error;
1003
1147
  }
1004
- if (response.status === 204) return {};
1005
- const limit = DESIGN_WORKSPACE_MAX_BYTES * 1.5 + 65536;
1006
- const reader = response.body?.getReader();
1007
- if (!reader) throw new Error("Design service returned an empty response.");
1008
- const chunks = [];
1009
- let length = 0;
1010
- try {
1011
- for (; ; ) {
1012
- const { value, done } = await reader.read();
1013
- if (done) break;
1014
- length += value.length;
1015
- if (length > limit) {
1016
- await reader.cancel();
1017
- throw new RangeError("Design service returned an oversized response.");
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);
1018
1161
  }
1019
- chunks.push(value);
1162
+ } catch (error) {
1163
+ if (error instanceof RangeError) throw error;
1164
+ throw new Error("The sharing response was interrupted. Retry the pending operation.");
1020
1165
  }
1021
- } catch (error) {
1022
- if (error instanceof RangeError) throw error;
1023
- throw new Error("The sharing response was interrupted. Retry the pending operation.");
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;
1024
1173
  }
1025
- const bytes = new Uint8Array(length);
1026
- let offset = 0;
1027
- for (const chunk of chunks) {
1028
- bytes.set(chunk, offset);
1029
- offset += chunk.length;
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
+ }
1030
1189
  }
1031
- return JSON.parse(decoder.decode(bytes));
1032
- }
1033
- async function commitWorkspace(custody, options = {}) {
1034
- if (!custody.pendingCreate) return getWorkspace(custody, options);
1035
- const result = await request(custody, "", {
1036
- ...options,
1037
- method: "PUT",
1038
- body: custody.pendingCreate
1039
- });
1040
- assertWorkspaceContract(result, DESIGN_WORKSPACE_SCHEMA);
1041
- 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))
1042
- throw new Error("The sharing creation receipt is invalid. Retry the saved operation.");
1043
- custody.version = result.version;
1044
- custody.currentRevision = result.currentRevision;
1045
- delete custody.pendingCreate;
1046
- return result;
1047
- }
1048
- async function getWorkspace(access, options = {}) {
1049
- const result = assertWorkspaceContract(
1050
- await request(access, "", options),
1051
- DESIGN_WORKSPACE_SCHEMA
1052
- );
1053
- if (result.id !== access.id) throw new Error("Shared review identity mismatch.");
1054
- const ring = await unseal(
1055
- result.keyring,
1056
- await tokenMaterial(access.token, access.id, "key-wrap"),
1057
- keyringContext(access.id, result.epoch)
1058
- );
1059
- if (canonicalizeJson(ring.ownerPublicKey) !== canonicalizeJson(result.ownerPublicKey) || access.ownerPublicKey && canonicalizeJson(access.ownerPublicKey) !== canonicalizeJson(result.ownerPublicKey))
1060
- throw new Error("Shared review owner identity changed.");
1061
- if (!ring.keys || !ring.keys[result.epoch] || Object.entries(ring.keys).some(
1062
- ([keyEpoch, key]) => !/^[1-9][0-9]*$/u.test(keyEpoch) || !tokenPattern.test(key)
1063
- ))
1064
- throw new Error("Invalid shared review key history.");
1065
- Object.assign(access, {
1066
- keys: ring.keys,
1067
- ownerPublicKey: result.ownerPublicKey,
1068
- epoch: result.epoch,
1069
- keyring: result.keyring,
1070
- version: result.version,
1071
- currentRevision: result.currentRevision,
1072
- commentsPaused: result.commentsPaused
1073
- });
1074
- return result;
1075
- }
1076
- async function decryptWorkspaceRevision(access, revisionId = access.currentRevision, options = {}) {
1077
- if (!access.keys) await getWorkspace(access, options);
1078
- if (!idPattern.test(revisionId)) throw new TypeError("Invalid design revision.");
1079
- const revision = assertWorkspaceContract(
1080
- await request(access, `/revisions/${revisionId}`, options),
1081
- DESIGN_WORKSPACE_REVISION_SCHEMA
1082
- );
1083
- if (revision.id !== revisionId || !await verifyWorkspaceSignature(revision, access.ownerPublicKey))
1084
- throw new Error("The published design signature is invalid.");
1085
- if (!access.keys[revision.epoch]) throw new Error("The access token cannot open this revision.");
1086
- const bundle = assertDesignReviewBundle(
1087
- await unseal(
1088
- revision,
1089
- decodeWorkspaceBytes(access.keys[revision.epoch]),
1090
- revisionContext(access.id, revision)
1091
- )
1092
- );
1093
- if (workspaceEnvelopeDigest(bundle.envelope) !== revision.reviewOf)
1094
- throw new Error("Published design content does not match its revision.");
1095
- return { ...bundle, workspaceRevision: revision.id, reviewOf: revision.reviewOf };
1096
- }
1097
- async function prepareWorkspaceMutation(custody, action, payload = void 0) {
1098
- if (custody.pendingCreate)
1099
- throw new Error("Finish creating this shared review before changing it.");
1100
- if (custody.pendingMutation)
1101
- throw new Error("A sharing operation is pending. Retry it before making another change.");
1102
- const body = {
1103
- operationId: newWorkspaceId(),
1104
- expectedVersion: custody.version,
1105
- epoch: custody.epoch
1106
- };
1107
- let next = {};
1108
- if (action === "publish") body.revision = await prepareRevision(custody, payload);
1109
- else if (action === "rotate") {
1110
- const token = newWorkspaceToken();
1111
- const epoch = custody.epoch + 1;
1112
- const keys = { ...custody.keys, [epoch]: newWorkspaceToken() };
1113
- const keyring = await wrapKeyring(custody, token, epoch, keys);
1114
- Object.assign(body, {
1115
- epoch,
1116
- reviewerAuthHash: sha256Hex(await deriveWorkspaceAuthentication(token, custody.id)),
1117
- keyring
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
1118
1196
  });
1119
- next = { token, epoch, keys, keyring };
1120
- } else if (["pause", "resume", "revoke", "delete"].includes(action)) body.action = action;
1121
- else throw new TypeError("Unknown design sharing operation.");
1122
- custody.pendingMutation = {
1123
- action,
1124
- body: await signWorkspaceValue(body, custody.ownerPrivateKey),
1125
- next
1126
- };
1127
- return custody.pendingMutation;
1128
- }
1129
- async function commitWorkspaceMutation(custody, options = {}) {
1130
- const pending = custody.pendingMutation;
1131
- if (!pending) throw new Error("No sharing operation is pending.");
1132
- const suffix = ["publish", "rotate"].includes(pending.action) ? pending.action : "manage";
1133
- const result = await request(custody, `/${suffix}`, {
1134
- ...options,
1135
- method: "POST",
1136
- body: pending.body
1137
- });
1138
- if (pending.action === "delete") {
1139
- if (result.schemaVersion !== DESIGN_WORKSPACE_VERSION || result.id !== custody.id || result.deleted !== true || Object.keys(result).some((key) => !["schemaVersion", "id", "deleted"].includes(key)))
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)))
1140
1297
  throw new Error("The deletion receipt is invalid. Retry the saved operation.");
1141
- } else {
1142
- assertWorkspaceContract(result, DESIGN_WORKSPACE_SCHEMA);
1298
+ }
1299
+ function assertMutationReceipt(custody, pending, result) {
1300
+ if (pending.action === "delete") return assertDeletionReceipt(custody, result);
1301
+ assertContract(result, schemas.workspace);
1143
1302
  const expectedRevision = pending.action === "publish" ? pending.body.revision.id : custody.currentRevision;
1144
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"))
1145
1304
  throw new Error("The sharing operation receipt is invalid. Retry the saved operation.");
1146
1305
  }
1147
- Object.assign(custody, pending.next, { version: pending.body.expectedVersion + 1 });
1148
- if (pending.action === "publish") custody.currentRevision = pending.body.revision.id;
1149
- if (pending.action === "pause" || pending.action === "resume")
1150
- custody.commentsPaused = pending.action === "pause";
1151
- if (pending.action === "revoke" || pending.action === "delete")
1152
- custody.status = pending.action === "revoke" ? "revoked" : "deleted";
1153
- delete custody.pendingMutation;
1154
- return result;
1155
- }
1156
- async function prepareWorkspaceEvent(access, payload, { revisionId = access.currentRevision, reviewOf = payload.reviewOf, signer } = {}) {
1157
- if (!access.keys) throw new Error("Unlock the shared review before commenting.");
1158
- if (["category", "disposition"].includes(payload.kind)) assertDesignReviewMetadata(payload);
1159
- if (!["review", "direction", "category", "disposition"].includes(payload.kind))
1160
- throw new TypeError("Unknown design feedback kind.");
1161
- if (typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160)
1162
- throw new TypeError("Enter your name before leaving feedback.");
1163
- const identity = signer ?? await createWorkspaceSigner();
1164
- const publicKey = canonicalWorkspacePublicKey(identity.publicKey);
1165
- const header = { id: newWorkspaceId(), revisionId, reviewOf, epoch: access.epoch };
1166
- const event = await signWorkspaceValue(
1167
- {
1168
- ...header,
1169
- ...await seal(
1170
- payload,
1171
- decodeWorkspaceBytes(access.keys[access.epoch]),
1172
- eventContext(access.id, header),
1173
- DESIGN_WORKSPACE_MAX_EVENT_BYTES
1174
- ),
1175
- publicKey
1176
- },
1177
- identity.privateKey
1178
- );
1179
- return assertWorkspaceContract(event, DESIGN_WORKSPACE_EVENT_SCHEMA);
1180
- }
1181
- async function appendWorkspaceEvent(access, payload, { preparedEvent, signer, revisionId, reviewOf, ...options } = {}) {
1182
- const event = preparedEvent ?? await prepareWorkspaceEvent(access, payload, {
1183
- signer,
1184
- revisionId,
1185
- reviewOf: reviewOf ?? payload.reviewOf
1186
- });
1187
- const result = await request(access, "/events", { ...options, method: "POST", body: event });
1188
- 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))
1189
- throw new Error("The feedback receipt is invalid. Retry the saved operation.");
1190
- const { sequence: eventSequence, ...received } = result.event;
1191
- if (eventSequence !== void 0 && eventSequence !== result.sequence || canonicalizeJson(received) !== canonicalizeJson(event))
1192
- throw new Error(
1193
- "The feedback receipt does not match the saved event. Retry the saved operation."
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
1194
1355
  );
1195
- return { event: { ...received, sequence: result.sequence }, sequence: result.sequence };
1196
- }
1197
- async function readWorkspaceEvents(access, { after = 0, ...options } = {}) {
1198
- if (!Number.isSafeInteger(after) || after < 0) throw new TypeError("Invalid feedback cursor.");
1199
- if (!access.keys) await getWorkspace(access, options);
1200
- const page = await request(access, `/events?after=${after}`, options);
1201
- if (!Array.isArray(page.events) || page.events.length > 100 || !Number.isSafeInteger(page.cursor) || page.cursor < after || typeof page.hasMore !== "boolean")
1202
- throw new Error("Invalid shared feedback page.");
1203
- const events = [];
1204
- const issues = [];
1205
- let previousSequence = after;
1206
- const pageIds = /* @__PURE__ */ new Set();
1207
- for (const item of page.events) {
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) {
1208
1394
  const { sequence, ...record } = item?.event ? { ...item.event, sequence: item.sequence } : item ?? {};
1209
- if (!Number.isSafeInteger(sequence) || sequence <= previousSequence || sequence > page.cursor)
1395
+ if (!Number.isSafeInteger(sequence) || sequence <= previousSequence || sequence > cursor)
1210
1396
  throw new Error("Invalid shared feedback sequence.");
1211
- previousSequence = sequence;
1212
1397
  if (typeof record.id !== "string" || pageIds.has(record.id))
1213
1398
  throw new Error("Invalid shared feedback event identity.");
1214
1399
  pageIds.add(record.id);
1215
- try {
1216
- assertWorkspaceContract(record, DESIGN_WORKSPACE_EVENT_SCHEMA);
1217
- if (!await verifyWorkspaceSignature(record, record.publicKey))
1218
- throw new Error("Signature verification failed.");
1219
- if (!access.keys[record.epoch]) throw new Error("The encryption epoch is unavailable.");
1220
- const payload = await unseal(
1221
- record,
1222
- decodeWorkspaceBytes(access.keys[record.epoch]),
1223
- eventContext(access.id, record),
1224
- DESIGN_WORKSPACE_MAX_EVENT_BYTES
1225
- );
1226
- if (["category", "disposition"].includes(payload.kind)) assertDesignReviewMetadata(payload);
1227
- if (!["review", "direction", "category", "disposition"].includes(payload.kind) || typeof payload.author !== "string" || !payload.author.trim() || payload.author.length > 160 || payload.reviewOf !== record.reviewOf)
1228
- throw new Error("Feedback author, kind or revision is invalid.");
1229
- events.push({ ...record, sequence, payload });
1230
- } catch {
1231
- issues.push({
1232
- sequence,
1233
- id: typeof record.id === "string" && idPattern.test(record.id) ? record.id : null,
1234
- reason: "Invalid encrypted feedback; not imported."
1235
- });
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
+ }
1424
+ return {
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
1450
+ };
1451
+ }
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 });
1457
+ var PACKED_BUNDLE_KIND = "openplanr-design-review-bundle-packed";
1458
+ var PACKED_BUNDLE_MAX_BYTES = 128 * 1024 * 1024;
1459
+ var SHARED_BLOCK_MIN_LENGTH = 2048;
1460
+ var sharedBlockPattern = /(<(script|style)\b[^>]*>)([\s\S]*?)<\/\2\s*>/gi;
1461
+ async function deflateRaw(bytes) {
1462
+ const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
1463
+ return new Uint8Array(await new Response(stream).arrayBuffer());
1464
+ }
1465
+ async function inflateRaw(bytes, limit) {
1466
+ const reader = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw")).getReader();
1467
+ const chunks = [];
1468
+ let size = 0;
1469
+ for (; ; ) {
1470
+ const { done, value } = await reader.read();
1471
+ if (done) break;
1472
+ size += value.byteLength;
1473
+ if (size > limit) {
1474
+ await reader.cancel();
1475
+ throw new RangeError("The shared design expands beyond its size limit.");
1236
1476
  }
1477
+ chunks.push(value);
1237
1478
  }
1238
- if (previousSequence > page.cursor) throw new Error("Invalid shared feedback cursor.");
1239
- return { ...page, events, issues };
1479
+ const inflated = new Uint8Array(size);
1480
+ let offset = 0;
1481
+ for (const chunk of chunks) {
1482
+ inflated.set(chunk, offset);
1483
+ offset += chunk.byteLength;
1484
+ }
1485
+ return inflated;
1486
+ }
1487
+ async function packDesignReviewBundle(bundle) {
1488
+ const pages = [];
1489
+ const pageIndex = /* @__PURE__ */ new Map();
1490
+ const blocks = [];
1491
+ const blockIndex = /* @__PURE__ */ new Map();
1492
+ const segmentsOf = (html) => {
1493
+ const segments = [];
1494
+ let offset = 0;
1495
+ for (const match of html.matchAll(sharedBlockPattern)) {
1496
+ const body = match[3];
1497
+ if (body.length < SHARED_BLOCK_MIN_LENGTH) continue;
1498
+ const start = match.index + match[1].length;
1499
+ segments.push(html.slice(offset, start));
1500
+ if (!blockIndex.has(body)) blockIndex.set(body, blocks.push(body) - 1);
1501
+ segments.push(blockIndex.get(body));
1502
+ offset = start + body.length;
1503
+ }
1504
+ segments.push(html.slice(offset));
1505
+ return segments;
1506
+ };
1507
+ const artifactPages = [];
1508
+ const artifacts = bundle.envelope.artifacts.map(({ html, ...artifact }) => {
1509
+ if (!pageIndex.has(html)) pageIndex.set(html, pages.push(segmentsOf(html)) - 1);
1510
+ artifactPages.push(pageIndex.get(html));
1511
+ return artifact;
1512
+ });
1513
+ const packed = {
1514
+ bundle: { ...bundle, envelope: { ...bundle.envelope, artifacts } },
1515
+ artifactPages,
1516
+ pages,
1517
+ blocks
1518
+ };
1519
+ return {
1520
+ kind: PACKED_BUNDLE_KIND,
1521
+ version: 1,
1522
+ data: encodeWorkspaceBytes(await deflateRaw(encoder.encode(JSON.stringify(packed))))
1523
+ };
1240
1524
  }
1525
+ async function unpackDesignReviewBundle(value, { maxBytes = PACKED_BUNDLE_MAX_BYTES } = {}) {
1526
+ if (value?.kind !== PACKED_BUNDLE_KIND) return value;
1527
+ if (value.version !== 1 || typeof value.data !== "string")
1528
+ throw new Error("The shared design uses an unsupported packing.");
1529
+ const { bundle, artifactPages, pages, blocks } = JSON.parse(
1530
+ decoder.decode(await inflateRaw(decodeWorkspaceBytes(value.data), maxBytes))
1531
+ );
1532
+ const invalid2 = () => new Error("The shared design packing is invalid.");
1533
+ if (!Array.isArray(bundle?.envelope?.artifacts) || !Array.isArray(pages) || !Array.isArray(blocks) || !Array.isArray(artifactPages) || artifactPages.length !== bundle.envelope.artifacts.length)
1534
+ throw invalid2();
1535
+ let expanded = 0;
1536
+ const html = pages.map((segments) => {
1537
+ if (!Array.isArray(segments)) throw invalid2();
1538
+ const page = segments.map((segment) => {
1539
+ if (typeof segment === "string") return segment;
1540
+ if (!Number.isInteger(segment) || typeof blocks[segment] !== "string") throw invalid2();
1541
+ return blocks[segment];
1542
+ }).join("");
1543
+ expanded += page.length;
1544
+ if (expanded > maxBytes)
1545
+ throw new RangeError("The shared design expands beyond its size limit.");
1546
+ return page;
1547
+ });
1548
+ const artifacts = bundle.envelope.artifacts.map((artifact, index) => {
1549
+ const page = html[artifactPages[index]];
1550
+ if (page === void 0) throw invalid2();
1551
+ return { ...artifact, html: page };
1552
+ });
1553
+ return { ...bundle, envelope: { ...bundle.envelope, artifacts } };
1554
+ }
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(
1582
+ canonicalizeJson({
1583
+ schemaVersion: envelope.schemaVersion,
1584
+ artifacts: envelope.artifacts,
1585
+ viewer: envelope.viewer
1586
+ })
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;
1241
1618
 
1242
1619
  // packages/artifact/lib/artifact/ui/annotations.mjs
1243
1620
  var ARTIFACT_ANNOTATION_EVENTS = Object.freeze({
@@ -1621,97 +1998,47 @@ function mergeWorkspaceFeedback(events, { revisionId, reviewOf, ownerPublicKey }
1621
1998
  var FORMAT = "openplanr-design-owner-custody";
1622
1999
  function prepareDesignShareBundle(file) {
1623
2000
  const current = currentDesign(file);
1624
- 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;
1625
2002
  return bundleDesignRevision(current, saved);
1626
2003
  }
1627
2004
  function custodyLocation(file, options = {}, { allowMissing = false } = {}) {
1628
2005
  const current = currentDesign(file);
1629
2006
  const env = options.env ?? process.env;
1630
- const root = resolve2(
1631
- options.custodyRoot ?? join2(
1632
- 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"),
1633
2010
  "design-shares"
1634
2011
  )
1635
2012
  );
1636
2013
  let project = current.root;
1637
- for (let candidate = current.root; dirname3(candidate) !== candidate; candidate = dirname3(candidate)) {
1638
- 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"))) {
1639
2016
  project = candidate;
1640
2017
  break;
1641
2018
  }
1642
2019
  }
1643
2020
  const key = hash(`${current.root}
1644
2021
  ${current.document.id}`);
1645
- const path = join2(root, `${key}.json`);
1646
- const within = relative2(project, root);
1647
- 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)))
1648
2025
  throw new Error(
1649
2026
  "Design owner credentials must be stored outside the project. Set PLANR_HOME to a private user-level directory."
1650
2027
  );
1651
2028
  return { root, path, current };
1652
2029
  }
1653
- function ensurePrivateDirectory(root) {
1654
- for (let path = root; dirname3(path) !== path; path = dirname3(path)) {
1655
- if (existsSync3(path) && lstatSync3(path).isSymbolicLink())
1656
- throw new Error("Design custody directory must not contain symbolic links.");
1657
- }
1658
- mkdirSync3(root, { recursive: true, mode: 448 });
1659
- const stat = lstatSync3(root);
1660
- if (!stat.isDirectory() || stat.isSymbolicLink())
1661
- throw new Error("Design custody must use a private local directory.");
1662
- if (process.platform !== "win32") chmodSync(root, 448);
1663
- }
1664
- function readCustody(path) {
1665
- if (!existsSync3(path)) return null;
1666
- const stat = lstatSync3(path);
1667
- if (!stat.isFile() || stat.isSymbolicLink() || process.platform !== "win32" && stat.mode & 63)
1668
- throw new Error("Design owner custody must be a private 0600 file.");
1669
- const record = JSON.parse(readFileSync3(path, "utf8"));
1670
- if (record.kind !== FORMAT || record.schemaVersion !== "1.0.0" || !record.custody)
1671
- throw new Error("Design owner custody is invalid.");
1672
- return record;
1673
- }
1674
- function writeCustody(path, record) {
1675
- const temp = `${path}.${randomBytes2(8).toString("hex")}.tmp`;
1676
- const fd = openSync(temp, "wx", 384);
1677
- try {
1678
- writeFileSync3(fd, `${JSON.stringify(record)}
1679
- `);
1680
- fsyncSync(fd);
1681
- } finally {
1682
- closeSync(fd);
1683
- }
1684
- try {
1685
- renameSync3(temp, path);
1686
- if (process.platform !== "win32") {
1687
- chmodSync(path, 384);
1688
- const directory = openSync(dirname3(path), "r");
1689
- try {
1690
- fsyncSync(directory);
1691
- } finally {
1692
- closeSync(directory);
1693
- }
1694
- }
1695
- } catch (error) {
1696
- try {
1697
- unlinkSync(temp);
1698
- } catch {
1699
- }
1700
- throw error;
1701
- }
1702
- }
1703
2030
  async function withCustody(file, options, action) {
1704
2031
  const location = custodyLocation(file, options);
1705
- ensurePrivateDirectory(location.root);
2032
+ ensurePrivateDirectory(location.root, { label: "Design" });
1706
2033
  const unlock = await acquireStartLock(`${location.path}.lock`);
1707
2034
  try {
1708
- let record = readCustody(location.path);
2035
+ let record = readCustody(location.path, { label: "Design", format: FORMAT });
1709
2036
  return await action({
1710
2037
  ...location,
1711
2038
  record,
1712
2039
  save(value = record) {
1713
2040
  record = value;
1714
- writeCustody(location.path, value);
2041
+ writeCustody(location.path, value, { label: "Design" });
1715
2042
  }
1716
2043
  });
1717
2044
  } finally {
@@ -1743,7 +2070,7 @@ async function commitMutation(record, save, options) {
1743
2070
  }
1744
2071
  }
1745
2072
  function presentationFingerprint(current) {
1746
- 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;
1747
2074
  return hash(
1748
2075
  JSON.stringify({
1749
2076
  revision: current.revision,
@@ -1778,7 +2105,7 @@ var safeStatus = (record, current) => ({
1778
2105
  });
1779
2106
  function getDesignShareStatus(file, options = {}) {
1780
2107
  const { path, current } = custodyLocation(file, options);
1781
- return safeStatus(readCustody(path), current);
2108
+ return safeStatus(readCustody(path, { label: "Design", format: FORMAT }), current);
1782
2109
  }
1783
2110
  async function shareDesign(file, options = {}) {
1784
2111
  return withCustody(file, options, async ({ record, current, save }) => {
@@ -1883,7 +2210,7 @@ async function flushOwnerMetadata(record, save, options) {
1883
2210
  }
1884
2211
  async function publishDesignReviewMetadata(file, payload, { revisionId, ...options } = {}) {
1885
2212
  const location = custodyLocation(file, options, { allowMissing: true });
1886
- if (!existsSync3(location.path) || !revisionId) return { shared: false };
2213
+ if (!existsSync4(location.path) || !revisionId) return { shared: false };
1887
2214
  return withCustody(file, options, async ({ record, save }) => {
1888
2215
  if (!record || record.deleted || record.revoked || record.custody.pendingCreate)
1889
2216
  return { shared: false };
@@ -1910,16 +2237,20 @@ async function exportDesignShareRecovery(file, { output, ...options } = {}) {
1910
2237
  if (!output) throw new Error("Recovery export requires a new private output path.");
1911
2238
  return withCustody(file, options, async ({ record }) => {
1912
2239
  if (!record) throw new Error("Create the shared review first.");
1913
- const target = resolve2(output);
1914
- mkdirSync3(dirname3(target), { recursive: true, mode: 448 });
1915
- 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)}
1916
2243
  `, { flag: "wx", mode: 384 });
1917
2244
  return { ok: true, output: target };
1918
2245
  });
1919
2246
  }
1920
2247
  async function importDesignShareRecovery(file, { input, ...options } = {}) {
1921
2248
  if (!input) throw new Error("Recovery restore requires --input <private recovery file>.");
1922
- const recovered = readCustody(resolve2(input));
2249
+ const recovered = readCustody(resolve3(input), {
2250
+ label: "Design",
2251
+ format: FORMAT,
2252
+ recoveryInput: true
2253
+ });
1923
2254
  if (!recovered) throw new Error("Recovery file could not be found.");
1924
2255
  const custody = recovered.custody;
1925
2256
  workspaceReviewUrl(custody);
@@ -1956,7 +2287,7 @@ async function importDesignShareRecovery(file, { input, ...options } = {}) {
1956
2287
  }
1957
2288
  async function syncDesignShare(file, options = {}) {
1958
2289
  const location = custodyLocation(file, options, { allowMissing: true });
1959
- if (!existsSync3(location.path)) return { ok: true, shared: false, imported: 0 };
2290
+ if (!existsSync4(location.path)) return { ok: true, shared: false, imported: 0 };
1960
2291
  return withCustody(file, options, async ({ record, current, save }) => {
1961
2292
  if (!record || record.custody.pendingCreate || record.deleted)
1962
2293
  return { ok: true, shared: Boolean(record), imported: 0 };
@@ -1969,7 +2300,7 @@ async function syncDesignShare(file, options = {}) {
1969
2300
  }).path;
1970
2301
  const currentDigest = digestArtifactEnvelope(current.envelope);
1971
2302
  let imported = 0, hasMore = true, issues = [];
1972
- const ledgerPath = join2(current.root, ".design/shared-feedback.json");
2303
+ const ledgerPath = join3(current.root, ".design/shared-feedback.json");
1973
2304
  while (hasMore) {
1974
2305
  const page = await readWorkspaceEvents(record.custody, {
1975
2306
  after: record.lastEvent ?? 0,