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