@wildwinter/app-shell 0.49.0 → 0.51.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/README.md +37 -0
- package/dist/{chunk-FGAPO4N5.js → chunk-B56FWJ5Y.js} +21 -12
- package/dist/chunk-B56FWJ5Y.js.map +1 -0
- package/dist/chunk-ELEDQRZ7.js +435 -0
- package/dist/chunk-ELEDQRZ7.js.map +1 -0
- package/dist/controls.css +107 -0
- package/dist/controls.css.map +1 -1
- package/dist/history.cjs +194 -0
- package/dist/history.cjs.map +1 -0
- package/dist/history.d.cts +111 -0
- package/dist/history.d.ts +111 -0
- package/dist/history.js +188 -0
- package/dist/history.js.map +1 -0
- package/dist/index.cjs +368 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{property-list-BPGU3rhF.d.cts → property-list-DlpxIkv6.d.cts} +9 -0
- package/dist/{property-list-BPGU3rhF.d.ts → property-list-DlpxIkv6.d.ts} +9 -0
- package/dist/property-list.cjs +18 -9
- package/dist/property-list.cjs.map +1 -1
- package/dist/property-list.d.cts +1 -1
- package/dist/property-list.d.ts +1 -1
- package/dist/property-list.js +2 -2
- package/dist/value-control.cjs +584 -0
- package/dist/value-control.cjs.map +1 -1
- package/dist/value-control.d.cts +61 -1
- package/dist/value-control.d.ts +61 -1
- package/dist/value-control.js +1 -1
- package/dist/value-rows.cjs +1104 -0
- package/dist/value-rows.cjs.map +1 -0
- package/dist/value-rows.d.cts +71 -0
- package/dist/value-rows.d.ts +71 -0
- package/dist/value-rows.js +298 -0
- package/dist/value-rows.js.map +1 -0
- package/dist/window-guard.cjs +74 -0
- package/dist/window-guard.cjs.map +1 -0
- package/dist/window-guard.d.cts +32 -0
- package/dist/window-guard.d.ts +32 -0
- package/dist/window-guard.js +67 -0
- package/dist/window-guard.js.map +1 -0
- package/package.json +26 -4
- package/dist/chunk-EAE7UWQ5.js +0 -84
- package/dist/chunk-EAE7UWQ5.js.map +0 -1
- package/dist/chunk-FGAPO4N5.js.map +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { fieldRow, fieldRowFocus } from './chunk-ELEDQRZ7.js';
|
|
2
|
+
import { el, iconBtn, moveItem } from './chunk-W4WC4FO5.js';
|
|
3
|
+
import { valueProblems, defaultOf } from '@wildwinter/toolkit/extensions';
|
|
4
|
+
|
|
5
|
+
var SCALARS = /* @__PURE__ */ new Set(["text", "multiline", "number", "boolean", "enum", "flags", "quality", "string"]);
|
|
6
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7
|
+
var nameOf = (decl) => decl.label ?? decl.name;
|
|
8
|
+
var asDecl = (d, name) => ({ ...d, name: d.name ?? name });
|
|
9
|
+
function ownProblems(decl, value, adapter) {
|
|
10
|
+
if (value === void 0) return "";
|
|
11
|
+
const exists = adapter ? (kind, id) => adapter.nodes(kind).some((n) => n.id === id) : void 0;
|
|
12
|
+
return valueProblems(decl, value, exists ? { exists } : {}).filter((p) => p.path === "" && p.severity === "error" && p.code !== "not-a-value" && p.code !== "not-a-stage").map((p) => `${p.message.replace(/^the value/, "The value")}.`).join(" ");
|
|
13
|
+
}
|
|
14
|
+
function valueRow(initial) {
|
|
15
|
+
const decl = initial.decl;
|
|
16
|
+
if (SCALARS.has(decl.type)) return scalarRow(initial);
|
|
17
|
+
if (decl.type === "reference") return referenceRow(initial);
|
|
18
|
+
return structuredRow(initial);
|
|
19
|
+
}
|
|
20
|
+
function scalarRow(initial) {
|
|
21
|
+
const map = (o) => {
|
|
22
|
+
const range = ownProblems(o.decl, o.value, o.adapter);
|
|
23
|
+
const problem = [range, o.problem ?? ""].filter((w) => w !== "").join(" ");
|
|
24
|
+
return {
|
|
25
|
+
decl: { ...o.decl, ...o.decl.help !== void 0 && o.decl.purpose === void 0 ? { purpose: o.decl.help } : {} },
|
|
26
|
+
value: o.value,
|
|
27
|
+
...o.onCommit ? { onCommit: (v) => o.onCommit(v) } : {},
|
|
28
|
+
...o.disabled !== void 0 ? { disabled: o.disabled } : {},
|
|
29
|
+
...problem !== "" ? { problem } : {},
|
|
30
|
+
label: o.label ?? nameOf(o.decl),
|
|
31
|
+
...o.className !== void 0 ? { className: o.className } : {},
|
|
32
|
+
...o.labelClassName !== void 0 ? { labelClassName: o.labelClassName } : {},
|
|
33
|
+
...o.controlClassName !== void 0 ? { controlClassName: o.controlClassName } : {}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
const handle = fieldRow(map(initial));
|
|
37
|
+
return { el: handle.el, update: (o) => handle.update(map(o)) };
|
|
38
|
+
}
|
|
39
|
+
function referenceRow(initial) {
|
|
40
|
+
let opts = initial;
|
|
41
|
+
if (!opts.adapter) {
|
|
42
|
+
const inner = scalarRow({ ...opts, decl: { ...opts.decl, type: "text" } });
|
|
43
|
+
return { el: inner.el, update: (o) => {
|
|
44
|
+
opts = o;
|
|
45
|
+
inner.update({ ...o, decl: { ...o.decl, type: "text" } });
|
|
46
|
+
} };
|
|
47
|
+
}
|
|
48
|
+
const row = el("div", "shell-fieldrow shell-valuerow-ref");
|
|
49
|
+
const caption = el("span", "shell-fieldrow-label");
|
|
50
|
+
const body = el("div", "shell-fieldrow-body");
|
|
51
|
+
const sel = el("select", "field shell-fieldrow-select");
|
|
52
|
+
const help = el("p", "shell-fieldrow-purpose");
|
|
53
|
+
const problem = el("p", "shell-fieldrow-problem");
|
|
54
|
+
problem.setAttribute("role", "status");
|
|
55
|
+
body.append(sel, help, problem);
|
|
56
|
+
row.append(caption, body);
|
|
57
|
+
sel.addEventListener("change", () => {
|
|
58
|
+
const v = sel.value === "" ? void 0 : sel.value;
|
|
59
|
+
opts = { ...opts, value: v };
|
|
60
|
+
draw(opts);
|
|
61
|
+
opts.onCommit?.(v);
|
|
62
|
+
});
|
|
63
|
+
const draw = (o) => {
|
|
64
|
+
opts = o;
|
|
65
|
+
row.className = `shell-fieldrow shell-valuerow-ref${o.className ? ` ${o.className}` : ""}`;
|
|
66
|
+
const label = o.label ?? nameOf(o.decl);
|
|
67
|
+
if (typeof label === "string") caption.textContent = label;
|
|
68
|
+
else caption.replaceChildren(label);
|
|
69
|
+
if (o.controlClassName) sel.className = `shell-fieldrow-select ${o.controlClassName}`;
|
|
70
|
+
const nodes = o.adapter.nodes(o.decl.to ?? "");
|
|
71
|
+
const held = typeof o.value === "string" ? o.value : "";
|
|
72
|
+
sel.replaceChildren();
|
|
73
|
+
const add = (v, text) => {
|
|
74
|
+
const opt = el("option", void 0, text);
|
|
75
|
+
opt.value = v;
|
|
76
|
+
sel.append(opt);
|
|
77
|
+
};
|
|
78
|
+
add("", "default (none)");
|
|
79
|
+
for (const n of nodes) add(n.id, n.label);
|
|
80
|
+
if (held !== "" && !nodes.some((n) => n.id === held)) add(held, `${held} (gone)`);
|
|
81
|
+
sel.value = held;
|
|
82
|
+
sel.disabled = o.onCommit === void 0 || o.disabled === true;
|
|
83
|
+
help.textContent = o.decl.help ?? o.decl.purpose ?? "";
|
|
84
|
+
help.hidden = help.textContent === "";
|
|
85
|
+
const words = [ownProblems(o.decl, o.value, o.adapter), o.problem ?? ""].filter((w) => w !== "").join(" ");
|
|
86
|
+
problem.textContent = words;
|
|
87
|
+
problem.hidden = words === "";
|
|
88
|
+
row.classList.toggle("has-problem", words !== "");
|
|
89
|
+
};
|
|
90
|
+
draw(initial);
|
|
91
|
+
return { el: row, update: draw };
|
|
92
|
+
}
|
|
93
|
+
function structuredRow(initial) {
|
|
94
|
+
let opts = initial;
|
|
95
|
+
const row = el("div", "shell-fieldrow shell-valuerow");
|
|
96
|
+
const caption = el("span", "shell-fieldrow-label");
|
|
97
|
+
const body = el("div", "shell-fieldrow-body shell-valuerow-parts");
|
|
98
|
+
const help = el("p", "shell-fieldrow-purpose");
|
|
99
|
+
const problem = el("p", "shell-fieldrow-problem");
|
|
100
|
+
problem.setAttribute("role", "status");
|
|
101
|
+
const adder = el("div", "shell-valuerow-add");
|
|
102
|
+
row.append(caption, body);
|
|
103
|
+
const parts = /* @__PURE__ */ new Map();
|
|
104
|
+
let kept = [];
|
|
105
|
+
let addKey = "";
|
|
106
|
+
const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
|
|
107
|
+
const commit = (v) => {
|
|
108
|
+
opts = { ...opts, value: v };
|
|
109
|
+
draw(opts);
|
|
110
|
+
opts.onCommit?.(v);
|
|
111
|
+
};
|
|
112
|
+
const part = (key, o, controls = []) => {
|
|
113
|
+
const had = parts.get(key);
|
|
114
|
+
if (had && had.handle.el.dataset["type"] === o.decl.type) {
|
|
115
|
+
fieldRowFocus.settle(had.handle.el);
|
|
116
|
+
had.handle.update(o);
|
|
117
|
+
had.tools.replaceChildren(...controls);
|
|
118
|
+
kept.push(had.handle.el);
|
|
119
|
+
return had.holder;
|
|
120
|
+
}
|
|
121
|
+
const handle = valueRow(o);
|
|
122
|
+
handle.el.dataset["type"] = o.decl.type;
|
|
123
|
+
const holder = el("div", "shell-valuerow-part");
|
|
124
|
+
const tools = el("span", "shell-valuerow-controls");
|
|
125
|
+
tools.append(...controls);
|
|
126
|
+
holder.append(handle.el, tools);
|
|
127
|
+
parts.set(key, { key, handle, holder, tools });
|
|
128
|
+
return holder;
|
|
129
|
+
};
|
|
130
|
+
const draw = (o) => {
|
|
131
|
+
opts = o;
|
|
132
|
+
kept = [];
|
|
133
|
+
const decl = o.decl;
|
|
134
|
+
row.className = `shell-fieldrow shell-valuerow shell-valuerow-${decl.type}${o.className ? ` ${o.className}` : ""}`;
|
|
135
|
+
const label = o.label ?? nameOf(decl);
|
|
136
|
+
if (typeof label === "string") caption.textContent = label;
|
|
137
|
+
else caption.replaceChildren(label);
|
|
138
|
+
const used = /* @__PURE__ */ new Set();
|
|
139
|
+
const rows = [];
|
|
140
|
+
const child = (key, childDecl, value, onCommit, controls, childLabel, extraProblem) => {
|
|
141
|
+
used.add(key);
|
|
142
|
+
rows.push(part(key, {
|
|
143
|
+
decl: childDecl,
|
|
144
|
+
value,
|
|
145
|
+
...editable() ? { onCommit } : {},
|
|
146
|
+
...o.adapter ? { adapter: o.adapter } : {},
|
|
147
|
+
...o.disabled !== void 0 ? { disabled: o.disabled } : {},
|
|
148
|
+
...childLabel !== void 0 ? { label: childLabel } : {},
|
|
149
|
+
...extraProblem ? { problem: extraProblem } : {},
|
|
150
|
+
...o.controlClassName !== void 0 ? { controlClassName: o.controlClassName } : {}
|
|
151
|
+
}, controls));
|
|
152
|
+
};
|
|
153
|
+
if (decl.type === "record") {
|
|
154
|
+
const value = isObject(o.value) ? o.value : void 0;
|
|
155
|
+
const required = value === void 0 ? [] : valueProblems(decl, value).filter((p) => p.code === "required");
|
|
156
|
+
for (const f of decl.fields ?? []) {
|
|
157
|
+
const missing = required.find((p) => p.path === f.name);
|
|
158
|
+
child(`f:${f.name}`, f, value?.[f.name], (v) => {
|
|
159
|
+
const next = { ...isObject(opts.value) ? opts.value : {} };
|
|
160
|
+
if (v === void 0) delete next[f.name];
|
|
161
|
+
else next[f.name] = v;
|
|
162
|
+
commit(Object.keys(next).length > 0 ? next : void 0);
|
|
163
|
+
}, void 0, void 0, missing ? "Required." : void 0);
|
|
164
|
+
}
|
|
165
|
+
} else if (decl.type === "map" && decl.of) {
|
|
166
|
+
const value = isObject(o.value) ? o.value : {};
|
|
167
|
+
const keyNodes = decl.key?.type === "reference" && o.adapter ? o.adapter.nodes(decl.key.to ?? "") : void 0;
|
|
168
|
+
const keyLabel = (k) => keyNodes?.find((n) => n.id === k)?.label ?? k;
|
|
169
|
+
for (const k of Object.keys(value)) {
|
|
170
|
+
const remove = editable() ? [iconBtn("close", `Remove ${keyLabel(k)}`, () => {
|
|
171
|
+
const next = { ...isObject(opts.value) ? opts.value : {} };
|
|
172
|
+
delete next[k];
|
|
173
|
+
commit(Object.keys(next).length > 0 ? next : void 0);
|
|
174
|
+
})] : [];
|
|
175
|
+
const gone = keyNodes !== void 0 && !keyNodes.some((n) => n.id === k) ? `${keyLabel(k)} is not here any more.` : void 0;
|
|
176
|
+
child(`k:${k}`, asDecl(decl.of, k), value[k], (v) => {
|
|
177
|
+
const next = { ...isObject(opts.value) ? opts.value : {} };
|
|
178
|
+
next[k] = v === void 0 ? defaultOf(decl.of) : v;
|
|
179
|
+
commit(next);
|
|
180
|
+
}, remove, keyLabel(k), gone);
|
|
181
|
+
}
|
|
182
|
+
adder.replaceChildren();
|
|
183
|
+
if (editable()) {
|
|
184
|
+
const add = (k) => {
|
|
185
|
+
if (k === "" || Object.prototype.hasOwnProperty.call(value, k)) return;
|
|
186
|
+
commit({ ...value, [k]: defaultOf(decl.of) });
|
|
187
|
+
};
|
|
188
|
+
if (keyNodes !== void 0) {
|
|
189
|
+
const pick = el("select", "field shell-valuerow-key");
|
|
190
|
+
const first = el("option", void 0, "Add...");
|
|
191
|
+
first.value = "";
|
|
192
|
+
pick.append(first);
|
|
193
|
+
for (const n of keyNodes.filter((n2) => !Object.prototype.hasOwnProperty.call(value, n2.id))) {
|
|
194
|
+
const opt = el("option", void 0, n.label);
|
|
195
|
+
opt.value = n.id;
|
|
196
|
+
pick.append(opt);
|
|
197
|
+
}
|
|
198
|
+
pick.addEventListener("change", () => add(pick.value));
|
|
199
|
+
adder.append(pick);
|
|
200
|
+
} else {
|
|
201
|
+
const input = el("input", "field shell-valuerow-key");
|
|
202
|
+
input.type = "text";
|
|
203
|
+
input.placeholder = "New key";
|
|
204
|
+
input.value = addKey;
|
|
205
|
+
input.addEventListener("input", () => {
|
|
206
|
+
addKey = input.value;
|
|
207
|
+
});
|
|
208
|
+
const go = () => {
|
|
209
|
+
const k = input.value.trim();
|
|
210
|
+
addKey = "";
|
|
211
|
+
add(k);
|
|
212
|
+
};
|
|
213
|
+
input.addEventListener("keydown", (e) => {
|
|
214
|
+
if (e.key === "Enter") go();
|
|
215
|
+
});
|
|
216
|
+
adder.append(input, iconBtn("add", "Add", go));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else if (decl.type === "list" && decl.of) {
|
|
220
|
+
const value = Array.isArray(o.value) ? o.value : [];
|
|
221
|
+
const idOf = (item, i) => decl.id !== void 0 && isObject(item) && typeof item[decl.id] === "string" ? `id:${item[decl.id]}` : `i:${i}`;
|
|
222
|
+
value.forEach((item, i) => {
|
|
223
|
+
const set = (next) => commit(next.length > 0 ? next : void 0);
|
|
224
|
+
const controls = editable() ? [
|
|
225
|
+
iconBtn("up", "Move up", () => {
|
|
226
|
+
const next = [...value];
|
|
227
|
+
if (moveItem(next, i, -1)) set(next);
|
|
228
|
+
}, i === 0),
|
|
229
|
+
iconBtn("down", "Move down", () => {
|
|
230
|
+
const next = [...value];
|
|
231
|
+
if (moveItem(next, i, 1)) set(next);
|
|
232
|
+
}, i === value.length - 1),
|
|
233
|
+
iconBtn("close", "Remove", () => set(value.filter((_, j) => j !== i)), false, true)
|
|
234
|
+
] : [];
|
|
235
|
+
const itemLabel = decl.id !== void 0 && isObject(item) && typeof item[decl.id] === "string" ? item[decl.id] : `${i + 1}`;
|
|
236
|
+
child(idOf(item, i), asDecl(decl.of, itemLabel), item, (v) => {
|
|
237
|
+
const next = [...Array.isArray(opts.value) ? opts.value : []];
|
|
238
|
+
next[i] = v === void 0 ? defaultOf(decl.of) : v;
|
|
239
|
+
set(next);
|
|
240
|
+
}, controls, itemLabel);
|
|
241
|
+
});
|
|
242
|
+
adder.replaceChildren();
|
|
243
|
+
if (editable()) {
|
|
244
|
+
adder.append(iconBtn("add", "Add", () => {
|
|
245
|
+
const fresh = defaultOf(decl.of);
|
|
246
|
+
if (decl.id !== void 0 && isObject(fresh)) {
|
|
247
|
+
const taken = new Set(value.map((x) => isObject(x) ? x[decl.id] : void 0));
|
|
248
|
+
let n = value.length + 1;
|
|
249
|
+
while (taken.has(`item-${n}`)) n++;
|
|
250
|
+
fresh[decl.id] = `item-${n}`;
|
|
251
|
+
}
|
|
252
|
+
commit([...value, fresh]);
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const key of [...parts.keys()]) if (!used.has(key)) parts.delete(key);
|
|
257
|
+
body.replaceChildren(...rows);
|
|
258
|
+
if (decl.type !== "record" && editable()) body.append(adder);
|
|
259
|
+
help.textContent = decl.help ?? decl.purpose ?? "";
|
|
260
|
+
if (help.textContent !== "") body.append(help);
|
|
261
|
+
const words = [ownProblems(decl, o.value, o.adapter), o.problem ?? ""].filter((w) => w !== "").join(" ");
|
|
262
|
+
problem.textContent = words;
|
|
263
|
+
problem.hidden = words === "";
|
|
264
|
+
body.append(problem);
|
|
265
|
+
row.classList.toggle("has-problem", words !== "");
|
|
266
|
+
for (const k of kept) fieldRowFocus.restore(k);
|
|
267
|
+
};
|
|
268
|
+
draw(initial);
|
|
269
|
+
return { el: row, update: draw };
|
|
270
|
+
}
|
|
271
|
+
function valueRows() {
|
|
272
|
+
const rows = /* @__PURE__ */ new Map();
|
|
273
|
+
return {
|
|
274
|
+
row(key, opts) {
|
|
275
|
+
const had = rows.get(key);
|
|
276
|
+
if (had && had.type === opts.decl.type) {
|
|
277
|
+
had.used = true;
|
|
278
|
+
fieldRowFocus.settle(had.handle.el);
|
|
279
|
+
had.handle.update(opts);
|
|
280
|
+
fieldRowFocus.restore(had.handle.el);
|
|
281
|
+
return had.handle.el;
|
|
282
|
+
}
|
|
283
|
+
const handle = valueRow(opts);
|
|
284
|
+
rows.set(key, { handle, type: opts.decl.type, used: true });
|
|
285
|
+
return handle.el;
|
|
286
|
+
},
|
|
287
|
+
sweep() {
|
|
288
|
+
for (const [key, entry] of rows) {
|
|
289
|
+
if (!entry.used) rows.delete(key);
|
|
290
|
+
else entry.used = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { valueRow, valueRows };
|
|
297
|
+
//# sourceMappingURL=value-rows.js.map
|
|
298
|
+
//# sourceMappingURL=value-rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/value-rows.ts"],"names":["n"],"mappings":"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","file":"value-rows.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Rows for a value of ANY declared type: the seven scalars through the field\n// row, and the types an extension's fields add (patterkit design/proposals/\n// patter-storylet-extensions.md, \"Tier 1: fields\"): a `reference` to one of the\n// host's nodes, a `record` of named fields, a `map` keyed by a reference or by\n// text, and a `list` with ids or without.\n//\n// A structured value is drawn as its parts, each part a row of its own, so the\n// family's field rules hold all the way down: commit as typed, Escape restores,\n// the unset state names what it falls back to, a value the declaration refuses\n// is said under it, and a row keeps its element (and its focus) across a redraw.\n// The checks are `@wildwinter/toolkit/extensions`' (`valueProblems`), the same\n// ones the command lines run, so the editor and validation never disagree.\n//\n// The host supplies what only it knows through an adapter: which nodes there are\n// of a kind (a map keyed by tag offers the tags, a reference to a card the\n// cards). No Electron, no app state: DOM only.\n//\n// Its own entry (`@wildwinter/app-shell/value-rows`) rather than part of\n// value-control, because it needs `@wildwinter/toolkit/extensions` (an optional\n// peer, toolkit 0.2.0 or later) and the scalar row does not: a host that draws\n// only the family's seven types takes nothing new.\n// ---------------------------------------------------------------------------\n\nimport { defaultOf, valueProblems } from \"@wildwinter/toolkit/extensions\";\nimport type { TypeDecl } from \"@wildwinter/toolkit/extensions\";\nimport { el, iconBtn, moveItem } from \"./dom.js\";\nimport { fieldRow, fieldRowFocus } from \"./value-control.js\";\nimport type { FieldDeclLike, FieldRowHandle, FieldValue } from \"./value-control.js\";\n\n/** A declaration of any type, as an extension's manifest writes it. */\nexport interface ValueDecl extends FieldDeclLike {\n /** The caption; the name when absent. */\n label?: string;\n /** A sentence or two shown under the control (an extension's `help`, a field's `purpose`). */\n help?: string;\n /** A reference's node kind. */\n to?: string;\n /** A record's fields. */\n fields?: ValueDecl[];\n /** A map's key: text, or a reference to a node kind. */\n key?: { type: string; to?: string };\n /** A map's or a list's value type. */\n of?: Omit<ValueDecl, \"name\"> & { name?: string };\n /** A list's id field: the text field of its records that keys each item. */\n id?: string;\n min?: number;\n max?: number;\n required?: boolean;\n}\n\n/** One node the host offers, by id, with the name the author knows it by. */\nexport interface ValueNode { id: string; label: string }\n\n/** What only the host knows: the nodes of a kind (`card`, `tag`, `cast`...), for references and map keys. */\nexport interface ValueAdapter {\n nodes(kind: string): readonly ValueNode[];\n}\n\nexport interface ValueRowOptions {\n decl: ValueDecl;\n /** The node's own value, or undefined where it sets none. */\n value: unknown;\n /** Commit a value; undefined clears it, back to the default. Absent: read-only. */\n onCommit?: (value: unknown) => void;\n /** The host's nodes, for references and map keys. Absent: a reference is typed as an id. */\n adapter?: ValueAdapter;\n disabled?: boolean;\n /** A message of the app's own about this value, shown under it. */\n problem?: string;\n /** The caption; default the declaration's label, else its name. */\n label?: string | Node;\n className?: string;\n labelClassName?: string;\n controlClassName?: string;\n}\n\nexport interface ValueRowHandle {\n readonly el: HTMLElement;\n update(opts: ValueRowOptions): void;\n}\n\nconst SCALARS = new Set([\"text\", \"multiline\", \"number\", \"boolean\", \"enum\", \"flags\", \"quality\", \"string\"]);\nconst isObject = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\nconst nameOf = (decl: ValueDecl): string => decl.label ?? decl.name;\nconst asDecl = (d: Omit<ValueDecl, \"name\"> & { name?: string }, name: string): ValueDecl => ({ ...d, name: d.name ?? name });\n\n/** The toolkit's checks for this value, as words for the row itself (the parts say their own). */\nfunction ownProblems(decl: ValueDecl, value: unknown, adapter: ValueAdapter | undefined): string {\n if (value === undefined) return \"\";\n const exists = adapter ? (kind: string, id: string) => adapter.nodes(kind).some((n) => n.id === id) : undefined;\n return valueProblems(decl as unknown as TypeDecl, value, exists ? { exists } : {})\n .filter((p) => p.path === \"\" && p.severity === \"error\" && p.code !== \"not-a-value\" && p.code !== \"not-a-stage\")\n .map((p) => `${p.message.replace(/^the value/, \"The value\")}.`)\n .join(\" \");\n}\n\n/**\n * A row for a value of `decl`'s type, whatever it is: a scalar as the field row, a reference as a choice of\n * the host's nodes, and a record, map or list as its parts, each a row of its own. Keeps its element, and its\n * parts' elements, across `update`, so a host that redraws on every change keeps the author's place.\n */\nexport function valueRow(initial: ValueRowOptions): ValueRowHandle {\n const decl = initial.decl;\n if (SCALARS.has(decl.type)) return scalarRow(initial);\n if (decl.type === \"reference\") return referenceRow(initial);\n return structuredRow(initial);\n}\n\n/** The field row, with an extension's help as its purpose and the toolkit's range checks. */\nfunction scalarRow(initial: ValueRowOptions): ValueRowHandle {\n const map = (o: ValueRowOptions) => {\n const range = ownProblems(o.decl, o.value, o.adapter);\n const problem = [range, o.problem ?? \"\"].filter((w) => w !== \"\").join(\" \");\n return {\n decl: { ...o.decl, ...(o.decl.help !== undefined && o.decl.purpose === undefined ? { purpose: o.decl.help } : {}) },\n value: o.value,\n ...(o.onCommit ? { onCommit: (v: FieldValue | undefined) => o.onCommit!(v) } : {}),\n ...(o.disabled !== undefined ? { disabled: o.disabled } : {}),\n ...(problem !== \"\" ? { problem } : {}),\n label: o.label ?? nameOf(o.decl),\n ...(o.className !== undefined ? { className: o.className } : {}),\n ...(o.labelClassName !== undefined ? { labelClassName: o.labelClassName } : {}),\n ...(o.controlClassName !== undefined ? { controlClassName: o.controlClassName } : {}),\n };\n };\n const handle: FieldRowHandle = fieldRow(map(initial));\n return { el: handle.el, update: (o) => handle.update(map(o)) };\n}\n\n/** A reference: a choice of the host's nodes of its kind, \"default (none)\" first; a node that has gone stays\n * visible as what is held. Without an adapter, the id is typed. */\nfunction referenceRow(initial: ValueRowOptions): ValueRowHandle {\n let opts = initial;\n if (!opts.adapter) {\n const inner = scalarRow({ ...opts, decl: { ...opts.decl, type: \"text\" } });\n return { el: inner.el, update: (o) => { opts = o; inner.update({ ...o, decl: { ...o.decl, type: \"text\" } }); } };\n }\n const row = el(\"div\", \"shell-fieldrow shell-valuerow-ref\");\n const caption = el(\"span\", \"shell-fieldrow-label\");\n const body = el(\"div\", \"shell-fieldrow-body\");\n const sel = el(\"select\", \"field shell-fieldrow-select\");\n const help = el(\"p\", \"shell-fieldrow-purpose\");\n const problem = el(\"p\", \"shell-fieldrow-problem\");\n problem.setAttribute(\"role\", \"status\");\n body.append(sel, help, problem);\n row.append(caption, body);\n sel.addEventListener(\"change\", () => {\n const v = sel.value === \"\" ? undefined : sel.value;\n opts = { ...opts, value: v };\n draw(opts);\n opts.onCommit?.(v);\n });\n const draw = (o: ValueRowOptions): void => {\n opts = o;\n row.className = `shell-fieldrow shell-valuerow-ref${o.className ? ` ${o.className}` : \"\"}`;\n const label = o.label ?? nameOf(o.decl);\n if (typeof label === \"string\") caption.textContent = label; else caption.replaceChildren(label);\n if (o.controlClassName) sel.className = `shell-fieldrow-select ${o.controlClassName}`;\n const nodes = o.adapter!.nodes(o.decl.to ?? \"\");\n const held = typeof o.value === \"string\" ? o.value : \"\";\n sel.replaceChildren();\n const add = (v: string, text: string): void => { const opt = el(\"option\", undefined, text); opt.value = v; sel.append(opt); };\n add(\"\", \"default (none)\");\n for (const n of nodes) add(n.id, n.label);\n if (held !== \"\" && !nodes.some((n) => n.id === held)) add(held, `${held} (gone)`);\n sel.value = held;\n sel.disabled = o.onCommit === undefined || o.disabled === true;\n help.textContent = o.decl.help ?? o.decl.purpose ?? \"\";\n help.hidden = help.textContent === \"\";\n const words = [ownProblems(o.decl, o.value, o.adapter), o.problem ?? \"\"].filter((w) => w !== \"\").join(\" \");\n problem.textContent = words;\n problem.hidden = words === \"\";\n row.classList.toggle(\"has-problem\", words !== \"\");\n };\n draw(initial);\n return { el: row, update: draw };\n}\n\n/** A part of a structured value: a row, keyed so it is the same element next time. */\ninterface Part { key: string; handle: ValueRowHandle; holder: HTMLElement; tools: HTMLElement }\n\n/** A record, a map or a list: a caption, then its parts, each a row of its own, with the controls the type\n * calls for (add and remove for a map; add, remove and move for a list). */\nfunction structuredRow(initial: ValueRowOptions): ValueRowHandle {\n let opts = initial;\n const row = el(\"div\", \"shell-fieldrow shell-valuerow\");\n const caption = el(\"span\", \"shell-fieldrow-label\");\n const body = el(\"div\", \"shell-fieldrow-body shell-valuerow-parts\");\n const help = el(\"p\", \"shell-fieldrow-purpose\");\n const problem = el(\"p\", \"shell-fieldrow-problem\");\n problem.setAttribute(\"role\", \"status\");\n const adder = el(\"div\", \"shell-valuerow-add\");\n row.append(caption, body);\n const parts = new Map<string, Part>();\n /** The parts kept from the last draw, whose focus goes back to them once they are in the page again. */\n let kept: HTMLElement[] = [];\n let addKey = \"\";\n\n const editable = (): boolean => opts.onCommit !== undefined && opts.disabled !== true;\n const commit = (v: unknown): void => { opts = { ...opts, value: v }; draw(opts); opts.onCommit?.(v); };\n\n /** The row for one part, the same element as last time for the same key. */\n const part = (key: string, o: ValueRowOptions, controls: HTMLElement[] = []): HTMLElement => {\n const had = parts.get(key);\n if (had && had.handle.el.dataset[\"type\"] === o.decl.type) {\n fieldRowFocus.settle(had.handle.el);\n had.handle.update(o);\n // Its own slot, not the first one inside it: a structured part has slots of its own further down.\n had.tools.replaceChildren(...controls);\n kept.push(had.handle.el);\n return had.holder;\n }\n const handle = valueRow(o);\n handle.el.dataset[\"type\"] = o.decl.type;\n const holder = el(\"div\", \"shell-valuerow-part\");\n const tools = el(\"span\", \"shell-valuerow-controls\");\n tools.append(...controls);\n holder.append(handle.el, tools);\n parts.set(key, { key, handle, holder, tools });\n return holder;\n };\n\n const draw = (o: ValueRowOptions): void => {\n opts = o;\n kept = [];\n const decl = o.decl;\n row.className = `shell-fieldrow shell-valuerow shell-valuerow-${decl.type}${o.className ? ` ${o.className}` : \"\"}`;\n const label = o.label ?? nameOf(decl);\n if (typeof label === \"string\") caption.textContent = label; else caption.replaceChildren(label);\n const used = new Set<string>();\n const rows: HTMLElement[] = [];\n const child = (key: string, childDecl: ValueDecl, value: unknown, onCommit: (v: unknown) => void, controls?: HTMLElement[], childLabel?: string, extraProblem?: string): void => {\n used.add(key);\n rows.push(part(key, {\n decl: childDecl, value,\n ...(editable() ? { onCommit } : {}),\n ...(o.adapter ? { adapter: o.adapter } : {}),\n ...(o.disabled !== undefined ? { disabled: o.disabled } : {}),\n ...(childLabel !== undefined ? { label: childLabel } : {}),\n ...(extraProblem ? { problem: extraProblem } : {}),\n ...(o.controlClassName !== undefined ? { controlClassName: o.controlClassName } : {}),\n }, controls));\n };\n\n if (decl.type === \"record\") {\n const value = isObject(o.value) ? o.value : undefined;\n const required = value === undefined ? [] : valueProblems(decl as unknown as TypeDecl, value).filter((p) => p.code === \"required\");\n for (const f of decl.fields ?? []) {\n const missing = required.find((p) => p.path === f.name);\n child(`f:${f.name}`, f, value?.[f.name], (v) => {\n const next: Record<string, unknown> = { ...(isObject(opts.value) ? opts.value : {}) };\n if (v === undefined) delete next[f.name]; else next[f.name] = v;\n commit(Object.keys(next).length > 0 ? next : undefined);\n }, undefined, undefined, missing ? \"Required.\" : undefined);\n }\n } else if (decl.type === \"map\" && decl.of) {\n const value = isObject(o.value) ? o.value : {};\n const keyNodes = decl.key?.type === \"reference\" && o.adapter ? o.adapter.nodes(decl.key.to ?? \"\") : undefined;\n const keyLabel = (k: string): string => keyNodes?.find((n) => n.id === k)?.label ?? k;\n for (const k of Object.keys(value)) {\n const remove = editable() ? [iconBtn(\"close\", `Remove ${keyLabel(k)}`, () => {\n const next = { ...(isObject(opts.value) ? opts.value : {}) };\n delete next[k];\n commit(Object.keys(next).length > 0 ? next : undefined);\n })] : [];\n const gone = keyNodes !== undefined && !keyNodes.some((n) => n.id === k) ? `${keyLabel(k)} is not here any more.` : undefined;\n child(`k:${k}`, asDecl(decl.of, k), value[k], (v) => {\n const next = { ...(isObject(opts.value) ? opts.value : {}) };\n next[k] = v === undefined ? defaultOf(decl.of as unknown as TypeDecl) : v;\n commit(next);\n }, remove, keyLabel(k), gone);\n }\n adder.replaceChildren();\n if (editable()) {\n const add = (k: string): void => {\n if (k === \"\" || Object.prototype.hasOwnProperty.call(value, k)) return;\n commit({ ...value, [k]: defaultOf(decl.of as unknown as TypeDecl) });\n };\n if (keyNodes !== undefined) {\n const pick = el(\"select\", \"field shell-valuerow-key\");\n const first = el(\"option\", undefined, \"Add...\"); first.value = \"\"; pick.append(first);\n for (const n of keyNodes.filter((n) => !Object.prototype.hasOwnProperty.call(value, n.id))) {\n const opt = el(\"option\", undefined, n.label); opt.value = n.id; pick.append(opt);\n }\n pick.addEventListener(\"change\", () => add(pick.value));\n adder.append(pick);\n } else {\n const input = el(\"input\", \"field shell-valuerow-key\");\n input.type = \"text\"; input.placeholder = \"New key\"; input.value = addKey;\n input.addEventListener(\"input\", () => { addKey = input.value; });\n const go = (): void => { const k = input.value.trim(); addKey = \"\"; add(k); };\n input.addEventListener(\"keydown\", (e) => { if ((e as KeyboardEvent).key === \"Enter\") go(); });\n adder.append(input, iconBtn(\"add\", \"Add\", go));\n }\n }\n } else if (decl.type === \"list\" && decl.of) {\n const value = Array.isArray(o.value) ? o.value : [];\n const idOf = (item: unknown, i: number): string =>\n decl.id !== undefined && isObject(item) && typeof item[decl.id] === \"string\" ? `id:${item[decl.id] as string}` : `i:${i}`;\n value.forEach((item, i) => {\n const set = (next: unknown[]): void => commit(next.length > 0 ? next : undefined);\n const controls = editable() ? [\n iconBtn(\"up\", \"Move up\", () => { const next = [...value]; if (moveItem(next, i, -1)) set(next); }, i === 0),\n iconBtn(\"down\", \"Move down\", () => { const next = [...value]; if (moveItem(next, i, 1)) set(next); }, i === value.length - 1),\n iconBtn(\"close\", \"Remove\", () => set(value.filter((_, j) => j !== i)), false, true),\n ] : [];\n const itemLabel = decl.id !== undefined && isObject(item) && typeof item[decl.id] === \"string\" ? item[decl.id] as string : `${i + 1}`;\n child(idOf(item, i), asDecl(decl.of!, itemLabel), item, (v) => {\n const next = [...(Array.isArray(opts.value) ? opts.value : [])];\n next[i] = v === undefined ? defaultOf(decl.of as unknown as TypeDecl) : v;\n set(next);\n }, controls, itemLabel);\n });\n adder.replaceChildren();\n if (editable()) {\n adder.append(iconBtn(\"add\", \"Add\", () => {\n const fresh = defaultOf(decl.of as unknown as TypeDecl);\n if (decl.id !== undefined && isObject(fresh)) {\n const taken = new Set(value.map((x) => (isObject(x) ? x[decl.id!] : undefined)));\n let n = value.length + 1;\n while (taken.has(`item-${n}`)) n++;\n fresh[decl.id] = `item-${n}`;\n }\n commit([...value, fresh]);\n }));\n }\n }\n\n for (const key of [...parts.keys()]) if (!used.has(key)) parts.delete(key);\n body.replaceChildren(...rows);\n if (decl.type !== \"record\" && editable()) body.append(adder);\n help.textContent = decl.help ?? decl.purpose ?? \"\";\n if (help.textContent !== \"\") body.append(help);\n const words = [ownProblems(decl, o.value, o.adapter), o.problem ?? \"\"].filter((w) => w !== \"\").join(\" \");\n problem.textContent = words;\n problem.hidden = words === \"\";\n body.append(problem);\n row.classList.toggle(\"has-problem\", words !== \"\");\n for (const k of kept) fieldRowFocus.restore(k);\n };\n draw(initial);\n return { el: row, update: draw };\n}\n\n/** A cache of value rows for a host that redraws its whole view (as `fieldRows` is for the scalar row). */\nexport interface ValueRows {\n row(key: string, opts: ValueRowOptions): HTMLElement;\n sweep(): void;\n}\n\nexport function valueRows(): ValueRows {\n const rows = new Map<string, { handle: ValueRowHandle; type: string; used: boolean }>();\n return {\n row(key, opts) {\n const had = rows.get(key);\n if (had && had.type === opts.decl.type) {\n had.used = true;\n fieldRowFocus.settle(had.handle.el);\n had.handle.update(opts);\n fieldRowFocus.restore(had.handle.el);\n return had.handle.el;\n }\n const handle = valueRow(opts);\n rows.set(key, { handle, type: opts.decl.type, used: true });\n return handle.el;\n },\n sweep() {\n for (const [key, entry] of rows) { if (!entry.used) rows.delete(key); else entry.used = false; }\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/window-guard.ts
|
|
4
|
+
var FAMILY_CSP = {
|
|
5
|
+
"default-src": ["'self'"],
|
|
6
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
7
|
+
"img-src": ["'self'", "data:"],
|
|
8
|
+
"object-src": ["'none'"],
|
|
9
|
+
"base-uri": ["'none'"]
|
|
10
|
+
};
|
|
11
|
+
function familyCsp(extra = {}) {
|
|
12
|
+
const merged = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);
|
|
14
|
+
for (const [name, sources] of Object.entries(extra)) {
|
|
15
|
+
const into = merged.get(name) ?? [...FAMILY_CSP["default-src"] ?? []];
|
|
16
|
+
for (const s of sources) if (!into.includes(s)) into.push(s);
|
|
17
|
+
merged.set(name, into);
|
|
18
|
+
}
|
|
19
|
+
return [...merged].map(([name, sources]) => `${name} ${sources.join(" ")}`).join("; ");
|
|
20
|
+
}
|
|
21
|
+
function cspOfPage(html) {
|
|
22
|
+
for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
|
|
23
|
+
if (!/http-equiv\s*=\s*["']Content-Security-Policy["']/i.test(tag)) continue;
|
|
24
|
+
const content = /\bcontent\s*=\s*"([^"]*)"/i.exec(tag) ?? /\bcontent\s*=\s*'([^']*)'/i.exec(tag);
|
|
25
|
+
return content ? content[1].trim() : null;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
var EXTERNAL = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
|
|
30
|
+
var parse = (url) => {
|
|
31
|
+
try {
|
|
32
|
+
return new URL(url);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function isSamePage(from, to) {
|
|
38
|
+
const a = parse(from), b = parse(to);
|
|
39
|
+
if (!a || !b || a.protocol !== b.protocol) return false;
|
|
40
|
+
if (a.protocol === "http:" || a.protocol === "https:") return a.origin === b.origin;
|
|
41
|
+
return a.host === b.host && a.pathname === b.pathname;
|
|
42
|
+
}
|
|
43
|
+
function guardWebContents(contents, opts = {}) {
|
|
44
|
+
const away = (what, url) => {
|
|
45
|
+
opts.onBlocked?.(what, url);
|
|
46
|
+
const target = parse(url);
|
|
47
|
+
if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);
|
|
48
|
+
};
|
|
49
|
+
contents.on("will-navigate", (event, url) => {
|
|
50
|
+
if (isSamePage(contents.getURL(), url)) return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
away("navigate", url);
|
|
53
|
+
});
|
|
54
|
+
contents.setWindowOpenHandler(({ url }) => {
|
|
55
|
+
away("window", url);
|
|
56
|
+
return { action: "deny" };
|
|
57
|
+
});
|
|
58
|
+
contents.on("will-attach-webview", (event) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
opts.onBlocked?.("webview", "");
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function guardAllWindows(app, opts = {}) {
|
|
64
|
+
app.on("web-contents-created", (_event, contents) => guardWebContents(contents, opts));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exports.FAMILY_CSP = FAMILY_CSP;
|
|
68
|
+
exports.cspOfPage = cspOfPage;
|
|
69
|
+
exports.familyCsp = familyCsp;
|
|
70
|
+
exports.guardAllWindows = guardAllWindows;
|
|
71
|
+
exports.guardWebContents = guardWebContents;
|
|
72
|
+
exports.isSamePage = isSamePage;
|
|
73
|
+
//# sourceMappingURL=window-guard.cjs.map
|
|
74
|
+
//# sourceMappingURL=window-guard.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/window-guard.ts"],"names":[],"mappings":"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","file":"window-guard.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Window hardening for both apps (patterkit design/family-parity-2026-10.md\n// item 12): one content security policy, and guards on navigation and\n// window.open for every window the app makes.\n//\n// The policy is a `<meta http-equiv=\"Content-Security-Policy\">` on each page,\n// since the apps load their pages from files in a release build and a header\n// cannot reach those. `familyCsp` is the one spelling of it: a page's meta\n// carries `familyCsp(...)` for that page, and each app's tests check every page\n// against it, so a new page cannot ship without one. Styles allow\n// 'unsafe-inline' because both editors set inline styles (ProseMirror does, in\n// Patterpad); scripts never do.\n//\n// The guards: a window never navigates away from the page it loaded, never\n// opens another window, and never attaches a webview. A link to the web asks\n// the app to open it in the browser instead (`openExternal`), and nothing else\n// leaves the app. `guardAllWindows` applies them to every web contents the app\n// creates, tool windows and any later window included, which is why it is the\n// one call each app makes, at startup.\n//\n// No Electron import: the module takes the app and the web contents it is\n// handed, so it runs under test without Electron.\n// ---------------------------------------------------------------------------\n\nimport type { App, WebContents } from \"electron\";\n\n/** A policy as directives: each name with its sources. */\nexport type CspDirectives = Record<string, readonly string[]>;\n\n/** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */\nexport const FAMILY_CSP: CspDirectives = {\n \"default-src\": [\"'self'\"],\n \"style-src\": [\"'self'\", \"'unsafe-inline'\"],\n \"img-src\": [\"'self'\", \"data:\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'none'\"],\n};\n\n/**\n * The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base\n * has extends it (`{ \"img-src\": [\"blob:\"] }` keeps `'self' data:`); a new directive is added. Directives come\n * out in the base's order, then the page's.\n */\nexport function familyCsp(extra: CspDirectives = {}): string {\n const merged = new Map<string, string[]>();\n for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);\n for (const [name, sources] of Object.entries(extra)) {\n // A directive the base leaves out falls back to default-src, so an addition starts from that.\n const into = merged.get(name) ?? [...(FAMILY_CSP[\"default-src\"] ?? [])];\n for (const s of sources) if (!into.includes(s)) into.push(s);\n merged.set(name, into);\n }\n return [...merged].map(([name, sources]) => `${name} ${sources.join(\" \")}`).join(\"; \");\n}\n\n/** The policy a page's HTML carries in its meta, or null when it has none. */\nexport function cspOfPage(html: string): string | null {\n for (const tag of html.match(/<meta\\b[^>]*>/gi) ?? []) {\n if (!/http-equiv\\s*=\\s*[\"']Content-Security-Policy[\"']/i.test(tag)) continue;\n const content = /\\bcontent\\s*=\\s*\"([^\"]*)\"/i.exec(tag) ?? /\\bcontent\\s*=\\s*'([^']*)'/i.exec(tag);\n return content ? content[1]!.trim() : null;\n }\n return null;\n}\n\nexport interface WindowGuardOptions {\n /** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or\n * mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */\n openExternal?: (url: string) => void;\n /** Told of everything refused, for a dev console. */\n onBlocked?: (what: \"navigate\" | \"window\" | \"webview\", url: string) => void;\n}\n\nconst EXTERNAL = new Set([\"http:\", \"https:\", \"mailto:\"]);\n\nconst parse = (url: string): URL | null => { try { return new URL(url); } catch { return null; } };\n\n/**\n * Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page\n * may move within its own origin (a reload, its own client); a file page may not move to another file.\n */\nexport function isSamePage(from: string, to: string): boolean {\n const a = parse(from), b = parse(to);\n if (!a || !b || a.protocol !== b.protocol) return false;\n if (a.protocol === \"http:\" || a.protocol === \"https:\") return a.origin === b.origin;\n return a.host === b.host && a.pathname === b.pathname;\n}\n\n/** The guards on one web contents. */\nexport function guardWebContents(contents: WebContents, opts: WindowGuardOptions = {}): void {\n const away = (what: \"navigate\" | \"window\", url: string): void => {\n opts.onBlocked?.(what, url);\n const target = parse(url);\n if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);\n };\n contents.on(\"will-navigate\", (event, url) => {\n if (isSamePage(contents.getURL(), url)) return;\n event.preventDefault();\n away(\"navigate\", url);\n });\n contents.setWindowOpenHandler(({ url }) => {\n away(\"window\", url);\n return { action: \"deny\" };\n });\n contents.on(\"will-attach-webview\", (event) => {\n event.preventDefault();\n opts.onBlocked?.(\"webview\", \"\");\n });\n}\n\n/** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */\nexport function guardAllWindows(app: App, opts: WindowGuardOptions = {}): void {\n app.on(\"web-contents-created\", (_event, contents) => guardWebContents(contents, opts));\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { App, WebContents } from 'electron';
|
|
2
|
+
|
|
3
|
+
/** A policy as directives: each name with its sources. */
|
|
4
|
+
type CspDirectives = Record<string, readonly string[]>;
|
|
5
|
+
/** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */
|
|
6
|
+
declare const FAMILY_CSP: CspDirectives;
|
|
7
|
+
/**
|
|
8
|
+
* The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base
|
|
9
|
+
* has extends it (`{ "img-src": ["blob:"] }` keeps `'self' data:`); a new directive is added. Directives come
|
|
10
|
+
* out in the base's order, then the page's.
|
|
11
|
+
*/
|
|
12
|
+
declare function familyCsp(extra?: CspDirectives): string;
|
|
13
|
+
/** The policy a page's HTML carries in its meta, or null when it has none. */
|
|
14
|
+
declare function cspOfPage(html: string): string | null;
|
|
15
|
+
interface WindowGuardOptions {
|
|
16
|
+
/** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or
|
|
17
|
+
* mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */
|
|
18
|
+
openExternal?: (url: string) => void;
|
|
19
|
+
/** Told of everything refused, for a dev console. */
|
|
20
|
+
onBlocked?: (what: "navigate" | "window" | "webview", url: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page
|
|
24
|
+
* may move within its own origin (a reload, its own client); a file page may not move to another file.
|
|
25
|
+
*/
|
|
26
|
+
declare function isSamePage(from: string, to: string): boolean;
|
|
27
|
+
/** The guards on one web contents. */
|
|
28
|
+
declare function guardWebContents(contents: WebContents, opts?: WindowGuardOptions): void;
|
|
29
|
+
/** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */
|
|
30
|
+
declare function guardAllWindows(app: App, opts?: WindowGuardOptions): void;
|
|
31
|
+
|
|
32
|
+
export { type CspDirectives, FAMILY_CSP, type WindowGuardOptions, cspOfPage, familyCsp, guardAllWindows, guardWebContents, isSamePage };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { App, WebContents } from 'electron';
|
|
2
|
+
|
|
3
|
+
/** A policy as directives: each name with its sources. */
|
|
4
|
+
type CspDirectives = Record<string, readonly string[]>;
|
|
5
|
+
/** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */
|
|
6
|
+
declare const FAMILY_CSP: CspDirectives;
|
|
7
|
+
/**
|
|
8
|
+
* The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base
|
|
9
|
+
* has extends it (`{ "img-src": ["blob:"] }` keeps `'self' data:`); a new directive is added. Directives come
|
|
10
|
+
* out in the base's order, then the page's.
|
|
11
|
+
*/
|
|
12
|
+
declare function familyCsp(extra?: CspDirectives): string;
|
|
13
|
+
/** The policy a page's HTML carries in its meta, or null when it has none. */
|
|
14
|
+
declare function cspOfPage(html: string): string | null;
|
|
15
|
+
interface WindowGuardOptions {
|
|
16
|
+
/** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or
|
|
17
|
+
* mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */
|
|
18
|
+
openExternal?: (url: string) => void;
|
|
19
|
+
/** Told of everything refused, for a dev console. */
|
|
20
|
+
onBlocked?: (what: "navigate" | "window" | "webview", url: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page
|
|
24
|
+
* may move within its own origin (a reload, its own client); a file page may not move to another file.
|
|
25
|
+
*/
|
|
26
|
+
declare function isSamePage(from: string, to: string): boolean;
|
|
27
|
+
/** The guards on one web contents. */
|
|
28
|
+
declare function guardWebContents(contents: WebContents, opts?: WindowGuardOptions): void;
|
|
29
|
+
/** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */
|
|
30
|
+
declare function guardAllWindows(app: App, opts?: WindowGuardOptions): void;
|
|
31
|
+
|
|
32
|
+
export { type CspDirectives, FAMILY_CSP, type WindowGuardOptions, cspOfPage, familyCsp, guardAllWindows, guardWebContents, isSamePage };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/window-guard.ts
|
|
2
|
+
var FAMILY_CSP = {
|
|
3
|
+
"default-src": ["'self'"],
|
|
4
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
5
|
+
"img-src": ["'self'", "data:"],
|
|
6
|
+
"object-src": ["'none'"],
|
|
7
|
+
"base-uri": ["'none'"]
|
|
8
|
+
};
|
|
9
|
+
function familyCsp(extra = {}) {
|
|
10
|
+
const merged = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);
|
|
12
|
+
for (const [name, sources] of Object.entries(extra)) {
|
|
13
|
+
const into = merged.get(name) ?? [...FAMILY_CSP["default-src"] ?? []];
|
|
14
|
+
for (const s of sources) if (!into.includes(s)) into.push(s);
|
|
15
|
+
merged.set(name, into);
|
|
16
|
+
}
|
|
17
|
+
return [...merged].map(([name, sources]) => `${name} ${sources.join(" ")}`).join("; ");
|
|
18
|
+
}
|
|
19
|
+
function cspOfPage(html) {
|
|
20
|
+
for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
|
|
21
|
+
if (!/http-equiv\s*=\s*["']Content-Security-Policy["']/i.test(tag)) continue;
|
|
22
|
+
const content = /\bcontent\s*=\s*"([^"]*)"/i.exec(tag) ?? /\bcontent\s*=\s*'([^']*)'/i.exec(tag);
|
|
23
|
+
return content ? content[1].trim() : null;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
var EXTERNAL = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
|
|
28
|
+
var parse = (url) => {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(url);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function isSamePage(from, to) {
|
|
36
|
+
const a = parse(from), b = parse(to);
|
|
37
|
+
if (!a || !b || a.protocol !== b.protocol) return false;
|
|
38
|
+
if (a.protocol === "http:" || a.protocol === "https:") return a.origin === b.origin;
|
|
39
|
+
return a.host === b.host && a.pathname === b.pathname;
|
|
40
|
+
}
|
|
41
|
+
function guardWebContents(contents, opts = {}) {
|
|
42
|
+
const away = (what, url) => {
|
|
43
|
+
opts.onBlocked?.(what, url);
|
|
44
|
+
const target = parse(url);
|
|
45
|
+
if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);
|
|
46
|
+
};
|
|
47
|
+
contents.on("will-navigate", (event, url) => {
|
|
48
|
+
if (isSamePage(contents.getURL(), url)) return;
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
away("navigate", url);
|
|
51
|
+
});
|
|
52
|
+
contents.setWindowOpenHandler(({ url }) => {
|
|
53
|
+
away("window", url);
|
|
54
|
+
return { action: "deny" };
|
|
55
|
+
});
|
|
56
|
+
contents.on("will-attach-webview", (event) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
opts.onBlocked?.("webview", "");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function guardAllWindows(app, opts = {}) {
|
|
62
|
+
app.on("web-contents-created", (_event, contents) => guardWebContents(contents, opts));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { FAMILY_CSP, cspOfPage, familyCsp, guardAllWindows, guardWebContents, isSamePage };
|
|
66
|
+
//# sourceMappingURL=window-guard.js.map
|
|
67
|
+
//# sourceMappingURL=window-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/window-guard.ts"],"names":[],"mappings":"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","file":"window-guard.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Window hardening for both apps (patterkit design/family-parity-2026-10.md\n// item 12): one content security policy, and guards on navigation and\n// window.open for every window the app makes.\n//\n// The policy is a `<meta http-equiv=\"Content-Security-Policy\">` on each page,\n// since the apps load their pages from files in a release build and a header\n// cannot reach those. `familyCsp` is the one spelling of it: a page's meta\n// carries `familyCsp(...)` for that page, and each app's tests check every page\n// against it, so a new page cannot ship without one. Styles allow\n// 'unsafe-inline' because both editors set inline styles (ProseMirror does, in\n// Patterpad); scripts never do.\n//\n// The guards: a window never navigates away from the page it loaded, never\n// opens another window, and never attaches a webview. A link to the web asks\n// the app to open it in the browser instead (`openExternal`), and nothing else\n// leaves the app. `guardAllWindows` applies them to every web contents the app\n// creates, tool windows and any later window included, which is why it is the\n// one call each app makes, at startup.\n//\n// No Electron import: the module takes the app and the web contents it is\n// handed, so it runs under test without Electron.\n// ---------------------------------------------------------------------------\n\nimport type { App, WebContents } from \"electron\";\n\n/** A policy as directives: each name with its sources. */\nexport type CspDirectives = Record<string, readonly string[]>;\n\n/** The family's base policy: everything from the app itself, inline styles, and images also from data URLs. */\nexport const FAMILY_CSP: CspDirectives = {\n \"default-src\": [\"'self'\"],\n \"style-src\": [\"'self'\", \"'unsafe-inline'\"],\n \"img-src\": [\"'self'\", \"data:\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'none'\"],\n};\n\n/**\n * The family's policy with a page's additions, as the meta's `content`. An addition to a directive the base\n * has extends it (`{ \"img-src\": [\"blob:\"] }` keeps `'self' data:`); a new directive is added. Directives come\n * out in the base's order, then the page's.\n */\nexport function familyCsp(extra: CspDirectives = {}): string {\n const merged = new Map<string, string[]>();\n for (const [name, sources] of Object.entries(FAMILY_CSP)) merged.set(name, [...sources]);\n for (const [name, sources] of Object.entries(extra)) {\n // A directive the base leaves out falls back to default-src, so an addition starts from that.\n const into = merged.get(name) ?? [...(FAMILY_CSP[\"default-src\"] ?? [])];\n for (const s of sources) if (!into.includes(s)) into.push(s);\n merged.set(name, into);\n }\n return [...merged].map(([name, sources]) => `${name} ${sources.join(\" \")}`).join(\"; \");\n}\n\n/** The policy a page's HTML carries in its meta, or null when it has none. */\nexport function cspOfPage(html: string): string | null {\n for (const tag of html.match(/<meta\\b[^>]*>/gi) ?? []) {\n if (!/http-equiv\\s*=\\s*[\"']Content-Security-Policy[\"']/i.test(tag)) continue;\n const content = /\\bcontent\\s*=\\s*\"([^\"]*)\"/i.exec(tag) ?? /\\bcontent\\s*=\\s*'([^']*)'/i.exec(tag);\n return content ? content[1]!.trim() : null;\n }\n return null;\n}\n\nexport interface WindowGuardOptions {\n /** Open a web address in the browser (Electron's `shell.openExternal`). Given, a link to an http, https or\n * mailto address that the page tried to navigate to or open goes here; absent, it goes nowhere. */\n openExternal?: (url: string) => void;\n /** Told of everything refused, for a dev console. */\n onBlocked?: (what: \"navigate\" | \"window\" | \"webview\", url: string) => void;\n}\n\nconst EXTERNAL = new Set([\"http:\", \"https:\", \"mailto:\"]);\n\nconst parse = (url: string): URL | null => { try { return new URL(url); } catch { return null; } };\n\n/**\n * Whether `to` is the page `from` already is: the same document, a hash or query apart. A dev server's page\n * may move within its own origin (a reload, its own client); a file page may not move to another file.\n */\nexport function isSamePage(from: string, to: string): boolean {\n const a = parse(from), b = parse(to);\n if (!a || !b || a.protocol !== b.protocol) return false;\n if (a.protocol === \"http:\" || a.protocol === \"https:\") return a.origin === b.origin;\n return a.host === b.host && a.pathname === b.pathname;\n}\n\n/** The guards on one web contents. */\nexport function guardWebContents(contents: WebContents, opts: WindowGuardOptions = {}): void {\n const away = (what: \"navigate\" | \"window\", url: string): void => {\n opts.onBlocked?.(what, url);\n const target = parse(url);\n if (target && EXTERNAL.has(target.protocol)) opts.openExternal?.(url);\n };\n contents.on(\"will-navigate\", (event, url) => {\n if (isSamePage(contents.getURL(), url)) return;\n event.preventDefault();\n away(\"navigate\", url);\n });\n contents.setWindowOpenHandler(({ url }) => {\n away(\"window\", url);\n return { action: \"deny\" };\n });\n contents.on(\"will-attach-webview\", (event) => {\n event.preventDefault();\n opts.onBlocked?.(\"webview\", \"\");\n });\n}\n\n/** The guards on every web contents the app creates from now on: the one call each app makes, at startup. */\nexport function guardAllWindows(app: App, opts: WindowGuardOptions = {}): void {\n app.on(\"web-contents-created\", (_event, contents) => guardWebContents(contents, opts));\n}\n"]}
|