openplanr 2.2639.4 → 2.2640.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cli/commands/github.d.ts.map +1 -1
- package/dist/cli/commands/github.js +4 -3
- package/dist/cli/commands/github.js.map +1 -1
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-p83hrHZD.js → index-9DLb9EO5.js} +2 -2
- package/dist/dashboard/assets/{index-p83hrHZD.js.map → index-9DLb9EO5.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +13 -13
- package/dist/dashboard/index.html +1 -1
- package/dist/models/schema.d.ts.map +1 -1
- package/dist/models/schema.js +24 -1
- package/dist/models/schema.js.map +1 -1
- package/dist/services/artifact-service.js +2 -2
- package/dist/services/artifact-service.js.map +1 -1
- package/dist/services/github-service.d.ts +6 -4
- package/dist/services/github-service.d.ts.map +1 -1
- package/dist/services/github-service.js +7 -2
- package/dist/services/github-service.js.map +1 -1
- package/dist/services/id-service.d.ts +4 -1
- package/dist/services/id-service.d.ts.map +1 -1
- package/dist/services/id-service.js +28 -15
- package/dist/services/id-service.js.map +1 -1
- package/dist/services/landing/receipt-custody.d.ts.map +1 -1
- package/dist/services/landing/receipt-custody.js +2 -0
- package/dist/services/landing/receipt-custody.js.map +1 -1
- package/dist/services/report-linter-service.d.ts.map +1 -1
- package/dist/services/report-linter-service.js +2 -4
- package/dist/services/report-linter-service.js.map +1 -1
- package/docs/CLI.md +2 -0
- package/lib/host-packages/adapter-registry.json +5 -5
- package/lib/host-packages/capability-map.json +1 -1
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/claude/openplanr/.claude-plugin/icon.svg +11 -0
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +8 -3
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +160 -28
- package/lib/host-packages/claude/openplanr/README.md +4 -3
- package/lib/host-packages/claude/openplanr/skills/ceo-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/chair-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/challenger-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/cmo-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/coo-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/cpo-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/cto-review/SKILL.md +1 -1
- package/lib/host-packages/claude/openplanr/skills/design/openplanr.skill.json +88 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/design.mjs +83 -21459
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/skills/design-loop/openplanr.skill.json +88 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design.mjs +83 -21459
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/skills/design-review/openplanr.skill.json +88 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design.mjs +83 -21459
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/skills/plan/openplanr.skill.json +88 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/design.mjs +83 -21459
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/skills/sync/SKILL.md +17 -6
- package/lib/host-packages/claude/openplanr/skills/sync/scripts/sync.mjs +7 -104
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +144 -16
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design.mjs +83 -21459
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design.mjs +83 -21459
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design.mjs +83 -21459
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design.mjs +83 -21459
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-sync/scripts/sync.mjs +7 -104
- package/lib/host-packages/cursor/openplanr/rules/planr-sync.mdc +17 -6
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +3 -2
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +155 -27
- package/lib/host-packages/openai/openplanr/skills/ceo-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/chair-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/challenger-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/cmo-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/coo-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/cpo-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/cto-review/SKILL.md +1 -1
- package/lib/host-packages/openai/openplanr/skills/design/openplanr.skill.json +88 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/design.mjs +83 -21459
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/design-loop/openplanr.skill.json +88 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design.mjs +83 -21459
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/design-review/openplanr.skill.json +88 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design.mjs +83 -21459
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +88 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-artifact-shell.mjs +2506 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-document.mjs +3674 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-entities.mjs +489 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-parse5-parser.mjs +3970 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-parse5-tokenizer.mjs +3501 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-review-handoff.mjs +4408 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-sandbox-guards.mjs +1073 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design-share.mjs +2127 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/design.mjs +83 -21459
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/studio.js +1 -0
- package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/sync/SKILL.md +17 -6
- package/lib/host-packages/openai/openplanr/skills/sync/scripts/sync.mjs +7 -104
- package/lib/integrations.d.mts +0 -10
- package/lib/integrations.mjs +7 -104
- package/package.json +2 -2
|
@@ -0,0 +1,3674 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Parser,
|
|
3
|
+
defaultTreeAdapter
|
|
4
|
+
} from "./design-parse5-parser.mjs";
|
|
5
|
+
import {
|
|
6
|
+
NS,
|
|
7
|
+
TAG_NAMES,
|
|
8
|
+
hasUnescapedText
|
|
9
|
+
} from "./design-parse5-tokenizer.mjs";
|
|
10
|
+
import {
|
|
11
|
+
AA_NORMAL,
|
|
12
|
+
ARTIFACT_ERROR_CODES,
|
|
13
|
+
PipelineError,
|
|
14
|
+
contrastRatio,
|
|
15
|
+
createArtifactEnvelope,
|
|
16
|
+
digestArtifactEnvelope,
|
|
17
|
+
embedJson,
|
|
18
|
+
escapeHtml,
|
|
19
|
+
renderArtifactShellDocument,
|
|
20
|
+
renderPlanrMark,
|
|
21
|
+
validateJson
|
|
22
|
+
} from "./design-artifact-shell.mjs";
|
|
23
|
+
import {
|
|
24
|
+
ARTIFACT_FRAME_GUARD_TEMPLATE,
|
|
25
|
+
ARTIFACT_HOST_GUARD_TEMPLATE,
|
|
26
|
+
ARTIFACT_WORKER_GUARD_SOURCE
|
|
27
|
+
} from "./design-sandbox-guards.mjs";
|
|
28
|
+
|
|
29
|
+
// packages/design/lib/design/document.mjs
|
|
30
|
+
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
31
|
+
import {
|
|
32
|
+
closeSync,
|
|
33
|
+
existsSync as existsSync3,
|
|
34
|
+
mkdirSync as mkdirSync2,
|
|
35
|
+
openSync,
|
|
36
|
+
readFileSync as readFileSync6,
|
|
37
|
+
realpathSync as realpathSync3,
|
|
38
|
+
renameSync as renameSync2,
|
|
39
|
+
rmSync as rmSync2,
|
|
40
|
+
writeFileSync as writeFileSync2
|
|
41
|
+
} from "node:fs";
|
|
42
|
+
import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
|
|
43
|
+
|
|
44
|
+
// packages/artifact/lib/artifact/bridge.mjs
|
|
45
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
46
|
+
|
|
47
|
+
// node_modules/entities/dist/escape.js
|
|
48
|
+
var getCodePoint = typeof String.prototype.codePointAt === "function" ? (input, index) => input.codePointAt(index) : (
|
|
49
|
+
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
|
50
|
+
(c, index) => (c.charCodeAt(index) & 64512) === 55296 ? (c.charCodeAt(index) - 55296) * 1024 + c.charCodeAt(index + 1) - 56320 + 65536 : c.charCodeAt(index)
|
|
51
|
+
);
|
|
52
|
+
function getEscaper(regex, map) {
|
|
53
|
+
return function escape(data) {
|
|
54
|
+
let match;
|
|
55
|
+
let lastIndex = 0;
|
|
56
|
+
let result = "";
|
|
57
|
+
while (match = regex.exec(data)) {
|
|
58
|
+
if (lastIndex !== match.index) {
|
|
59
|
+
result += data.substring(lastIndex, match.index);
|
|
60
|
+
}
|
|
61
|
+
result += map.get(match[0].charCodeAt(0));
|
|
62
|
+
lastIndex = match.index + 1;
|
|
63
|
+
}
|
|
64
|
+
return result + data.substring(lastIndex);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var escapeAttribute = /* @__PURE__ */ getEscaper(/["&\u00A0]/g, /* @__PURE__ */ new Map([
|
|
68
|
+
[34, """],
|
|
69
|
+
[38, "&"],
|
|
70
|
+
[160, " "]
|
|
71
|
+
]));
|
|
72
|
+
var escapeText = /* @__PURE__ */ getEscaper(/[&<>\u00A0]/g, /* @__PURE__ */ new Map([
|
|
73
|
+
[38, "&"],
|
|
74
|
+
[60, "<"],
|
|
75
|
+
[62, ">"],
|
|
76
|
+
[160, " "]
|
|
77
|
+
]));
|
|
78
|
+
|
|
79
|
+
// node_modules/parse5/dist/serializer/index.js
|
|
80
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
81
|
+
TAG_NAMES.AREA,
|
|
82
|
+
TAG_NAMES.BASE,
|
|
83
|
+
TAG_NAMES.BASEFONT,
|
|
84
|
+
TAG_NAMES.BGSOUND,
|
|
85
|
+
TAG_NAMES.BR,
|
|
86
|
+
TAG_NAMES.COL,
|
|
87
|
+
TAG_NAMES.EMBED,
|
|
88
|
+
TAG_NAMES.FRAME,
|
|
89
|
+
TAG_NAMES.HR,
|
|
90
|
+
TAG_NAMES.IMG,
|
|
91
|
+
TAG_NAMES.INPUT,
|
|
92
|
+
TAG_NAMES.KEYGEN,
|
|
93
|
+
TAG_NAMES.LINK,
|
|
94
|
+
TAG_NAMES.META,
|
|
95
|
+
TAG_NAMES.PARAM,
|
|
96
|
+
TAG_NAMES.SOURCE,
|
|
97
|
+
TAG_NAMES.TRACK,
|
|
98
|
+
TAG_NAMES.WBR
|
|
99
|
+
]);
|
|
100
|
+
function isVoidElement(node, options) {
|
|
101
|
+
return options.treeAdapter.isElementNode(node) && options.treeAdapter.getNamespaceURI(node) === NS.HTML && VOID_ELEMENTS.has(options.treeAdapter.getTagName(node));
|
|
102
|
+
}
|
|
103
|
+
var defaultOpts = { treeAdapter: defaultTreeAdapter, scriptingEnabled: true };
|
|
104
|
+
function serialize(node, options) {
|
|
105
|
+
const opts = { ...defaultOpts, ...options };
|
|
106
|
+
if (isVoidElement(node, opts)) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
return serializeChildNodes(node, opts);
|
|
110
|
+
}
|
|
111
|
+
function serializeChildNodes(parentNode, options) {
|
|
112
|
+
let html = "";
|
|
113
|
+
const container = options.treeAdapter.isElementNode(parentNode) && options.treeAdapter.getTagName(parentNode) === TAG_NAMES.TEMPLATE && options.treeAdapter.getNamespaceURI(parentNode) === NS.HTML ? options.treeAdapter.getTemplateContent(parentNode) : parentNode;
|
|
114
|
+
const childNodes = options.treeAdapter.getChildNodes(container);
|
|
115
|
+
if (childNodes) {
|
|
116
|
+
for (const currentNode of childNodes) {
|
|
117
|
+
html += serializeNode(currentNode, options);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return html;
|
|
121
|
+
}
|
|
122
|
+
function serializeNode(node, options) {
|
|
123
|
+
if (options.treeAdapter.isElementNode(node)) {
|
|
124
|
+
return serializeElement(node, options);
|
|
125
|
+
}
|
|
126
|
+
if (options.treeAdapter.isTextNode(node)) {
|
|
127
|
+
return serializeTextNode(node, options);
|
|
128
|
+
}
|
|
129
|
+
if (options.treeAdapter.isCommentNode(node)) {
|
|
130
|
+
return serializeCommentNode(node, options);
|
|
131
|
+
}
|
|
132
|
+
if (options.treeAdapter.isDocumentTypeNode(node)) {
|
|
133
|
+
return serializeDocumentTypeNode(node, options);
|
|
134
|
+
}
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
function serializeElement(node, options) {
|
|
138
|
+
const tn = options.treeAdapter.getTagName(node);
|
|
139
|
+
return `<${tn}${serializeAttributes(node, options)}>${isVoidElement(node, options) ? "" : `${serializeChildNodes(node, options)}</${tn}>`}`;
|
|
140
|
+
}
|
|
141
|
+
function serializeAttributes(node, { treeAdapter }) {
|
|
142
|
+
let html = "";
|
|
143
|
+
for (const attr2 of treeAdapter.getAttrList(node)) {
|
|
144
|
+
html += " ";
|
|
145
|
+
if (attr2.namespace) {
|
|
146
|
+
switch (attr2.namespace) {
|
|
147
|
+
case NS.XML: {
|
|
148
|
+
html += `xml:${attr2.name}`;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case NS.XMLNS: {
|
|
152
|
+
if (attr2.name !== "xmlns") {
|
|
153
|
+
html += "xmlns:";
|
|
154
|
+
}
|
|
155
|
+
html += attr2.name;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case NS.XLINK: {
|
|
159
|
+
html += `xlink:${attr2.name}`;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
default: {
|
|
163
|
+
html += `${attr2.prefix}:${attr2.name}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
html += attr2.name;
|
|
168
|
+
}
|
|
169
|
+
html += `="${escapeAttribute(attr2.value)}"`;
|
|
170
|
+
}
|
|
171
|
+
return html;
|
|
172
|
+
}
|
|
173
|
+
function serializeTextNode(node, options) {
|
|
174
|
+
const { treeAdapter } = options;
|
|
175
|
+
const content = treeAdapter.getTextNodeContent(node);
|
|
176
|
+
const parent = treeAdapter.getParentNode(node);
|
|
177
|
+
const parentTn = parent && treeAdapter.isElementNode(parent) && treeAdapter.getTagName(parent);
|
|
178
|
+
return parentTn && treeAdapter.getNamespaceURI(parent) === NS.HTML && hasUnescapedText(parentTn, options.scriptingEnabled) ? content : escapeText(content);
|
|
179
|
+
}
|
|
180
|
+
function serializeCommentNode(node, { treeAdapter }) {
|
|
181
|
+
return `<!--${treeAdapter.getCommentNodeContent(node)}-->`;
|
|
182
|
+
}
|
|
183
|
+
function serializeDocumentTypeNode(node, { treeAdapter }) {
|
|
184
|
+
return `<!DOCTYPE ${treeAdapter.getDocumentTypeNodeName(node)}>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// node_modules/parse5/dist/index.js
|
|
188
|
+
function parse(html, options) {
|
|
189
|
+
return Parser.parse(html, options);
|
|
190
|
+
}
|
|
191
|
+
function parseFragment(fragmentContext, html, options) {
|
|
192
|
+
if (typeof fragmentContext === "string") {
|
|
193
|
+
options = html;
|
|
194
|
+
html = fragmentContext;
|
|
195
|
+
fragmentContext = null;
|
|
196
|
+
}
|
|
197
|
+
const parser = Parser.getFragmentParser(fragmentContext, options);
|
|
198
|
+
parser.tokenizer.write(html, true);
|
|
199
|
+
return parser.getFragment();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// packages/artifact/lib/artifact/internal/board-token.mjs
|
|
203
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
204
|
+
|
|
205
|
+
// packages/artifact/lib/artifact/internal/planr-home.mjs
|
|
206
|
+
import { homedir } from "node:os";
|
|
207
|
+
import { join, resolve } from "node:path";
|
|
208
|
+
var WARNED = /* @__PURE__ */ Symbol.for("openplanr.home-variable-warning");
|
|
209
|
+
function nonBlank(value) {
|
|
210
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
211
|
+
}
|
|
212
|
+
function warnOnce(message) {
|
|
213
|
+
if (globalThis[WARNED]) return;
|
|
214
|
+
globalThis[WARNED] = true;
|
|
215
|
+
process.stderr.write(`Warning: ${message}
|
|
216
|
+
`);
|
|
217
|
+
}
|
|
218
|
+
function homeVariables(env) {
|
|
219
|
+
const home = nonBlank(env.PLANR_HOME);
|
|
220
|
+
const legacy = nonBlank(env.OPENPLANR_HOME);
|
|
221
|
+
if (legacy === void 0) return { home, legacy };
|
|
222
|
+
const legacyHome = join(legacy, ".planr");
|
|
223
|
+
if (home === void 0) {
|
|
224
|
+
warnOnce(`OPENPLANR_HOME is deprecated; set PLANR_HOME=${legacyHome} instead.`);
|
|
225
|
+
return { home, legacy };
|
|
226
|
+
}
|
|
227
|
+
warnOnce(
|
|
228
|
+
resolve(home) === resolve(legacyHome) ? "OPENPLANR_HOME is deprecated and ignored because PLANR_HOME is set; unset OPENPLANR_HOME." : `PLANR_HOME=${home} and OPENPLANR_HOME=${legacy} name different OpenPlanr homes; using PLANR_HOME. OPENPLANR_HOME is deprecated; unset it.`
|
|
229
|
+
);
|
|
230
|
+
return { home, legacy: void 0 };
|
|
231
|
+
}
|
|
232
|
+
function configuredPlanrHome(env = process.env) {
|
|
233
|
+
const { home, legacy } = homeVariables(env);
|
|
234
|
+
return home ?? (legacy === void 0 ? void 0 : join(legacy, ".planr"));
|
|
235
|
+
}
|
|
236
|
+
function planrHome(env = process.env) {
|
|
237
|
+
return configuredPlanrHome(env) ?? join(homedir(), ".planr");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// packages/artifact/lib/artifact/internal/server-util.mjs
|
|
241
|
+
import { randomBytes } from "node:crypto";
|
|
242
|
+
import {
|
|
243
|
+
existsSync,
|
|
244
|
+
lstatSync,
|
|
245
|
+
mkdirSync,
|
|
246
|
+
readdirSync,
|
|
247
|
+
readFileSync,
|
|
248
|
+
renameSync,
|
|
249
|
+
rmSync,
|
|
250
|
+
writeFileSync
|
|
251
|
+
} from "node:fs";
|
|
252
|
+
import { dirname, join as join2 } from "node:path";
|
|
253
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
254
|
+
function codedError(code, message, details) {
|
|
255
|
+
const error = new Error(message);
|
|
256
|
+
error.code = code;
|
|
257
|
+
if (details !== void 0) error.details = details;
|
|
258
|
+
return error;
|
|
259
|
+
}
|
|
260
|
+
function readJsonState(path) {
|
|
261
|
+
try {
|
|
262
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
263
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function writePrivateJsonState(path, value, { mode = 384 } = {}) {
|
|
269
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
270
|
+
const temporary = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
271
|
+
try {
|
|
272
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
273
|
+
`, { mode, flag: "wx" });
|
|
274
|
+
renameSync(temporary, path);
|
|
275
|
+
} finally {
|
|
276
|
+
rmSync(temporary, { force: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function isProcessAlive(pid) {
|
|
280
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
281
|
+
try {
|
|
282
|
+
process.kill(pid, 0);
|
|
283
|
+
return true;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return err && err.code === "EPERM";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function listenLoopback(server, port = 0, { host = LOOPBACK_HOST } = {}) {
|
|
289
|
+
if (host !== LOOPBACK_HOST) {
|
|
290
|
+
return Promise.reject(
|
|
291
|
+
codedError("E_LOOPBACK_HOST", `Refusing non-loopback bind host: ${host}`)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
295
|
+
return Promise.reject(codedError("E_LOOPBACK_PORT", `Invalid loopback port: ${String(port)}`));
|
|
296
|
+
}
|
|
297
|
+
return new Promise((resolveListen, reject) => {
|
|
298
|
+
const onError = (error) => {
|
|
299
|
+
server.off("listening", onListening);
|
|
300
|
+
reject(error);
|
|
301
|
+
};
|
|
302
|
+
const onListening = () => {
|
|
303
|
+
server.off("error", onError);
|
|
304
|
+
const address = server.address();
|
|
305
|
+
if (!address || typeof address === "string" || address.address !== LOOPBACK_HOST) {
|
|
306
|
+
closeHttpServer(server).finally(
|
|
307
|
+
() => reject(
|
|
308
|
+
codedError(
|
|
309
|
+
"E_LOOPBACK_BIND",
|
|
310
|
+
"Server did not bind to the required IPv4 loopback interface."
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
resolveListen(address.port);
|
|
317
|
+
};
|
|
318
|
+
server.once("error", onError);
|
|
319
|
+
server.once("listening", onListening);
|
|
320
|
+
server.listen({ port, host, exclusive: true });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function closeHttpServer(server) {
|
|
324
|
+
if (!server?.listening) return Promise.resolve();
|
|
325
|
+
return new Promise((resolveClose, reject) => {
|
|
326
|
+
server.close((error) => error ? reject(error) : resolveClose());
|
|
327
|
+
server.closeIdleConnections?.();
|
|
328
|
+
server.closeAllConnections?.();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function readRequestBody(req, { maxBytes, encoding = null } = {}) {
|
|
332
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 1) {
|
|
333
|
+
return Promise.reject(
|
|
334
|
+
codedError("E_REQUEST_BODY_LIMIT", "A positive request byte limit is required.")
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const declared = Number(req.headers?.["content-length"]);
|
|
338
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
339
|
+
req.resume?.();
|
|
340
|
+
return Promise.reject(
|
|
341
|
+
codedError("E_REQUEST_BODY_LIMIT", `Request body exceeds ${maxBytes} bytes.`, {
|
|
342
|
+
maxBytes,
|
|
343
|
+
declaredBytes: declared
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return new Promise((resolveBody, reject) => {
|
|
348
|
+
const chunks = [];
|
|
349
|
+
let bytes = 0;
|
|
350
|
+
let settled = false;
|
|
351
|
+
const rejectOnce = (error) => {
|
|
352
|
+
if (settled) return;
|
|
353
|
+
settled = true;
|
|
354
|
+
reject(error);
|
|
355
|
+
};
|
|
356
|
+
req.on("data", (chunk) => {
|
|
357
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
358
|
+
bytes += buffer.byteLength;
|
|
359
|
+
if (bytes > maxBytes) {
|
|
360
|
+
chunks.length = 0;
|
|
361
|
+
rejectOnce(
|
|
362
|
+
codedError("E_REQUEST_BODY_LIMIT", `Request body exceeds ${maxBytes} bytes.`, {
|
|
363
|
+
maxBytes,
|
|
364
|
+
receivedBytes: bytes
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!settled) chunks.push(buffer);
|
|
370
|
+
});
|
|
371
|
+
req.on("end", () => {
|
|
372
|
+
if (settled) return;
|
|
373
|
+
settled = true;
|
|
374
|
+
const body = Buffer.concat(chunks, bytes);
|
|
375
|
+
resolveBody(encoding ? body.toString(encoding) : body);
|
|
376
|
+
});
|
|
377
|
+
req.on("error", rejectOnce);
|
|
378
|
+
req.on(
|
|
379
|
+
"aborted",
|
|
380
|
+
() => rejectOnce(codedError("E_REQUEST_ABORTED", "Request body was aborted."))
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function assertLoopbackRequest(req, { port, mutating = false, internal = false, hosts = [LOOPBACK_HOST] } = {}) {
|
|
385
|
+
const allowedHosts = new Set(hosts);
|
|
386
|
+
const hostHeaders = req.headersDistinct?.host;
|
|
387
|
+
const receivedHost = req.headers?.host;
|
|
388
|
+
const separator = typeof receivedHost === "string" ? receivedHost.lastIndexOf(":") : -1;
|
|
389
|
+
const receivedName = separator > 0 ? receivedHost.slice(0, separator) : "";
|
|
390
|
+
const receivedPort = separator > 0 ? receivedHost.slice(separator + 1) : "";
|
|
391
|
+
const expectedHost = `${receivedName}:${port}`;
|
|
392
|
+
const expectedOrigin = `http://${expectedHost}`;
|
|
393
|
+
if (!allowedHosts.has(receivedName) || receivedPort !== String(port) || Array.isArray(hostHeaders) && hostHeaders.length !== 1) {
|
|
394
|
+
throw codedError("E_LOOPBACK_HOST", "Loopback Host header rejected.");
|
|
395
|
+
}
|
|
396
|
+
const origin = req.headers?.origin;
|
|
397
|
+
if (origin !== void 0 && origin !== expectedOrigin) {
|
|
398
|
+
throw codedError("E_LOOPBACK_ORIGIN", "Loopback Origin header rejected.");
|
|
399
|
+
}
|
|
400
|
+
if (mutating && !internal && origin !== expectedOrigin) {
|
|
401
|
+
throw codedError(
|
|
402
|
+
"E_LOOPBACK_ORIGIN",
|
|
403
|
+
"State-changing browser requests require the exact loopback origin."
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (internal && origin !== void 0) {
|
|
407
|
+
throw codedError(
|
|
408
|
+
"E_LOOPBACK_ORIGIN",
|
|
409
|
+
"Internal control requests must not carry a browser Origin."
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const fetchSite = String(req.headers?.["sec-fetch-site"] ?? "").toLowerCase();
|
|
413
|
+
if (fetchSite && !["none", "same-origin"].includes(fetchSite)) {
|
|
414
|
+
throw codedError("E_LOOPBACK_FETCH_SITE", "Cross-site loopback request rejected.");
|
|
415
|
+
}
|
|
416
|
+
return { expectedHost, expectedOrigin };
|
|
417
|
+
}
|
|
418
|
+
var wait = (milliseconds) => new Promise((resolveWait) => setTimeout(resolveWait, milliseconds));
|
|
419
|
+
async function acquireStartLock(path, {
|
|
420
|
+
timeout = 5e3,
|
|
421
|
+
poll = 25,
|
|
422
|
+
pid = process.pid,
|
|
423
|
+
now = () => Date.now(),
|
|
424
|
+
isAlive = isProcessAlive,
|
|
425
|
+
waitImpl = wait
|
|
426
|
+
} = {}) {
|
|
427
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
428
|
+
const started = now();
|
|
429
|
+
if (existsSync(path)) {
|
|
430
|
+
throw codedError(
|
|
431
|
+
"E_START_LOCK_LEGACY",
|
|
432
|
+
`A legacy startup lock must be cleared after confirming its owner is stopped: ${path}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const directory = `${path}.writers`;
|
|
436
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
437
|
+
const directoryInfo = lstatSync(directory);
|
|
438
|
+
if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink()) {
|
|
439
|
+
throw codedError("E_START_LOCK_UNSAFE", `Startup lock directory is unsafe: ${directory}`);
|
|
440
|
+
}
|
|
441
|
+
const owner = randomBytes(16).toString("hex");
|
|
442
|
+
const name = `${pid}-${owner}.json`;
|
|
443
|
+
const recordPath = join2(directory, name);
|
|
444
|
+
const announce = (ticket) => writePrivateJsonState(recordPath, { pid, owner, ticket });
|
|
445
|
+
const writers = () => {
|
|
446
|
+
const result = [];
|
|
447
|
+
for (const entry of readdirSync(directory)) {
|
|
448
|
+
if (!entry.endsWith(".json")) continue;
|
|
449
|
+
const match = /^([1-9]\d*)-([a-f0-9]{32})\.json$/u.exec(entry);
|
|
450
|
+
if (!match)
|
|
451
|
+
throw codedError("E_START_LOCK_UNSAFE", `Startup lock record is invalid: ${entry}`);
|
|
452
|
+
const entryPath = join2(directory, entry);
|
|
453
|
+
let info;
|
|
454
|
+
try {
|
|
455
|
+
info = lstatSync(entryPath);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error?.code === "ENOENT") continue;
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024) {
|
|
461
|
+
throw codedError("E_START_LOCK_UNSAFE", `Startup lock record is unsafe: ${entry}`);
|
|
462
|
+
}
|
|
463
|
+
const value = readJsonState(entryPath);
|
|
464
|
+
if (value?.pid !== Number(match[1]) || value?.owner !== match[2] || !Number.isSafeInteger(value.ticket) || value.ticket < 0)
|
|
465
|
+
throw codedError("E_START_LOCK_UNSAFE", `Startup lock record is invalid: ${entry}`);
|
|
466
|
+
if (!isAlive(value.pid)) {
|
|
467
|
+
rmSync(entryPath, { force: true });
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
result.push({ ...value, name: entry });
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
};
|
|
474
|
+
try {
|
|
475
|
+
announce(0);
|
|
476
|
+
const ticket = Math.max(0, ...writers().map((writer) => writer.ticket)) + 1;
|
|
477
|
+
if (!Number.isSafeInteger(ticket)) {
|
|
478
|
+
throw codedError("E_START_LOCK_UNSAFE", "Startup lock ticket limit reached.");
|
|
479
|
+
}
|
|
480
|
+
announce(ticket);
|
|
481
|
+
while (now() - started <= timeout) {
|
|
482
|
+
const blocked = writers().some(
|
|
483
|
+
(writer) => writer.name !== name && (writer.ticket === 0 || writer.ticket < ticket || writer.ticket === ticket && writer.name < name)
|
|
484
|
+
);
|
|
485
|
+
if (!blocked) {
|
|
486
|
+
return () => {
|
|
487
|
+
const current = readJsonState(recordPath);
|
|
488
|
+
if (current?.owner === owner && current?.pid === pid) rmSync(recordPath, { force: true });
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
await waitImpl(poll);
|
|
492
|
+
}
|
|
493
|
+
throw codedError("E_START_LOCK_TIMEOUT", `Timed out waiting for startup lock: ${path}`);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
rmSync(recordPath, { force: true });
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// packages/artifact/lib/artifact/internal/board-token.mjs
|
|
501
|
+
var BASE64URL_TOKEN_RE = /^[A-Za-z0-9_-]+$/;
|
|
502
|
+
function mintCapabilityToken({
|
|
503
|
+
bytes = 32,
|
|
504
|
+
encoding = "base64url",
|
|
505
|
+
randomBytesImpl = randomBytes2
|
|
506
|
+
} = {}) {
|
|
507
|
+
if (!Number.isInteger(bytes) || bytes < 12 || bytes > 64) {
|
|
508
|
+
throw new RangeError("Capability tokens require 12 through 64 random bytes.");
|
|
509
|
+
}
|
|
510
|
+
if (!["base64url", "hex"].includes(encoding)) {
|
|
511
|
+
throw new TypeError(`Unsupported capability token encoding: ${String(encoding)}`);
|
|
512
|
+
}
|
|
513
|
+
const value = randomBytesImpl(bytes);
|
|
514
|
+
if (!Buffer.isBuffer(value) || value.byteLength !== bytes) {
|
|
515
|
+
throw new TypeError("Capability token random source returned the wrong byte length.");
|
|
516
|
+
}
|
|
517
|
+
return value.toString(encoding);
|
|
518
|
+
}
|
|
519
|
+
function isCapabilityToken(value, { bytes = 32, encoding = "base64url" } = {}) {
|
|
520
|
+
if (typeof value !== "string") return false;
|
|
521
|
+
if (encoding === "hex") return value.length === bytes * 2 && /^[a-f0-9]+$/.test(value);
|
|
522
|
+
if (encoding !== "base64url" || !BASE64URL_TOKEN_RE.test(value)) return false;
|
|
523
|
+
try {
|
|
524
|
+
return Buffer.from(value, "base64url").byteLength === bytes && Buffer.from(value, "base64url").toString("base64url") === value;
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function timingSafeTokenEqual(left, right) {
|
|
530
|
+
if (typeof left !== "string" || typeof right !== "string") return false;
|
|
531
|
+
const leftBytes = Buffer.from(left, "utf8");
|
|
532
|
+
const rightBytes = Buffer.from(right, "utf8");
|
|
533
|
+
return leftBytes.byteLength === rightBytes.byteLength && timingSafeEqual(leftBytes, rightBytes);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// packages/artifact/lib/artifact/ui/bridge-tools.mjs
|
|
537
|
+
var ARTIFACT_THUMBNAIL_MAX_EDGE = 320;
|
|
538
|
+
var ARTIFACT_THUMBNAIL_MAX_DATA_URL = 256 * 1024;
|
|
539
|
+
var ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA = 1e3;
|
|
540
|
+
var ARTIFACT_VIEWPORT_PAN_MAX_DELTA = 1e3;
|
|
541
|
+
function normalizeArtifactViewportZoom(value, viewport) {
|
|
542
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
543
|
+
return null;
|
|
544
|
+
const keys = ["x", "y", "deltaY"];
|
|
545
|
+
if (Object.keys(value).length !== keys.length || !Object.keys(value).every((key) => keys.includes(key)))
|
|
546
|
+
return null;
|
|
547
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
548
|
+
if (!keys.every(
|
|
549
|
+
(key) => Object.hasOwn(descriptors[key], "value") && typeof descriptors[key].value === "number" && Number.isFinite(descriptors[key].value)
|
|
550
|
+
))
|
|
551
|
+
return null;
|
|
552
|
+
const { x, y, deltaY } = value;
|
|
553
|
+
if (!Number.isFinite(viewport?.width) || !Number.isFinite(viewport?.height) || viewport.width < 1 || viewport.height < 1 || viewport.width > 16384 || viewport.height > 16384 || x < 0 || y < 0 || x > viewport.width || y > viewport.height || deltaY === 0 || Math.abs(deltaY) > ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA)
|
|
554
|
+
return null;
|
|
555
|
+
return Object.freeze({ x, y, deltaY });
|
|
556
|
+
}
|
|
557
|
+
function normalizeArtifactViewportPan(value) {
|
|
558
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
559
|
+
return null;
|
|
560
|
+
const keys = ["deltaX", "deltaY"];
|
|
561
|
+
if (Object.keys(value).length !== keys.length || !Object.keys(value).every((key) => keys.includes(key)))
|
|
562
|
+
return null;
|
|
563
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
564
|
+
if (!keys.every(
|
|
565
|
+
(key) => Object.hasOwn(descriptors[key], "value") && typeof descriptors[key].value === "number" && Number.isFinite(descriptors[key].value)
|
|
566
|
+
))
|
|
567
|
+
return null;
|
|
568
|
+
const { deltaX, deltaY } = value;
|
|
569
|
+
if (!deltaX && !deltaY || Math.abs(deltaX) > ARTIFACT_VIEWPORT_PAN_MAX_DELTA || Math.abs(deltaY) > ARTIFACT_VIEWPORT_PAN_MAX_DELTA)
|
|
570
|
+
return null;
|
|
571
|
+
return Object.freeze({ deltaX, deltaY });
|
|
572
|
+
}
|
|
573
|
+
function createArtifactViewportGestures(window, emit) {
|
|
574
|
+
const add = window.addEventListener.bind(window), remove = window.removeEventListener.bind(window);
|
|
575
|
+
const schedule = window.setTimeout.bind(window), cancel = window.clearTimeout.bind(window);
|
|
576
|
+
const prevent = window.Event.prototype.preventDefault, stop = window.Event.prototype.stopImmediatePropagation;
|
|
577
|
+
const getter = (prototype, name) => Object.getOwnPropertyDescriptor(prototype, name)?.get;
|
|
578
|
+
const native = {
|
|
579
|
+
x: getter(window.MouseEvent.prototype, "clientX"),
|
|
580
|
+
y: getter(window.MouseEvent.prototype, "clientY"),
|
|
581
|
+
ctrl: getter(window.MouseEvent.prototype, "ctrlKey"),
|
|
582
|
+
meta: getter(window.MouseEvent.prototype, "metaKey"),
|
|
583
|
+
shift: getter(window.MouseEvent.prototype, "shiftKey"),
|
|
584
|
+
deltaX: getter(window.WheelEvent.prototype, "deltaX"),
|
|
585
|
+
deltaY: getter(window.WheelEvent.prototype, "deltaY"),
|
|
586
|
+
mode: getter(window.WheelEvent.prototype, "deltaMode")
|
|
587
|
+
};
|
|
588
|
+
let enabled = false, disposed = false, timer = 0, pending = null, pendingType = "";
|
|
589
|
+
const clear = () => {
|
|
590
|
+
if (timer) cancel(timer);
|
|
591
|
+
timer = 0;
|
|
592
|
+
pending = null;
|
|
593
|
+
pendingType = "";
|
|
594
|
+
};
|
|
595
|
+
const flush = () => {
|
|
596
|
+
timer = 0;
|
|
597
|
+
const value = pending;
|
|
598
|
+
const type = pendingType;
|
|
599
|
+
pending = null;
|
|
600
|
+
pendingType = "";
|
|
601
|
+
if (enabled && !disposed && value) emit(type, value);
|
|
602
|
+
};
|
|
603
|
+
const wheel = (event) => {
|
|
604
|
+
if (!enabled || disposed || !event.isTrusted || !event.cancelable) return;
|
|
605
|
+
let x, y, deltaX, deltaY, mode, zooming, shift;
|
|
606
|
+
try {
|
|
607
|
+
x = native.x.call(event);
|
|
608
|
+
y = native.y.call(event);
|
|
609
|
+
mode = native.mode.call(event);
|
|
610
|
+
zooming = native.ctrl.call(event) || native.meta.call(event);
|
|
611
|
+
shift = native.shift.call(event);
|
|
612
|
+
deltaX = native.deltaX.call(event);
|
|
613
|
+
deltaY = native.deltaY.call(event);
|
|
614
|
+
} catch {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) return;
|
|
618
|
+
const scaleX = mode === 1 ? 16 : mode === 2 ? window.innerWidth : 1;
|
|
619
|
+
const scaleY = mode === 1 ? 16 : mode === 2 ? window.innerHeight : 1;
|
|
620
|
+
deltaX *= scaleX;
|
|
621
|
+
deltaY *= scaleY;
|
|
622
|
+
let type;
|
|
623
|
+
let value;
|
|
624
|
+
if (zooming) {
|
|
625
|
+
type = "viewport.zoom";
|
|
626
|
+
deltaY = Math.max(
|
|
627
|
+
-ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA,
|
|
628
|
+
Math.min(ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA, deltaY)
|
|
629
|
+
);
|
|
630
|
+
value = normalizeArtifactViewportZoom(
|
|
631
|
+
{ x, y, deltaY },
|
|
632
|
+
{ width: window.innerWidth, height: window.innerHeight }
|
|
633
|
+
);
|
|
634
|
+
} else {
|
|
635
|
+
type = "viewport.pan";
|
|
636
|
+
if (shift && !deltaX) {
|
|
637
|
+
deltaX = deltaY;
|
|
638
|
+
deltaY = 0;
|
|
639
|
+
}
|
|
640
|
+
deltaX = Math.max(
|
|
641
|
+
-ARTIFACT_VIEWPORT_PAN_MAX_DELTA,
|
|
642
|
+
Math.min(ARTIFACT_VIEWPORT_PAN_MAX_DELTA, deltaX)
|
|
643
|
+
);
|
|
644
|
+
deltaY = Math.max(
|
|
645
|
+
-ARTIFACT_VIEWPORT_PAN_MAX_DELTA,
|
|
646
|
+
Math.min(ARTIFACT_VIEWPORT_PAN_MAX_DELTA, deltaY)
|
|
647
|
+
);
|
|
648
|
+
value = normalizeArtifactViewportPan({ deltaX, deltaY });
|
|
649
|
+
}
|
|
650
|
+
if (!value) return;
|
|
651
|
+
prevent.call(event);
|
|
652
|
+
stop.call(event);
|
|
653
|
+
if (pendingType && pendingType !== type) flush();
|
|
654
|
+
pendingType = type;
|
|
655
|
+
pending = type === "viewport.zoom" ? {
|
|
656
|
+
...value,
|
|
657
|
+
deltaY: Math.max(
|
|
658
|
+
-ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA,
|
|
659
|
+
Math.min(ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA, (pending?.deltaY || 0) + deltaY)
|
|
660
|
+
)
|
|
661
|
+
} : {
|
|
662
|
+
deltaX: Math.max(
|
|
663
|
+
-ARTIFACT_VIEWPORT_PAN_MAX_DELTA,
|
|
664
|
+
Math.min(ARTIFACT_VIEWPORT_PAN_MAX_DELTA, (pending?.deltaX || 0) + deltaX)
|
|
665
|
+
),
|
|
666
|
+
deltaY: Math.max(
|
|
667
|
+
-ARTIFACT_VIEWPORT_PAN_MAX_DELTA,
|
|
668
|
+
Math.min(ARTIFACT_VIEWPORT_PAN_MAX_DELTA, (pending?.deltaY || 0) + deltaY)
|
|
669
|
+
)
|
|
670
|
+
};
|
|
671
|
+
if (!timer) timer = schedule(flush, 16);
|
|
672
|
+
};
|
|
673
|
+
const destroy = () => {
|
|
674
|
+
disposed = true;
|
|
675
|
+
enabled = false;
|
|
676
|
+
clear();
|
|
677
|
+
remove("wheel", wheel, true);
|
|
678
|
+
remove("pagehide", destroy);
|
|
679
|
+
};
|
|
680
|
+
add("wheel", wheel, { capture: true, passive: false });
|
|
681
|
+
add("pagehide", destroy, { once: true });
|
|
682
|
+
return Object.freeze({
|
|
683
|
+
setEnabled(value) {
|
|
684
|
+
if (disposed || typeof value !== "boolean") return false;
|
|
685
|
+
enabled = value;
|
|
686
|
+
if (!value) clear();
|
|
687
|
+
return true;
|
|
688
|
+
},
|
|
689
|
+
destroy
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
var ARTIFACT_BRIDGE_OPERATION_TIMEOUTS = Object.freeze({
|
|
693
|
+
"inspect.point": 1200,
|
|
694
|
+
"inspect.anchor": 1200,
|
|
695
|
+
"thumbnail.request": 3e3,
|
|
696
|
+
"export.request": 8e3
|
|
697
|
+
});
|
|
698
|
+
var ARTIFACT_INSPECTION_PROPERTIES = Object.freeze([
|
|
699
|
+
"font-family",
|
|
700
|
+
"font-size",
|
|
701
|
+
"font-weight",
|
|
702
|
+
"line-height",
|
|
703
|
+
"letter-spacing",
|
|
704
|
+
"color",
|
|
705
|
+
"background-color",
|
|
706
|
+
"display",
|
|
707
|
+
"position",
|
|
708
|
+
"gap",
|
|
709
|
+
"padding-top",
|
|
710
|
+
"padding-right",
|
|
711
|
+
"padding-bottom",
|
|
712
|
+
"padding-left",
|
|
713
|
+
"margin-top",
|
|
714
|
+
"margin-right",
|
|
715
|
+
"margin-bottom",
|
|
716
|
+
"margin-left",
|
|
717
|
+
"border-radius",
|
|
718
|
+
"border-width",
|
|
719
|
+
"border-color",
|
|
720
|
+
"box-sizing"
|
|
721
|
+
]);
|
|
722
|
+
function normalizeArtifactInspection(value, viewport) {
|
|
723
|
+
const plain = (entry) => entry && typeof entry === "object" && !Array.isArray(entry) && [Object.prototype, null].includes(Object.getPrototypeOf(entry));
|
|
724
|
+
const own = (entry, key) => {
|
|
725
|
+
const descriptor = plain(entry) ? Object.getOwnPropertyDescriptor(entry, key) : null;
|
|
726
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : void 0;
|
|
727
|
+
};
|
|
728
|
+
const exact = (entry, keys) => plain(entry) && Object.keys(entry).length === keys.length && Object.keys(entry).every((key) => keys.includes(key));
|
|
729
|
+
if (!exact(value, ["tagName", "anchor", "rect", "viewport", "styles", "accessibility"]))
|
|
730
|
+
return null;
|
|
731
|
+
const tagName = own(value, "tagName");
|
|
732
|
+
const anchor = own(value, "anchor");
|
|
733
|
+
const rect = own(value, "rect");
|
|
734
|
+
const size = own(value, "viewport");
|
|
735
|
+
const styles = own(value, "styles");
|
|
736
|
+
const accessibility = own(value, "accessibility");
|
|
737
|
+
if (typeof tagName !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(tagName)) return null;
|
|
738
|
+
if (!exact(size, ["width", "height"]) || !exact(rect, ["x", "y", "width", "height"])) return null;
|
|
739
|
+
if (!["width", "height"].every(
|
|
740
|
+
(key) => Number.isInteger(own(size, key)) && own(size, key) >= 1 && own(size, key) <= 16384 && own(size, key) === viewport?.[key]
|
|
741
|
+
))
|
|
742
|
+
return null;
|
|
743
|
+
if (!["x", "y", "width", "height"].every(
|
|
744
|
+
(key) => typeof own(rect, key) === "number" && Number.isFinite(own(rect, key)) && own(rect, key) >= 0
|
|
745
|
+
))
|
|
746
|
+
return null;
|
|
747
|
+
if (rect.x + rect.width > size.width || rect.y + rect.height > size.height) return null;
|
|
748
|
+
if (anchor !== null && (!exact(anchor, own(anchor, "screen") === void 0 ? ["planrId"] : ["planrId", "screen"]) || typeof own(anchor, "planrId") !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/.test(anchor.planrId) || own(anchor, "screen") !== void 0 && (typeof anchor.screen !== "string" || !/^[^\u0000-\u001f\u007f]{1,128}$/.test(anchor.screen))))
|
|
749
|
+
return null;
|
|
750
|
+
if (!exact(styles, ARTIFACT_INSPECTION_PROPERTIES) || !ARTIFACT_INSPECTION_PROPERTIES.every((key) => {
|
|
751
|
+
const text3 = own(styles, key);
|
|
752
|
+
return typeof text3 === "string" && text3.length <= 256 && !/[\u0000-\u001f\u007f]/.test(text3) && !/(?:url\s*\(|https?:|file:)/i.test(text3);
|
|
753
|
+
}))
|
|
754
|
+
return null;
|
|
755
|
+
if (!exact(accessibility, ["role", "ariaLabel", "alt", "tabIndex", "disabled"]) || !["role", "ariaLabel", "alt"].every(
|
|
756
|
+
(key) => typeof own(accessibility, key) === "string" && own(accessibility, key).length <= 256 && !/[\u0000-\u001f\u007f]/.test(own(accessibility, key))
|
|
757
|
+
) || !Number.isInteger(own(accessibility, "tabIndex")) || accessibility.tabIndex < -1 || accessibility.tabIndex > 32767 || typeof own(accessibility, "disabled") !== "boolean")
|
|
758
|
+
return null;
|
|
759
|
+
return Object.freeze({
|
|
760
|
+
tagName,
|
|
761
|
+
anchor: anchor === null ? null : Object.freeze({ ...anchor }),
|
|
762
|
+
rect: Object.freeze({ ...rect }),
|
|
763
|
+
viewport: Object.freeze({ ...size }),
|
|
764
|
+
styles: Object.freeze({ ...styles }),
|
|
765
|
+
accessibility: Object.freeze({ ...accessibility })
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function normalizeArtifactThumbnail(value) {
|
|
769
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length !== 4 || !Object.keys(value).every((key) => ["dataUrl", "width", "height", "label"].includes(key)) || !Object.values(Object.getOwnPropertyDescriptors(value)).every(
|
|
770
|
+
(descriptor) => Object.hasOwn(descriptor, "value")
|
|
771
|
+
))
|
|
772
|
+
return null;
|
|
773
|
+
const { dataUrl, width, height, label } = value;
|
|
774
|
+
if (typeof dataUrl !== "string" || dataUrl.length > ARTIFACT_THUMBNAIL_MAX_DATA_URL || !/^data:image\/png;base64,[A-Za-z0-9+/]+=*$/.test(dataUrl) || !Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > ARTIFACT_THUMBNAIL_MAX_EDGE || height > ARTIFACT_THUMBNAIL_MAX_EDGE || typeof label !== "string" || label.length < 1 || label.length > 128)
|
|
775
|
+
return null;
|
|
776
|
+
return Object.freeze({ dataUrl, width, height, label });
|
|
777
|
+
}
|
|
778
|
+
function normalizeArtifactBridgeToolResult(requestType, message, viewport) {
|
|
779
|
+
const base = ["channel", "schemaVersion", "type", "nonce", "artifactId", "requestId"];
|
|
780
|
+
const exact = (keys) => message && typeof message === "object" && !Array.isArray(message) && Object.keys(message).length === keys.length && Object.keys(message).every((key) => keys.includes(key)) && Object.values(Object.getOwnPropertyDescriptors(message)).every(
|
|
781
|
+
(descriptor) => Object.hasOwn(descriptor, "value")
|
|
782
|
+
);
|
|
783
|
+
if (requestType === "inspect.point" || requestType === "inspect.anchor") {
|
|
784
|
+
if (exact(base) && message.type === "inspect.miss") return { valid: true, value: null };
|
|
785
|
+
if (!exact([...base, "inspection"]) || message.type !== "inspect.result")
|
|
786
|
+
return { valid: false };
|
|
787
|
+
const value = normalizeArtifactInspection(message.inspection, viewport);
|
|
788
|
+
return value ? { valid: true, value } : { valid: false };
|
|
789
|
+
}
|
|
790
|
+
if (requestType === "thumbnail.request") {
|
|
791
|
+
if (exact([...base, "reason"]) && message.type === "thumbnail.error" && typeof message.reason === "string" && message.reason.length <= 256)
|
|
792
|
+
return { valid: true, value: null };
|
|
793
|
+
if (!exact([...base, "dataUrl", "width", "height", "label"]) || message.type !== "thumbnail.result")
|
|
794
|
+
return { valid: false };
|
|
795
|
+
const value = normalizeArtifactThumbnail({
|
|
796
|
+
dataUrl: message.dataUrl,
|
|
797
|
+
width: message.width,
|
|
798
|
+
height: message.height,
|
|
799
|
+
label: message.label
|
|
800
|
+
});
|
|
801
|
+
return value ? { valid: true, value } : { valid: false };
|
|
802
|
+
}
|
|
803
|
+
return { valid: false };
|
|
804
|
+
}
|
|
805
|
+
function createArtifactBridgeTools(document2, window) {
|
|
806
|
+
const fromPoint = document2.elementFromPoint.bind(document2);
|
|
807
|
+
const query = document2.querySelectorAll.bind(document2);
|
|
808
|
+
const attr2 = window.Element.prototype.getAttribute;
|
|
809
|
+
const closest = window.Element.prototype.closest;
|
|
810
|
+
const bounds = window.Element.prototype.getBoundingClientRect;
|
|
811
|
+
const computed = window.getComputedStyle.bind(window);
|
|
812
|
+
const cloneNode = window.Node.prototype.cloneNode;
|
|
813
|
+
const append = window.Node.prototype.appendChild;
|
|
814
|
+
const create = document2.createElement.bind(document2);
|
|
815
|
+
const setAttribute = window.Element.prototype.setAttribute;
|
|
816
|
+
const serialize3 = window.XMLSerializer.prototype.serializeToString;
|
|
817
|
+
const Image = window.Image;
|
|
818
|
+
const Serializer = window.XMLSerializer;
|
|
819
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
820
|
+
const get = (element2, key) => attr2.call(element2, key);
|
|
821
|
+
const validId = (id3) => typeof id3 === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/.test(id3);
|
|
822
|
+
const screen = (element2) => {
|
|
823
|
+
const owner = closest.call(element2, "[data-planr-screen]");
|
|
824
|
+
const value = owner && get(owner, "data-planr-screen");
|
|
825
|
+
return typeof value === "string" && /^[^\u0000-\u001f\u007f]{1,128}$/.test(value) ? value : void 0;
|
|
826
|
+
};
|
|
827
|
+
const clean = (value) => String(value || "").replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 256);
|
|
828
|
+
const inspectElement = (element2) => {
|
|
829
|
+
if (!(element2 instanceof window.Element)) return null;
|
|
830
|
+
const tagName = element2.localName;
|
|
831
|
+
if (typeof tagName !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(tagName)) return null;
|
|
832
|
+
const rect = bounds.call(element2), width = window.innerWidth, height = window.innerHeight;
|
|
833
|
+
const x = clamp(rect.left, 0, width), y = clamp(rect.top, 0, height);
|
|
834
|
+
const owner = closest.call(element2, "[data-planr-id]");
|
|
835
|
+
const planrId = owner && get(owner, "data-planr-id");
|
|
836
|
+
const anchorScreen = owner && screen(owner);
|
|
837
|
+
const style = computed(element2);
|
|
838
|
+
const styles = Object.fromEntries(
|
|
839
|
+
ARTIFACT_INSPECTION_PROPERTIES.map((key) => {
|
|
840
|
+
const value = clean(style.getPropertyValue(key));
|
|
841
|
+
return [key, /(?:url\s*\(|https?:|file:)/i.test(value) ? "" : value];
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
return {
|
|
845
|
+
tagName,
|
|
846
|
+
anchor: validId(planrId) ? { planrId, ...anchorScreen ? { screen: anchorScreen } : {} } : null,
|
|
847
|
+
rect: {
|
|
848
|
+
x,
|
|
849
|
+
y,
|
|
850
|
+
width: Math.max(0, clamp(rect.right, 0, width) - x),
|
|
851
|
+
height: Math.max(0, clamp(rect.bottom, 0, height) - y)
|
|
852
|
+
},
|
|
853
|
+
viewport: { width, height },
|
|
854
|
+
styles,
|
|
855
|
+
accessibility: {
|
|
856
|
+
role: clean(get(element2, "role")),
|
|
857
|
+
ariaLabel: clean(get(element2, "aria-label")),
|
|
858
|
+
alt: clean(get(element2, "alt")),
|
|
859
|
+
tabIndex: clamp(Number(element2.tabIndex) || 0, -1, 32767),
|
|
860
|
+
disabled: get(element2, "disabled") !== null || get(element2, "aria-disabled") === "true"
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
let capturing = false;
|
|
865
|
+
return Object.freeze({
|
|
866
|
+
inspectAt(x, y) {
|
|
867
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight)
|
|
868
|
+
return null;
|
|
869
|
+
return inspectElement(fromPoint(x, y));
|
|
870
|
+
},
|
|
871
|
+
inspect(anchor) {
|
|
872
|
+
if (!anchor || !validId(anchor.planrId) || Object.keys(anchor).some((key) => !["planrId", "screen"].includes(key)) || anchor.screen !== void 0 && (typeof anchor.screen !== "string" || !/^[^\u0000-\u001f\u007f]{1,128}$/.test(anchor.screen)))
|
|
873
|
+
return null;
|
|
874
|
+
let count = 0;
|
|
875
|
+
for (const element2 of query("[data-planr-id]")) {
|
|
876
|
+
if (++count > 1e4) return null;
|
|
877
|
+
if (get(element2, "data-planr-id") === anchor.planrId && (anchor.screen === void 0 || screen(element2) === anchor.screen))
|
|
878
|
+
return inspectElement(element2);
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
},
|
|
882
|
+
async thumbnail() {
|
|
883
|
+
if (capturing) throw new Error("Thumbnail capture is busy.");
|
|
884
|
+
capturing = true;
|
|
885
|
+
let image;
|
|
886
|
+
try {
|
|
887
|
+
const deadline = Date.now() + 2400;
|
|
888
|
+
const width = window.innerWidth, height = window.innerHeight;
|
|
889
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 16384 || height > 16384)
|
|
890
|
+
throw new Error("Thumbnail dimensions are unavailable.");
|
|
891
|
+
let count = 0, contentSize = 0;
|
|
892
|
+
const cloneStyled = (source) => {
|
|
893
|
+
if (++count > 4e3 || Date.now() > deadline)
|
|
894
|
+
throw new Error("Thumbnail capture limit exceeded.");
|
|
895
|
+
if (source.nodeType === 8 || source.nodeType === 1 && ["SCRIPT", "STYLE", "LINK", "META"].includes(source.tagName))
|
|
896
|
+
return document2.createTextNode("");
|
|
897
|
+
const target = cloneNode.call(source, false);
|
|
898
|
+
if (source.nodeType === 1) {
|
|
899
|
+
const styles = computed(source);
|
|
900
|
+
let css = "";
|
|
901
|
+
for (let index = 0; index < styles.length; index++) {
|
|
902
|
+
if (index >= 2048 || Date.now() > deadline)
|
|
903
|
+
throw new Error("Thumbnail style limit exceeded.");
|
|
904
|
+
const name = styles[index];
|
|
905
|
+
if (!name.startsWith("--")) css += name + ":" + styles.getPropertyValue(name) + ";";
|
|
906
|
+
}
|
|
907
|
+
contentSize += css.length + (source.textContent?.length || 0);
|
|
908
|
+
if (contentSize > 4 * 1024 * 1024) throw new Error("Thumbnail markup limit exceeded.");
|
|
909
|
+
setAttribute.call(target, "style", css + "animation:none;transition:none;");
|
|
910
|
+
for (const attribute of [...target.attributes])
|
|
911
|
+
if (/^on/i.test(attribute.name) || ["value", "srcdoc"].includes(attribute.name))
|
|
912
|
+
target.removeAttribute(attribute.name);
|
|
913
|
+
if (source.tagName === "INPUT" || source.tagName === "TEXTAREA") {
|
|
914
|
+
target.value = "";
|
|
915
|
+
target.textContent = "";
|
|
916
|
+
}
|
|
917
|
+
if (source.tagName === "CANVAS") {
|
|
918
|
+
try {
|
|
919
|
+
const replacement = create("img");
|
|
920
|
+
replacement.src = source.toDataURL("image/png");
|
|
921
|
+
setAttribute.call(replacement, "style", css);
|
|
922
|
+
return replacement;
|
|
923
|
+
} catch {
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (source.nodeType !== 1 || source.tagName !== "TEXTAREA")
|
|
928
|
+
for (let child = source.firstChild; child; child = child.nextSibling)
|
|
929
|
+
append.call(target, cloneStyled(child));
|
|
930
|
+
return target;
|
|
931
|
+
};
|
|
932
|
+
const clone = cloneStyled(document2.documentElement);
|
|
933
|
+
setAttribute.call(clone, "xmlns", "http://www.w3.org/1999/xhtml");
|
|
934
|
+
const markup = serialize3.call(new Serializer(), clone);
|
|
935
|
+
if (markup.length > 4 * 1024 * 1024) throw new Error("Thumbnail markup limit exceeded.");
|
|
936
|
+
const scale = Math.min(1, ARTIFACT_THUMBNAIL_MAX_EDGE / Math.max(width, height));
|
|
937
|
+
const outputWidth = Math.max(1, Math.round(width * scale)), outputHeight = Math.max(1, Math.round(height * scale));
|
|
938
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + outputWidth + '" height="' + outputHeight + '" viewBox="0 0 ' + width + " " + height + '"><foreignObject width="' + width + '" height="' + height + '">' + markup + "</foreignObject></svg>";
|
|
939
|
+
image = new Image();
|
|
940
|
+
await new Promise((resolve5, reject) => {
|
|
941
|
+
const timer = setTimeout(
|
|
942
|
+
() => reject(new Error("Thumbnail capture timed out.")),
|
|
943
|
+
Math.max(1, deadline - Date.now())
|
|
944
|
+
);
|
|
945
|
+
image.onload = () => {
|
|
946
|
+
clearTimeout(timer);
|
|
947
|
+
resolve5();
|
|
948
|
+
};
|
|
949
|
+
image.onerror = () => {
|
|
950
|
+
clearTimeout(timer);
|
|
951
|
+
reject(new Error("Thumbnail rendering unavailable."));
|
|
952
|
+
};
|
|
953
|
+
image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
954
|
+
});
|
|
955
|
+
const canvas = create("canvas");
|
|
956
|
+
canvas.width = outputWidth;
|
|
957
|
+
canvas.height = outputHeight;
|
|
958
|
+
canvas.getContext("2d").drawImage(image, 0, 0, outputWidth, outputHeight);
|
|
959
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
960
|
+
if (dataUrl.length > ARTIFACT_THUMBNAIL_MAX_DATA_URL)
|
|
961
|
+
throw new Error("Thumbnail output limit exceeded.");
|
|
962
|
+
return { dataUrl, width: outputWidth, height: outputHeight, label: "screen" };
|
|
963
|
+
} finally {
|
|
964
|
+
if (image) {
|
|
965
|
+
image.onload = null;
|
|
966
|
+
image.onerror = null;
|
|
967
|
+
}
|
|
968
|
+
capturing = false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
function renderArtifactBridgeToolsSource() {
|
|
974
|
+
return `const ARTIFACT_THUMBNAIL_MAX_EDGE=${ARTIFACT_THUMBNAIL_MAX_EDGE};
|
|
975
|
+
const ARTIFACT_THUMBNAIL_MAX_DATA_URL=${ARTIFACT_THUMBNAIL_MAX_DATA_URL};
|
|
976
|
+
const ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA=${ARTIFACT_VIEWPORT_ZOOM_MAX_DELTA};
|
|
977
|
+
const ARTIFACT_VIEWPORT_PAN_MAX_DELTA=${ARTIFACT_VIEWPORT_PAN_MAX_DELTA};
|
|
978
|
+
const ARTIFACT_INSPECTION_PROPERTIES=${JSON.stringify(ARTIFACT_INSPECTION_PROPERTIES)};
|
|
979
|
+
const ARTIFACT_BRIDGE_OPERATION_TIMEOUTS=${JSON.stringify(ARTIFACT_BRIDGE_OPERATION_TIMEOUTS)};
|
|
980
|
+
${normalizeArtifactInspection.toString()}
|
|
981
|
+
${normalizeArtifactThumbnail.toString()}
|
|
982
|
+
${normalizeArtifactBridgeToolResult.toString()}
|
|
983
|
+
${normalizeArtifactViewportZoom.toString()}
|
|
984
|
+
${normalizeArtifactViewportPan.toString()}
|
|
985
|
+
${createArtifactViewportGestures.toString()}
|
|
986
|
+
${createArtifactBridgeTools.toString()}`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// packages/artifact/lib/artifact/bridge.mjs
|
|
990
|
+
var ARTIFACT_BRIDGE_CHANNEL = "openplanr.artifact-anchor";
|
|
991
|
+
var ARTIFACT_BRIDGE_VERSION = "1.0.0";
|
|
992
|
+
var ARTIFACT_BRIDGE_EVENT = "planr:artifact-anchor";
|
|
993
|
+
var ARTIFACT_BRIDGE_READY_EVENT = "planr:artifact-bridge-ready";
|
|
994
|
+
var ARTIFACT_BRIDGE_LAYOUT_EVENT = "planr:artifact-layout";
|
|
995
|
+
var ARTIFACT_VIEWPORT_ZOOM_EVENT = "planr:artifact-viewport-zoom";
|
|
996
|
+
var ARTIFACT_VIEWPORT_PAN_EVENT = "planr:artifact-viewport-pan";
|
|
997
|
+
var ARTIFACT_EXPORT_MAX_EDGE = 11e3;
|
|
998
|
+
var ARTIFACT_EXPORT_MAX_DATA_URL = 24 * 1024 * 1024;
|
|
999
|
+
var ARTIFACT_LAYOUT_MAX_WIDTH = 16384;
|
|
1000
|
+
var ARTIFACT_LAYOUT_MAX_HEIGHT = 262144;
|
|
1001
|
+
var ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/;
|
|
1002
|
+
var SCRIPT_NONCE_RE = /^[A-Za-z0-9_-]{24}$/;
|
|
1003
|
+
var FORBIDDEN_TAGS = /* @__PURE__ */ new Set([
|
|
1004
|
+
"applet",
|
|
1005
|
+
"base",
|
|
1006
|
+
"embed",
|
|
1007
|
+
"fencedframe",
|
|
1008
|
+
"form",
|
|
1009
|
+
"frame",
|
|
1010
|
+
"frameset",
|
|
1011
|
+
"iframe",
|
|
1012
|
+
"noembed",
|
|
1013
|
+
"noframes",
|
|
1014
|
+
"noscript",
|
|
1015
|
+
"object",
|
|
1016
|
+
"portal"
|
|
1017
|
+
]);
|
|
1018
|
+
var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
1019
|
+
"action",
|
|
1020
|
+
"archive",
|
|
1021
|
+
"background",
|
|
1022
|
+
"classid",
|
|
1023
|
+
"codebase",
|
|
1024
|
+
"formaction",
|
|
1025
|
+
"href",
|
|
1026
|
+
"longdesc",
|
|
1027
|
+
"manifest",
|
|
1028
|
+
"ping",
|
|
1029
|
+
"profile",
|
|
1030
|
+
"src",
|
|
1031
|
+
"srcdoc",
|
|
1032
|
+
"target",
|
|
1033
|
+
"xlink:href"
|
|
1034
|
+
]);
|
|
1035
|
+
var REMOTE_URL_RE = /^(?:https?:|file:|ftp:|wss?:|\/\/)/i;
|
|
1036
|
+
function pipelineError(code, message, details) {
|
|
1037
|
+
return new PipelineError(code, message, "", details);
|
|
1038
|
+
}
|
|
1039
|
+
function getAttr(node, name) {
|
|
1040
|
+
return node.attrs?.find((attribute) => attribute.name.toLowerCase() === name)?.value;
|
|
1041
|
+
}
|
|
1042
|
+
function setAttr(node, name, value) {
|
|
1043
|
+
const existing = node.attrs?.find((attribute) => attribute.name.toLowerCase() === name);
|
|
1044
|
+
if (existing) existing.value = value;
|
|
1045
|
+
else (node.attrs ??= []).push({ name, value });
|
|
1046
|
+
}
|
|
1047
|
+
function createElement(tagName) {
|
|
1048
|
+
return parseFragment(`<${tagName}></${tagName}>`).childNodes[0];
|
|
1049
|
+
}
|
|
1050
|
+
function createText(value, parentNode) {
|
|
1051
|
+
return { nodeName: "#text", value, parentNode };
|
|
1052
|
+
}
|
|
1053
|
+
function descendants(node) {
|
|
1054
|
+
return [...node?.childNodes ?? [], ...node?.content?.childNodes ?? []];
|
|
1055
|
+
}
|
|
1056
|
+
function removeNode(node) {
|
|
1057
|
+
const parent = node.parentNode;
|
|
1058
|
+
const index = parent?.childNodes?.indexOf(node) ?? -1;
|
|
1059
|
+
if (index >= 0) parent.childNodes.splice(index, 1);
|
|
1060
|
+
}
|
|
1061
|
+
function findElement(document2, tagName) {
|
|
1062
|
+
const queue = descendants(document2);
|
|
1063
|
+
while (queue.length > 0) {
|
|
1064
|
+
const node = queue.shift();
|
|
1065
|
+
if (node?.tagName?.toLowerCase() === tagName) return node;
|
|
1066
|
+
queue.push(...descendants(node));
|
|
1067
|
+
}
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
function createArtifactBridgeNonce({ randomBytesImpl = randomBytes3 } = {}) {
|
|
1071
|
+
return mintCapabilityToken({ bytes: 32, randomBytesImpl });
|
|
1072
|
+
}
|
|
1073
|
+
function artifactContentSecurityPolicy(scriptNonce) {
|
|
1074
|
+
if (typeof scriptNonce !== "string" || !SCRIPT_NONCE_RE.test(scriptNonce)) {
|
|
1075
|
+
throw pipelineError(ARTIFACT_ERROR_CODES.SANDBOX_POLICY, "Artifact script nonce is invalid.");
|
|
1076
|
+
}
|
|
1077
|
+
return [
|
|
1078
|
+
"default-src 'none'",
|
|
1079
|
+
`script-src 'nonce-${scriptNonce}' data: blob:`,
|
|
1080
|
+
`script-src-elem 'nonce-${scriptNonce}' data: blob:`,
|
|
1081
|
+
"script-src-attr 'unsafe-inline'",
|
|
1082
|
+
"style-src 'unsafe-inline' data: blob:",
|
|
1083
|
+
"img-src data: blob:",
|
|
1084
|
+
"media-src data: blob:",
|
|
1085
|
+
"font-src data:",
|
|
1086
|
+
"worker-src data: blob:",
|
|
1087
|
+
"connect-src 'none'",
|
|
1088
|
+
"frame-src 'none'",
|
|
1089
|
+
"child-src 'none'",
|
|
1090
|
+
"object-src 'none'",
|
|
1091
|
+
"manifest-src 'none'",
|
|
1092
|
+
"form-action 'none'",
|
|
1093
|
+
"base-uri 'none'"
|
|
1094
|
+
].join("; ");
|
|
1095
|
+
}
|
|
1096
|
+
function assertSandboxableTree(document2, { allowLocalForms = false } = {}) {
|
|
1097
|
+
const queue = descendants(document2);
|
|
1098
|
+
while (queue.length > 0) {
|
|
1099
|
+
const node = queue.shift();
|
|
1100
|
+
if (!node?.tagName) {
|
|
1101
|
+
queue.push(...descendants(node));
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const tag = node.tagName.toLowerCase();
|
|
1105
|
+
if (FORBIDDEN_TAGS.has(tag) && !(tag === "form" && allowLocalForms)) {
|
|
1106
|
+
throw pipelineError(
|
|
1107
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1108
|
+
`Unsafe <${tag}> cannot enter an artifact review sandbox.`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
if (tag === "meta" && getAttr(node, "http-equiv")?.trim().toLowerCase() === "refresh") {
|
|
1112
|
+
throw pipelineError(
|
|
1113
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1114
|
+
"Meta refresh navigation is forbidden."
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
for (const attribute of node.attrs ?? []) {
|
|
1118
|
+
const name = attribute.name.toLowerCase();
|
|
1119
|
+
const value = attribute.value.trim();
|
|
1120
|
+
if (["srcdoc", "target", "action", "formaction"].includes(name)) {
|
|
1121
|
+
throw pipelineError(
|
|
1122
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1123
|
+
`Artifact navigation attribute ${name} is forbidden.`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
if (URL_ATTRIBUTES.has(name) && REMOTE_URL_RE.test(value)) {
|
|
1127
|
+
throw pipelineError(
|
|
1128
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1129
|
+
`Remote artifact resource is forbidden: ${value.slice(0, 80)}`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
if (name === "style" && /(?:@import|url\s*\(\s*['"]?(?:https?:|file:|\/\/))/i.test(value)) {
|
|
1133
|
+
throw pipelineError(ARTIFACT_ERROR_CODES.SANDBOX_POLICY, "Remote inline CSS is forbidden.");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (tag === "style") {
|
|
1137
|
+
const css = (node.childNodes ?? []).map((child) => child.value ?? "").join("");
|
|
1138
|
+
if (/(?:@import|url\s*\(\s*['"]?(?:https?:|file:|\/\/))/i.test(css)) {
|
|
1139
|
+
throw pipelineError(
|
|
1140
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1141
|
+
"Remote stylesheet resources are forbidden."
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
queue.push(...descendants(node));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
var SANDBOX_GUARD_LIMITS = Object.freeze({
|
|
1149
|
+
__PLANR_SANDBOX_EXPORT_MAX_EDGE__: String(ARTIFACT_EXPORT_MAX_EDGE),
|
|
1150
|
+
__PLANR_SANDBOX_EXPORT_MAX_DATA_URL__: String(ARTIFACT_EXPORT_MAX_DATA_URL),
|
|
1151
|
+
__PLANR_SANDBOX_LAYOUT_MAX_WIDTH__: String(ARTIFACT_LAYOUT_MAX_WIDTH),
|
|
1152
|
+
__PLANR_SANDBOX_LAYOUT_MAX_HEIGHT__: String(ARTIFACT_LAYOUT_MAX_HEIGHT)
|
|
1153
|
+
});
|
|
1154
|
+
function sandboxGuardFiller(name, template, keys) {
|
|
1155
|
+
const pattern = new RegExp(keys.join("|"), "gu");
|
|
1156
|
+
const unfilled = [...new Set(template.replace(pattern, "").match(/__PLANR_SANDBOX_[A-Z_]+__/gu))];
|
|
1157
|
+
const missing = keys.filter((key) => !template.includes(key));
|
|
1158
|
+
if (unfilled.length > 0 || missing.length > 0) {
|
|
1159
|
+
throw pipelineError(
|
|
1160
|
+
ARTIFACT_ERROR_CODES.BRIDGE_INVALID,
|
|
1161
|
+
`The generated ${name} does not match bridge.mjs (unfilled: ${unfilled.join(", ") || "none"}; missing: ${missing.join(", ") || "none"}). Run npm run generate.`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return (values) => template.replace(pattern, (key) => {
|
|
1165
|
+
if (typeof values[key] !== "string") {
|
|
1166
|
+
throw pipelineError(
|
|
1167
|
+
ARTIFACT_ERROR_CODES.BRIDGE_INVALID,
|
|
1168
|
+
`The ${name} needs a string for ${key}; received ${typeof values[key]}.`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return values[key];
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
var fillFrameGuard = sandboxGuardFiller("frame guard", ARTIFACT_FRAME_GUARD_TEMPLATE, [
|
|
1175
|
+
"__PLANR_SANDBOX_CONTRACT__",
|
|
1176
|
+
"__PLANR_SANDBOX_BRIDGE_TOOLS__;",
|
|
1177
|
+
"__PLANR_SANDBOX_WORKER_GUARD__",
|
|
1178
|
+
...Object.keys(SANDBOX_GUARD_LIMITS)
|
|
1179
|
+
]);
|
|
1180
|
+
var fillHostGuard = sandboxGuardFiller("host guard", ARTIFACT_HOST_GUARD_TEMPLATE, [
|
|
1181
|
+
"__PLANR_SANDBOX_CONFIG__",
|
|
1182
|
+
"__PLANR_SANDBOX_BRIDGE_TOOLS__;",
|
|
1183
|
+
...Object.keys(SANDBOX_GUARD_LIMITS)
|
|
1184
|
+
]);
|
|
1185
|
+
function artifactGuardAndBridgeSource({ artifactId, nonce, parentOrigin }) {
|
|
1186
|
+
const contract = JSON.stringify({
|
|
1187
|
+
channel: ARTIFACT_BRIDGE_CHANNEL,
|
|
1188
|
+
schemaVersion: ARTIFACT_BRIDGE_VERSION,
|
|
1189
|
+
artifactId,
|
|
1190
|
+
nonce,
|
|
1191
|
+
parentOrigin
|
|
1192
|
+
});
|
|
1193
|
+
return fillFrameGuard({
|
|
1194
|
+
__PLANR_SANDBOX_CONTRACT__: contract,
|
|
1195
|
+
"__PLANR_SANDBOX_BRIDGE_TOOLS__;": renderArtifactBridgeToolsSource(),
|
|
1196
|
+
__PLANR_SANDBOX_WORKER_GUARD__: JSON.stringify(ARTIFACT_WORKER_GUARD_SOURCE),
|
|
1197
|
+
...SANDBOX_GUARD_LIMITS
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
function prepareArtifactDocument({
|
|
1201
|
+
html,
|
|
1202
|
+
artifactId,
|
|
1203
|
+
nonce,
|
|
1204
|
+
parentOrigin,
|
|
1205
|
+
scriptNonce = mintCapabilityToken({ bytes: 18 }),
|
|
1206
|
+
allowLocalForms = false,
|
|
1207
|
+
portable = false
|
|
1208
|
+
} = {}) {
|
|
1209
|
+
if (typeof html !== "string" || html.length === 0) {
|
|
1210
|
+
throw pipelineError(ARTIFACT_ERROR_CODES.SANDBOX_POLICY, "Artifact HTML is required.");
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof artifactId !== "string" || !ID_RE.test(artifactId)) {
|
|
1213
|
+
throw pipelineError(ARTIFACT_ERROR_CODES.SANDBOX_POLICY, "Artifact id is invalid.");
|
|
1214
|
+
}
|
|
1215
|
+
if (!isCapabilityToken(nonce)) {
|
|
1216
|
+
throw pipelineError(ARTIFACT_ERROR_CODES.SANDBOX_POLICY, "Artifact bridge nonce is invalid.");
|
|
1217
|
+
}
|
|
1218
|
+
const originMatch = /^http:\/\/127\.0\.0\.1:(\d{1,5})$/.exec(parentOrigin ?? "");
|
|
1219
|
+
const originPort = Number(originMatch?.[1]);
|
|
1220
|
+
if (!(portable && parentOrigin === "null") && (!originMatch || !Number.isInteger(originPort) || originPort < 1 || originPort > 65535 || String(originPort) !== originMatch[1])) {
|
|
1221
|
+
throw pipelineError(
|
|
1222
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1223
|
+
"Artifact parent origin must be IPv4 loopback."
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
const document2 = parse(html, { sourceCodeLocationInfo: false });
|
|
1227
|
+
assertSandboxableTree(document2, { allowLocalForms });
|
|
1228
|
+
const head = findElement(document2, "head");
|
|
1229
|
+
if (!head)
|
|
1230
|
+
throw pipelineError(
|
|
1231
|
+
ARTIFACT_ERROR_CODES.SANDBOX_POLICY,
|
|
1232
|
+
"Artifact document has no head element."
|
|
1233
|
+
);
|
|
1234
|
+
for (const node of [...descendants(head)]) {
|
|
1235
|
+
if (node?.tagName?.toLowerCase() === "meta") {
|
|
1236
|
+
const httpEquiv = getAttr(node, "http-equiv")?.trim().toLowerCase();
|
|
1237
|
+
if (httpEquiv === "content-security-policy") removeNode(node);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const csp = artifactContentSecurityPolicy(scriptNonce);
|
|
1241
|
+
const hasViewport = descendants(head).some(
|
|
1242
|
+
(node) => node?.tagName?.toLowerCase() === "meta" && getAttr(node, "name")?.trim().toLowerCase() === "viewport"
|
|
1243
|
+
);
|
|
1244
|
+
const viewportMeta = hasViewport ? null : createElement("meta");
|
|
1245
|
+
if (viewportMeta) {
|
|
1246
|
+
setAttr(viewportMeta, "name", "viewport");
|
|
1247
|
+
setAttr(viewportMeta, "content", "width=device-width,initial-scale=1,viewport-fit=cover");
|
|
1248
|
+
viewportMeta.parentNode = head;
|
|
1249
|
+
}
|
|
1250
|
+
const cspMeta = createElement("meta");
|
|
1251
|
+
setAttr(cspMeta, "http-equiv", "Content-Security-Policy");
|
|
1252
|
+
setAttr(cspMeta, "content", csp);
|
|
1253
|
+
cspMeta.parentNode = head;
|
|
1254
|
+
const referrerMeta = createElement("meta");
|
|
1255
|
+
setAttr(referrerMeta, "name", "referrer");
|
|
1256
|
+
setAttr(referrerMeta, "content", "no-referrer");
|
|
1257
|
+
referrerMeta.parentNode = head;
|
|
1258
|
+
const bridge = createElement("script");
|
|
1259
|
+
setAttr(bridge, "nonce", scriptNonce);
|
|
1260
|
+
bridge.childNodes = [
|
|
1261
|
+
createText(artifactGuardAndBridgeSource({ artifactId, nonce, parentOrigin }), bridge)
|
|
1262
|
+
];
|
|
1263
|
+
bridge.parentNode = head;
|
|
1264
|
+
const queue = descendants(document2);
|
|
1265
|
+
while (queue.length > 0) {
|
|
1266
|
+
const node = queue.shift();
|
|
1267
|
+
if (node?.tagName?.toLowerCase() === "script") setAttr(node, "nonce", scriptNonce);
|
|
1268
|
+
queue.push(...descendants(node));
|
|
1269
|
+
}
|
|
1270
|
+
head.childNodes = [
|
|
1271
|
+
cspMeta,
|
|
1272
|
+
referrerMeta,
|
|
1273
|
+
...viewportMeta ? [viewportMeta] : [],
|
|
1274
|
+
bridge,
|
|
1275
|
+
...head.childNodes ?? []
|
|
1276
|
+
];
|
|
1277
|
+
return Object.freeze({ html: serialize(document2), csp, scriptNonce });
|
|
1278
|
+
}
|
|
1279
|
+
function renderArtifactParentRuntime({
|
|
1280
|
+
artifactBaseUrl,
|
|
1281
|
+
stageRuntimeUrl,
|
|
1282
|
+
adapterRuntimeUrl,
|
|
1283
|
+
nonce,
|
|
1284
|
+
inlineArtifacts
|
|
1285
|
+
} = {}) {
|
|
1286
|
+
const canonicalPath = (value, { trailingSlash = false } = {}) => {
|
|
1287
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("\\") || value.includes("?") || value.includes("#") || /[\u0000-\u001f]/.test(value) || /%(?:00|2f|5c)/i.test(value) || (trailingSlash ? !value.endsWith("/") : value.endsWith("/")))
|
|
1288
|
+
return false;
|
|
1289
|
+
try {
|
|
1290
|
+
return value.split("/").filter(Boolean).every((segment) => {
|
|
1291
|
+
const decoded = decodeURIComponent(segment);
|
|
1292
|
+
return decoded !== "." && decoded !== ".." && !decoded.includes("/") && !decoded.includes("\\");
|
|
1293
|
+
});
|
|
1294
|
+
} catch {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const portable = inlineArtifacts && typeof inlineArtifacts === "object" && !Array.isArray(inlineArtifacts) && Object.values(inlineArtifacts).every((html) => typeof html === "string");
|
|
1299
|
+
if (!portable && !canonicalPath(artifactBaseUrl, { trailingSlash: true }) || !(canonicalPath(stageRuntimeUrl) || portable && /^data:text\/javascript;base64,[A-Za-z0-9+/=]+$/u.test(stageRuntimeUrl)) || adapterRuntimeUrl !== void 0 && !canonicalPath(adapterRuntimeUrl) || !isCapabilityToken(nonce)) {
|
|
1300
|
+
throw pipelineError(
|
|
1301
|
+
ARTIFACT_ERROR_CODES.BRIDGE_INVALID,
|
|
1302
|
+
"Artifact parent runtime configuration is invalid."
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
const config = JSON.stringify({
|
|
1306
|
+
artifactBaseUrl,
|
|
1307
|
+
stageRuntimeUrl,
|
|
1308
|
+
adapterRuntimeUrl: adapterRuntimeUrl ?? null,
|
|
1309
|
+
nonce,
|
|
1310
|
+
inlineArtifacts: portable ? inlineArtifacts : null,
|
|
1311
|
+
channel: ARTIFACT_BRIDGE_CHANNEL,
|
|
1312
|
+
schemaVersion: ARTIFACT_BRIDGE_VERSION,
|
|
1313
|
+
anchorEvent: ARTIFACT_BRIDGE_EVENT,
|
|
1314
|
+
readyEvent: ARTIFACT_BRIDGE_READY_EVENT,
|
|
1315
|
+
layoutEvent: ARTIFACT_BRIDGE_LAYOUT_EVENT,
|
|
1316
|
+
viewportZoomEvent: ARTIFACT_VIEWPORT_ZOOM_EVENT,
|
|
1317
|
+
viewportPanEvent: ARTIFACT_VIEWPORT_PAN_EVENT,
|
|
1318
|
+
navigationEvent: "planr:artifact-navigation-blocked",
|
|
1319
|
+
frameCsp: [
|
|
1320
|
+
"default-src 'none'",
|
|
1321
|
+
"script-src 'unsafe-inline' data: blob:",
|
|
1322
|
+
"script-src-attr 'unsafe-inline'",
|
|
1323
|
+
"style-src 'unsafe-inline' data: blob:",
|
|
1324
|
+
"img-src data: blob:",
|
|
1325
|
+
"media-src data: blob:",
|
|
1326
|
+
"font-src data:",
|
|
1327
|
+
"worker-src data: blob:",
|
|
1328
|
+
"connect-src 'none'",
|
|
1329
|
+
"frame-src 'none'",
|
|
1330
|
+
"child-src 'none'",
|
|
1331
|
+
"object-src 'none'",
|
|
1332
|
+
"manifest-src 'none'",
|
|
1333
|
+
"form-action 'none'",
|
|
1334
|
+
"base-uri 'none'"
|
|
1335
|
+
].join("; ")
|
|
1336
|
+
});
|
|
1337
|
+
return fillHostGuard({
|
|
1338
|
+
__PLANR_SANDBOX_CONFIG__: config,
|
|
1339
|
+
"__PLANR_SANDBOX_BRIDGE_TOOLS__;": renderArtifactBridgeToolsSource(),
|
|
1340
|
+
...SANDBOX_GUARD_LIMITS
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// packages/artifact/lib/artifact/local-document.mjs
|
|
1345
|
+
import { createHash } from "node:crypto";
|
|
1346
|
+
import { readFileSync as readFileSync2, realpathSync, statSync } from "node:fs";
|
|
1347
|
+
import { dirname as dirname2, extname, isAbsolute, relative, resolve as resolve2 } from "node:path";
|
|
1348
|
+
import { Script } from "node:vm";
|
|
1349
|
+
var MIME = {
|
|
1350
|
+
".png": "image/png",
|
|
1351
|
+
".jpg": "image/jpeg",
|
|
1352
|
+
".jpeg": "image/jpeg",
|
|
1353
|
+
".svg": "image/svg+xml",
|
|
1354
|
+
".webp": "image/webp",
|
|
1355
|
+
".avif": "image/avif",
|
|
1356
|
+
".gif": "image/gif",
|
|
1357
|
+
".woff": "font/woff",
|
|
1358
|
+
".woff2": "font/woff2",
|
|
1359
|
+
".ttf": "font/ttf",
|
|
1360
|
+
".ico": "image/x-icon"
|
|
1361
|
+
};
|
|
1362
|
+
var children = (node) => [...node.childNodes ?? [], ...node.content?.childNodes ?? []];
|
|
1363
|
+
var attr = (node, name) => node.attrs?.find((item2) => item2.name === name);
|
|
1364
|
+
var text = (node) => children(node).map((item2) => item2.value ?? "").join("");
|
|
1365
|
+
function setText(node, value) {
|
|
1366
|
+
node.childNodes = [{ nodeName: "#text", value, parentNode: node }];
|
|
1367
|
+
}
|
|
1368
|
+
function element(name, value) {
|
|
1369
|
+
const node = parseFragment(`<${name}></${name}>`).childNodes[0];
|
|
1370
|
+
setText(node, value);
|
|
1371
|
+
return node;
|
|
1372
|
+
}
|
|
1373
|
+
function resolveLocalDocumentFile(root, path, from = root) {
|
|
1374
|
+
if (typeof path !== "string" || !path || /^(?:[a-z][a-z\d+.-]*:|\/|\\)/iu.test(path))
|
|
1375
|
+
throw new Error(`Expected a local relative asset: ${path}`);
|
|
1376
|
+
const candidate = resolve2(from, path);
|
|
1377
|
+
const inside = (file) => {
|
|
1378
|
+
const r = relative(root, file);
|
|
1379
|
+
return r !== ".." && !r.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(r);
|
|
1380
|
+
};
|
|
1381
|
+
if (!inside(candidate)) throw new Error(`Asset escapes design root: ${path}`);
|
|
1382
|
+
const actual = realpathSync(candidate);
|
|
1383
|
+
if (!inside(actual) || !statSync(actual).isFile())
|
|
1384
|
+
throw new Error(`Asset is not a contained regular file: ${path}`);
|
|
1385
|
+
return actual;
|
|
1386
|
+
}
|
|
1387
|
+
function bundleLocalDocument({
|
|
1388
|
+
root: inputRoot,
|
|
1389
|
+
source,
|
|
1390
|
+
sharedStyles = [],
|
|
1391
|
+
screenId,
|
|
1392
|
+
maxBytes = 10 * 1024 * 1024,
|
|
1393
|
+
readSource,
|
|
1394
|
+
passive = false
|
|
1395
|
+
}) {
|
|
1396
|
+
const root = realpathSync(inputRoot);
|
|
1397
|
+
const files = /* @__PURE__ */ new Map();
|
|
1398
|
+
const mediaStack = /* @__PURE__ */ new Set();
|
|
1399
|
+
let svgDepth = 0;
|
|
1400
|
+
let cssExpansionBytes = 0;
|
|
1401
|
+
let cssExpansions = 0;
|
|
1402
|
+
let bytes = 0;
|
|
1403
|
+
function read(path, from = root) {
|
|
1404
|
+
const checked = readSource?.(path, from);
|
|
1405
|
+
const file = checked?.file ?? resolveLocalDocumentFile(root, path, from);
|
|
1406
|
+
if (!files.has(file)) {
|
|
1407
|
+
const value = checked?.value ?? readFileSync2(file);
|
|
1408
|
+
bytes += value.length;
|
|
1409
|
+
if (bytes > maxBytes || files.size >= 1e3)
|
|
1410
|
+
throw new Error("Design source exceeds the local asset budget.");
|
|
1411
|
+
files.set(file, value);
|
|
1412
|
+
}
|
|
1413
|
+
return { file, value: files.get(file) };
|
|
1414
|
+
}
|
|
1415
|
+
function asset(ref, from) {
|
|
1416
|
+
if (/^#/iu.test(ref)) return ref;
|
|
1417
|
+
if (/^data:/iu.test(ref)) {
|
|
1418
|
+
if (!passive) return ref;
|
|
1419
|
+
const match = /^data:([^;,]+)(;base64)?,([\s\S]*)$/iu.exec(ref);
|
|
1420
|
+
if (!match || !Object.values(MIME).includes(match[1].toLowerCase()))
|
|
1421
|
+
throw new Error("Passive design media must be a supported image or font.");
|
|
1422
|
+
if (match[1].toLowerCase() !== "image/svg+xml") return ref;
|
|
1423
|
+
const svg = match[2] ? Buffer.from(match[3], "base64").toString("utf8") : decodeURIComponent(match[3]);
|
|
1424
|
+
return `data:image/svg+xml;base64,${Buffer.from(passiveSvg(svg, from)).toString("base64")}`;
|
|
1425
|
+
}
|
|
1426
|
+
const fragmentIndex = ref.indexOf("#");
|
|
1427
|
+
const fragment = fragmentIndex < 0 ? "" : ref.slice(fragmentIndex);
|
|
1428
|
+
const { file, value } = read(fragmentIndex < 0 ? ref : ref.slice(0, fragmentIndex), from);
|
|
1429
|
+
if (!MIME[extname(file).toLowerCase()]) throw new Error(`Unsupported local media: ${ref}`);
|
|
1430
|
+
let payload = value;
|
|
1431
|
+
if (passive && extname(file).toLowerCase() === ".svg") {
|
|
1432
|
+
if (mediaStack.has(file) || mediaStack.size >= 16)
|
|
1433
|
+
throw new Error("Circular or excessively nested SVG media cannot be published.");
|
|
1434
|
+
mediaStack.add(file);
|
|
1435
|
+
try {
|
|
1436
|
+
payload = Buffer.from(passiveSvg(value.toString("utf8"), dirname2(file)));
|
|
1437
|
+
} finally {
|
|
1438
|
+
mediaStack.delete(file);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return `data:${MIME[extname(file).toLowerCase()]};base64,${payload.toString("base64")}${fragment}`;
|
|
1442
|
+
}
|
|
1443
|
+
function passiveSvg(value, from) {
|
|
1444
|
+
if (++svgDepth > 16) throw new Error("Excessively nested SVG media cannot be published.");
|
|
1445
|
+
try {
|
|
1446
|
+
const fragment = parseFragment(value);
|
|
1447
|
+
if (!fragment.childNodes.some((node) => node.tagName === "svg"))
|
|
1448
|
+
throw new Error("SVG media must contain an SVG drawing.");
|
|
1449
|
+
const queue2 = [...children(fragment)];
|
|
1450
|
+
while (queue2.length) {
|
|
1451
|
+
const node = queue2.shift();
|
|
1452
|
+
if (["script", "foreignObject", "iframe", "object", "embed"].includes(node.tagName)) {
|
|
1453
|
+
node.parentNode.childNodes = node.parentNode.childNodes.filter((child) => child !== node);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
node.attrs = (node.attrs ?? []).filter((item2) => !/^on/iu.test(item2.name));
|
|
1457
|
+
if (node.tagName === "style") setText(node, css(text(node), from));
|
|
1458
|
+
for (const item2 of node.attrs) {
|
|
1459
|
+
if (item2.name === "style") item2.value = css(item2.value, from);
|
|
1460
|
+
if (["href", "src"].includes(item2.name)) item2.value = asset(item2.value, from);
|
|
1461
|
+
}
|
|
1462
|
+
queue2.unshift(...children(node));
|
|
1463
|
+
}
|
|
1464
|
+
return serialize(fragment);
|
|
1465
|
+
} finally {
|
|
1466
|
+
svgDepth--;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function replaceCss(value, pattern, replacement) {
|
|
1470
|
+
const parts = [];
|
|
1471
|
+
let offset = 0, outputBytes = 0;
|
|
1472
|
+
function append(part) {
|
|
1473
|
+
const size = Buffer.byteLength(part);
|
|
1474
|
+
outputBytes += size;
|
|
1475
|
+
cssExpansionBytes += size;
|
|
1476
|
+
if (outputBytes > maxBytes || cssExpansionBytes > maxBytes * 8)
|
|
1477
|
+
throw new Error("Stylesheet expansion exceeds the local asset budget.");
|
|
1478
|
+
parts.push(part);
|
|
1479
|
+
}
|
|
1480
|
+
for (const match of value.matchAll(pattern)) {
|
|
1481
|
+
append(value.slice(offset, match.index));
|
|
1482
|
+
append(replacement(...match));
|
|
1483
|
+
offset = match.index + match[0].length;
|
|
1484
|
+
}
|
|
1485
|
+
append(value.slice(offset));
|
|
1486
|
+
return parts.join("");
|
|
1487
|
+
}
|
|
1488
|
+
function css(value, from, stack = /* @__PURE__ */ new Set()) {
|
|
1489
|
+
if (++cssExpansions > 4096)
|
|
1490
|
+
throw new Error("Stylesheet expansion complexity exceeds the local asset budget.");
|
|
1491
|
+
let result = replaceCss(
|
|
1492
|
+
value,
|
|
1493
|
+
/@import\s+(?:url\(\s*)?["']([^"']+)["']\s*\)?\s*([^;]*);/giu,
|
|
1494
|
+
(_all, ref, media) => {
|
|
1495
|
+
const next = read(ref, from);
|
|
1496
|
+
if (stack.has(next.file)) throw new Error(`Circular stylesheet import: ${ref}`);
|
|
1497
|
+
const expanded = css(
|
|
1498
|
+
next.value.toString("utf8"),
|
|
1499
|
+
dirname2(next.file),
|
|
1500
|
+
/* @__PURE__ */ new Set([...stack, next.file])
|
|
1501
|
+
);
|
|
1502
|
+
return media.trim() ? `@media ${media.trim()}{${expanded}}` : expanded;
|
|
1503
|
+
}
|
|
1504
|
+
);
|
|
1505
|
+
if (/@import\b/iu.test(result)) throw new Error("Use quoted local stylesheet imports.");
|
|
1506
|
+
result = replaceCss(
|
|
1507
|
+
result,
|
|
1508
|
+
/url\(\s*(["']?)(.*?)\1\s*\)/giu,
|
|
1509
|
+
(_all, _quote, ref) => `url("${asset(ref, from)}")`
|
|
1510
|
+
);
|
|
1511
|
+
return replaceCss(result, /<\/style/giu, () => "<\\/style");
|
|
1512
|
+
}
|
|
1513
|
+
function js(value, label) {
|
|
1514
|
+
try {
|
|
1515
|
+
new Script(value, { filename: label });
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
`Use compiled, self-contained browser JavaScript in ${label}: ${error.message}`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
return value.replace(/<\/script/giu, "<\\/script");
|
|
1522
|
+
}
|
|
1523
|
+
const input = read(source.html);
|
|
1524
|
+
const document2 = parse(input.value.toString("utf8"));
|
|
1525
|
+
let head, body;
|
|
1526
|
+
const deferredScripts = [];
|
|
1527
|
+
const queue = [...children(document2)];
|
|
1528
|
+
while (queue.length) {
|
|
1529
|
+
const node = queue.shift();
|
|
1530
|
+
if (node.tagName === "head") head = node;
|
|
1531
|
+
if (node.tagName === "body") body = node;
|
|
1532
|
+
if (["base", "iframe", "frame", "frameset", "object", "embed"].includes(node.tagName))
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
`Unsupported <${node.tagName}> in a local design. Use local controls and button handlers.`
|
|
1535
|
+
);
|
|
1536
|
+
if (node.tagName === "meta" && attr(node, "http-equiv")?.value.toLowerCase() === "refresh")
|
|
1537
|
+
throw new Error("Design screens cannot redirect.");
|
|
1538
|
+
if (node.tagName === "style") setText(node, css(text(node), dirname2(input.file)));
|
|
1539
|
+
if (node.tagName === "link" && attr(node, "rel")?.value.toLowerCase().split(/\s+/u).includes("stylesheet")) {
|
|
1540
|
+
const linked = read(attr(node, "href")?.value, dirname2(input.file));
|
|
1541
|
+
node.tagName = "style";
|
|
1542
|
+
node.nodeName = "style";
|
|
1543
|
+
node.attrs = [];
|
|
1544
|
+
setText(
|
|
1545
|
+
node,
|
|
1546
|
+
css(linked.value.toString("utf8"), dirname2(linked.file), /* @__PURE__ */ new Set([linked.file]))
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (node.tagName === "script") {
|
|
1550
|
+
const src = attr(node, "src");
|
|
1551
|
+
if (passive) {
|
|
1552
|
+
if (src) read(src.value, dirname2(input.file));
|
|
1553
|
+
node.parentNode.childNodes = node.parentNode.childNodes.filter((child) => child !== node);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
const type = attr(node, "type")?.value.trim().toLowerCase();
|
|
1557
|
+
if (type === "module")
|
|
1558
|
+
throw new Error("Compile module scripts before using them in a portable design.");
|
|
1559
|
+
if (!type || /(?:javascript|ecmascript)/u.test(type)) {
|
|
1560
|
+
const linked = src ? read(src.value, dirname2(input.file)) : null;
|
|
1561
|
+
setText(
|
|
1562
|
+
node,
|
|
1563
|
+
js(linked ? linked.value.toString("utf8") : text(node), linked?.file ?? input.file)
|
|
1564
|
+
);
|
|
1565
|
+
if (src && attr(node, "defer")) {
|
|
1566
|
+
deferredScripts.push(node);
|
|
1567
|
+
node.parentNode.childNodes = node.parentNode.childNodes.filter((child) => child !== node);
|
|
1568
|
+
}
|
|
1569
|
+
node.attrs = (node.attrs ?? []).filter(
|
|
1570
|
+
(item2) => !["src", "async", "defer"].includes(item2.name)
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (passive) node.attrs = (node.attrs ?? []).filter((item2) => !/^on/iu.test(item2.name));
|
|
1575
|
+
for (const item2 of node.attrs ?? []) {
|
|
1576
|
+
if (item2.name === "style") item2.value = css(item2.value, dirname2(input.file));
|
|
1577
|
+
if (["src", "poster"].includes(item2.name))
|
|
1578
|
+
item2.value = asset(item2.value, dirname2(input.file));
|
|
1579
|
+
if (item2.name === "srcset")
|
|
1580
|
+
throw new Error("Use a local src and responsive CSS for portable design images.");
|
|
1581
|
+
if (["action", "formaction", "target", "formtarget"].includes(item2.name) && item2.value.trim())
|
|
1582
|
+
throw new Error("Design forms must use local submit handlers without navigation targets.");
|
|
1583
|
+
if (item2.name === "href" && !item2.value.startsWith("#")) {
|
|
1584
|
+
if (node.tagName === "link" || node.namespaceURI === "http://www.w3.org/2000/svg")
|
|
1585
|
+
item2.value = asset(item2.value, dirname2(input.file));
|
|
1586
|
+
else if (node.tagName === "a")
|
|
1587
|
+
throw new Error('Use data-design-navigate="screen-id" for prototype navigation.');
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
queue.unshift(...children(node));
|
|
1591
|
+
}
|
|
1592
|
+
const prepend = [];
|
|
1593
|
+
for (const path of [...sharedStyles, ...source.styles ?? []]) {
|
|
1594
|
+
const linked = read(path);
|
|
1595
|
+
prepend.push(
|
|
1596
|
+
element(
|
|
1597
|
+
"style",
|
|
1598
|
+
css(linked.value.toString("utf8"), dirname2(linked.file), /* @__PURE__ */ new Set([linked.file]))
|
|
1599
|
+
)
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
head.childNodes = [...prepend, ...head.childNodes];
|
|
1603
|
+
for (const node of prepend) node.parentNode = head;
|
|
1604
|
+
for (const path of source.scripts ?? []) {
|
|
1605
|
+
const linked = read(path);
|
|
1606
|
+
if (passive) continue;
|
|
1607
|
+
const node = element("script", js(linked.value.toString("utf8"), path));
|
|
1608
|
+
node.parentNode = body;
|
|
1609
|
+
body.childNodes.push(node);
|
|
1610
|
+
}
|
|
1611
|
+
for (const node of deferredScripts) {
|
|
1612
|
+
node.parentNode = body;
|
|
1613
|
+
body.childNodes.push(node);
|
|
1614
|
+
}
|
|
1615
|
+
body.attrs.push({ name: "data-planr-screen", value: screenId });
|
|
1616
|
+
const html = serialize(document2);
|
|
1617
|
+
if (Buffer.byteLength(html) > maxBytes)
|
|
1618
|
+
throw new Error("Bundled design exceeds the output budget.");
|
|
1619
|
+
return {
|
|
1620
|
+
html,
|
|
1621
|
+
files: [...files.keys()].map((path) => relative(root, path)),
|
|
1622
|
+
sourceDigests: Object.fromEntries(
|
|
1623
|
+
[...files].map(([path, value]) => [
|
|
1624
|
+
relative(root, path),
|
|
1625
|
+
createHash("sha256").update(value).digest("hex")
|
|
1626
|
+
])
|
|
1627
|
+
),
|
|
1628
|
+
inputBytes: bytes,
|
|
1629
|
+
bytes: Buffer.byteLength(html)
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// packages/protocol/src/design-contracts.mjs
|
|
1634
|
+
var DESIGN_DOCUMENT_VERSION = "1.0.0";
|
|
1635
|
+
var freeze = (value) => {
|
|
1636
|
+
if (value && typeof value === "object") {
|
|
1637
|
+
for (const nested of Object.values(value)) freeze(nested);
|
|
1638
|
+
Object.freeze(value);
|
|
1639
|
+
}
|
|
1640
|
+
return value;
|
|
1641
|
+
};
|
|
1642
|
+
var DESIGN_DOCUMENT_SCHEMA = freeze({
|
|
1643
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1644
|
+
$id: "https://openplanr.dev/schemas/v1.9.0/design-document.schema.json",
|
|
1645
|
+
"x-openplanr-contract": { id: "design-document", version: "1.9.0" },
|
|
1646
|
+
title: "OpenPlanr authored design document",
|
|
1647
|
+
type: "object",
|
|
1648
|
+
additionalProperties: false,
|
|
1649
|
+
required: [
|
|
1650
|
+
"kind",
|
|
1651
|
+
"schemaVersion",
|
|
1652
|
+
"id",
|
|
1653
|
+
"title",
|
|
1654
|
+
"brief",
|
|
1655
|
+
"frames",
|
|
1656
|
+
"screens",
|
|
1657
|
+
"screenOrder",
|
|
1658
|
+
"variants",
|
|
1659
|
+
"selectedVariant",
|
|
1660
|
+
"defaultView"
|
|
1661
|
+
],
|
|
1662
|
+
properties: {
|
|
1663
|
+
kind: { const: "openplanr-design-document" },
|
|
1664
|
+
schemaVersion: { const: DESIGN_DOCUMENT_VERSION },
|
|
1665
|
+
id: { $ref: "#/$defs/id" },
|
|
1666
|
+
title: { $ref: "#/$defs/text" },
|
|
1667
|
+
brief: {
|
|
1668
|
+
type: "object",
|
|
1669
|
+
additionalProperties: false,
|
|
1670
|
+
required: ["text", "source", "provenance"],
|
|
1671
|
+
properties: {
|
|
1672
|
+
text: { $ref: "#/$defs/text" },
|
|
1673
|
+
source: { enum: ["spec", "png", "describe"] },
|
|
1674
|
+
provenance: { enum: ["spec", "inferred"] },
|
|
1675
|
+
references: { type: "array", items: { $ref: "#/$defs/text" }, uniqueItems: true }
|
|
1676
|
+
}
|
|
1677
|
+
},
|
|
1678
|
+
designSystem: {
|
|
1679
|
+
type: "object",
|
|
1680
|
+
additionalProperties: false,
|
|
1681
|
+
properties: {
|
|
1682
|
+
path: { $ref: "#/$defs/localPath" },
|
|
1683
|
+
tokens: { $ref: "#/$defs/localPath" },
|
|
1684
|
+
spacing: {
|
|
1685
|
+
type: "array",
|
|
1686
|
+
minItems: 1,
|
|
1687
|
+
uniqueItems: true,
|
|
1688
|
+
items: { type: "number", minimum: 0, maximum: 16384 }
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
assets: { $ref: "#/$defs/paths" },
|
|
1693
|
+
frames: {
|
|
1694
|
+
type: "array",
|
|
1695
|
+
minItems: 1,
|
|
1696
|
+
items: {
|
|
1697
|
+
type: "object",
|
|
1698
|
+
additionalProperties: false,
|
|
1699
|
+
required: ["id", "label", "width", "height"],
|
|
1700
|
+
properties: {
|
|
1701
|
+
id: { $ref: "#/$defs/id" },
|
|
1702
|
+
label: { $ref: "#/$defs/text" },
|
|
1703
|
+
width: { type: "integer", minimum: 1, maximum: 16384 },
|
|
1704
|
+
height: { type: "integer", minimum: 1, maximum: 16384 }
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
screens: {
|
|
1709
|
+
type: "array",
|
|
1710
|
+
minItems: 1,
|
|
1711
|
+
items: {
|
|
1712
|
+
type: "object",
|
|
1713
|
+
additionalProperties: false,
|
|
1714
|
+
required: ["id", "title", "source"],
|
|
1715
|
+
properties: {
|
|
1716
|
+
id: { $ref: "#/$defs/id" },
|
|
1717
|
+
title: { $ref: "#/$defs/text" },
|
|
1718
|
+
description: { $ref: "#/$defs/text" },
|
|
1719
|
+
source: { $ref: "#/$defs/source" },
|
|
1720
|
+
anchors: { $ref: "#/$defs/ids" }
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
screenOrder: { type: "array", minItems: 1, items: { $ref: "#/$defs/id" }, uniqueItems: true },
|
|
1725
|
+
flows: {
|
|
1726
|
+
type: "array",
|
|
1727
|
+
items: {
|
|
1728
|
+
type: "object",
|
|
1729
|
+
additionalProperties: false,
|
|
1730
|
+
required: ["id", "title", "screens"],
|
|
1731
|
+
properties: {
|
|
1732
|
+
id: { $ref: "#/$defs/id" },
|
|
1733
|
+
title: { $ref: "#/$defs/text" },
|
|
1734
|
+
screens: { type: "array", minItems: 1, items: { $ref: "#/$defs/id" } }
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
variants: {
|
|
1739
|
+
type: "array",
|
|
1740
|
+
minItems: 1,
|
|
1741
|
+
items: {
|
|
1742
|
+
type: "object",
|
|
1743
|
+
additionalProperties: false,
|
|
1744
|
+
required: ["id", "label", "status"],
|
|
1745
|
+
properties: {
|
|
1746
|
+
id: { $ref: "#/$defs/id" },
|
|
1747
|
+
label: { $ref: "#/$defs/text" },
|
|
1748
|
+
description: { $ref: "#/$defs/text" },
|
|
1749
|
+
status: { enum: ["ready", "failed"] },
|
|
1750
|
+
issue: { $ref: "#/$defs/text" },
|
|
1751
|
+
sources: { type: "object", additionalProperties: { $ref: "#/$defs/source" } }
|
|
1752
|
+
},
|
|
1753
|
+
if: { properties: { status: { const: "failed" } } },
|
|
1754
|
+
then: { required: ["issue"] }
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
selectedVariant: { $ref: "#/$defs/id" },
|
|
1758
|
+
defaultView: { enum: ["canvas", "prototype", "walkthrough"] }
|
|
1759
|
+
},
|
|
1760
|
+
$defs: {
|
|
1761
|
+
id: { type: "string", minLength: 1, maxLength: 128, pattern: "^[A-Za-z][A-Za-z0-9_-]*$" },
|
|
1762
|
+
text: { type: "string", minLength: 1, pattern: "\\S" },
|
|
1763
|
+
ids: { type: "array", items: { $ref: "#/$defs/id" }, uniqueItems: true },
|
|
1764
|
+
localPath: {
|
|
1765
|
+
type: "string",
|
|
1766
|
+
minLength: 1,
|
|
1767
|
+
description: "Path relative to the authored document directory. No URL, traversal, encoded path, query or fragment.",
|
|
1768
|
+
pattern: "^(?!/)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*//)(?!.*[/ ]$)[^\\\\:\\u0000-\\u001F%?#]+$"
|
|
1769
|
+
},
|
|
1770
|
+
paths: { type: "array", items: { $ref: "#/$defs/localPath" }, uniqueItems: true },
|
|
1771
|
+
source: {
|
|
1772
|
+
type: "object",
|
|
1773
|
+
additionalProperties: false,
|
|
1774
|
+
required: ["html"],
|
|
1775
|
+
properties: {
|
|
1776
|
+
html: { $ref: "#/$defs/localPath" },
|
|
1777
|
+
styles: { $ref: "#/$defs/paths" },
|
|
1778
|
+
scripts: { $ref: "#/$defs/paths" }
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
function validateDesignDocument(value) {
|
|
1784
|
+
const errors = validateJson(value, DESIGN_DOCUMENT_SCHEMA).map(
|
|
1785
|
+
({ path, detail }) => `${path}: ${detail}`
|
|
1786
|
+
);
|
|
1787
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
1788
|
+
for (const field of ["frames", "screens", "flows", "variants"]) {
|
|
1789
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1790
|
+
for (const [index, item2] of (value[field] ?? []).entries()) {
|
|
1791
|
+
if (seen.has(item2.id))
|
|
1792
|
+
errors.push(`$.${field}[${index}].id: duplicate identity '${item2.id}'`);
|
|
1793
|
+
seen.add(item2.id);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
const screenIds = new Set(value.screens.map(({ id: id3 }) => id3));
|
|
1797
|
+
const orderedIds = new Set(value.screenOrder);
|
|
1798
|
+
for (const id3 of value.screenOrder) {
|
|
1799
|
+
if (!screenIds.has(id3)) errors.push(`$.screenOrder: unknown screen '${id3}'`);
|
|
1800
|
+
}
|
|
1801
|
+
for (const id3 of screenIds) {
|
|
1802
|
+
if (!orderedIds.has(id3)) errors.push(`$.screenOrder: missing screen '${id3}'`);
|
|
1803
|
+
}
|
|
1804
|
+
for (const [index, flow] of (value.flows ?? []).entries()) {
|
|
1805
|
+
for (const id3 of flow.screens) {
|
|
1806
|
+
if (!screenIds.has(id3)) errors.push(`$.flows[${index}].screens: unknown screen '${id3}'`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
for (const [index, variant] of value.variants.entries()) {
|
|
1810
|
+
for (const id3 of Object.keys(variant.sources ?? {})) {
|
|
1811
|
+
if (!screenIds.has(id3)) errors.push(`$.variants[${index}].sources: unknown screen '${id3}'`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const selected = value.variants.find(({ id: id3 }) => id3 === value.selectedVariant);
|
|
1815
|
+
if (!selected) errors.push(`$.selectedVariant: unknown variant '${value.selectedVariant}'`);
|
|
1816
|
+
else if (selected.status !== "ready")
|
|
1817
|
+
errors.push("$.selectedVariant: selected variant must be ready");
|
|
1818
|
+
for (const [index, spacing] of (value.designSystem?.spacing ?? []).entries()) {
|
|
1819
|
+
if (!Number.isFinite(spacing))
|
|
1820
|
+
errors.push(`$.designSystem.spacing[${index}]: expected a finite spacing value`);
|
|
1821
|
+
}
|
|
1822
|
+
return { ok: errors.length === 0, errors };
|
|
1823
|
+
}
|
|
1824
|
+
function assertDesignDocument(value) {
|
|
1825
|
+
const result = validateDesignDocument(value);
|
|
1826
|
+
if (!result.ok) throw new TypeError(`Invalid design document:
|
|
1827
|
+
${result.errors.join("\n")}`);
|
|
1828
|
+
return value;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// packages/design/lib/design/context.mjs
|
|
1832
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1833
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
1834
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
|
|
1835
|
+
|
|
1836
|
+
// packages/protocol/src/canonical-json.mjs
|
|
1837
|
+
var hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
1838
|
+
function assertUnicodeScalarString(value, path) {
|
|
1839
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1840
|
+
const code = value.charCodeAt(index);
|
|
1841
|
+
if (code >= 55296 && code <= 56319) {
|
|
1842
|
+
const next = value.charCodeAt(index + 1);
|
|
1843
|
+
if (!(next >= 56320 && next <= 57343)) {
|
|
1844
|
+
throw new TypeError(`JCS cannot canonicalize a lone high surrogate at ${path}.`);
|
|
1845
|
+
}
|
|
1846
|
+
index += 1;
|
|
1847
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
1848
|
+
throw new TypeError(`JCS cannot canonicalize a lone low surrogate at ${path}.`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function serialize2(value, path, seen) {
|
|
1853
|
+
if (value === null) return "null";
|
|
1854
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1855
|
+
if (typeof value === "string") {
|
|
1856
|
+
assertUnicodeScalarString(value, path);
|
|
1857
|
+
return JSON.stringify(value);
|
|
1858
|
+
}
|
|
1859
|
+
if (typeof value === "number") {
|
|
1860
|
+
if (!Number.isFinite(value)) throw new TypeError(`JCS requires a finite number at ${path}.`);
|
|
1861
|
+
return JSON.stringify(value);
|
|
1862
|
+
}
|
|
1863
|
+
if (typeof value !== "object")
|
|
1864
|
+
throw new TypeError(`JCS cannot canonicalize ${typeof value} at ${path}.`);
|
|
1865
|
+
if (seen.has(value)) throw new TypeError(`JCS cannot canonicalize a cycle at ${path}.`);
|
|
1866
|
+
seen.add(value);
|
|
1867
|
+
try {
|
|
1868
|
+
if (Array.isArray(value)) {
|
|
1869
|
+
const entries2 = [];
|
|
1870
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1871
|
+
if (!hasOwn(value, index))
|
|
1872
|
+
throw new TypeError(`JCS cannot canonicalize a sparse array at ${path}[${index}].`);
|
|
1873
|
+
entries2.push(serialize2(value[index], `${path}[${index}]`, seen));
|
|
1874
|
+
}
|
|
1875
|
+
return `[${entries2.join(",")}]`;
|
|
1876
|
+
}
|
|
1877
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1878
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1879
|
+
throw new TypeError(`JCS requires a plain JSON object at ${path}.`);
|
|
1880
|
+
}
|
|
1881
|
+
const entries = Object.keys(value).sort().map((key) => {
|
|
1882
|
+
assertUnicodeScalarString(key, `${path} key`);
|
|
1883
|
+
return `${JSON.stringify(key)}:${serialize2(value[key], `${path}.${key}`, seen)}`;
|
|
1884
|
+
});
|
|
1885
|
+
return `{${entries.join(",")}}`;
|
|
1886
|
+
} finally {
|
|
1887
|
+
seen.delete(value);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
function canonicalizeJson(value) {
|
|
1891
|
+
return serialize2(value, "$", /* @__PURE__ */ new Set());
|
|
1892
|
+
}
|
|
1893
|
+
var SHA256_K = /* @__PURE__ */ new Uint32Array([
|
|
1894
|
+
1116352408,
|
|
1895
|
+
1899447441,
|
|
1896
|
+
3049323471,
|
|
1897
|
+
3921009573,
|
|
1898
|
+
961987163,
|
|
1899
|
+
1508970993,
|
|
1900
|
+
2453635748,
|
|
1901
|
+
2870763221,
|
|
1902
|
+
3624381080,
|
|
1903
|
+
310598401,
|
|
1904
|
+
607225278,
|
|
1905
|
+
1426881987,
|
|
1906
|
+
1925078388,
|
|
1907
|
+
2162078206,
|
|
1908
|
+
2614888103,
|
|
1909
|
+
3248222580,
|
|
1910
|
+
3835390401,
|
|
1911
|
+
4022224774,
|
|
1912
|
+
264347078,
|
|
1913
|
+
604807628,
|
|
1914
|
+
770255983,
|
|
1915
|
+
1249150122,
|
|
1916
|
+
1555081692,
|
|
1917
|
+
1996064986,
|
|
1918
|
+
2554220882,
|
|
1919
|
+
2821834349,
|
|
1920
|
+
2952996808,
|
|
1921
|
+
3210313671,
|
|
1922
|
+
3336571891,
|
|
1923
|
+
3584528711,
|
|
1924
|
+
113926993,
|
|
1925
|
+
338241895,
|
|
1926
|
+
666307205,
|
|
1927
|
+
773529912,
|
|
1928
|
+
1294757372,
|
|
1929
|
+
1396182291,
|
|
1930
|
+
1695183700,
|
|
1931
|
+
1986661051,
|
|
1932
|
+
2177026350,
|
|
1933
|
+
2456956037,
|
|
1934
|
+
2730485921,
|
|
1935
|
+
2820302411,
|
|
1936
|
+
3259730800,
|
|
1937
|
+
3345764771,
|
|
1938
|
+
3516065817,
|
|
1939
|
+
3600352804,
|
|
1940
|
+
4094571909,
|
|
1941
|
+
275423344,
|
|
1942
|
+
430227734,
|
|
1943
|
+
506948616,
|
|
1944
|
+
659060556,
|
|
1945
|
+
883997877,
|
|
1946
|
+
958139571,
|
|
1947
|
+
1322822218,
|
|
1948
|
+
1537002063,
|
|
1949
|
+
1747873779,
|
|
1950
|
+
1955562222,
|
|
1951
|
+
2024104815,
|
|
1952
|
+
2227730452,
|
|
1953
|
+
2361852424,
|
|
1954
|
+
2428436474,
|
|
1955
|
+
2756734187,
|
|
1956
|
+
3204031479,
|
|
1957
|
+
3329325298
|
|
1958
|
+
]);
|
|
1959
|
+
var rotr = (value, bits) => value >>> bits | value << 32 - bits;
|
|
1960
|
+
function sha256Hex(value) {
|
|
1961
|
+
const candidate = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
1962
|
+
if (!ArrayBuffer.isView(candidate) || Object.prototype.toString.call(candidate) !== "[object Uint8Array]") {
|
|
1963
|
+
throw new TypeError("sha256Hex expects a string or Uint8Array.");
|
|
1964
|
+
}
|
|
1965
|
+
const input = new Uint8Array(candidate.buffer, candidate.byteOffset, candidate.byteLength);
|
|
1966
|
+
const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
|
|
1967
|
+
const bytes = new Uint8Array(paddedLength);
|
|
1968
|
+
bytes.set(input);
|
|
1969
|
+
bytes[input.length] = 128;
|
|
1970
|
+
const view = new DataView(bytes.buffer);
|
|
1971
|
+
const bitLength = BigInt(input.length) * 8n;
|
|
1972
|
+
view.setUint32(paddedLength - 8, Number(bitLength >> 32n & 0xffffffffn));
|
|
1973
|
+
view.setUint32(paddedLength - 4, Number(bitLength & 0xffffffffn));
|
|
1974
|
+
let h0 = 1779033703;
|
|
1975
|
+
let h1 = 3144134277;
|
|
1976
|
+
let h2 = 1013904242;
|
|
1977
|
+
let h3 = 2773480762;
|
|
1978
|
+
let h4 = 1359893119;
|
|
1979
|
+
let h5 = 2600822924;
|
|
1980
|
+
let h6 = 528734635;
|
|
1981
|
+
let h7 = 1541459225;
|
|
1982
|
+
const words = new Uint32Array(64);
|
|
1983
|
+
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
1984
|
+
for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4);
|
|
1985
|
+
for (let index = 16; index < 64; index += 1) {
|
|
1986
|
+
const s0 = rotr(words[index - 15], 7) ^ rotr(words[index - 15], 18) ^ words[index - 15] >>> 3;
|
|
1987
|
+
const s1 = rotr(words[index - 2], 17) ^ rotr(words[index - 2], 19) ^ words[index - 2] >>> 10;
|
|
1988
|
+
words[index] = words[index - 16] + s0 + words[index - 7] + s1 >>> 0;
|
|
1989
|
+
}
|
|
1990
|
+
let a = h0;
|
|
1991
|
+
let b = h1;
|
|
1992
|
+
let c = h2;
|
|
1993
|
+
let d = h3;
|
|
1994
|
+
let e = h4;
|
|
1995
|
+
let f = h5;
|
|
1996
|
+
let g = h6;
|
|
1997
|
+
let h = h7;
|
|
1998
|
+
for (let index = 0; index < 64; index += 1) {
|
|
1999
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
2000
|
+
const choice = e & f ^ ~e & g;
|
|
2001
|
+
const temp1 = h + s1 + choice + SHA256_K[index] + words[index] >>> 0;
|
|
2002
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
2003
|
+
const majority = a & b ^ a & c ^ b & c;
|
|
2004
|
+
const temp2 = s0 + majority >>> 0;
|
|
2005
|
+
h = g;
|
|
2006
|
+
g = f;
|
|
2007
|
+
f = e;
|
|
2008
|
+
e = d + temp1 >>> 0;
|
|
2009
|
+
d = c;
|
|
2010
|
+
c = b;
|
|
2011
|
+
b = a;
|
|
2012
|
+
a = temp1 + temp2 >>> 0;
|
|
2013
|
+
}
|
|
2014
|
+
h0 = h0 + a >>> 0;
|
|
2015
|
+
h1 = h1 + b >>> 0;
|
|
2016
|
+
h2 = h2 + c >>> 0;
|
|
2017
|
+
h3 = h3 + d >>> 0;
|
|
2018
|
+
h4 = h4 + e >>> 0;
|
|
2019
|
+
h5 = h5 + f >>> 0;
|
|
2020
|
+
h6 = h6 + g >>> 0;
|
|
2021
|
+
h7 = h7 + h >>> 0;
|
|
2022
|
+
}
|
|
2023
|
+
return [h0, h1, h2, h3, h4, h5, h6, h7].map((part) => part.toString(16).padStart(8, "0")).join("");
|
|
2024
|
+
}
|
|
2025
|
+
function deepFreeze(value) {
|
|
2026
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
2027
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
2028
|
+
Object.freeze(value);
|
|
2029
|
+
}
|
|
2030
|
+
return value;
|
|
2031
|
+
}
|
|
2032
|
+
function assertPlainDataAt(value, label, depth, seen) {
|
|
2033
|
+
if (depth > 64) throw new TypeError(`${label} exceeds the maximum nesting depth.`);
|
|
2034
|
+
if (value === null || ["string", "boolean"].includes(typeof value)) return;
|
|
2035
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
2036
|
+
if (typeof value !== "object" || seen.has(value))
|
|
2037
|
+
throw new TypeError(`${label} must be finite, acyclic JSON.`);
|
|
2038
|
+
if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
2039
|
+
throw new TypeError(`${label} must contain only plain JSON objects.`);
|
|
2040
|
+
seen.add(value);
|
|
2041
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
2042
|
+
if (["__proto__", "prototype", "constructor"].includes(key) || !Object.hasOwn(descriptor, "value"))
|
|
2043
|
+
throw new TypeError(`${label} contains a forbidden property.`);
|
|
2044
|
+
assertPlainDataAt(descriptor.value, label, depth + 1, seen);
|
|
2045
|
+
}
|
|
2046
|
+
seen.delete(value);
|
|
2047
|
+
}
|
|
2048
|
+
function assertPlainData(value, label) {
|
|
2049
|
+
assertPlainDataAt(value, label, 0, /* @__PURE__ */ new Set());
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// packages/protocol/src/workspace-contracts.mjs
|
|
2053
|
+
var DESIGN_WORKSPACE_VERSION = "1.0.0";
|
|
2054
|
+
var DESIGN_WORKSPACE_API = "/api/v1/design-workspaces";
|
|
2055
|
+
var DESIGN_WORKSPACE_MAX_BYTES = 5 * 1024 * 1024;
|
|
2056
|
+
var DESIGN_WORKSPACE_MAX_EVENT_BYTES = 256 * 1024;
|
|
2057
|
+
var DESIGN_WORKSPACE_ID_PATTERN = "^[A-Za-z0-9_-]{22,64}$";
|
|
2058
|
+
var id = { type: "string", pattern: DESIGN_WORKSPACE_ID_PATTERN };
|
|
2059
|
+
var digest = { type: "string", pattern: "^[a-f0-9]{64}$" };
|
|
2060
|
+
var b64 = { type: "string", pattern: "^[A-Za-z0-9_-]+$" };
|
|
2061
|
+
var epoch = { type: "integer", minimum: 1, maximum: 2147483647 };
|
|
2062
|
+
var cipherProperties = {
|
|
2063
|
+
iv: { ...b64, minLength: 16, maxLength: 16 },
|
|
2064
|
+
ciphertext: { ...b64, minLength: 22, maxLength: Math.ceil(DESIGN_WORKSPACE_MAX_BYTES * 4 / 3) }
|
|
2065
|
+
};
|
|
2066
|
+
var signature = { ...b64, minLength: 86, maxLength: 86 };
|
|
2067
|
+
var publicKey = {
|
|
2068
|
+
type: "object",
|
|
2069
|
+
additionalProperties: false,
|
|
2070
|
+
required: ["kty", "crv", "x", "y"],
|
|
2071
|
+
properties: {
|
|
2072
|
+
kty: { const: "EC" },
|
|
2073
|
+
crv: { const: "P-256" },
|
|
2074
|
+
x: { ...b64, minLength: 43, maxLength: 43 },
|
|
2075
|
+
y: { ...b64, minLength: 43, maxLength: 43 },
|
|
2076
|
+
ext: { type: "boolean" },
|
|
2077
|
+
key_ops: { type: "array", items: { const: "verify" }, maxItems: 1 }
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
var schema = (name, properties, required = Object.keys(properties)) => ({
|
|
2081
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
2082
|
+
$id: `https://openplanr.dev/schemas/v1.9.0/${name}.schema.json`,
|
|
2083
|
+
"x-openplanr-contract": { id: name, version: "1.9.0" },
|
|
2084
|
+
type: "object",
|
|
2085
|
+
additionalProperties: false,
|
|
2086
|
+
properties,
|
|
2087
|
+
required
|
|
2088
|
+
});
|
|
2089
|
+
var DESIGN_WORKSPACE_REVISION_SCHEMA = schema("design-workspace-revision", {
|
|
2090
|
+
id,
|
|
2091
|
+
epoch,
|
|
2092
|
+
reviewOf: digest,
|
|
2093
|
+
createdAt: { type: "string", format: "date-time" },
|
|
2094
|
+
...cipherProperties,
|
|
2095
|
+
signature
|
|
2096
|
+
});
|
|
2097
|
+
var DESIGN_WORKSPACE_EVENT_SCHEMA = schema("design-workspace-event", {
|
|
2098
|
+
id,
|
|
2099
|
+
revisionId: id,
|
|
2100
|
+
reviewOf: digest,
|
|
2101
|
+
epoch,
|
|
2102
|
+
...cipherProperties,
|
|
2103
|
+
publicKey,
|
|
2104
|
+
signature
|
|
2105
|
+
});
|
|
2106
|
+
var sealed = {
|
|
2107
|
+
type: "object",
|
|
2108
|
+
additionalProperties: false,
|
|
2109
|
+
required: ["iv", "ciphertext"],
|
|
2110
|
+
properties: cipherProperties
|
|
2111
|
+
};
|
|
2112
|
+
var DESIGN_WORKSPACE_CREATE_SCHEMA = schema("design-workspace-create", {
|
|
2113
|
+
schemaVersion: { const: DESIGN_WORKSPACE_VERSION },
|
|
2114
|
+
id,
|
|
2115
|
+
ownerPublicKey: publicKey,
|
|
2116
|
+
ownerAuthHash: digest,
|
|
2117
|
+
reviewerAuthHash: digest,
|
|
2118
|
+
epoch: { const: 1 },
|
|
2119
|
+
keyring: sealed,
|
|
2120
|
+
revision: DESIGN_WORKSPACE_REVISION_SCHEMA,
|
|
2121
|
+
operationId: id,
|
|
2122
|
+
signature
|
|
2123
|
+
});
|
|
2124
|
+
var DESIGN_WORKSPACE_SCHEMA = schema("design-review-workspace", {
|
|
2125
|
+
schemaVersion: { const: DESIGN_WORKSPACE_VERSION },
|
|
2126
|
+
id,
|
|
2127
|
+
version: epoch,
|
|
2128
|
+
epoch,
|
|
2129
|
+
currentRevision: id,
|
|
2130
|
+
commentsPaused: { type: "boolean" },
|
|
2131
|
+
ownerPublicKey: publicKey,
|
|
2132
|
+
keyring: sealed
|
|
2133
|
+
});
|
|
2134
|
+
var designProperties = (
|
|
2135
|
+
/** @type {Record<string, { items: { properties: Record<string, unknown>; required: string[] } }>} */
|
|
2136
|
+
structuredClone(DESIGN_DOCUMENT_SCHEMA.properties)
|
|
2137
|
+
);
|
|
2138
|
+
for (const key of ["kind", "schemaVersion", "brief", "assets", "designSystem"])
|
|
2139
|
+
delete designProperties[key];
|
|
2140
|
+
delete designProperties.screens.items.properties.source;
|
|
2141
|
+
designProperties.screens.items.required = ["id", "title"];
|
|
2142
|
+
delete designProperties.variants.items.properties.sources;
|
|
2143
|
+
var DESIGN_REVIEW_BUNDLE_SCHEMA = {
|
|
2144
|
+
...schema(
|
|
2145
|
+
"design-review-bundle",
|
|
2146
|
+
{
|
|
2147
|
+
kind: { const: "openplanr-design-review-bundle" },
|
|
2148
|
+
schemaVersion: { const: DESIGN_WORKSPACE_VERSION },
|
|
2149
|
+
design: {
|
|
2150
|
+
type: "object",
|
|
2151
|
+
additionalProperties: false,
|
|
2152
|
+
properties: designProperties,
|
|
2153
|
+
required: [
|
|
2154
|
+
"id",
|
|
2155
|
+
"title",
|
|
2156
|
+
"frames",
|
|
2157
|
+
"screens",
|
|
2158
|
+
"screenOrder",
|
|
2159
|
+
"variants",
|
|
2160
|
+
"selectedVariant",
|
|
2161
|
+
"defaultView"
|
|
2162
|
+
]
|
|
2163
|
+
},
|
|
2164
|
+
envelope: { type: "object", required: ["schemaVersion", "artifacts", "viewer"] },
|
|
2165
|
+
entries: {
|
|
2166
|
+
type: "array",
|
|
2167
|
+
minItems: 1,
|
|
2168
|
+
maxItems: 256,
|
|
2169
|
+
items: {
|
|
2170
|
+
type: "object",
|
|
2171
|
+
additionalProperties: false,
|
|
2172
|
+
required: ["artifactId", "screenId", "variantId", "frameId"],
|
|
2173
|
+
properties: Object.fromEntries(
|
|
2174
|
+
["artifactId", "screenId", "variantId", "frameId"].map((key) => [
|
|
2175
|
+
key,
|
|
2176
|
+
{ type: "string", minLength: 1, maxLength: 128 }
|
|
2177
|
+
])
|
|
2178
|
+
)
|
|
2179
|
+
}
|
|
2180
|
+
},
|
|
2181
|
+
state: {
|
|
2182
|
+
type: ["object", "null"],
|
|
2183
|
+
additionalProperties: false,
|
|
2184
|
+
properties: {
|
|
2185
|
+
positions: {
|
|
2186
|
+
type: "object",
|
|
2187
|
+
additionalProperties: {
|
|
2188
|
+
type: "object",
|
|
2189
|
+
additionalProperties: false,
|
|
2190
|
+
required: ["x", "y"],
|
|
2191
|
+
properties: {
|
|
2192
|
+
x: { type: "number", minimum: -1e7, maximum: 1e7 },
|
|
2193
|
+
y: { type: "number", minimum: -1e7, maximum: 1e7 }
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
},
|
|
2199
|
+
revision: { type: "string", minLength: 1, maxLength: 128 },
|
|
2200
|
+
verification: {
|
|
2201
|
+
type: ["object", "null"],
|
|
2202
|
+
additionalProperties: false,
|
|
2203
|
+
properties: { status: { enum: ["verified", "unverified", "failed", "pending"] } }
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
["kind", "schemaVersion", "design", "envelope", "entries", "revision"]
|
|
2207
|
+
),
|
|
2208
|
+
$defs: structuredClone(DESIGN_DOCUMENT_SCHEMA.$defs)
|
|
2209
|
+
};
|
|
2210
|
+
function assertWorkspaceContract(value, contract) {
|
|
2211
|
+
const errors = validateJson(value, contract);
|
|
2212
|
+
if (errors.length)
|
|
2213
|
+
throw new TypeError(
|
|
2214
|
+
`Invalid ${contract["x-openplanr-contract"].id}: ${errors.slice(0, 5).map(({ path, detail }) => `${path}: ${detail}`).join("; ")}`
|
|
2215
|
+
);
|
|
2216
|
+
return value;
|
|
2217
|
+
}
|
|
2218
|
+
var DESIGN_WORKSPACE_SCHEMAS = Object.freeze({
|
|
2219
|
+
"design-review-workspace": DESIGN_WORKSPACE_SCHEMA,
|
|
2220
|
+
"design-workspace-create": DESIGN_WORKSPACE_CREATE_SCHEMA,
|
|
2221
|
+
"design-workspace-revision": DESIGN_WORKSPACE_REVISION_SCHEMA,
|
|
2222
|
+
"design-workspace-event": DESIGN_WORKSPACE_EVENT_SCHEMA,
|
|
2223
|
+
"design-review-bundle": DESIGN_REVIEW_BUNDLE_SCHEMA
|
|
2224
|
+
});
|
|
2225
|
+
|
|
2226
|
+
// packages/protocol/src/review-experience-contracts.mjs
|
|
2227
|
+
var text2 = { type: "string", maxLength: 16384 };
|
|
2228
|
+
var id2 = { type: "string", minLength: 1, maxLength: 128 };
|
|
2229
|
+
var digest2 = { type: "string", pattern: "^[a-f0-9]{64}$" };
|
|
2230
|
+
var texts = { type: "array", maxItems: 256, items: text2 };
|
|
2231
|
+
var closed = (properties, required = Object.keys(properties)) => ({
|
|
2232
|
+
type: "object",
|
|
2233
|
+
additionalProperties: false,
|
|
2234
|
+
properties,
|
|
2235
|
+
required
|
|
2236
|
+
});
|
|
2237
|
+
var list = (items, maxItems = 256) => ({ type: "array", maxItems, items });
|
|
2238
|
+
var schema2 = (name, properties, required) => ({
|
|
2239
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
2240
|
+
$id: `https://openplanr.dev/schemas/v1.10.0/${name}.schema.json`,
|
|
2241
|
+
"x-openplanr-contract": { id: name, version: "1.10.0" },
|
|
2242
|
+
...closed(properties, required)
|
|
2243
|
+
});
|
|
2244
|
+
var DESIGN_REVIEW_CONTEXT_SCHEMA = schema2(
|
|
2245
|
+
"design-review-context",
|
|
2246
|
+
{
|
|
2247
|
+
kind: { const: "openplanr-design-review-context" },
|
|
2248
|
+
schemaVersion: { const: "1.0.0" },
|
|
2249
|
+
designId: id2,
|
|
2250
|
+
brief: closed({ purpose: text2, requests: { ...texts, maxItems: 3 }, audience: text2 }, [
|
|
2251
|
+
"purpose",
|
|
2252
|
+
"requests"
|
|
2253
|
+
]),
|
|
2254
|
+
revisionSummary: text2,
|
|
2255
|
+
implementation: closed({
|
|
2256
|
+
tokens: list(closed({ name: id2, value: text2, description: text2 }, ["name", "value"]), 512),
|
|
2257
|
+
components: list(
|
|
2258
|
+
closed(
|
|
2259
|
+
{
|
|
2260
|
+
id: id2,
|
|
2261
|
+
name: text2,
|
|
2262
|
+
screenIds: list(id2),
|
|
2263
|
+
anchorIds: list(id2),
|
|
2264
|
+
states: list(closed({ name: id2, description: text2 })),
|
|
2265
|
+
notes: text2,
|
|
2266
|
+
responsive: text2,
|
|
2267
|
+
accessibility: text2
|
|
2268
|
+
},
|
|
2269
|
+
["id", "name"]
|
|
2270
|
+
)
|
|
2271
|
+
),
|
|
2272
|
+
responsive: texts,
|
|
2273
|
+
accessibility: texts
|
|
2274
|
+
})
|
|
2275
|
+
},
|
|
2276
|
+
["kind", "schemaVersion", "designId", "brief", "implementation"]
|
|
2277
|
+
);
|
|
2278
|
+
var DESIGN_FINGERPRINT_SCHEMA = closed({
|
|
2279
|
+
screenId: id2,
|
|
2280
|
+
variantId: id2,
|
|
2281
|
+
frameId: id2,
|
|
2282
|
+
contentDigest: digest2,
|
|
2283
|
+
guidanceDigest: digest2
|
|
2284
|
+
});
|
|
2285
|
+
var bundleV11 = {
|
|
2286
|
+
.../** @type {MutableSchema} */
|
|
2287
|
+
structuredClone(DESIGN_REVIEW_BUNDLE_SCHEMA),
|
|
2288
|
+
$id: "https://openplanr.dev/schemas/v1.10.0/design-review-bundle.schema.json",
|
|
2289
|
+
"x-openplanr-contract": { id: "design-review-bundle", version: "1.10.0" }
|
|
2290
|
+
};
|
|
2291
|
+
Object.assign(bundleV11.properties, {
|
|
2292
|
+
schemaVersion: { const: "1.1.0" },
|
|
2293
|
+
reviewContext: DESIGN_REVIEW_CONTEXT_SCHEMA,
|
|
2294
|
+
contextDigest: digest2,
|
|
2295
|
+
fingerprints: list(DESIGN_FINGERPRINT_SCHEMA)
|
|
2296
|
+
});
|
|
2297
|
+
bundleV11.required.push("reviewContext", "contextDigest", "fingerprints");
|
|
2298
|
+
var DESIGN_REVIEW_BUNDLE_V11_SCHEMA = bundleV11;
|
|
2299
|
+
var item = closed(
|
|
2300
|
+
{
|
|
2301
|
+
pinId: id2,
|
|
2302
|
+
reviewId: id2,
|
|
2303
|
+
screenId: id2,
|
|
2304
|
+
revisionId: id2,
|
|
2305
|
+
text: text2,
|
|
2306
|
+
refinement: text2,
|
|
2307
|
+
stale: { type: "boolean" },
|
|
2308
|
+
author: text2,
|
|
2309
|
+
reviewOf: digest2,
|
|
2310
|
+
source: text2
|
|
2311
|
+
},
|
|
2312
|
+
["pinId", "text"]
|
|
2313
|
+
);
|
|
2314
|
+
var DESIGN_HANDOFF_CONTENT_SCHEMA = closed({
|
|
2315
|
+
summary: text2,
|
|
2316
|
+
agreedChanges: list(item, 1e4),
|
|
2317
|
+
openQuestions: list(item, 1e4),
|
|
2318
|
+
deferred: list(item, 1e4),
|
|
2319
|
+
rejected: list(item, 1e4)
|
|
2320
|
+
});
|
|
2321
|
+
var DESIGN_HANDOFF_SCHEMA = schema2(
|
|
2322
|
+
"design-review-handoff",
|
|
2323
|
+
{
|
|
2324
|
+
kind: { const: "openplanr-design-review-handoff" },
|
|
2325
|
+
schemaVersion: { const: "1.0.0" },
|
|
2326
|
+
title: text2,
|
|
2327
|
+
version: { type: "integer", minimum: 1 },
|
|
2328
|
+
status: { enum: ["draft", "approved"] },
|
|
2329
|
+
basis: closed({
|
|
2330
|
+
designId: id2,
|
|
2331
|
+
sourceRevision: digest2,
|
|
2332
|
+
contextDigest: digest2,
|
|
2333
|
+
reviewOf: digest2,
|
|
2334
|
+
selectedVariant: id2,
|
|
2335
|
+
feedbackDigest: digest2,
|
|
2336
|
+
verificationDigest: digest2,
|
|
2337
|
+
feedbackWatermark: { type: "integer", minimum: 0 }
|
|
2338
|
+
}),
|
|
2339
|
+
content: DESIGN_HANDOFF_CONTENT_SCHEMA,
|
|
2340
|
+
contentHash: digest2,
|
|
2341
|
+
markdown: { type: "string", maxLength: 2097152 },
|
|
2342
|
+
affectedScreens: list(id2),
|
|
2343
|
+
verificationGaps: texts,
|
|
2344
|
+
reviewNotes: list(closed({ reviewId: id2, text: text2 }), 1e4),
|
|
2345
|
+
approval: closed({ contentHash: digest2, at: { type: "string", format: "date-time" } })
|
|
2346
|
+
},
|
|
2347
|
+
[
|
|
2348
|
+
"kind",
|
|
2349
|
+
"schemaVersion",
|
|
2350
|
+
"title",
|
|
2351
|
+
"version",
|
|
2352
|
+
"status",
|
|
2353
|
+
"basis",
|
|
2354
|
+
"content",
|
|
2355
|
+
"contentHash",
|
|
2356
|
+
"markdown",
|
|
2357
|
+
"affectedScreens",
|
|
2358
|
+
"verificationGaps",
|
|
2359
|
+
"reviewNotes"
|
|
2360
|
+
]
|
|
2361
|
+
);
|
|
2362
|
+
var DESIGN_REVIEW_METADATA_PAYLOAD_SCHEMA = {
|
|
2363
|
+
...schema2(
|
|
2364
|
+
"design-review-metadata-payload",
|
|
2365
|
+
{
|
|
2366
|
+
schemaVersion: { const: "1.0.0" },
|
|
2367
|
+
kind: { enum: ["category", "disposition"] },
|
|
2368
|
+
author: { ...text2, minLength: 1, maxLength: 160 },
|
|
2369
|
+
reviewOf: digest2,
|
|
2370
|
+
pinId: id2,
|
|
2371
|
+
category: { enum: ["question", "suggestion", "blocker"] },
|
|
2372
|
+
disposition: { enum: ["accepted", "deferred", "rejected"] },
|
|
2373
|
+
reason: text2,
|
|
2374
|
+
updatedAt: { type: "string", format: "date-time" }
|
|
2375
|
+
},
|
|
2376
|
+
["schemaVersion", "kind", "author", "reviewOf", "pinId", "updatedAt"]
|
|
2377
|
+
),
|
|
2378
|
+
allOf: [
|
|
2379
|
+
{
|
|
2380
|
+
if: { properties: { kind: { const: "category" } } },
|
|
2381
|
+
then: { required: ["category"], not: { required: ["disposition"] } }
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
if: { properties: { kind: { const: "disposition" } } },
|
|
2385
|
+
then: { required: ["disposition", "reason"], not: { required: ["category"] } }
|
|
2386
|
+
}
|
|
2387
|
+
]
|
|
2388
|
+
};
|
|
2389
|
+
var payloadV11 = {
|
|
2390
|
+
.../** @type {MutableSchema} */
|
|
2391
|
+
structuredClone(DESIGN_REVIEW_METADATA_PAYLOAD_SCHEMA),
|
|
2392
|
+
$id: "https://openplanr.dev/schemas/v1.11.0/design-review-metadata-payload.schema.json",
|
|
2393
|
+
"x-openplanr-contract": { id: "design-review-metadata-payload", version: "1.11.0" }
|
|
2394
|
+
};
|
|
2395
|
+
Object.assign(payloadV11.properties, {
|
|
2396
|
+
schemaVersion: { const: "1.1.0" },
|
|
2397
|
+
category: { enum: ["question", "suggestion", "change-request", "blocker"] }
|
|
2398
|
+
});
|
|
2399
|
+
var DESIGN_REVIEW_METADATA_PAYLOAD_V11_SCHEMA = payloadV11;
|
|
2400
|
+
function assertReviewExperience(value, contract) {
|
|
2401
|
+
const errors = validateJson(value, contract);
|
|
2402
|
+
if (errors.length)
|
|
2403
|
+
throw new TypeError(
|
|
2404
|
+
`Invalid ${contract["x-openplanr-contract"]?.id ?? "review data"}: ${errors.slice(0, 4).map((item2) => `${item2.path} ${item2.detail}`).join("; ")}`
|
|
2405
|
+
);
|
|
2406
|
+
return value;
|
|
2407
|
+
}
|
|
2408
|
+
function assertDesignReviewMetadata(value) {
|
|
2409
|
+
return assertReviewExperience(
|
|
2410
|
+
value,
|
|
2411
|
+
value?.schemaVersion === "1.1.0" ? DESIGN_REVIEW_METADATA_PAYLOAD_V11_SCHEMA : DESIGN_REVIEW_METADATA_PAYLOAD_SCHEMA
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
function assertDesignReviewBundle(value) {
|
|
2415
|
+
assertReviewExperience(
|
|
2416
|
+
value,
|
|
2417
|
+
value?.schemaVersion === "1.1.0" ? DESIGN_REVIEW_BUNDLE_V11_SCHEMA : DESIGN_REVIEW_BUNDLE_SCHEMA
|
|
2418
|
+
);
|
|
2419
|
+
if (value.schemaVersion === "1.1.0") {
|
|
2420
|
+
if (value.reviewContext.designId !== value.design.id || value.contextDigest !== sha256Hex(canonicalizeJson(value.reviewContext)))
|
|
2421
|
+
throw new TypeError("Review context identity or digest does not match its published design.");
|
|
2422
|
+
const entries = new Set(
|
|
2423
|
+
value.entries.map((entry) => `${entry.screenId}:${entry.variantId}:${entry.frameId}`)
|
|
2424
|
+
);
|
|
2425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2426
|
+
for (const item2 of value.fingerprints) {
|
|
2427
|
+
const key = `${item2.screenId}:${item2.variantId}:${item2.frameId}`;
|
|
2428
|
+
if (!entries.has(key) || seen.has(key))
|
|
2429
|
+
throw new TypeError("Review fingerprints must identify distinct published artboards.");
|
|
2430
|
+
seen.add(key);
|
|
2431
|
+
}
|
|
2432
|
+
if (seen.size && seen.size !== entries.size)
|
|
2433
|
+
throw new TypeError("Review fingerprints must cover every published artboard.");
|
|
2434
|
+
}
|
|
2435
|
+
return value;
|
|
2436
|
+
}
|
|
2437
|
+
var REVIEW_EXPERIENCE_SCHEMAS = Object.freeze({
|
|
2438
|
+
"design-review-context": DESIGN_REVIEW_CONTEXT_SCHEMA,
|
|
2439
|
+
"design-review-bundle": DESIGN_REVIEW_BUNDLE_V11_SCHEMA,
|
|
2440
|
+
"design-review-handoff": DESIGN_HANDOFF_SCHEMA,
|
|
2441
|
+
"design-review-metadata-payload": DESIGN_REVIEW_METADATA_PAYLOAD_SCHEMA
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
// packages/design/lib/design/context.mjs
|
|
2445
|
+
var reviewDigest = (value) => createHash2("sha256").update(canonicalizeJson(value)).digest("hex");
|
|
2446
|
+
function emptyReviewContext(document2) {
|
|
2447
|
+
return {
|
|
2448
|
+
kind: "openplanr-design-review-context",
|
|
2449
|
+
schemaVersion: "1.0.0",
|
|
2450
|
+
designId: document2.id,
|
|
2451
|
+
brief: { purpose: "", requests: [] },
|
|
2452
|
+
implementation: { tokens: [], components: [], responsive: [], accessibility: [] }
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
function loadReviewContext(root, document2, { readSource } = {}) {
|
|
2456
|
+
const file = join3(root, "review-context.json");
|
|
2457
|
+
if (!existsSync2(file)) return emptyReviewContext(document2);
|
|
2458
|
+
const context = JSON.parse(
|
|
2459
|
+
readSource ? readSource("review-context.json", root).value.toString("utf8") : readFileSync3(resolveLocalDocumentFile(root, "review-context.json"), "utf8")
|
|
2460
|
+
);
|
|
2461
|
+
assertReviewExperience(context, DESIGN_REVIEW_CONTEXT_SCHEMA);
|
|
2462
|
+
if (context.designId !== document2.id)
|
|
2463
|
+
throw new Error("Review context belongs to a different design.");
|
|
2464
|
+
const screens = new Set(document2.screenOrder);
|
|
2465
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2466
|
+
for (const component of context.implementation.components) {
|
|
2467
|
+
if (ids.has(component.id)) throw new Error("Review component identities must be unique.");
|
|
2468
|
+
ids.add(component.id);
|
|
2469
|
+
if (component.screenIds?.some((id3) => !screens.has(id3)))
|
|
2470
|
+
throw new Error(`Component ${component.id} references an unknown screen.`);
|
|
2471
|
+
}
|
|
2472
|
+
if (/(?:file:\/\/|\/(?:Users|home|private|tmp|var|etc|opt|Volumes)\/|[A-Za-z]:\\\\|\\\\\\\\)/u.test(
|
|
2473
|
+
JSON.stringify(context)
|
|
2474
|
+
))
|
|
2475
|
+
throw new Error(
|
|
2476
|
+
"Review context contains a local filesystem path. Use share-safe implementation guidance."
|
|
2477
|
+
);
|
|
2478
|
+
return context;
|
|
2479
|
+
}
|
|
2480
|
+
function reviewFingerprints({ document: document2, context, screen, variant, frame, sourceDigests }) {
|
|
2481
|
+
const components = context.implementation.components.filter(
|
|
2482
|
+
(component) => !component.screenIds?.length || component.screenIds.includes(screen.id)
|
|
2483
|
+
);
|
|
2484
|
+
return {
|
|
2485
|
+
screenId: screen.id,
|
|
2486
|
+
variantId: variant.id,
|
|
2487
|
+
frameId: frame.id,
|
|
2488
|
+
contentDigest: reviewDigest({
|
|
2489
|
+
files: Object.entries(sourceDigests).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
2490
|
+
width: frame.width,
|
|
2491
|
+
height: frame.height
|
|
2492
|
+
}),
|
|
2493
|
+
guidanceDigest: reviewDigest({
|
|
2494
|
+
title: screen.title,
|
|
2495
|
+
description: screen.description ?? "",
|
|
2496
|
+
anchors: screen.anchors ?? [],
|
|
2497
|
+
variant: { label: variant.label, description: variant.description ?? "" },
|
|
2498
|
+
frameLabel: frame.label,
|
|
2499
|
+
components,
|
|
2500
|
+
tokens: context.implementation.tokens,
|
|
2501
|
+
responsive: context.implementation.responsive,
|
|
2502
|
+
accessibility: context.implementation.accessibility
|
|
2503
|
+
})
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
function bundleDesignRevision(current, state = {}) {
|
|
2507
|
+
const { document: document2 } = current;
|
|
2508
|
+
const pick = (value, keys) => Object.fromEntries(
|
|
2509
|
+
keys.filter((key) => value?.[key] !== void 0).map((key) => [key, structuredClone(value[key])])
|
|
2510
|
+
);
|
|
2511
|
+
const context = current.reviewContext ?? emptyReviewContext(document2);
|
|
2512
|
+
return {
|
|
2513
|
+
kind: "openplanr-design-review-bundle",
|
|
2514
|
+
schemaVersion: "1.1.0",
|
|
2515
|
+
design: {
|
|
2516
|
+
...pick(document2, [
|
|
2517
|
+
"id",
|
|
2518
|
+
"title",
|
|
2519
|
+
"defaultView",
|
|
2520
|
+
"selectedVariant",
|
|
2521
|
+
"screenOrder",
|
|
2522
|
+
"frames",
|
|
2523
|
+
"flows"
|
|
2524
|
+
]),
|
|
2525
|
+
screens: document2.screens.map(
|
|
2526
|
+
(screen) => pick(screen, ["id", "title", "description", "anchors"])
|
|
2527
|
+
),
|
|
2528
|
+
selectedVariant: document2.variants.some(
|
|
2529
|
+
(variant) => variant.id === state.selectedVariant && variant.status === "ready"
|
|
2530
|
+
) ? state.selectedVariant : document2.selectedVariant,
|
|
2531
|
+
variants: document2.variants.filter((variant) => variant.status === "ready").map((variant) => pick(variant, ["id", "label", "status"]))
|
|
2532
|
+
},
|
|
2533
|
+
envelope: structuredClone(current.envelope),
|
|
2534
|
+
entries: structuredClone(current.entries),
|
|
2535
|
+
state: pick(state, ["positions"]),
|
|
2536
|
+
revision: current.revision,
|
|
2537
|
+
verification: pick(current.verification ?? {}, ["status"]),
|
|
2538
|
+
reviewContext: context,
|
|
2539
|
+
contextDigest: current.contextDigest ?? reviewDigest(context),
|
|
2540
|
+
fingerprints: current.fingerprints ?? []
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
var localRoot = (file) => realpathSync2(dirname3(resolve3(file)));
|
|
2544
|
+
function listDesignRevisions(file) {
|
|
2545
|
+
const root = localRoot(file);
|
|
2546
|
+
const pointer = JSON.parse(readFileSync3(join3(root, ".design/current.json"), "utf8"));
|
|
2547
|
+
const revisions = readdirSync2(join3(root, ".design/revisions")).filter((name) => /^[a-f0-9]{64}$/u.test(name)).map((revision) => {
|
|
2548
|
+
const value = JSON.parse(
|
|
2549
|
+
readFileSync3(join3(root, ".design/revisions", revision, "render.json"), "utf8")
|
|
2550
|
+
);
|
|
2551
|
+
return {
|
|
2552
|
+
revision,
|
|
2553
|
+
createdAt: value.manifest.generated_at,
|
|
2554
|
+
summary: value.reviewContext?.revisionSummary ?? "",
|
|
2555
|
+
fingerprints: value.fingerprints ?? []
|
|
2556
|
+
};
|
|
2557
|
+
}).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2558
|
+
return { revisions, currentRevision: pointer.revision };
|
|
2559
|
+
}
|
|
2560
|
+
function readDesignRevision(file, revision) {
|
|
2561
|
+
if (!/^[a-f0-9]{64}$/u.test(revision)) throw new Error("Invalid design revision identity.");
|
|
2562
|
+
const root = localRoot(file);
|
|
2563
|
+
const value = JSON.parse(
|
|
2564
|
+
readFileSync3(
|
|
2565
|
+
resolveLocalDocumentFile(root, `.design/revisions/${revision}/render.json`),
|
|
2566
|
+
"utf8"
|
|
2567
|
+
)
|
|
2568
|
+
);
|
|
2569
|
+
return bundleDesignRevision(value);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// packages/design/lib/design/lint.mjs
|
|
2573
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2574
|
+
import { fileURLToPath } from "node:url";
|
|
2575
|
+
|
|
2576
|
+
// packages/design/lib/design/tokens.mjs
|
|
2577
|
+
var SPACING_STEP = 4;
|
|
2578
|
+
var FRAMES = {
|
|
2579
|
+
desktop: { w: 1440, h: 1024 },
|
|
2580
|
+
tablet: { w: 834, h: 1194 },
|
|
2581
|
+
mobile: { w: 390, h: 844 }
|
|
2582
|
+
};
|
|
2583
|
+
var DEFAULT_FRAME = FRAMES.desktop;
|
|
2584
|
+
var RESPONSIVE_FRAMES = [
|
|
2585
|
+
{ name: "desktop", ...FRAMES.desktop },
|
|
2586
|
+
{ name: "tablet", ...FRAMES.tablet },
|
|
2587
|
+
{ name: "mobile", ...FRAMES.mobile }
|
|
2588
|
+
];
|
|
2589
|
+
function isOnSpacingScale(px) {
|
|
2590
|
+
const n = Math.abs(Number(px));
|
|
2591
|
+
if (!Number.isInteger(n)) return false;
|
|
2592
|
+
return n === 0 || n === 2 || n % SPACING_STEP === 0;
|
|
2593
|
+
}
|
|
2594
|
+
function nearestSpacing(px) {
|
|
2595
|
+
const v = Number(px) || 0;
|
|
2596
|
+
const n = Math.abs(v);
|
|
2597
|
+
const sign = v < 0 ? -1 : 1;
|
|
2598
|
+
const candidates = [.../* @__PURE__ */ new Set([0, 2, Math.round(n / SPACING_STEP) * SPACING_STEP])].sort(
|
|
2599
|
+
(a, b) => a - b
|
|
2600
|
+
);
|
|
2601
|
+
const best = candidates.reduce(
|
|
2602
|
+
(b, c) => Math.abs(c - n) <= Math.abs(b - n) ? c : b,
|
|
2603
|
+
candidates[0]
|
|
2604
|
+
);
|
|
2605
|
+
return sign * best;
|
|
2606
|
+
}
|
|
2607
|
+
function isCanonicalFrame({ w, h } = {}) {
|
|
2608
|
+
return Object.values(FRAMES).some((f) => f.w === Number(w) && f.h === Number(h));
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// packages/design/lib/design/lint.mjs
|
|
2612
|
+
var SPACING_PROP = /^(padding|margin|gap|row-gap|column-gap|inset|top|right|bottom|left)(-(top|right|bottom|left|block|inline)(-(start|end))?)?$/;
|
|
2613
|
+
var SIZING_PROP = /^(width|height|min-width|min-height|max-width|max-height|padding|margin|font-size|gap)/;
|
|
2614
|
+
var COLOR_PROP = /^(color|background|background-color|border(-(top|right|bottom|left))?-color|outline-color|fill|stroke|caret-color|text-decoration-color)$/;
|
|
2615
|
+
var COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|oklch\([^)]*\)/gi;
|
|
2616
|
+
var SYSTEM_FONTS = /* @__PURE__ */ new Set([
|
|
2617
|
+
"inherit",
|
|
2618
|
+
"initial",
|
|
2619
|
+
"sans-serif",
|
|
2620
|
+
"serif",
|
|
2621
|
+
"monospace",
|
|
2622
|
+
"system-ui",
|
|
2623
|
+
"ui-sans-serif",
|
|
2624
|
+
"ui-monospace",
|
|
2625
|
+
"ui-serif",
|
|
2626
|
+
"-apple-system",
|
|
2627
|
+
"blinkmacsystemfont",
|
|
2628
|
+
"segoe ui",
|
|
2629
|
+
"roboto",
|
|
2630
|
+
"helvetica neue",
|
|
2631
|
+
"helvetica",
|
|
2632
|
+
"arial",
|
|
2633
|
+
"sfmono-regular",
|
|
2634
|
+
"sf mono",
|
|
2635
|
+
"menlo",
|
|
2636
|
+
"consolas",
|
|
2637
|
+
"liberation mono",
|
|
2638
|
+
"cursive"
|
|
2639
|
+
]);
|
|
2640
|
+
function literalColors(value) {
|
|
2641
|
+
return /gradient/i.test(value) ? [] : value.match(COLOR_LITERAL) || [];
|
|
2642
|
+
}
|
|
2643
|
+
function firstColor(value) {
|
|
2644
|
+
if (/var\(|gradient/i.test(value)) return null;
|
|
2645
|
+
const m = value.match(/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|oklch\([^)]*\)|\b(?:white|black)\b/i);
|
|
2646
|
+
return m ? m[0] : null;
|
|
2647
|
+
}
|
|
2648
|
+
function firstFontFamily(value) {
|
|
2649
|
+
const first = value.split(",")[0].trim().replace(/^["']|["']$/g, "");
|
|
2650
|
+
return first ? first.toLowerCase() : null;
|
|
2651
|
+
}
|
|
2652
|
+
function declBlocks(html) {
|
|
2653
|
+
const blocks = [];
|
|
2654
|
+
for (const m of html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi))
|
|
2655
|
+
for (const b of cssDeclBlocks(m[1])) blocks.push(declsFromBlock(b));
|
|
2656
|
+
for (const m of html.matchAll(/style\s*=\s*(["'])([\s\S]*?)\1/gi))
|
|
2657
|
+
blocks.push(declsFromBlock(m[2]));
|
|
2658
|
+
return blocks;
|
|
2659
|
+
}
|
|
2660
|
+
function pxValues(value) {
|
|
2661
|
+
if (/\bvar\(|\bcalc\(/.test(value)) return [];
|
|
2662
|
+
const out = [];
|
|
2663
|
+
for (const m of value.matchAll(/-?\d+(?:\.\d+)?(px|rem|em)\b/g)) {
|
|
2664
|
+
const n = parseFloat(m[0]);
|
|
2665
|
+
out.push(/r?em/.test(m[1]) ? Math.round(n * 16 * 1e3) / 1e3 : n);
|
|
2666
|
+
}
|
|
2667
|
+
return out;
|
|
2668
|
+
}
|
|
2669
|
+
function declsFromBlock(block) {
|
|
2670
|
+
return block.split(";").map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
2671
|
+
const i = s.indexOf(":");
|
|
2672
|
+
if (i < 0) return null;
|
|
2673
|
+
return { prop: s.slice(0, i).trim().toLowerCase(), value: s.slice(i + 1).trim() };
|
|
2674
|
+
}).filter(Boolean);
|
|
2675
|
+
}
|
|
2676
|
+
function cssDeclBlocks(css) {
|
|
2677
|
+
const clean = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2678
|
+
const blocks = [];
|
|
2679
|
+
let depth = 0;
|
|
2680
|
+
let buf = "";
|
|
2681
|
+
for (const ch of clean) {
|
|
2682
|
+
if (ch === "{") {
|
|
2683
|
+
depth += 1;
|
|
2684
|
+
buf = "";
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
if (ch === "}") {
|
|
2688
|
+
if (depth >= 1) blocks.push(buf);
|
|
2689
|
+
depth -= 1;
|
|
2690
|
+
buf = "";
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (depth >= 1) buf += ch;
|
|
2694
|
+
}
|
|
2695
|
+
return blocks;
|
|
2696
|
+
}
|
|
2697
|
+
function styleBlockDecls(html) {
|
|
2698
|
+
const out = [];
|
|
2699
|
+
for (const m of html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
2700
|
+
for (const block of cssDeclBlocks(m[1])) {
|
|
2701
|
+
for (const d of declsFromBlock(block)) out.push({ ...d, where: "style" });
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return out;
|
|
2705
|
+
}
|
|
2706
|
+
function inlineStyleDecls(html) {
|
|
2707
|
+
const out = [];
|
|
2708
|
+
for (const m of html.matchAll(/style\s*=\s*(["'])([\s\S]*?)\1/gi)) {
|
|
2709
|
+
for (const d of declsFromBlock(m[2])) out.push({ ...d, where: "inline" });
|
|
2710
|
+
}
|
|
2711
|
+
return out;
|
|
2712
|
+
}
|
|
2713
|
+
function inlineSizingDrift(html) {
|
|
2714
|
+
const out = [];
|
|
2715
|
+
for (const m of html.matchAll(/<([a-z][\w-]*)\b([^>]*)>/gi)) {
|
|
2716
|
+
const attrs = m[2];
|
|
2717
|
+
const cls = /\bclass\s*=\s*(["'])([\s\S]*?)\1/i.exec(attrs);
|
|
2718
|
+
const style = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/i.exec(attrs);
|
|
2719
|
+
if (!cls || !style) continue;
|
|
2720
|
+
const sized = declsFromBlock(style[2]).filter((d) => SIZING_PROP.test(d.prop));
|
|
2721
|
+
if (sized.length) {
|
|
2722
|
+
out.push({
|
|
2723
|
+
rule: "inline-sizing-drift",
|
|
2724
|
+
level: "warn",
|
|
2725
|
+
tag: m[1],
|
|
2726
|
+
class: cls[2].trim(),
|
|
2727
|
+
props: sized.map((d) => d.prop).join(", "),
|
|
2728
|
+
message: `<${m[1]} class="${cls[2].trim()}"> carries inline sizing (${sized.map((d) => d.prop).join(", ")}) \u2014 move it to the shared class so siblings can't drift`
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
return out;
|
|
2733
|
+
}
|
|
2734
|
+
function lintDesign(html, opts = {}) {
|
|
2735
|
+
const { designSystem = null } = opts;
|
|
2736
|
+
const errors = [];
|
|
2737
|
+
const warnings = [];
|
|
2738
|
+
const decls = [...styleBlockDecls(html), ...inlineStyleDecls(html)];
|
|
2739
|
+
for (const d of decls) {
|
|
2740
|
+
if (SPACING_PROP.test(d.prop)) {
|
|
2741
|
+
for (const px of pxValues(d.value)) {
|
|
2742
|
+
const scale = opts.spacing ?? designSystem?.spacing;
|
|
2743
|
+
const onScale = scale?.length ? scale.includes(Math.abs(px)) : isOnSpacingScale(px);
|
|
2744
|
+
if (!onScale) {
|
|
2745
|
+
errors.push({
|
|
2746
|
+
rule: "spacing-off-grid",
|
|
2747
|
+
level: "error",
|
|
2748
|
+
prop: d.prop,
|
|
2749
|
+
value: `${px}px`,
|
|
2750
|
+
suggestion: `${nearestSpacing(px)}px`,
|
|
2751
|
+
where: d.where,
|
|
2752
|
+
message: `${d.prop}: ${px}px is off the 4-point grid (${d.where}) \u2192 use ${nearestSpacing(px)}px`
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
if (COLOR_PROP.test(d.prop)) {
|
|
2758
|
+
for (const lit of literalColors(d.value)) {
|
|
2759
|
+
warnings.push({
|
|
2760
|
+
rule: "color-not-token",
|
|
2761
|
+
level: "warn",
|
|
2762
|
+
prop: d.prop,
|
|
2763
|
+
value: lit,
|
|
2764
|
+
where: d.where,
|
|
2765
|
+
message: `${d.prop}: ${lit} is a raw color \u2014 use a design-system token via var(--\u2026)`
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
if ((d.prop === "font-family" || d.prop === "font") && designSystem?.fonts?.length) {
|
|
2770
|
+
const fam = firstFontFamily(d.value);
|
|
2771
|
+
const allowed = new Set(designSystem.fonts.map((f) => String(f.family || "").toLowerCase()));
|
|
2772
|
+
if (fam && !allowed.has(fam) && !SYSTEM_FONTS.has(fam)) {
|
|
2773
|
+
warnings.push({
|
|
2774
|
+
rule: "font-not-token",
|
|
2775
|
+
level: "warn",
|
|
2776
|
+
value: fam,
|
|
2777
|
+
where: d.where,
|
|
2778
|
+
message: `font "${fam}" is not in the design system (${[...allowed].join(", ") || "none"})`
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
for (const block of declBlocks(html)) {
|
|
2784
|
+
const c = block.find((d) => d.prop === "color");
|
|
2785
|
+
const bg = block.find((d) => d.prop === "background" || d.prop === "background-color");
|
|
2786
|
+
if (!c || !bg) continue;
|
|
2787
|
+
const fg = firstColor(c.value);
|
|
2788
|
+
const back = firstColor(bg.value);
|
|
2789
|
+
if (!fg || !back) continue;
|
|
2790
|
+
const ratio = contrastRatio(fg, back);
|
|
2791
|
+
if (ratio != null && ratio < AA_NORMAL) {
|
|
2792
|
+
errors.push({
|
|
2793
|
+
rule: "contrast-below-aa",
|
|
2794
|
+
level: "error",
|
|
2795
|
+
value: `${ratio.toFixed(1)}:1`,
|
|
2796
|
+
fg,
|
|
2797
|
+
bg: back,
|
|
2798
|
+
message: `text/background contrast ${ratio.toFixed(1)}:1 is below AA 4.5:1 (${fg} on ${back})`
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
warnings.push(...inlineSizingDrift(html));
|
|
2803
|
+
return {
|
|
2804
|
+
ok: errors.length === 0,
|
|
2805
|
+
errors,
|
|
2806
|
+
warnings,
|
|
2807
|
+
declarations: decls.length,
|
|
2808
|
+
summary: `${errors.length} error(s), ${warnings.length} warning(s)`
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
function lintCanvasData(data, { frames } = {}) {
|
|
2812
|
+
const errors = [];
|
|
2813
|
+
for (const section of data?.sections || []) {
|
|
2814
|
+
for (const a of section.artboards || []) {
|
|
2815
|
+
const accepted = frames?.length ? frames.some((frame) => frame.width === a.width && frame.height === a.height) : isCanonicalFrame({ w: a.width, h: a.height });
|
|
2816
|
+
if (!accepted) {
|
|
2817
|
+
errors.push({
|
|
2818
|
+
rule: "frame-not-canonical",
|
|
2819
|
+
level: "error",
|
|
2820
|
+
artboard: a.id || a.label || "(unnamed)",
|
|
2821
|
+
value: `${a.width}\xD7${a.height}`,
|
|
2822
|
+
suggestion: `${FRAMES.desktop.w}\xD7${FRAMES.desktop.h}`,
|
|
2823
|
+
message: `artboard "${a.id || a.label || "?"}" is ${a.width}\xD7${a.height} \u2014 use the canonical ${FRAMES.desktop.w}\xD7${FRAMES.desktop.h} desktop frame`
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return { ok: errors.length === 0, errors };
|
|
2829
|
+
}
|
|
2830
|
+
function extractCanvasData(html) {
|
|
2831
|
+
const m = /(?:var\s+DATA\s*=|__CANVAS_DATA\s*=)\s*(\{[\s\S]*?\})\s*;/.exec(html);
|
|
2832
|
+
if (!m) return null;
|
|
2833
|
+
try {
|
|
2834
|
+
return JSON.parse(m[1]);
|
|
2835
|
+
} catch {
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
var isMain = process.argv[1] && fileURLToPath(new URL("./runtime/packages/design/lib/design/lint.mjs", import.meta.url).href) === process.argv[1];
|
|
2840
|
+
if (isMain) {
|
|
2841
|
+
const expectStyles = process.argv.includes("--expect-styles");
|
|
2842
|
+
const files = process.argv.slice(2).filter((a) => a !== "--expect-styles");
|
|
2843
|
+
if (!files.length) {
|
|
2844
|
+
console.error("usage: node lib/design/lint.mjs [--expect-styles] <file.html> [<file2.html> \u2026]");
|
|
2845
|
+
process.exit(2);
|
|
2846
|
+
}
|
|
2847
|
+
let totalErrors = 0;
|
|
2848
|
+
let emptyParsed = false;
|
|
2849
|
+
for (const file of files) {
|
|
2850
|
+
let html;
|
|
2851
|
+
try {
|
|
2852
|
+
html = readFileSync4(file, "utf-8");
|
|
2853
|
+
} catch (e) {
|
|
2854
|
+
console.error(`\u2717 cannot read ${file}: ${e.message}`);
|
|
2855
|
+
totalErrors += 1;
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
if (/\.s?css$/i.test(file) || !/<style[\s>]/i.test(html) && /\{[^}]*:[^}]*\}/.test(html)) {
|
|
2859
|
+
html = `<style>${html}</style>`;
|
|
2860
|
+
}
|
|
2861
|
+
const res = lintDesign(html);
|
|
2862
|
+
const data = extractCanvasData(html);
|
|
2863
|
+
const frame = data ? lintCanvasData(data) : { ok: true, errors: [] };
|
|
2864
|
+
const allErrors = [...res.errors, ...frame.errors];
|
|
2865
|
+
totalErrors += allErrors.length;
|
|
2866
|
+
console.log(
|
|
2867
|
+
`
|
|
2868
|
+
${file} \u2014 ${res.declarations} declaration(s), ${allErrors.length} error(s), ${res.warnings.length} warning(s)`
|
|
2869
|
+
);
|
|
2870
|
+
for (const e of allErrors) console.log(` \u2717 [${e.rule}] ${e.message}`);
|
|
2871
|
+
for (const w of res.warnings) console.log(` \u26A0 [${w.rule}] ${w.message}`);
|
|
2872
|
+
if (res.declarations === 0) {
|
|
2873
|
+
emptyParsed = true;
|
|
2874
|
+
console.log(
|
|
2875
|
+
` \u26A0 [no-styles-parsed] 0 CSS declarations found \u2014 point at COMPILED CSS, or wrap raw CSS as <style>\u2026</style>${expectStyles ? " (fails --expect-styles)" : ""}`
|
|
2876
|
+
);
|
|
2877
|
+
} else if (!allErrors.length && !res.warnings.length) {
|
|
2878
|
+
console.log(" \u2713 clean");
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
process.exit(totalErrors ? 1 : expectStyles && emptyParsed ? 3 : 0);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
// packages/design/lib/design/recommend-format.mjs
|
|
2885
|
+
var DESIGN_FORMATS = Object.freeze(["prototype", "walkthrough", "canvas"]);
|
|
2886
|
+
var EXPLORATORY_KEYWORDS = Object.freeze([
|
|
2887
|
+
"option",
|
|
2888
|
+
"options",
|
|
2889
|
+
"concept",
|
|
2890
|
+
"concepts",
|
|
2891
|
+
"explore",
|
|
2892
|
+
"exploration",
|
|
2893
|
+
"variant",
|
|
2894
|
+
"variants",
|
|
2895
|
+
"moodboard",
|
|
2896
|
+
"brainstorm",
|
|
2897
|
+
"compare",
|
|
2898
|
+
"comparison"
|
|
2899
|
+
]);
|
|
2900
|
+
|
|
2901
|
+
// packages/design/lib/design/manifest.mjs
|
|
2902
|
+
var DESIGN_SOURCES = Object.freeze(["spec", "png", "describe"]);
|
|
2903
|
+
var CONTENT_PROVENANCE = Object.freeze(["spec", "inferred"]);
|
|
2904
|
+
var FRAMEWORKS = Object.freeze(["vanilla", "react"]);
|
|
2905
|
+
var NAV_MODES = Object.freeze(["anchor", "lazy"]);
|
|
2906
|
+
var SCHEMA_VERSION = "1.0.0";
|
|
2907
|
+
function defaultFramework(designFormat) {
|
|
2908
|
+
return designFormat === "canvas" ? "react" : "vanilla";
|
|
2909
|
+
}
|
|
2910
|
+
function buildManifest({
|
|
2911
|
+
designFormat,
|
|
2912
|
+
source,
|
|
2913
|
+
generatedAt,
|
|
2914
|
+
screens = [],
|
|
2915
|
+
contentProvenance = "spec",
|
|
2916
|
+
framework,
|
|
2917
|
+
navMode = null,
|
|
2918
|
+
screenName = "",
|
|
2919
|
+
iterations = 0,
|
|
2920
|
+
branch = "",
|
|
2921
|
+
specId = "",
|
|
2922
|
+
htmlFile = "",
|
|
2923
|
+
pretextTier = ""
|
|
2924
|
+
} = {}) {
|
|
2925
|
+
if (!designFormat) throw new Error("buildManifest: designFormat is required");
|
|
2926
|
+
if (!source) throw new Error("buildManifest: source is required");
|
|
2927
|
+
if (!generatedAt) throw new Error("buildManifest: generatedAt is required");
|
|
2928
|
+
const manifest = {
|
|
2929
|
+
schema_version: SCHEMA_VERSION,
|
|
2930
|
+
design_format: designFormat,
|
|
2931
|
+
source,
|
|
2932
|
+
content_provenance: contentProvenance,
|
|
2933
|
+
framework: framework ?? defaultFramework(designFormat),
|
|
2934
|
+
screens: [...screens],
|
|
2935
|
+
screen_count: screens.length,
|
|
2936
|
+
iterations,
|
|
2937
|
+
generated_at: generatedAt
|
|
2938
|
+
};
|
|
2939
|
+
if (navMode) manifest.nav_mode = navMode;
|
|
2940
|
+
if (screenName) manifest.screen_name = screenName;
|
|
2941
|
+
if (branch) manifest.branch = branch;
|
|
2942
|
+
if (specId) manifest.spec_id = specId;
|
|
2943
|
+
if (htmlFile) manifest.html_file = htmlFile;
|
|
2944
|
+
if (pretextTier) manifest.pretext_tier = pretextTier;
|
|
2945
|
+
return manifest;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// packages/design/lib/design/studio.mjs
|
|
2949
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2950
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2951
|
+
|
|
2952
|
+
// packages/design/lib/design/studio-render.mjs
|
|
2953
|
+
var DESIGN_STUDIO_VERSION = "1.9.0";
|
|
2954
|
+
var DESIGN_STUDIO_ASSETS = Object.freeze({
|
|
2955
|
+
style: "templates/studio/studio.css",
|
|
2956
|
+
runtime: "templates/studio/studio.js",
|
|
2957
|
+
enhancementsStyle: "templates/studio/enhancements.css",
|
|
2958
|
+
handoffCenterStyle: "templates/studio/handoff-center.css"
|
|
2959
|
+
});
|
|
2960
|
+
function applyDesignStudioTheme(preference) {
|
|
2961
|
+
if (!preference) {
|
|
2962
|
+
try {
|
|
2963
|
+
preference = localStorage.getItem("openplanr.design.theme");
|
|
2964
|
+
} catch {
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
if (!["dark", "light", "system"].includes(preference)) preference = "dark";
|
|
2968
|
+
const resolved = preference === "system" ? globalThis.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark" : preference;
|
|
2969
|
+
Object.assign(document.documentElement.dataset, {
|
|
2970
|
+
planrTheme: resolved,
|
|
2971
|
+
designTheme: resolved,
|
|
2972
|
+
designThemePreference: preference
|
|
2973
|
+
});
|
|
2974
|
+
const modelNode = document.getElementById("planr-artifact-shell-model");
|
|
2975
|
+
if (modelNode) {
|
|
2976
|
+
try {
|
|
2977
|
+
const model = JSON.parse(modelNode.textContent);
|
|
2978
|
+
model.theme = resolved;
|
|
2979
|
+
modelNode.textContent = JSON.stringify(model);
|
|
2980
|
+
} catch {
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
return { preference, resolved };
|
|
2984
|
+
}
|
|
2985
|
+
function applyDesignStudioResourcePolicy() {
|
|
2986
|
+
const root = document.querySelector(".planr-shell");
|
|
2987
|
+
if (!root || !document.documentElement.hasAttribute("data-design-studio")) return;
|
|
2988
|
+
if (globalThis.matchMedia?.("(pointer: coarse)").matches) {
|
|
2989
|
+
root.dataset.planrFrameBudget = "3";
|
|
2990
|
+
root.dataset.designResourceMode = "bounded";
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
function designStudioThemeBootstrapSource() {
|
|
2994
|
+
return `(${applyDesignStudioTheme.toString()})();(${applyDesignStudioResourcePolicy.toString()})();`;
|
|
2995
|
+
}
|
|
2996
|
+
function button(label, attributes = "", className = "") {
|
|
2997
|
+
return `<button type="button"${className ? ` class="${className}"` : ""} ${attributes}>${escapeHtml(label)}</button>`;
|
|
2998
|
+
}
|
|
2999
|
+
var icons = {
|
|
3000
|
+
left: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/>',
|
|
3001
|
+
right: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M15 4v16"/>',
|
|
3002
|
+
canvas: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
|
|
3003
|
+
prototype: '<path d="m8 4 12 8-12 8Z"/>',
|
|
3004
|
+
walkthrough: '<rect x="3" y="4" width="18" height="14" rx="2"/><path d="m9 22 3-4 3 4M9 9h6M9 13h3"/>',
|
|
3005
|
+
share: '<path d="M12 16V3m-4 4 4-4 4 4M5 13v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6"/>',
|
|
3006
|
+
export: '<path d="M12 3v12m-4-4 4 4 4-4M5 16v4h14v-4"/>',
|
|
3007
|
+
pointer: '<path d="m5 3 15 9-7 1-3 7Z"/>',
|
|
3008
|
+
comment: '<path d="M21 11a8 8 0 0 1-8 8H7l-4 3V11a9 9 0 0 1 18 0Z"/><path d="M8 9h8M8 13h5"/>'
|
|
3009
|
+
};
|
|
3010
|
+
function icon(name) {
|
|
3011
|
+
return `<svg class="design-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name]}</svg>`;
|
|
3012
|
+
}
|
|
3013
|
+
function iconButton(label, name, attributes = "", className = "") {
|
|
3014
|
+
return `<button type="button" class="${className}" ${attributes} title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}">${icon(name)}<span class="design-button-label">${escapeHtml(label)}</span></button>`;
|
|
3015
|
+
}
|
|
3016
|
+
function toolbar(document2) {
|
|
3017
|
+
return `<header class="planr-toolbar design-toolbar">
|
|
3018
|
+
<div class="design-toolbar-leading">
|
|
3019
|
+
${iconButton("Screens", "left", 'data-design-toggle-nav aria-controls="design-navigator" aria-expanded="true"', "planr-toolbar-action design-panel-toggle")}
|
|
3020
|
+
<div class="planr-brand">${renderPlanrMark()}<span class="design-wordmark" aria-label="OpenPlanr">Open<span>Planr</span></span><span class="planr-title-block"><strong title="${escapeHtml(document2.title)}">${escapeHtml(document2.title)}</strong></span></div>
|
|
3021
|
+
</div>
|
|
3022
|
+
<div class="planr-segment design-view-picker" role="group" aria-label="Design view">${["canvas", "prototype", "walkthrough"].map((view) => iconButton(view[0].toUpperCase() + view.slice(1), view, `data-design-view="${view}" aria-pressed="${document2.defaultView === view}"`)).join("")}</div>
|
|
3023
|
+
<div class="design-toolbar-trailing">
|
|
3024
|
+
<span class="design-save-state" role="status" aria-live="polite" data-design-save-state>Loading studio</span>
|
|
3025
|
+
${iconButton("Review", "right", 'data-planr-action="feedback" data-planr-review-label="Review" aria-controls="planr-review-rail" aria-expanded="true"', "planr-toolbar-action design-review-toggle")}
|
|
3026
|
+
${iconButton("Share design", "share", 'data-planr-action="share" aria-haspopup="dialog"', "planr-toolbar-action design-share")}
|
|
3027
|
+
<details class="design-export"><summary aria-label="Export" title="Export">${icon("export")}</summary><div>${button("Portable HTML", 'data-design-export="html"')}${button("Screen PNG", 'data-design-export="png"')}</div></details>
|
|
3028
|
+
</div>
|
|
3029
|
+
</header>`;
|
|
3030
|
+
}
|
|
3031
|
+
function navigator(document2) {
|
|
3032
|
+
const failed = document2.variants.filter(({ status }) => status === "failed");
|
|
3033
|
+
return `<nav class="design-navigator" id="design-navigator" aria-label="Design screens">
|
|
3034
|
+
<div class="design-nav-title"><strong>Screens <span>${document2.screenOrder.length}</span></strong>${iconButton("Close screens", "left", 'data-design-toggle-nav aria-controls="design-navigator" aria-expanded="true"', "design-nav-close")}</div>
|
|
3035
|
+
<label class="design-field" for="design-variant">Direction<select id="design-variant" data-design-variant>${document2.variants.map((variant) => `<option value="${escapeHtml(variant.id)}"${variant.status !== "ready" ? " disabled" : ""}>${escapeHtml(variant.label)}${variant.status === "failed" ? " \u2014 unavailable" : ""}</option>`).join("")}</select></label>
|
|
3036
|
+
${document2.variants.filter(({ status }) => status === "ready").length > 1 ? `<label class="design-compare"><input type="checkbox" data-design-compare> Compare directions</label>` : ""}
|
|
3037
|
+
<div class="design-screen-list">${document2.screenOrder.map((screenId, index) => {
|
|
3038
|
+
const screen = document2.screens.find(({ id: id3 }) => id3 === screenId);
|
|
3039
|
+
return `<button type="button" class="design-screen" data-design-screen="${escapeHtml(screen.id)}" aria-current="${index === 0 ? "page" : "false"}" title="${escapeHtml(screen.title)}"><span class="design-screen-number" aria-hidden="true">${String(index + 1).padStart(2, "0")}</span><span>${escapeHtml(screen.title)}</span></button>`;
|
|
3040
|
+
}).join("")}</div>
|
|
3041
|
+
<div class="design-nav-footer"><span data-design-verification>Browser inspection pending</span><span class="design-local">Local workspace</span></div>
|
|
3042
|
+
${failed.length ? `<details class="design-failed-variants"><summary>${failed.length} unavailable ${failed.length === 1 ? "direction" : "directions"}</summary>${failed.map((variant) => `<p><strong>${escapeHtml(variant.label)}</strong><br>${escapeHtml(variant.issue ?? "Generation did not complete. The previous design remains available.")}</p>`).join("")}</details>` : ""}
|
|
3043
|
+
</nav>`;
|
|
3044
|
+
}
|
|
3045
|
+
function designNotes(document2) {
|
|
3046
|
+
const screens = document2.screens.filter(({ description }) => description);
|
|
3047
|
+
if (!screens.length) return "";
|
|
3048
|
+
return `<details class="design-guidance" data-design-notes><summary aria-controls="design-guidance-panel" aria-expanded="false">Notes <span>${screens.length}</span></summary><div class="design-guidance-panel" id="design-guidance-panel" role="region" aria-labelledby="design-guidance-title"><header><div><strong id="design-guidance-title">Design notes</strong><p>Implementation context stays outside the product UI.</p></div><button type="button" data-design-close-notes aria-label="Close design notes" title="Close design notes">\xD7</button></header>${screens.map(
|
|
3049
|
+
(screen, index) => `<details data-design-note-screen="${escapeHtml(screen.id)}"${index === 0 ? " open" : ""}><summary>${escapeHtml(screen.title)}</summary><p>${escapeHtml(screen.description)}</p></details>`
|
|
3050
|
+
).join("")}</div></details>`;
|
|
3051
|
+
}
|
|
3052
|
+
function stageDetails(document2) {
|
|
3053
|
+
return `<div class="design-stage-context"><div><strong data-design-screen-title>${escapeHtml(document2.screens.find(({ id: id3 }) => id3 === document2.screenOrder[0]).title)}</strong><span data-design-stage-description>Every screen, one connected design</span></div><div class="design-stage-actions">${designNotes(document2)}<label class="design-frame-picker">Frame<select aria-label="Responsive frame" data-design-frame>${document2.frames.map((frame) => `<option value="${escapeHtml(frame.id)}">${escapeHtml(frame.label)} \xB7 ${frame.width} \xD7 ${frame.height}</option>`).join("")}</select></label></div></div>
|
|
3054
|
+
<div class="design-canvas-tools" role="group" aria-label="Canvas controls"><div class="planr-segment design-interaction-picker" role="group" aria-label="Review mode">${iconButton("Interact", "pointer", 'data-planr-mode="interact" aria-pressed="true" aria-keyshortcuts="I"')}${iconButton("Annotate", "comment", 'data-planr-mode="comment" aria-pressed="false" aria-keyshortcuts="C"')}</div><span></span>${button("\u2212", 'data-design-zoom="out" aria-label="Zoom out"')}${button("100%", 'data-design-zoom="reset" aria-label="Reset zoom"')}${button("+", 'data-design-zoom="in" aria-label="Zoom in"')}<span></span>${button("Fit", 'data-design-fit aria-label="Fit all visible artboards"')}${button("Pan", 'data-design-pan aria-pressed="false" aria-label="Pan canvas" aria-keyshortcuts="H Space" title="Pan canvas (H). Hold Space to pan temporarily; Escape returns to Interact."')}</div>
|
|
3055
|
+
<div class="design-walkthrough-caption" hidden><div><span data-design-step></span><h2 data-design-narrative-title></h2><p data-design-narrative></p></div><div>${button("Previous", 'data-design-step-change="-1"')}${button("Next", 'data-design-step-change="1"')}</div></div>
|
|
3056
|
+
<div class="design-notice" role="status" aria-live="polite" hidden></div>`;
|
|
3057
|
+
}
|
|
3058
|
+
function directionDetails() {
|
|
3059
|
+
return `<section class="planr-domain-rail design-direction-review" data-planr-slot="domain-rail" aria-label="Direction review"><div><strong data-design-direction-label>Direction</strong><span data-design-selected-direction></span></div><div class="design-rating" role="group" aria-label="Rate this direction">${[1, 2, 3, 4, 5].map((rating) => button("\u2606", `data-design-rating="${rating}" aria-label="Rate ${rating} out of 5" aria-pressed="false"`)).join("")}</div>${button("Use this direction", "data-design-select-direction")}<details class="design-refinement"><summary>Refine this direction</summary><div><label for="design-remix">Refinement note<textarea id="design-remix" maxlength="8192" data-design-remix placeholder="What should change in the next iteration?"></textarea></label>${button("Save refinement note", "data-design-save-remix")}<p data-design-review-hint>Saved with your review for the next iteration.</p></div></details></section>`;
|
|
3060
|
+
}
|
|
3061
|
+
function earlierFeedback(pins) {
|
|
3062
|
+
if (!pins.length) return "";
|
|
3063
|
+
return `<section class="design-stale-feedback" aria-label="Earlier feedback"><details open><summary>Earlier feedback <span>${pins.length}</span></summary><p>These comments refer to an earlier revision or a missing anchor. Review them before resolving.</p>${pins.map((pin) => `<article data-design-stale-pin="${escapeHtml(pin.id)}"><div><strong>${escapeHtml(pin.screenId ?? pin.anchor?.screen ?? "Earlier screen")}</strong><span>Stale</span></div><p>${escapeHtml(pin.comment)}</p>${pin.anchor?.planrId ? `<small>Anchor: ${escapeHtml(pin.anchor.planrId)}</small>` : ""}</article>`).join("")}</details></section>`;
|
|
3064
|
+
}
|
|
3065
|
+
function renderDesignStudioMarkup({
|
|
3066
|
+
document: document2,
|
|
3067
|
+
envelope,
|
|
3068
|
+
entries,
|
|
3069
|
+
state = null,
|
|
3070
|
+
revision = null,
|
|
3071
|
+
verification = null,
|
|
3072
|
+
stalePins = [],
|
|
3073
|
+
reviewContext = null,
|
|
3074
|
+
contextDigest = null,
|
|
3075
|
+
fingerprints = []
|
|
3076
|
+
} = {}, { stageRuntimeUrl = "./artifact-review-stage.js", renderShell, style = "", runtime = "" } = {}) {
|
|
3077
|
+
if (!document2 || !envelope)
|
|
3078
|
+
throw new TypeError("Design studio requires a design document and artifact envelope.");
|
|
3079
|
+
const artifactIds = new Set(envelope.artifacts.map(({ id: id3 }) => id3));
|
|
3080
|
+
for (const entry of entries) {
|
|
3081
|
+
if (!artifactIds.has(entry.artifactId) || !document2.screens.some(({ id: id3 }) => id3 === entry.screenId) || !document2.variants.some(({ id: id3, status }) => id3 === entry.variantId && status === "ready") || !document2.frames.some(({ id: id3 }) => id3 === entry.frameId)) {
|
|
3082
|
+
throw new TypeError(`Invalid design studio entry: ${entry.artifactId}.`);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
const activeEntry = entries.find(
|
|
3086
|
+
({ variantId, screenId, frameId }) => variantId === document2.selectedVariant && screenId === document2.screenOrder[0] && frameId === document2.frames[0].id
|
|
3087
|
+
);
|
|
3088
|
+
if (!activeEntry)
|
|
3089
|
+
throw new TypeError("Design studio requires the selected direction and first screen/frame.");
|
|
3090
|
+
const payload = {
|
|
3091
|
+
schemaVersion: DESIGN_STUDIO_VERSION,
|
|
3092
|
+
document: document2,
|
|
3093
|
+
entries,
|
|
3094
|
+
state,
|
|
3095
|
+
revision,
|
|
3096
|
+
verification,
|
|
3097
|
+
reviewContext,
|
|
3098
|
+
contextDigest,
|
|
3099
|
+
fingerprints,
|
|
3100
|
+
artifactKinds: Object.fromEntries(
|
|
3101
|
+
envelope.artifacts.map((artifact) => [artifact.id, artifact.kind])
|
|
3102
|
+
),
|
|
3103
|
+
staticArtifacts: envelope.artifacts.filter(
|
|
3104
|
+
(artifact) => artifact.kind !== "html" || /\bdata-(?:design|planr)-static(?:\s|=|>)/u.test(artifact.html || "")
|
|
3105
|
+
).map((artifact) => artifact.id)
|
|
3106
|
+
};
|
|
3107
|
+
let html = renderShell(
|
|
3108
|
+
{
|
|
3109
|
+
envelope,
|
|
3110
|
+
viewer: {
|
|
3111
|
+
mode: "single",
|
|
3112
|
+
presentation: "canvas",
|
|
3113
|
+
activeArtifactId: activeEntry.artifactId
|
|
3114
|
+
},
|
|
3115
|
+
shell: {
|
|
3116
|
+
title: document2.title,
|
|
3117
|
+
theme: "dark",
|
|
3118
|
+
railOpen: true,
|
|
3119
|
+
zoom: 100
|
|
3120
|
+
}
|
|
3121
|
+
},
|
|
3122
|
+
{ stageRuntimeUrl }
|
|
3123
|
+
);
|
|
3124
|
+
html = html.replace(
|
|
3125
|
+
/(<iframe\b[^>]*\bsandbox=")allow-scripts(")/g,
|
|
3126
|
+
"$1allow-scripts allow-forms$2"
|
|
3127
|
+
);
|
|
3128
|
+
html = html.replace(/<header class="planr-toolbar">[\s\S]*?<\/header>/, toolbar(document2));
|
|
3129
|
+
html = html.replace(
|
|
3130
|
+
'<div class="planr-workspace">',
|
|
3131
|
+
`<div class="planr-workspace">${navigator(document2)}`
|
|
3132
|
+
);
|
|
3133
|
+
html = html.replace(
|
|
3134
|
+
'<main class="planr-stage" aria-label="Artifact review stage">',
|
|
3135
|
+
`<main class="planr-stage" aria-label="Design canvas">${stageDetails(document2)}`
|
|
3136
|
+
);
|
|
3137
|
+
html = html.replace(/<section class="planr-domain-rail"[^>]*><\/section>/, directionDetails());
|
|
3138
|
+
html = html.replace("<h2>Review comments</h2>", "<h2>Review</h2>");
|
|
3139
|
+
html = html.replace(
|
|
3140
|
+
'aria-label="Close comments">\xD7',
|
|
3141
|
+
`aria-label="Close review" title="Close review">${icon("right")}`
|
|
3142
|
+
);
|
|
3143
|
+
html = html.replace("Overall note for the coding agent\u2026", "Summarize your review\u2026");
|
|
3144
|
+
html = html.replace(
|
|
3145
|
+
'<div class="planr-feedback-slot"',
|
|
3146
|
+
`${earlierFeedback(stalePins)}<div class="design-comment-action">${iconButton("Add comment", "comment", 'data-planr-action="add-comment" aria-pressed="false"')}</div><div class="planr-feedback-slot"`
|
|
3147
|
+
);
|
|
3148
|
+
html = html.replace(
|
|
3149
|
+
'<html lang="en"',
|
|
3150
|
+
`<html lang="en" data-design-studio="${DESIGN_STUDIO_VERSION}" data-design-opening="true"`
|
|
3151
|
+
);
|
|
3152
|
+
html = html.replace(
|
|
3153
|
+
"</head>",
|
|
3154
|
+
`<style>${style}</style><script>${designStudioThemeBootstrapSource()}</script></head>`
|
|
3155
|
+
);
|
|
3156
|
+
html = html.replace(
|
|
3157
|
+
"</body>",
|
|
3158
|
+
`<script type="application/json" id="planr-design-studio-payload">${embedJson(payload)}</script><script>${designStudioThemeBootstrapSource()}</script><script>${runtime}</script></body>`
|
|
3159
|
+
);
|
|
3160
|
+
return html;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// packages/design/lib/design/studio.mjs
|
|
3164
|
+
var templateRoot = new URL("../../templates/studio/", new URL("./runtime/packages/design/lib/design/studio.mjs", import.meta.url).href);
|
|
3165
|
+
function designStudioArtifactId(variantId, screenId, frameId) {
|
|
3166
|
+
const id3 = [
|
|
3167
|
+
"design",
|
|
3168
|
+
...[variantId, screenId, frameId].map((value) => `${value.length}-${value}`)
|
|
3169
|
+
].join(".");
|
|
3170
|
+
return id3.length <= 128 ? id3 : `design.${createHash3("sha256").update(JSON.stringify([variantId, screenId, frameId])).digest("hex")}`;
|
|
3171
|
+
}
|
|
3172
|
+
function createDesignStudioEntries(document2, envelope) {
|
|
3173
|
+
const ids = new Set(envelope.artifacts.map(({ id: id3 }) => id3));
|
|
3174
|
+
const entries = [];
|
|
3175
|
+
for (const variant of document2.variants.filter(({ status }) => status === "ready")) {
|
|
3176
|
+
for (const screenId of document2.screenOrder) {
|
|
3177
|
+
for (const frame of document2.frames) {
|
|
3178
|
+
const artifactId = designStudioArtifactId(variant.id, screenId, frame.id);
|
|
3179
|
+
if (!ids.has(artifactId))
|
|
3180
|
+
throw new Error(`Design studio is missing artifact ${artifactId}.`);
|
|
3181
|
+
entries.push({
|
|
3182
|
+
artifactId,
|
|
3183
|
+
variantId: variant.id,
|
|
3184
|
+
screenId,
|
|
3185
|
+
frameId: frame.id
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return entries;
|
|
3191
|
+
}
|
|
3192
|
+
function renderDesignStudio(input = {}, options = {}) {
|
|
3193
|
+
return renderDesignStudioMarkup(
|
|
3194
|
+
{
|
|
3195
|
+
...input,
|
|
3196
|
+
entries: input.entries ?? createDesignStudioEntries(input.document, input.envelope)
|
|
3197
|
+
},
|
|
3198
|
+
{
|
|
3199
|
+
...options,
|
|
3200
|
+
renderShell: renderArtifactShellDocument,
|
|
3201
|
+
style: ["studio.css", "enhancements.css", "handoff-center.css"].map((file) => readFileSync5(new URL(file, templateRoot), "utf8")).join("\n"),
|
|
3202
|
+
runtime: readFileSync5(new URL("studio.js", templateRoot), "utf8")
|
|
3203
|
+
}
|
|
3204
|
+
);
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// packages/design/lib/design/document.mjs
|
|
3208
|
+
var DESIGN_VIEWS = Object.freeze(["canvas", "prototype", "walkthrough"]);
|
|
3209
|
+
var DESIGN_RENDERER_VERSION = "1.2.0";
|
|
3210
|
+
var hash = (value) => createHash4("sha256").update(value).digest("hex");
|
|
3211
|
+
var json = (value) => `${JSON.stringify(value, null, 2)}
|
|
3212
|
+
`;
|
|
3213
|
+
function readJson(path, fallback = void 0) {
|
|
3214
|
+
try {
|
|
3215
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
if (error.code === "ENOENT" && fallback !== void 0) return fallback;
|
|
3218
|
+
throw error;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
function atomicJson(path, value) {
|
|
3222
|
+
atomicBytes(path, json(value));
|
|
3223
|
+
}
|
|
3224
|
+
function atomicBytes(path, bytes) {
|
|
3225
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
3226
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
3227
|
+
try {
|
|
3228
|
+
writeFileSync2(temporary, bytes, { mode: 384, flag: "wx" });
|
|
3229
|
+
renameSync2(temporary, path);
|
|
3230
|
+
} finally {
|
|
3231
|
+
rmSync2(temporary, { force: true });
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
function recoverDesignPublication(root, { ownsRenderLock = false } = {}) {
|
|
3235
|
+
const journalPath = join4(root, ".design/publication.json");
|
|
3236
|
+
let journal = readJson(journalPath, null);
|
|
3237
|
+
if (!journal) return;
|
|
3238
|
+
const lockPath = join4(root, ".design/render.lock");
|
|
3239
|
+
let recoveryOwner;
|
|
3240
|
+
if (!ownsRenderLock) {
|
|
3241
|
+
const lock = readJson(lockPath, null);
|
|
3242
|
+
if (lock && isProcessAlive(lock.pid)) return;
|
|
3243
|
+
if (lock) rmSync2(lockPath, { force: true });
|
|
3244
|
+
recoveryOwner = randomUUID();
|
|
3245
|
+
let descriptor;
|
|
3246
|
+
try {
|
|
3247
|
+
descriptor = openSync(lockPath, "wx", 384);
|
|
3248
|
+
writeFileSync2(
|
|
3249
|
+
descriptor,
|
|
3250
|
+
json({ pid: process.pid, owner: recoveryOwner, createdAt: Date.now() })
|
|
3251
|
+
);
|
|
3252
|
+
} catch (error) {
|
|
3253
|
+
if (error.code === "EEXIST") return;
|
|
3254
|
+
throw error;
|
|
3255
|
+
} finally {
|
|
3256
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
try {
|
|
3260
|
+
journal = readJson(journalPath, null);
|
|
3261
|
+
if (!journal) return;
|
|
3262
|
+
const pointer = readJson(join4(root, ".design/current.json"), null);
|
|
3263
|
+
const manifestPath = join4(root, "finalized.json");
|
|
3264
|
+
if (pointer?.revision === journal.revision) atomicJson(manifestPath, journal.manifest);
|
|
3265
|
+
else if ((pointer?.revision ?? null) === journal.previousRevision) {
|
|
3266
|
+
if (journal.previousManifest === null) rmSync2(manifestPath, { force: true });
|
|
3267
|
+
else atomicBytes(manifestPath, Buffer.from(journal.previousManifest, "base64"));
|
|
3268
|
+
} else if (pointer?.revision && /^[a-f0-9]{64}$/u.test(pointer.revision)) {
|
|
3269
|
+
atomicJson(
|
|
3270
|
+
manifestPath,
|
|
3271
|
+
readJson(join4(root, ".design/revisions", pointer.revision, "render.json")).manifest
|
|
3272
|
+
);
|
|
3273
|
+
} else
|
|
3274
|
+
throw new Error("Design publication recovery could not identify the committed revision.");
|
|
3275
|
+
rmSync2(journalPath, { force: true });
|
|
3276
|
+
} finally {
|
|
3277
|
+
if (recoveryOwner && readJson(lockPath, null)?.owner === recoveryOwner)
|
|
3278
|
+
rmSync2(lockPath, { force: true });
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
function loadDesignDocument(file, { readSource } = {}) {
|
|
3282
|
+
const checked = readSource?.(file, process.cwd());
|
|
3283
|
+
const path = checked?.file ?? realpathSync3(resolve4(file));
|
|
3284
|
+
const document2 = assertDesignDocument(
|
|
3285
|
+
checked ? JSON.parse(checked.value.toString("utf8")) : readJson(path)
|
|
3286
|
+
);
|
|
3287
|
+
return { path, root: dirname4(path), document: document2 };
|
|
3288
|
+
}
|
|
3289
|
+
function designSpecPath(root) {
|
|
3290
|
+
return /(?:^|\/)output\/feats\/feat-[^/]+\/design$/u.test(root.replaceAll("\\", "/")) ? join4(dirname4(root), "design-spec.md") : join4(root, "design-spec.md");
|
|
3291
|
+
}
|
|
3292
|
+
function inspectDesignDocument(file, { readSource } = {}) {
|
|
3293
|
+
const { path, root, document: document2 } = loadDesignDocument(file, { readSource });
|
|
3294
|
+
const sources = new Set(document2.assets ?? []);
|
|
3295
|
+
if (existsSync3(join4(root, "review-context.json"))) sources.add("review-context.json");
|
|
3296
|
+
if (document2.designSystem?.tokens) sources.add(document2.designSystem.tokens);
|
|
3297
|
+
for (const screen of document2.screens)
|
|
3298
|
+
for (const source of [
|
|
3299
|
+
screen.source,
|
|
3300
|
+
...document2.variants.filter((variant) => variant.status === "ready").map((variant) => variant.sources?.[screen.id]).filter(Boolean)
|
|
3301
|
+
]) {
|
|
3302
|
+
sources.add(source.html);
|
|
3303
|
+
for (const item2 of [...source.styles ?? [], ...source.scripts ?? []]) sources.add(item2);
|
|
3304
|
+
}
|
|
3305
|
+
const missing = [];
|
|
3306
|
+
for (const source of sources) {
|
|
3307
|
+
try {
|
|
3308
|
+
if (readSource) readSource(source, root);
|
|
3309
|
+
else resolveLocalDocumentFile(root, source);
|
|
3310
|
+
} catch (error) {
|
|
3311
|
+
missing.push({ path: source, message: error.message });
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
return {
|
|
3315
|
+
ok: missing.length === 0,
|
|
3316
|
+
path,
|
|
3317
|
+
root,
|
|
3318
|
+
document: document2,
|
|
3319
|
+
sources: [...sources],
|
|
3320
|
+
missing,
|
|
3321
|
+
specPath: designSpecPath(root),
|
|
3322
|
+
// Guarded source preparation is independent of local render cache state.
|
|
3323
|
+
current: readSource ? null : readJson(join4(root, ".design/current.json"), null)
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
function prepareDesignDocument(file, { readSource, passive = false, maxBytes } = {}) {
|
|
3327
|
+
const inspected = inspectDesignDocument(file, { readSource });
|
|
3328
|
+
if (!inspected.ok)
|
|
3329
|
+
throw new Error(inspected.missing.map((item2) => `${item2.path}: ${item2.message}`).join("\n"));
|
|
3330
|
+
const { document: document2, root } = inspected;
|
|
3331
|
+
const reviewContext = loadReviewContext(root, document2, { readSource });
|
|
3332
|
+
const contextDigest = reviewDigest(reviewContext);
|
|
3333
|
+
const fingerprints = [];
|
|
3334
|
+
const artifacts = [], entries = [], lint = [], sourceFiles = new Set(inspected.sources);
|
|
3335
|
+
const sourceContents = new Map(
|
|
3336
|
+
inspected.sources.map((source) => [
|
|
3337
|
+
source,
|
|
3338
|
+
readSource ? readSource(source, root).value : readFileSync6(resolveLocalDocumentFile(root, source))
|
|
3339
|
+
])
|
|
3340
|
+
);
|
|
3341
|
+
for (const variant of document2.variants.filter((item2) => item2.status === "ready")) {
|
|
3342
|
+
for (const screenId of document2.screenOrder) {
|
|
3343
|
+
const screen = document2.screens.find((item2) => item2.id === screenId);
|
|
3344
|
+
const bundled = bundleLocalDocument({
|
|
3345
|
+
root,
|
|
3346
|
+
source: variant.sources?.[screenId] ?? screen.source,
|
|
3347
|
+
sharedStyles: document2.designSystem?.tokens ? [document2.designSystem.tokens] : [],
|
|
3348
|
+
screenId,
|
|
3349
|
+
readSource,
|
|
3350
|
+
passive,
|
|
3351
|
+
...maxBytes === void 0 ? {} : { maxBytes }
|
|
3352
|
+
});
|
|
3353
|
+
const navigate = `<script>document.addEventListener('click',function(event){var link=event.target.closest('[data-design-target],[data-planr-navigate],[data-design-navigate]');if(!link)return;event.preventDefault();parent.postMessage({type:'openplanr:design-navigate',screenId:link.getAttribute('data-design-target')||link.getAttribute('data-planr-navigate')||link.getAttribute('data-design-navigate')},'*')});document.addEventListener('submit',function(event){event.preventDefault()});</script>`;
|
|
3354
|
+
if (!passive) bundled.html = bundled.html.replace("</body>", `${navigate}</body>`);
|
|
3355
|
+
for (const name of bundled.files) {
|
|
3356
|
+
sourceFiles.add(name);
|
|
3357
|
+
if (!sourceContents.has(name))
|
|
3358
|
+
sourceContents.set(
|
|
3359
|
+
name,
|
|
3360
|
+
readSource ? readSource(name, root).value : readFileSync6(resolveLocalDocumentFile(root, name))
|
|
3361
|
+
);
|
|
3362
|
+
if (hash(sourceContents.get(name)) !== bundled.sourceDigests[name])
|
|
3363
|
+
throw new Error(
|
|
3364
|
+
`Source ${name} changed during rendering. Retry with the completed source revision.`
|
|
3365
|
+
);
|
|
3366
|
+
}
|
|
3367
|
+
const report = lintDesign(bundled.html, {
|
|
3368
|
+
designSystem: document2.designSystem
|
|
3369
|
+
});
|
|
3370
|
+
lint.push({ variantId: variant.id, screenId, ...report });
|
|
3371
|
+
for (const anchor of screen.anchors ?? []) {
|
|
3372
|
+
if (!bundled.html.includes(`data-planr-id="${anchor}"`) && !bundled.html.includes(`id="${anchor}"`))
|
|
3373
|
+
throw new Error(`Screen ${screenId} is missing its declared anchor ${anchor}.`);
|
|
3374
|
+
}
|
|
3375
|
+
for (const frame of document2.frames) {
|
|
3376
|
+
fingerprints.push(
|
|
3377
|
+
reviewFingerprints({
|
|
3378
|
+
document: document2,
|
|
3379
|
+
context: reviewContext,
|
|
3380
|
+
screen,
|
|
3381
|
+
variant,
|
|
3382
|
+
frame,
|
|
3383
|
+
sourceDigests: bundled.sourceDigests
|
|
3384
|
+
})
|
|
3385
|
+
);
|
|
3386
|
+
const artifactId = designStudioArtifactId(
|
|
3387
|
+
`${document2.id}-${variant.id}`,
|
|
3388
|
+
screenId,
|
|
3389
|
+
frame.id
|
|
3390
|
+
);
|
|
3391
|
+
artifacts.push({
|
|
3392
|
+
id: artifactId,
|
|
3393
|
+
kind: "html",
|
|
3394
|
+
title: `${screen.title} \xB7 ${variant.label} \xB7 ${frame.label}`,
|
|
3395
|
+
html: bundled.html,
|
|
3396
|
+
viewport: { width: frame.width, height: frame.height },
|
|
3397
|
+
colorScheme: "light"
|
|
3398
|
+
});
|
|
3399
|
+
entries.push({
|
|
3400
|
+
artifactId,
|
|
3401
|
+
screenId,
|
|
3402
|
+
variantId: variant.id,
|
|
3403
|
+
frameId: frame.id
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
const activeArtifactId = [...artifacts].sort((a, b) => a.id.localeCompare(b.id))[0].id;
|
|
3409
|
+
const envelope = createArtifactEnvelope({
|
|
3410
|
+
artifacts,
|
|
3411
|
+
viewer: {
|
|
3412
|
+
mode: artifacts.length > 1 ? "variants" : "single",
|
|
3413
|
+
activeArtifactId,
|
|
3414
|
+
presentation: "canvas"
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
const revision = hash(
|
|
3418
|
+
json({ document: document2, contextDigest, digest: digestArtifactEnvelope(envelope) })
|
|
3419
|
+
);
|
|
3420
|
+
return {
|
|
3421
|
+
...inspected,
|
|
3422
|
+
reviewContext,
|
|
3423
|
+
contextDigest,
|
|
3424
|
+
fingerprints,
|
|
3425
|
+
envelope,
|
|
3426
|
+
entries,
|
|
3427
|
+
revision,
|
|
3428
|
+
lint,
|
|
3429
|
+
sourceFiles: [...sourceFiles],
|
|
3430
|
+
sourceContents
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
function currentDesign(file) {
|
|
3434
|
+
const root = realpathSync3(dirname4(resolve4(file)));
|
|
3435
|
+
recoverDesignPublication(root);
|
|
3436
|
+
const pointer = readJson(join4(root, ".design/current.json"), null);
|
|
3437
|
+
if (!pointer || !/^[a-f0-9]{64}$/u.test(pointer.revision))
|
|
3438
|
+
throw new Error("Design has no completed render. Run the render utility first.");
|
|
3439
|
+
const directory = join4(root, ".design/revisions", pointer.revision);
|
|
3440
|
+
const prepared = readJson(join4(directory, "render.json"));
|
|
3441
|
+
return {
|
|
3442
|
+
...prepared,
|
|
3443
|
+
root,
|
|
3444
|
+
directory,
|
|
3445
|
+
file: resolve4(file),
|
|
3446
|
+
verification: readJson(join4(root, ".design/verification", `${pointer.revision}.json`), {
|
|
3447
|
+
status: "unverified",
|
|
3448
|
+
revision: pointer.revision
|
|
3449
|
+
})
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
function stageRuntimeBytes() {
|
|
3453
|
+
const stagePath = new URL(
|
|
3454
|
+
"../../../artifact/templates/artifact-review-stage.js",
|
|
3455
|
+
new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href
|
|
3456
|
+
);
|
|
3457
|
+
try {
|
|
3458
|
+
return readFileSync6(stagePath);
|
|
3459
|
+
} catch {
|
|
3460
|
+
return readFileSync6(new URL("../../templates/artifact-review-stage.js", new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href));
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
function designRendererRevision() {
|
|
3464
|
+
return hash(
|
|
3465
|
+
json({
|
|
3466
|
+
version: DESIGN_RENDERER_VERSION,
|
|
3467
|
+
stage: hash(stageRuntimeBytes()),
|
|
3468
|
+
assets: ["studio.css", "studio.js", "enhancements.css", "handoff-center.css"].filter((name) => existsSync3(new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href))).map(
|
|
3469
|
+
(name) => hash(readFileSync6(new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href)))
|
|
3470
|
+
)
|
|
3471
|
+
})
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
function standaloneDesignHtml(prepared, view = prepared.document.defaultView) {
|
|
3475
|
+
const nonce = createArtifactBridgeNonce();
|
|
3476
|
+
const artifacts = Object.fromEntries(
|
|
3477
|
+
prepared.envelope.artifacts.map((artifact) => [
|
|
3478
|
+
artifact.id,
|
|
3479
|
+
prepareArtifactDocument({
|
|
3480
|
+
html: artifact.html,
|
|
3481
|
+
artifactId: artifact.id,
|
|
3482
|
+
nonce,
|
|
3483
|
+
parentOrigin: "null",
|
|
3484
|
+
portable: true,
|
|
3485
|
+
allowLocalForms: true
|
|
3486
|
+
}).html
|
|
3487
|
+
])
|
|
3488
|
+
);
|
|
3489
|
+
const stage = stageRuntimeBytes();
|
|
3490
|
+
const stageRuntimeUrl = `data:text/javascript;base64,${Buffer.from(stage).toString("base64")}`;
|
|
3491
|
+
const runtime = renderArtifactParentRuntime({
|
|
3492
|
+
nonce,
|
|
3493
|
+
parentOrigin: "null",
|
|
3494
|
+
inlineArtifacts: artifacts,
|
|
3495
|
+
stageRuntimeUrl
|
|
3496
|
+
});
|
|
3497
|
+
const runtimeUrl = `data:text/javascript;base64,${Buffer.from(runtime).toString("base64")}`;
|
|
3498
|
+
const state = { ...prepared.state, view };
|
|
3499
|
+
if (state.selectedVariant) state.variantId = state.selectedVariant;
|
|
3500
|
+
return renderDesignStudio(
|
|
3501
|
+
{
|
|
3502
|
+
...prepared,
|
|
3503
|
+
state,
|
|
3504
|
+
verification: prepared.verification ?? { status: "unverified" }
|
|
3505
|
+
},
|
|
3506
|
+
{ stageRuntimeUrl: runtimeUrl }
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
async function renderDesignDocument(file, {
|
|
3510
|
+
beforeCommit,
|
|
3511
|
+
writePointer = atomicJson,
|
|
3512
|
+
rendererRevision = designRendererRevision(),
|
|
3513
|
+
now = () => (/* @__PURE__ */ new Date()).toISOString()
|
|
3514
|
+
} = {}) {
|
|
3515
|
+
const { root } = loadDesignDocument(file);
|
|
3516
|
+
const work = join4(root, ".design");
|
|
3517
|
+
mkdirSync2(work, { recursive: true });
|
|
3518
|
+
const release = await acquireStartLock(join4(work, "render.lock"), {
|
|
3519
|
+
timeout: 1e3,
|
|
3520
|
+
stale: 3e4
|
|
3521
|
+
});
|
|
3522
|
+
let temporary;
|
|
3523
|
+
try {
|
|
3524
|
+
recoverDesignPublication(root, { ownsRenderLock: true });
|
|
3525
|
+
const prepared = prepareDesignDocument(file);
|
|
3526
|
+
const errors = prepared.lint.flatMap(
|
|
3527
|
+
(item2) => item2.errors.map((error) => `${item2.screenId}: ${error.message}`)
|
|
3528
|
+
);
|
|
3529
|
+
if (errors.length) throw new Error(`Design lint failed:
|
|
3530
|
+
${errors.join("\n")}`);
|
|
3531
|
+
const specPath = designSpecPath(root);
|
|
3532
|
+
const spec = readFileSync6(specPath, "utf8");
|
|
3533
|
+
for (let section = 1; section <= 10; section++)
|
|
3534
|
+
if (!new RegExp(`^## ${section}\\. `, "m").test(spec))
|
|
3535
|
+
throw new Error(`design-spec.md is missing section ${section}.`);
|
|
3536
|
+
prepared.revision = hash(json({ source: prepared.revision, spec, rendererRevision }));
|
|
3537
|
+
const directory = join4(work, "revisions", prepared.revision);
|
|
3538
|
+
const previous = readJson(join4(work, "current.json"), null);
|
|
3539
|
+
const generatedAt = now();
|
|
3540
|
+
const manifest = existsSync3(directory) ? readJson(join4(directory, "render.json")).manifest : buildManifest({
|
|
3541
|
+
framework: "vanilla",
|
|
3542
|
+
designFormat: prepared.document.defaultView,
|
|
3543
|
+
source: prepared.document.brief.source,
|
|
3544
|
+
contentProvenance: prepared.document.brief.provenance,
|
|
3545
|
+
generatedAt,
|
|
3546
|
+
screens: prepared.document.screenOrder.map(
|
|
3547
|
+
(id3) => prepared.document.screens.find((item2) => item2.id === id3).title
|
|
3548
|
+
),
|
|
3549
|
+
iterations: previous && previous.revision !== prepared.revision ? (previous.iterations ?? 0) + 1 : previous?.iterations ?? 0,
|
|
3550
|
+
htmlFile: `.design/revisions/${prepared.revision}/${prepared.document.defaultView}.html`
|
|
3551
|
+
});
|
|
3552
|
+
const record = {
|
|
3553
|
+
reviewContext: prepared.reviewContext,
|
|
3554
|
+
contextDigest: prepared.contextDigest,
|
|
3555
|
+
fingerprints: prepared.fingerprints,
|
|
3556
|
+
document: prepared.document,
|
|
3557
|
+
envelope: prepared.envelope,
|
|
3558
|
+
entries: prepared.entries,
|
|
3559
|
+
revision: prepared.revision,
|
|
3560
|
+
rendererRevision,
|
|
3561
|
+
lint: prepared.lint,
|
|
3562
|
+
manifest
|
|
3563
|
+
};
|
|
3564
|
+
if (!existsSync3(directory)) {
|
|
3565
|
+
temporary = join4(work, `pending-${randomUUID()}`);
|
|
3566
|
+
mkdirSync2(join4(temporary, "sources"), { recursive: true });
|
|
3567
|
+
writeFileSync2(join4(temporary, "render.json"), json(record));
|
|
3568
|
+
writeFileSync2(join4(temporary, "design-document.json"), json(prepared.document));
|
|
3569
|
+
writeFileSync2(join4(temporary, "design-spec.md"), spec);
|
|
3570
|
+
for (const source of prepared.sourceFiles) {
|
|
3571
|
+
const destination = join4(temporary, "sources", source);
|
|
3572
|
+
mkdirSync2(dirname4(destination), { recursive: true });
|
|
3573
|
+
writeFileSync2(destination, prepared.sourceContents.get(source));
|
|
3574
|
+
}
|
|
3575
|
+
for (const view of DESIGN_VIEWS)
|
|
3576
|
+
writeFileSync2(join4(temporary, `${view}.html`), standaloneDesignHtml(record, view));
|
|
3577
|
+
beforeCommit?.(record);
|
|
3578
|
+
mkdirSync2(dirname4(directory), { recursive: true });
|
|
3579
|
+
renameSync2(temporary, directory);
|
|
3580
|
+
temporary = null;
|
|
3581
|
+
}
|
|
3582
|
+
const manifestPath = join4(root, "finalized.json");
|
|
3583
|
+
atomicJson(join4(work, "publication.json"), {
|
|
3584
|
+
revision: prepared.revision,
|
|
3585
|
+
previousRevision: previous?.revision ?? null,
|
|
3586
|
+
manifest,
|
|
3587
|
+
previousManifest: existsSync3(manifestPath) ? readFileSync6(manifestPath).toString("base64") : null
|
|
3588
|
+
});
|
|
3589
|
+
try {
|
|
3590
|
+
atomicJson(manifestPath, manifest);
|
|
3591
|
+
writePointer(join4(work, "current.json"), {
|
|
3592
|
+
revision: prepared.revision,
|
|
3593
|
+
previousRevision: previous?.revision === prepared.revision ? previous.previousRevision : previous?.revision ?? null,
|
|
3594
|
+
iterations: manifest.iterations,
|
|
3595
|
+
generatedAt: manifest.generated_at
|
|
3596
|
+
});
|
|
3597
|
+
rmSync2(join4(work, "publication.json"), { force: true });
|
|
3598
|
+
} catch (error) {
|
|
3599
|
+
recoverDesignPublication(root, { ownsRenderLock: true });
|
|
3600
|
+
throw error;
|
|
3601
|
+
}
|
|
3602
|
+
return {
|
|
3603
|
+
ok: true,
|
|
3604
|
+
revision: prepared.revision,
|
|
3605
|
+
document: resolve4(file),
|
|
3606
|
+
artifact: join4(directory, `${prepared.document.defaultView}.html`),
|
|
3607
|
+
views: Object.fromEntries(
|
|
3608
|
+
DESIGN_VIEWS.map((view) => [view, join4(directory, `${view}.html`)])
|
|
3609
|
+
),
|
|
3610
|
+
manifest: join4(root, "finalized.json"),
|
|
3611
|
+
spec: specPath,
|
|
3612
|
+
verification: "unverified"
|
|
3613
|
+
};
|
|
3614
|
+
} finally {
|
|
3615
|
+
if (temporary) rmSync2(temporary, { recursive: true, force: true });
|
|
3616
|
+
release();
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
export {
|
|
3621
|
+
configuredPlanrHome,
|
|
3622
|
+
planrHome,
|
|
3623
|
+
LOOPBACK_HOST,
|
|
3624
|
+
listenLoopback,
|
|
3625
|
+
closeHttpServer,
|
|
3626
|
+
readRequestBody,
|
|
3627
|
+
assertLoopbackRequest,
|
|
3628
|
+
acquireStartLock,
|
|
3629
|
+
mintCapabilityToken,
|
|
3630
|
+
isCapabilityToken,
|
|
3631
|
+
timingSafeTokenEqual,
|
|
3632
|
+
createArtifactBridgeNonce,
|
|
3633
|
+
prepareArtifactDocument,
|
|
3634
|
+
renderArtifactParentRuntime,
|
|
3635
|
+
canonicalizeJson,
|
|
3636
|
+
sha256Hex,
|
|
3637
|
+
deepFreeze,
|
|
3638
|
+
assertPlainData,
|
|
3639
|
+
DESIGN_WORKSPACE_VERSION,
|
|
3640
|
+
DESIGN_WORKSPACE_API,
|
|
3641
|
+
DESIGN_WORKSPACE_MAX_BYTES,
|
|
3642
|
+
DESIGN_WORKSPACE_MAX_EVENT_BYTES,
|
|
3643
|
+
DESIGN_WORKSPACE_REVISION_SCHEMA,
|
|
3644
|
+
DESIGN_WORKSPACE_EVENT_SCHEMA,
|
|
3645
|
+
DESIGN_WORKSPACE_CREATE_SCHEMA,
|
|
3646
|
+
DESIGN_WORKSPACE_SCHEMA,
|
|
3647
|
+
assertWorkspaceContract,
|
|
3648
|
+
DESIGN_HANDOFF_CONTENT_SCHEMA,
|
|
3649
|
+
DESIGN_HANDOFF_SCHEMA,
|
|
3650
|
+
assertReviewExperience,
|
|
3651
|
+
assertDesignReviewMetadata,
|
|
3652
|
+
assertDesignReviewBundle,
|
|
3653
|
+
reviewDigest,
|
|
3654
|
+
emptyReviewContext,
|
|
3655
|
+
bundleDesignRevision,
|
|
3656
|
+
listDesignRevisions,
|
|
3657
|
+
readDesignRevision,
|
|
3658
|
+
renderDesignStudio,
|
|
3659
|
+
DESIGN_VIEWS,
|
|
3660
|
+
DESIGN_RENDERER_VERSION,
|
|
3661
|
+
hash,
|
|
3662
|
+
json,
|
|
3663
|
+
readJson,
|
|
3664
|
+
atomicJson,
|
|
3665
|
+
recoverDesignPublication,
|
|
3666
|
+
loadDesignDocument,
|
|
3667
|
+
designSpecPath,
|
|
3668
|
+
inspectDesignDocument,
|
|
3669
|
+
prepareDesignDocument,
|
|
3670
|
+
currentDesign,
|
|
3671
|
+
designRendererRevision,
|
|
3672
|
+
standaloneDesignHtml,
|
|
3673
|
+
renderDesignDocument
|
|
3674
|
+
};
|