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.
- package/CHANGELOG.md +48 -0
- package/dist/cli/commands/artifact.d.ts.map +1 -1
- package/dist/cli/commands/artifact.js +44 -21
- package/dist/cli/commands/artifact.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-CaX9EJp4.js → index-BqFEZO-9.js} +2 -2
- package/dist/dashboard/assets/{index-CaX9EJp4.js.map → index-BqFEZO-9.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +8 -8
- package/dist/dashboard/index.html +1 -1
- package/dist/services/diagram-artifact-service.d.ts +28 -1
- package/dist/services/diagram-artifact-service.d.ts.map +1 -1
- package/dist/services/diagram-artifact-service.js +64 -1
- package/dist/services/diagram-artifact-service.js.map +1 -1
- package/docs/ARTIFACT_REVIEW.md +38 -2
- package/docs/CLI.md +12 -2
- package/lib/host-packages/adapter-registry.json +5 -5
- package/lib/host-packages/capability-map.json +2 -2
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +33 -25
- package/lib/host-packages/claude/openplanr/README.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/artifact/SKILL.md +14 -4
- package/lib/host-packages/claude/openplanr/skills/artifact/references/diagram-sharing.md +51 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-share.mjs +875 -544
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-share.mjs +875 -544
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-share.mjs +875 -544
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/claude/openplanr/skills/diagram/SKILL.md +13 -0
- package/lib/host-packages/claude/openplanr/skills/diagram/references/diagram-sharing.md +51 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-share.mjs +875 -544
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +32 -24
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-artifact/references/diagram-sharing.md +51 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-artifact.mdc +14 -4
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-share.mjs +875 -544
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-share.mjs +875 -544
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-share.mjs +875 -544
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-diagram/references/diagram-sharing.md +51 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-diagram.mdc +13 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-share.mjs +875 -544
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +35 -27
- package/lib/host-packages/openai/openplanr/skills/artifact/SKILL.md +14 -4
- package/lib/host-packages/openai/openplanr/skills/artifact/agents/openai.yaml +2 -2
- package/lib/host-packages/openai/openplanr/skills/artifact/openplanr.skill.json +11 -0
- package/lib/host-packages/openai/openplanr/skills/artifact/references/diagram-sharing.md +51 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-share.mjs +875 -544
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-share.mjs +875 -544
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-share.mjs +875 -544
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +5 -1
- package/lib/host-packages/openai/openplanr/skills/diagram/SKILL.md +13 -0
- package/lib/host-packages/openai/openplanr/skills/diagram/openplanr.skill.json +11 -0
- package/lib/host-packages/openai/openplanr/skills/diagram/references/diagram-sharing.md +51 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-artifact-shell.mjs +127 -123
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-review-handoff.mjs +4 -3
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-share.mjs +875 -544
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/share.css +1 -1
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +5 -1
- package/package.json +2 -2
package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-share.mjs
CHANGED
|
@@ -31,23 +31,9 @@ import {
|
|
|
31
31
|
} from "./design-artifact-shell.mjs";
|
|
32
32
|
|
|
33
33
|
// packages/design/lib/design/share.mjs
|
|
34
|
-
import {
|
|
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
|
|
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/
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
799
|
-
|
|
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
|
|
802
|
-
|
|
803
|
-
|
|
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
|
|
763
|
+
return join2(realpathSync2(existing), ...missing);
|
|
806
764
|
}
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
923
|
-
)
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
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:
|
|
1124
|
+
413: `This ${label.toLowerCase()} exceeds the sharing upload limit.`,
|
|
995
1125
|
429: "Too many requests. Wait a moment and retry.",
|
|
996
|
-
503:
|
|
1126
|
+
503: `${label} sharing is temporarily unavailable. Retry shortly.`
|
|
997
1127
|
};
|
|
998
1128
|
const error = new Error(
|
|
999
|
-
messages[response.status] ??
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
}
|
|
1142
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
)
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1196
|
-
}
|
|
1197
|
-
async function
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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 >
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
);
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
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(
|
|
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 =
|
|
1631
|
-
options.custodyRoot ??
|
|
1632
|
-
configuredPlanrHome(env) ??
|
|
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;
|
|
1638
|
-
if (
|
|
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 =
|
|
1646
|
-
const within =
|
|
1647
|
-
if ((!allowMissing ||
|
|
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(
|
|
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 (!
|
|
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 =
|
|
1914
|
-
|
|
1915
|
-
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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,
|