@pwfabric/cli 0.0.0-stage → 1.0.0-rc.1
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/LICENSE +202 -0
- package/README.md +122 -2
- package/dist/args.d.ts +11 -0
- package/dist/args.js +43 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +10 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +56 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.js +55 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.js +119 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/dev.js +107 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/lib/flags.d.ts +10 -0
- package/dist/commands/lib/flags.js +33 -0
- package/dist/commands/lib/flags.js.map +1 -0
- package/dist/commands/lib/identity.d.ts +38 -0
- package/dist/commands/lib/identity.js +120 -0
- package/dist/commands/lib/identity.js.map +1 -0
- package/dist/commands/lib/project.d.ts +34 -0
- package/dist/commands/lib/project.js +78 -0
- package/dist/commands/lib/project.js.map +1 -0
- package/dist/commands/lib/report.d.ts +15 -0
- package/dist/commands/lib/report.js +75 -0
- package/dist/commands/lib/report.js.map +1 -0
- package/dist/commands/lib/starter.d.ts +41 -0
- package/dist/commands/lib/starter.js +136 -0
- package/dist/commands/lib/starter.js.map +1 -0
- package/dist/commands/lib/static-server.d.ts +45 -0
- package/dist/commands/lib/static-server.js +176 -0
- package/dist/commands/lib/static-server.js.map +1 -0
- package/dist/commands/pack.d.ts +5 -0
- package/dist/commands/pack.js +56 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.js +16 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/run.d.ts +14 -0
- package/dist/commands/run.js +79 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/types.d.ts +34 -0
- package/dist/commands/types.js +10 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +33 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/dev/assemble.d.ts +47 -0
- package/dist/dev/assemble.js +339 -0
- package/dist/dev/assemble.js.map +1 -0
- package/dist/dev/compile.d.ts +54 -0
- package/dist/dev/compile.js +215 -0
- package/dist/dev/compile.js.map +1 -0
- package/dist/dev/csp.d.ts +67 -0
- package/dist/dev/csp.js +260 -0
- package/dist/dev/csp.js.map +1 -0
- package/dist/dev/diagnostics.d.ts +21 -0
- package/dist/dev/diagnostics.js +22 -0
- package/dist/dev/diagnostics.js.map +1 -0
- package/dist/dev/envelope.d.ts +92 -0
- package/dist/dev/envelope.js +154 -0
- package/dist/dev/envelope.js.map +1 -0
- package/dist/dev/media.d.ts +53 -0
- package/dist/dev/media.js +128 -0
- package/dist/dev/media.js.map +1 -0
- package/dist/dev/needs-host.d.ts +2 -0
- package/dist/dev/needs-host.js +32 -0
- package/dist/dev/needs-host.js.map +1 -0
- package/dist/dev/output.d.ts +9 -0
- package/dist/dev/output.js +32 -0
- package/dist/dev/output.js.map +1 -0
- package/dist/dev/overlay.d.ts +21 -0
- package/dist/dev/overlay.js +59 -0
- package/dist/dev/overlay.js.map +1 -0
- package/dist/dev/server.d.ts +27 -0
- package/dist/dev/server.js +268 -0
- package/dist/dev/server.js.map +1 -0
- package/dist/dev/shell.d.ts +94 -0
- package/dist/dev/shell.js +267 -0
- package/dist/dev/shell.js.map +1 -0
- package/dist/dev/sources.d.ts +45 -0
- package/dist/dev/sources.js +165 -0
- package/dist/dev/sources.js.map +1 -0
- package/dist/dev/theme.d.ts +51 -0
- package/dist/dev/theme.js +185 -0
- package/dist/dev/theme.js.map +1 -0
- package/dist/dev/types.d.ts +40 -0
- package/dist/dev/types.js +11 -0
- package/dist/dev/types.js.map +1 -0
- package/dist/dev/watcher.d.ts +29 -0
- package/dist/dev/watcher.js +150 -0
- package/dist/dev/watcher.js.map +1 -0
- package/dist/host/definitions.d.ts +61 -0
- package/dist/host/definitions.js +136 -0
- package/dist/host/definitions.js.map +1 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +156 -0
- package/dist/host/http.js.map +1 -0
- package/dist/host/local-host.d.ts +59 -0
- package/dist/host/local-host.js +420 -0
- package/dist/host/local-host.js.map +1 -0
- package/dist/host/oidc.d.ts +46 -0
- package/dist/host/oidc.js +192 -0
- package/dist/host/oidc.js.map +1 -0
- package/dist/host/query.d.ts +35 -0
- package/dist/host/query.js +158 -0
- package/dist/host/query.js.map +1 -0
- package/dist/host/receipts.d.ts +25 -0
- package/dist/host/receipts.js +72 -0
- package/dist/host/receipts.js.map +1 -0
- package/dist/host/records.d.ts +73 -0
- package/dist/host/records.js +229 -0
- package/dist/host/records.js.map +1 -0
- package/dist/host/tokens.d.ts +28 -0
- package/dist/host/tokens.js +52 -0
- package/dist/host/tokens.js.map +1 -0
- package/dist/host/types.d.ts +32 -0
- package/dist/host/types.js +11 -0
- package/dist/host/types.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -3
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiling the atoms an app runs on — its own and those of every pinned
|
|
3
|
+
* pack — with the engine's compiler, and remembering the output by content
|
|
4
|
+
* hash so a rebuild only compiles what changed.
|
|
5
|
+
*
|
|
6
|
+
* The cache key of an atom is everything its output depends on: the compiler
|
|
7
|
+
* and esbuild versions, the atom's kind and entry, and the bytes of the files
|
|
8
|
+
* the compile can reach (the entry plus the shared companions it imports).
|
|
9
|
+
* Change one block and only that block — or the blocks sharing the changed
|
|
10
|
+
* companion — compile again.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { join, posix } from 'node:path';
|
|
15
|
+
import { COMPILER_VERSION, ESBUILD_VERSION, collectCompanions, compileAtom, } from '@pwfabric/compiler';
|
|
16
|
+
import { atomKindMeta, resolveSiblingEntry } from '@pwfabric/contracts';
|
|
17
|
+
import { isRecord, jsonOf } from './sources.js';
|
|
18
|
+
/** Kinds that carry a compiled module. */
|
|
19
|
+
export const COMPILED_KINDS = new Set(['block', 'capability', 'connector', 'provider']);
|
|
20
|
+
/**
|
|
21
|
+
* A cache in `dir` (one JSON file per key) with an in-memory layer in front.
|
|
22
|
+
* Disk failures are not build failures: a cache that cannot be read or
|
|
23
|
+
* written just compiles again.
|
|
24
|
+
*/
|
|
25
|
+
export function createCompileCache(dir) {
|
|
26
|
+
const memory = new Map();
|
|
27
|
+
return {
|
|
28
|
+
async get(key) {
|
|
29
|
+
const hit = memory.get(key);
|
|
30
|
+
if (hit)
|
|
31
|
+
return hit;
|
|
32
|
+
if (!dir)
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const value = JSON.parse(await readFile(join(dir, `${key}.json`), 'utf8'));
|
|
36
|
+
if (typeof value?.code !== 'string' || typeof value.ok !== 'boolean' || !Array.isArray(value.diagnostics))
|
|
37
|
+
return undefined;
|
|
38
|
+
memory.set(key, value);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async set(key, value) {
|
|
46
|
+
memory.set(key, value);
|
|
47
|
+
if (!dir)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(dir, { recursive: true });
|
|
51
|
+
await writeFile(join(dir, `${key}.json`), JSON.stringify(value));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// A read-only or full disk only costs the next build a recompile.
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function sha256(bytes) {
|
|
60
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
/** The root under which a source keeps its atoms: `atoms/` in the canonical layout, `` in the rootless one. */
|
|
63
|
+
export function atomRootOf(source) {
|
|
64
|
+
return [...source.folder.files.keys()].some((p) => p.startsWith('atoms/')) ? 'atoms/' : '';
|
|
65
|
+
}
|
|
66
|
+
/** The compiler's view of a source: atom files keyed by their path below the atom root. */
|
|
67
|
+
export function compileTreeOf(source) {
|
|
68
|
+
const root = atomRootOf(source);
|
|
69
|
+
const tree = {};
|
|
70
|
+
for (const [path, bytes] of source.folder.files) {
|
|
71
|
+
if (root) {
|
|
72
|
+
if (path.startsWith(root))
|
|
73
|
+
tree[path.slice(root.length)] = bytes;
|
|
74
|
+
}
|
|
75
|
+
else if (path !== 'pwpack.json' && !path.startsWith('app/')) {
|
|
76
|
+
tree[path] = bytes;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return tree;
|
|
80
|
+
}
|
|
81
|
+
function manifestOf(source, ref) {
|
|
82
|
+
const own = ref.relPath.endsWith('.json') ? ref.relPath : posix.join(posix.dirname(ref.relPath), 'manifest.json');
|
|
83
|
+
const value = jsonOf(source.folder, own);
|
|
84
|
+
return isRecord(value) ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
function keyOf(parts) {
|
|
87
|
+
return sha256(JSON.stringify(parts));
|
|
88
|
+
}
|
|
89
|
+
function fileHashes(tree, paths) {
|
|
90
|
+
return [...new Set(paths)].sort().map((p) => [p, tree[p] ? sha256(tree[p]) : '-']);
|
|
91
|
+
}
|
|
92
|
+
/** Runs `fn` over `items` with at most `limit` in flight, keeping order. */
|
|
93
|
+
export async function mapLimit(items, limit, fn) {
|
|
94
|
+
const out = new Array(items.length);
|
|
95
|
+
let next = 0;
|
|
96
|
+
const worker = async () => {
|
|
97
|
+
while (next < items.length) {
|
|
98
|
+
const i = next++;
|
|
99
|
+
out[i] = await fn(items[i]);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker));
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
async function compileOne(job, cache) {
|
|
106
|
+
const { source, ref, tree, root } = job;
|
|
107
|
+
const bytes = source.folder.files.get(ref.relPath);
|
|
108
|
+
const manifest = manifestOf(source, ref);
|
|
109
|
+
const base = {
|
|
110
|
+
source,
|
|
111
|
+
kind: ref.kind,
|
|
112
|
+
id: ref.id,
|
|
113
|
+
relPath: ref.relPath,
|
|
114
|
+
sourceHash: `sha256:${ref.contentHash || sha256(bytes)}`,
|
|
115
|
+
...(manifest ? { manifest } : {}),
|
|
116
|
+
};
|
|
117
|
+
const at = (virtual) => {
|
|
118
|
+
if (virtual === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const p = `${root}${virtual}`;
|
|
121
|
+
return source.label ? `${source.label}/${p}` : p;
|
|
122
|
+
};
|
|
123
|
+
const virtual = ref.relPath.slice(root.length);
|
|
124
|
+
const label = at(virtual);
|
|
125
|
+
const diagnostics = [];
|
|
126
|
+
const meta = atomKindMeta(ref.kind);
|
|
127
|
+
// A pack may ship an already-compiled block (`bundle.mjs`).
|
|
128
|
+
if (meta.precompiledFallback && ref.relPath.endsWith('.mjs')) {
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
status: 'precompiled',
|
|
132
|
+
code: new TextDecoder().decode(bytes),
|
|
133
|
+
hash: `sha256:${sha256(bytes)}`,
|
|
134
|
+
diagnostics,
|
|
135
|
+
fromCache: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
let entry;
|
|
139
|
+
let engineName;
|
|
140
|
+
let reach;
|
|
141
|
+
let companions;
|
|
142
|
+
if (meta.entry === 'identity') {
|
|
143
|
+
entry = virtual;
|
|
144
|
+
const scan = collectCompanions(tree, entry);
|
|
145
|
+
for (const v of scan.violations) {
|
|
146
|
+
diagnostics.push({ severity: 'warning', code: 'SCREEN', message: v, path: label });
|
|
147
|
+
}
|
|
148
|
+
companions = [...scan.companions];
|
|
149
|
+
reach = [entry, ...companions];
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const sibling = resolveSiblingEntry(ref.kind, virtual);
|
|
153
|
+
if (!sibling || !tree[sibling.enginePath]) {
|
|
154
|
+
// No engine source beside the knowledge file: the engine is data (a
|
|
155
|
+
// declaration) or the atom is knowledge only. Nothing to compile.
|
|
156
|
+
return { ...base, status: 'declared', diagnostics, fromCache: false };
|
|
157
|
+
}
|
|
158
|
+
entry = sibling.wrapperPath;
|
|
159
|
+
engineName = posix.basename(sibling.enginePath, '.ts');
|
|
160
|
+
// An engine may import neighbours that are not companions; key on the whole tree.
|
|
161
|
+
reach = Object.keys(tree);
|
|
162
|
+
}
|
|
163
|
+
const key = keyOf([
|
|
164
|
+
COMPILER_VERSION,
|
|
165
|
+
ESBUILD_VERSION,
|
|
166
|
+
ref.kind,
|
|
167
|
+
entry,
|
|
168
|
+
engineName ?? null,
|
|
169
|
+
companions ?? null,
|
|
170
|
+
fileHashes(tree, reach),
|
|
171
|
+
]);
|
|
172
|
+
let result = await cache.get(key);
|
|
173
|
+
const fromCache = result !== undefined;
|
|
174
|
+
if (!result) {
|
|
175
|
+
const compiled = await compileAtom({
|
|
176
|
+
files: tree,
|
|
177
|
+
entry,
|
|
178
|
+
kind: ref.kind,
|
|
179
|
+
...(engineName !== undefined ? { engineName } : {}),
|
|
180
|
+
...(companions !== undefined ? { companions } : {}),
|
|
181
|
+
});
|
|
182
|
+
result = { ok: compiled.ok, code: compiled.code, hash: compiled.hash, diagnostics: compiled.diagnostics };
|
|
183
|
+
await cache.set(key, result);
|
|
184
|
+
}
|
|
185
|
+
for (const d of result.diagnostics) {
|
|
186
|
+
diagnostics.push({
|
|
187
|
+
severity: d.severity,
|
|
188
|
+
code: d.code,
|
|
189
|
+
message: d.message,
|
|
190
|
+
path: at(d.file) ?? label,
|
|
191
|
+
...(d.line !== undefined ? { line: d.line } : {}),
|
|
192
|
+
...(d.column !== undefined ? { column: d.column } : {}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (!result.ok)
|
|
196
|
+
return { ...base, status: 'failed', diagnostics, fromCache };
|
|
197
|
+
return { ...base, status: 'compiled', code: result.code, hash: result.hash, diagnostics, fromCache };
|
|
198
|
+
}
|
|
199
|
+
/** Compiles every compiled-kind atom of every source. */
|
|
200
|
+
export async function compileSources(sources, cache, concurrency = 8) {
|
|
201
|
+
const jobs = [];
|
|
202
|
+
for (const source of sources) {
|
|
203
|
+
const tree = compileTreeOf(source);
|
|
204
|
+
const root = atomRootOf(source);
|
|
205
|
+
for (const ref of source.validation.atoms) {
|
|
206
|
+
if (!COMPILED_KINDS.has(ref.kind))
|
|
207
|
+
continue;
|
|
208
|
+
if (!ref.relPath.startsWith(root))
|
|
209
|
+
continue;
|
|
210
|
+
jobs.push({ source, ref, tree, root });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return mapLimit(jobs, concurrency, (job) => compileOne(job, cache));
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/dev/compile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compiling the atoms an app runs on — its own and those of every pinned\n * pack — with the engine's compiler, and remembering the output by content\n * hash so a rebuild only compiles what changed.\n *\n * The cache key of an atom is everything its output depends on: the compiler\n * and esbuild versions, the atom's kind and entry, and the bytes of the files\n * the compile can reach (the entry plus the shared companions it imports).\n * Change one block and only that block — or the blocks sharing the changed\n * companion — compile again.\n */\nimport { createHash } from 'node:crypto'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { join, posix } from 'node:path'\nimport {\n COMPILER_VERSION,\n ESBUILD_VERSION,\n collectCompanions,\n compileAtom,\n type CompileResult,\n} from '@pwfabric/compiler'\nimport { atomKindMeta, resolveSiblingEntry, type AtomKind, type AtomRef } from '@pwfabric/contracts'\nimport type { AssembleDiagnostic } from './diagnostics.js'\nimport { isRecord, jsonOf, type SourcePack } from './sources.js'\n\n/** Kinds that carry a compiled module. */\nexport const COMPILED_KINDS: ReadonlySet<AtomKind> = new Set<AtomKind>(['block', 'capability', 'connector', 'provider'])\n\n/** One atom after the compile step. */\nexport interface CompiledAtom {\n readonly source: SourcePack\n readonly kind: AtomKind\n readonly id: string\n /** Path of the atom's file inside its source folder. */\n readonly relPath: string\n /** `sha256:<hex>` of that file. */\n readonly sourceHash: string\n /**\n * `compiled` — a module was produced; `precompiled` — the pack shipped one;\n * `declared` — an engine that is data, nothing to compile; `failed`.\n */\n readonly status: 'compiled' | 'precompiled' | 'declared' | 'failed'\n readonly code?: string\n /** `sha256:<hex>` of `code`. */\n readonly hash?: string\n readonly diagnostics: readonly AssembleDiagnostic[]\n readonly fromCache: boolean\n /** The atom's manifest (`manifest.json` next to it, or the knowledge file itself). */\n readonly manifest?: Record<string, unknown>\n}\n\n/** What the cache stores per key. */\nexport interface CachedCompile {\n readonly ok: boolean\n readonly code: string\n readonly hash: string\n readonly diagnostics: CompileResult['diagnostics']\n}\n\nexport interface CompileCache {\n get(key: string): Promise<CachedCompile | undefined>\n set(key: string, value: CachedCompile): Promise<void>\n}\n\n/**\n * A cache in `dir` (one JSON file per key) with an in-memory layer in front.\n * Disk failures are not build failures: a cache that cannot be read or\n * written just compiles again.\n */\nexport function createCompileCache(dir: string | null): CompileCache {\n const memory = new Map<string, CachedCompile>()\n return {\n async get(key) {\n const hit = memory.get(key)\n if (hit) return hit\n if (!dir) return undefined\n try {\n const value = JSON.parse(await readFile(join(dir, `${key}.json`), 'utf8')) as CachedCompile\n if (typeof value?.code !== 'string' || typeof value.ok !== 'boolean' || !Array.isArray(value.diagnostics)) return undefined\n memory.set(key, value)\n return value\n } catch {\n return undefined\n }\n },\n async set(key, value) {\n memory.set(key, value)\n if (!dir) return\n try {\n await mkdir(dir, { recursive: true })\n await writeFile(join(dir, `${key}.json`), JSON.stringify(value))\n } catch {\n // A read-only or full disk only costs the next build a recompile.\n }\n },\n }\n}\n\nexport function sha256(bytes: Uint8Array | string): string {\n return createHash('sha256').update(bytes).digest('hex')\n}\n\n/** The root under which a source keeps its atoms: `atoms/` in the canonical layout, `` in the rootless one. */\nexport function atomRootOf(source: SourcePack): string {\n return [...source.folder.files.keys()].some((p) => p.startsWith('atoms/')) ? 'atoms/' : ''\n}\n\n/** The compiler's view of a source: atom files keyed by their path below the atom root. */\nexport function compileTreeOf(source: SourcePack): Record<string, Uint8Array> {\n const root = atomRootOf(source)\n const tree: Record<string, Uint8Array> = {}\n for (const [path, bytes] of source.folder.files) {\n if (root) {\n if (path.startsWith(root)) tree[path.slice(root.length)] = bytes\n } else if (path !== 'pwpack.json' && !path.startsWith('app/')) {\n tree[path] = bytes\n }\n }\n return tree\n}\n\nfunction manifestOf(source: SourcePack, ref: AtomRef): Record<string, unknown> | undefined {\n const own = ref.relPath.endsWith('.json') ? ref.relPath : posix.join(posix.dirname(ref.relPath), 'manifest.json')\n const value = jsonOf(source.folder, own)\n return isRecord(value) ? value : undefined\n}\n\nfunction keyOf(parts: unknown): string {\n return sha256(JSON.stringify(parts))\n}\n\nfunction fileHashes(tree: Readonly<Record<string, Uint8Array>>, paths: readonly string[]): [string, string][] {\n return [...new Set(paths)].sort().map((p) => [p, tree[p] ? sha256(tree[p] as Uint8Array) : '-'])\n}\n\n/** Runs `fn` over `items` with at most `limit` in flight, keeping order. */\nexport async function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const out: R[] = new Array<R>(items.length)\n let next = 0\n const worker = async (): Promise<void> => {\n while (next < items.length) {\n const i = next++\n out[i] = await fn(items[i] as T)\n }\n }\n await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker))\n return out\n}\n\ninterface AtomJob {\n readonly source: SourcePack\n readonly ref: AtomRef\n readonly tree: Readonly<Record<string, Uint8Array>>\n readonly root: string\n}\n\nasync function compileOne(job: AtomJob, cache: CompileCache): Promise<CompiledAtom> {\n const { source, ref, tree, root } = job\n const bytes = source.folder.files.get(ref.relPath) as Uint8Array\n const manifest = manifestOf(source, ref)\n const base = {\n source,\n kind: ref.kind,\n id: ref.id,\n relPath: ref.relPath,\n sourceHash: `sha256:${ref.contentHash || sha256(bytes)}`,\n ...(manifest ? { manifest } : {}),\n }\n const at = (virtual: string | undefined): string | undefined => {\n if (virtual === undefined) return undefined\n const p = `${root}${virtual}`\n return source.label ? `${source.label}/${p}` : p\n }\n const virtual = ref.relPath.slice(root.length)\n const label = at(virtual) as string\n const diagnostics: AssembleDiagnostic[] = []\n const meta = atomKindMeta(ref.kind)\n\n // A pack may ship an already-compiled block (`bundle.mjs`).\n if (meta.precompiledFallback && ref.relPath.endsWith('.mjs')) {\n return {\n ...base,\n status: 'precompiled',\n code: new TextDecoder().decode(bytes),\n hash: `sha256:${sha256(bytes)}`,\n diagnostics,\n fromCache: false,\n }\n }\n\n let entry: string\n let engineName: string | undefined\n let reach: string[]\n let companions: string[] | undefined\n if (meta.entry === 'identity') {\n entry = virtual\n const scan = collectCompanions(tree, entry)\n for (const v of scan.violations) {\n diagnostics.push({ severity: 'warning', code: 'SCREEN', message: v, path: label })\n }\n companions = [...scan.companions]\n reach = [entry, ...companions]\n } else {\n const sibling = resolveSiblingEntry(ref.kind, virtual)\n if (!sibling || !tree[sibling.enginePath]) {\n // No engine source beside the knowledge file: the engine is data (a\n // declaration) or the atom is knowledge only. Nothing to compile.\n return { ...base, status: 'declared', diagnostics, fromCache: false }\n }\n entry = sibling.wrapperPath\n engineName = posix.basename(sibling.enginePath, '.ts')\n // An engine may import neighbours that are not companions; key on the whole tree.\n reach = Object.keys(tree)\n }\n\n const key = keyOf([\n COMPILER_VERSION,\n ESBUILD_VERSION,\n ref.kind,\n entry,\n engineName ?? null,\n companions ?? null,\n fileHashes(tree, reach),\n ])\n let result = await cache.get(key)\n const fromCache = result !== undefined\n if (!result) {\n const compiled = await compileAtom({\n files: tree,\n entry,\n kind: ref.kind,\n ...(engineName !== undefined ? { engineName } : {}),\n ...(companions !== undefined ? { companions } : {}),\n })\n result = { ok: compiled.ok, code: compiled.code, hash: compiled.hash, diagnostics: compiled.diagnostics }\n await cache.set(key, result)\n }\n for (const d of result.diagnostics) {\n diagnostics.push({\n severity: d.severity,\n code: d.code,\n message: d.message,\n path: at(d.file) ?? label,\n ...(d.line !== undefined ? { line: d.line } : {}),\n ...(d.column !== undefined ? { column: d.column } : {}),\n })\n }\n if (!result.ok) return { ...base, status: 'failed', diagnostics, fromCache }\n return { ...base, status: 'compiled', code: result.code, hash: result.hash, diagnostics, fromCache }\n}\n\n/** Compiles every compiled-kind atom of every source. */\nexport async function compileSources(\n sources: readonly SourcePack[],\n cache: CompileCache,\n concurrency = 8\n): Promise<CompiledAtom[]> {\n const jobs: AtomJob[] = []\n for (const source of sources) {\n const tree = compileTreeOf(source)\n const root = atomRootOf(source)\n for (const ref of source.validation.atoms) {\n if (!COMPILED_KINDS.has(ref.kind)) continue\n if (!ref.relPath.startsWith(root)) continue\n jobs.push({ source, ref, tree, root })\n }\n }\n return mapLimit(jobs, concurrency, (job) => compileOne(job, cache))\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Directive name → its sources, in the order they are written. */
|
|
2
|
+
export type CspDirectives = Record<string, string[]>;
|
|
3
|
+
/** Something the policy could not resolve at build time. */
|
|
4
|
+
export interface CspUnresolved {
|
|
5
|
+
/** What declared it: `connection:<name>`, `engine:<atomId>`, `page:<key>`, `auth`. */
|
|
6
|
+
readonly source: string;
|
|
7
|
+
/** The template or address as written. */
|
|
8
|
+
readonly value: string;
|
|
9
|
+
readonly reason: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AppCsp {
|
|
12
|
+
readonly directives: CspDirectives;
|
|
13
|
+
/** The policy as a header value (every directive). */
|
|
14
|
+
readonly header: string;
|
|
15
|
+
/** The policy for a `<meta http-equiv>` (without the directives a meta cannot carry). */
|
|
16
|
+
readonly meta: string;
|
|
17
|
+
readonly unresolved: readonly CspUnresolved[];
|
|
18
|
+
}
|
|
19
|
+
export interface CspInputs {
|
|
20
|
+
/** The exact text of the shell's inline import map. */
|
|
21
|
+
readonly importMapText: string;
|
|
22
|
+
/** Served receipts by page key (after media slots are filled). */
|
|
23
|
+
readonly receipts: ReadonlyMap<string, unknown>;
|
|
24
|
+
/** `pwpack/connections.json`. */
|
|
25
|
+
readonly connections?: {
|
|
26
|
+
readonly connections: readonly Record<string, unknown>[];
|
|
27
|
+
} | undefined;
|
|
28
|
+
/** `engines/manifest.json` entries. */
|
|
29
|
+
readonly engines: readonly {
|
|
30
|
+
readonly connection: string;
|
|
31
|
+
readonly atomId: string;
|
|
32
|
+
readonly moduleUrl?: string;
|
|
33
|
+
readonly engine?: unknown;
|
|
34
|
+
}[];
|
|
35
|
+
/** The envelope `auth.json`, when the build has one. */
|
|
36
|
+
readonly envelopeAuth?: unknown;
|
|
37
|
+
/** `app/definitions/auth.json` (the declaration). */
|
|
38
|
+
readonly authDefinition?: unknown;
|
|
39
|
+
/** `publish.json` `media[]`. */
|
|
40
|
+
readonly media: readonly {
|
|
41
|
+
readonly url: string;
|
|
42
|
+
}[];
|
|
43
|
+
/** The webfont stylesheet the shell links, when any. */
|
|
44
|
+
readonly webfontHref?: string | null;
|
|
45
|
+
}
|
|
46
|
+
/** The CSP source for an inline script: `'sha256-<base64>'` of its exact text. */
|
|
47
|
+
export declare function scriptHash(text: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* The origin of an absolute http(s) URL, or null. Expressions (`{{…}}`) or
|
|
50
|
+
* templates (`{…}`) in the scheme or host make it unresolvable (`undefined`).
|
|
51
|
+
*/
|
|
52
|
+
export declare function originOf(value: string): string | null | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The origin a declared engine's `host` reaches (connector-manifest §3.1):
|
|
55
|
+
* `{name}` filled from the connection's string config, the scheme dropped and
|
|
56
|
+
* `https://` prepended, except an explicit `http://` on a loopback host.
|
|
57
|
+
* `undefined` when a placeholder stays unfilled.
|
|
58
|
+
*/
|
|
59
|
+
export declare function engineHostOrigin(host: string, config: Record<string, unknown> | undefined): string | undefined;
|
|
60
|
+
/** Writes the directives as a policy string. */
|
|
61
|
+
export declare function serializeCsp(directives: CspDirectives, options?: {
|
|
62
|
+
meta?: boolean;
|
|
63
|
+
}): string;
|
|
64
|
+
/** Derives the app's policy from what it declares. */
|
|
65
|
+
export declare function deriveCsp(input: CspInputs): AppCsp;
|
|
66
|
+
/** The static-host `_headers` file (`/*` and the policy). */
|
|
67
|
+
export declare function headersFile(policy: string): string;
|
package/dist/dev/csp.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's Content Security Policy, derived from what the app declares.
|
|
3
|
+
*
|
|
4
|
+
* A pwapp is static files; what it may load and reach is known at build time
|
|
5
|
+
* from its own declarations, so `pwf build` writes a policy that allows that
|
|
6
|
+
* and nothing else (pwapp-format §2.1):
|
|
7
|
+
*
|
|
8
|
+
* default-src 'none'
|
|
9
|
+
* script-src 'self' blob: 'sha256-<import map>'
|
|
10
|
+
* Blocks and compiled engines are verified and then imported
|
|
11
|
+
* from `blob:` URLs. The import map is the shell's only inline
|
|
12
|
+
* script (an import map cannot be an external file), so its
|
|
13
|
+
* hash is listed; the renderer seeding and the boot call are
|
|
14
|
+
* files (`assets/seed.js`, `assets/boot.js`).
|
|
15
|
+
* style-src 'self' 'unsafe-inline' [+ the webfont stylesheet origin]
|
|
16
|
+
* The renderer draws `<style>` elements at run time (the
|
|
17
|
+
* surface base, visibility, state, responsive and dark-theme
|
|
18
|
+
* rules, computed per page), and blocks set `style`
|
|
19
|
+
* attributes. Neither can be hashed ahead of time.
|
|
20
|
+
* img-src 'self' data: blob: [+ media origins]
|
|
21
|
+
* media-src 'self' data: blob: [+ media origins]
|
|
22
|
+
* frame-src [media origins] — only when there are any
|
|
23
|
+
* font-src 'self' [+ the webfont file origin]
|
|
24
|
+
* connect-src 'self' [+ every origin the app's data and sign-in reach]
|
|
25
|
+
* base-uri 'self' — a host that serves the app under a sub-path
|
|
26
|
+
* injects `<base href>` (pwapp-format §2); 'none' would make
|
|
27
|
+
* the browser ignore it and break path routing.
|
|
28
|
+
* object-src 'none'
|
|
29
|
+
* frame-ancestors 'none' — header only: browsers ignore it in a `<meta>`.
|
|
30
|
+
*
|
|
31
|
+
* Origins come from: absolute `rest` endpoints in the receipts; declared
|
|
32
|
+
* engines' `host` templates filled from `connections.json` config;
|
|
33
|
+
* the envelope `auth.json` addresses and an external issuer; `publish.json`
|
|
34
|
+
* media and absolute media URLs in receipt props. A template that cannot be
|
|
35
|
+
* resolved at build time, and a compiled engine (whose code may reach any
|
|
36
|
+
* origin), is listed in `unresolved` instead of being guessed.
|
|
37
|
+
*
|
|
38
|
+
* @module @pwfabric/cli/dev/csp
|
|
39
|
+
*/
|
|
40
|
+
import { createHash } from 'node:crypto';
|
|
41
|
+
import { isRecord } from './sources.js';
|
|
42
|
+
/** Directives a `<meta http-equiv="Content-Security-Policy">` cannot carry (CSP3 §3.3). */
|
|
43
|
+
const HEADER_ONLY = new Set(['frame-ancestors', 'report-uri', 'report-to', 'sandbox']);
|
|
44
|
+
/** The CSP source for an inline script: `'sha256-<base64>'` of its exact text. */
|
|
45
|
+
export function scriptHash(text) {
|
|
46
|
+
return `'sha256-${createHash('sha256').update(text, 'utf8').digest('base64')}'`;
|
|
47
|
+
}
|
|
48
|
+
const LOOPBACK = /^(?:localhost|127\.0\.0\.1|\[::1\]|[^/:]+\.localhost)(?::\d+)?$/i;
|
|
49
|
+
/**
|
|
50
|
+
* The origin of an absolute http(s) URL, or null. Expressions (`{{…}}`) or
|
|
51
|
+
* templates (`{…}`) in the scheme or host make it unresolvable (`undefined`).
|
|
52
|
+
*/
|
|
53
|
+
export function originOf(value) {
|
|
54
|
+
const m = /^(https?):\/\/([^/?#]*)/i.exec(value.trim());
|
|
55
|
+
if (!m)
|
|
56
|
+
return null;
|
|
57
|
+
const host = m[2];
|
|
58
|
+
if (!host || /[{}]/.test(host))
|
|
59
|
+
return undefined;
|
|
60
|
+
try {
|
|
61
|
+
return new URL(`${m[1].toLowerCase()}://${host}`).origin;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The origin a declared engine's `host` reaches (connector-manifest §3.1):
|
|
69
|
+
* `{name}` filled from the connection's string config, the scheme dropped and
|
|
70
|
+
* `https://` prepended, except an explicit `http://` on a loopback host.
|
|
71
|
+
* `undefined` when a placeholder stays unfilled.
|
|
72
|
+
*/
|
|
73
|
+
export function engineHostOrigin(host, config) {
|
|
74
|
+
const filled = host.replace(/\{([A-Za-z0-9_.-]+)\}/g, (whole, name) => {
|
|
75
|
+
const v = config?.[name];
|
|
76
|
+
return typeof v === 'string' ? v : whole;
|
|
77
|
+
});
|
|
78
|
+
if (/[{}]/.test(filled))
|
|
79
|
+
return undefined;
|
|
80
|
+
const explicitHttp = /^http:\/\//i.test(filled.trim());
|
|
81
|
+
const bare = filled.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
|
|
82
|
+
const hostPart = bare.split(/[/?#]/)[0] ?? '';
|
|
83
|
+
if (!hostPart)
|
|
84
|
+
return undefined;
|
|
85
|
+
const scheme = explicitHttp && LOOPBACK.test(hostPart) ? 'http' : 'https';
|
|
86
|
+
try {
|
|
87
|
+
return new URL(`${scheme}://${hostPart}`).origin;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A prop that names where a link goes (navigation is not governed by CSP). */
|
|
94
|
+
const LINK_PROP = /^(?:href|to|link|url|.*Href|.*Link)$/;
|
|
95
|
+
/** Every absolute URL a receipt's block props carry, except links. */
|
|
96
|
+
function mediaUrlsOf(blocks, out) {
|
|
97
|
+
if (!Array.isArray(blocks))
|
|
98
|
+
return;
|
|
99
|
+
for (const block of blocks) {
|
|
100
|
+
if (!isRecord(block))
|
|
101
|
+
continue;
|
|
102
|
+
const props = block['props'];
|
|
103
|
+
if (isRecord(props))
|
|
104
|
+
collectUrls(props, out);
|
|
105
|
+
mediaUrlsOf(block['children'], out);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function collectUrls(value, out) {
|
|
109
|
+
const entries = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value);
|
|
110
|
+
for (const [key, v] of entries) {
|
|
111
|
+
if (typeof v === 'string') {
|
|
112
|
+
if (!LINK_PROP.test(key) && /^https?:\/\//i.test(v.trim()))
|
|
113
|
+
out.push(v.trim());
|
|
114
|
+
}
|
|
115
|
+
else if (Array.isArray(v) || isRecord(v)) {
|
|
116
|
+
collectUrls(v, out);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** `rest` data sources of a receipt's persistence capability. */
|
|
121
|
+
function restEndpointsOf(receipt) {
|
|
122
|
+
if (!isRecord(receipt) || !Array.isArray(receipt['capabilities']))
|
|
123
|
+
return [];
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const cap of receipt['capabilities']) {
|
|
126
|
+
if (!isRecord(cap) || !isRecord(cap['config']))
|
|
127
|
+
continue;
|
|
128
|
+
const sources = cap['config']['dataSources'];
|
|
129
|
+
if (!Array.isArray(sources))
|
|
130
|
+
continue;
|
|
131
|
+
for (const s of sources) {
|
|
132
|
+
if (isRecord(s) && s['kind'] === 'rest' && typeof s['endpoint'] === 'string')
|
|
133
|
+
out.push(s['endpoint']);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
const AUTH_ADDRESS_KEYS = ['loginUrl', 'registerUrl', 'logoutUrl', 'sessionUrl', 'tokenUrl', 'issuer', 'startUrl', 'gatewaySessionUrl', 'gatewayLogoutUrl'];
|
|
139
|
+
/** Writes the directives as a policy string. */
|
|
140
|
+
export function serializeCsp(directives, options = {}) {
|
|
141
|
+
return Object.entries(directives)
|
|
142
|
+
.filter(([name, values]) => values.length > 0 && !(options.meta && HEADER_ONLY.has(name)))
|
|
143
|
+
.map(([name, values]) => `${name} ${values.join(' ')}`)
|
|
144
|
+
.join('; ');
|
|
145
|
+
}
|
|
146
|
+
/** Derives the app's policy from what it declares. */
|
|
147
|
+
export function deriveCsp(input) {
|
|
148
|
+
const unresolved = [];
|
|
149
|
+
const connect = new Set();
|
|
150
|
+
const media = new Set();
|
|
151
|
+
const style = new Set();
|
|
152
|
+
const font = new Set();
|
|
153
|
+
const addConnect = (value, source) => {
|
|
154
|
+
const o = originOf(value);
|
|
155
|
+
if (o === undefined)
|
|
156
|
+
unresolved.push({ source, value, reason: 'the address is a template or expression; its origin is known only at run time' });
|
|
157
|
+
else if (o)
|
|
158
|
+
connect.add(o);
|
|
159
|
+
};
|
|
160
|
+
// Data: absolute `rest` endpoints in the receipts. Relative ones are 'self'.
|
|
161
|
+
for (const [key, receipt] of input.receipts) {
|
|
162
|
+
for (const endpoint of restEndpointsOf(receipt))
|
|
163
|
+
addConnect(endpoint, `page:${key}`);
|
|
164
|
+
const urls = [];
|
|
165
|
+
if (isRecord(receipt))
|
|
166
|
+
mediaUrlsOf(receipt['blocks'], urls);
|
|
167
|
+
for (const url of urls) {
|
|
168
|
+
const o = originOf(url);
|
|
169
|
+
if (o)
|
|
170
|
+
media.add(o);
|
|
171
|
+
else if (o === undefined)
|
|
172
|
+
unresolved.push({ source: `page:${key}`, value: url, reason: 'a media address with an expression in its origin' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Connections: declared engines resolve their host; compiled ones cannot be read.
|
|
176
|
+
const configOf = new Map();
|
|
177
|
+
for (const c of input.connections?.connections ?? []) {
|
|
178
|
+
if (typeof c['name'] === 'string')
|
|
179
|
+
configOf.set(c['name'], isRecord(c['config']) ? c['config'] : {});
|
|
180
|
+
}
|
|
181
|
+
for (const e of input.engines) {
|
|
182
|
+
const host = isRecord(e.engine) && typeof e.engine['host'] === 'string' ? e.engine['host'] : undefined;
|
|
183
|
+
if (host !== undefined) {
|
|
184
|
+
const o = engineHostOrigin(host, configOf.get(e.connection));
|
|
185
|
+
if (o)
|
|
186
|
+
connect.add(o);
|
|
187
|
+
else
|
|
188
|
+
unresolved.push({ source: `connection:${e.connection}`, value: host, reason: 'the engine host names config the connection does not provide' });
|
|
189
|
+
}
|
|
190
|
+
else if (e.moduleUrl) {
|
|
191
|
+
unresolved.push({
|
|
192
|
+
source: `engine:${e.atomId}`,
|
|
193
|
+
value: e.moduleUrl,
|
|
194
|
+
reason: `compiled engine for connection "${e.connection}"; the origins its code reaches are not declared`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Sign-in: the envelope's addresses, and an external issuer's.
|
|
199
|
+
if (isRecord(input.envelopeAuth)) {
|
|
200
|
+
for (const k of AUTH_ADDRESS_KEYS) {
|
|
201
|
+
const v = input.envelopeAuth[k];
|
|
202
|
+
if (typeof v === 'string')
|
|
203
|
+
addConnect(v, 'auth');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const issuer = isRecord(input.authDefinition) && isRecord(input.authDefinition['issuer']) ? input.authDefinition['issuer'] : undefined;
|
|
207
|
+
if (issuer) {
|
|
208
|
+
for (const k of ['issuer', 'tokenUrl']) {
|
|
209
|
+
const v = issuer[k];
|
|
210
|
+
if (typeof v === 'string')
|
|
211
|
+
addConnect(v, 'auth');
|
|
212
|
+
}
|
|
213
|
+
if (typeof issuer['tokenUrl'] !== 'string' && typeof issuer['issuer'] === 'string') {
|
|
214
|
+
unresolved.push({
|
|
215
|
+
source: 'auth',
|
|
216
|
+
value: issuer['issuer'],
|
|
217
|
+
reason: "the token endpoint is discovered at run time; the policy allows the issuer's own origin only",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Media the app declares.
|
|
222
|
+
for (const m of input.media) {
|
|
223
|
+
const o = originOf(m.url);
|
|
224
|
+
if (o)
|
|
225
|
+
media.add(o);
|
|
226
|
+
}
|
|
227
|
+
// Webfonts: the stylesheet and the files it points at.
|
|
228
|
+
if (input.webfontHref) {
|
|
229
|
+
const o = originOf(input.webfontHref);
|
|
230
|
+
if (o)
|
|
231
|
+
style.add(o);
|
|
232
|
+
if (o === 'https://fonts.googleapis.com')
|
|
233
|
+
font.add('https://fonts.gstatic.com');
|
|
234
|
+
}
|
|
235
|
+
const sorted = (s) => [...s].sort();
|
|
236
|
+
const directives = {
|
|
237
|
+
'default-src': ["'none'"],
|
|
238
|
+
'script-src': ["'self'", 'blob:', scriptHash(input.importMapText)],
|
|
239
|
+
'style-src': ["'self'", "'unsafe-inline'", ...sorted(style)],
|
|
240
|
+
'img-src': ["'self'", 'data:', 'blob:', ...sorted(media)],
|
|
241
|
+
'media-src': ["'self'", 'data:', 'blob:', ...sorted(media)],
|
|
242
|
+
...(media.size > 0 ? { 'frame-src': sorted(media) } : {}),
|
|
243
|
+
'font-src': ["'self'", ...sorted(font)],
|
|
244
|
+
'connect-src': ["'self'", ...sorted(connect)],
|
|
245
|
+
'base-uri': ["'self'"],
|
|
246
|
+
'object-src': ["'none'"],
|
|
247
|
+
'frame-ancestors': ["'none'"],
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
directives,
|
|
251
|
+
header: serializeCsp(directives),
|
|
252
|
+
meta: serializeCsp(directives, { meta: true }),
|
|
253
|
+
unresolved,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** The static-host `_headers` file (`/*` and the policy). */
|
|
257
|
+
export function headersFile(policy) {
|
|
258
|
+
return `/*\n Content-Security-Policy: ${policy}\n`;
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=csp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp.js","sourceRoot":"","sources":["../../src/dev/csp.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The app's Content Security Policy, derived from what the app declares.\n *\n * A pwapp is static files; what it may load and reach is known at build time\n * from its own declarations, so `pwf build` writes a policy that allows that\n * and nothing else (pwapp-format §2.1):\n *\n * default-src 'none'\n * script-src 'self' blob: 'sha256-<import map>'\n * Blocks and compiled engines are verified and then imported\n * from `blob:` URLs. The import map is the shell's only inline\n * script (an import map cannot be an external file), so its\n * hash is listed; the renderer seeding and the boot call are\n * files (`assets/seed.js`, `assets/boot.js`).\n * style-src 'self' 'unsafe-inline' [+ the webfont stylesheet origin]\n * The renderer draws `<style>` elements at run time (the\n * surface base, visibility, state, responsive and dark-theme\n * rules, computed per page), and blocks set `style`\n * attributes. Neither can be hashed ahead of time.\n * img-src 'self' data: blob: [+ media origins]\n * media-src 'self' data: blob: [+ media origins]\n * frame-src [media origins] — only when there are any\n * font-src 'self' [+ the webfont file origin]\n * connect-src 'self' [+ every origin the app's data and sign-in reach]\n * base-uri 'self' — a host that serves the app under a sub-path\n * injects `<base href>` (pwapp-format §2); 'none' would make\n * the browser ignore it and break path routing.\n * object-src 'none'\n * frame-ancestors 'none' — header only: browsers ignore it in a `<meta>`.\n *\n * Origins come from: absolute `rest` endpoints in the receipts; declared\n * engines' `host` templates filled from `connections.json` config;\n * the envelope `auth.json` addresses and an external issuer; `publish.json`\n * media and absolute media URLs in receipt props. A template that cannot be\n * resolved at build time, and a compiled engine (whose code may reach any\n * origin), is listed in `unresolved` instead of being guessed.\n *\n * @module @pwfabric/cli/dev/csp\n */\nimport { createHash } from 'node:crypto'\nimport { isRecord } from './sources.js'\n\n/** Directive name → its sources, in the order they are written. */\nexport type CspDirectives = Record<string, string[]>\n\n/** Something the policy could not resolve at build time. */\nexport interface CspUnresolved {\n /** What declared it: `connection:<name>`, `engine:<atomId>`, `page:<key>`, `auth`. */\n readonly source: string\n /** The template or address as written. */\n readonly value: string\n readonly reason: string\n}\n\nexport interface AppCsp {\n readonly directives: CspDirectives\n /** The policy as a header value (every directive). */\n readonly header: string\n /** The policy for a `<meta http-equiv>` (without the directives a meta cannot carry). */\n readonly meta: string\n readonly unresolved: readonly CspUnresolved[]\n}\n\nexport interface CspInputs {\n /** The exact text of the shell's inline import map. */\n readonly importMapText: string\n /** Served receipts by page key (after media slots are filled). */\n readonly receipts: ReadonlyMap<string, unknown>\n /** `pwpack/connections.json`. */\n readonly connections?: { readonly connections: readonly Record<string, unknown>[] } | undefined\n /** `engines/manifest.json` entries. */\n readonly engines: readonly { readonly connection: string; readonly atomId: string; readonly moduleUrl?: string; readonly engine?: unknown }[]\n /** The envelope `auth.json`, when the build has one. */\n readonly envelopeAuth?: unknown\n /** `app/definitions/auth.json` (the declaration). */\n readonly authDefinition?: unknown\n /** `publish.json` `media[]`. */\n readonly media: readonly { readonly url: string }[]\n /** The webfont stylesheet the shell links, when any. */\n readonly webfontHref?: string | null\n}\n\n/** Directives a `<meta http-equiv=\"Content-Security-Policy\">` cannot carry (CSP3 §3.3). */\nconst HEADER_ONLY = new Set(['frame-ancestors', 'report-uri', 'report-to', 'sandbox'])\n\n/** The CSP source for an inline script: `'sha256-<base64>'` of its exact text. */\nexport function scriptHash(text: string): string {\n return `'sha256-${createHash('sha256').update(text, 'utf8').digest('base64')}'`\n}\n\nconst LOOPBACK = /^(?:localhost|127\\.0\\.0\\.1|\\[::1\\]|[^/:]+\\.localhost)(?::\\d+)?$/i\n\n/**\n * The origin of an absolute http(s) URL, or null. Expressions (`{{…}}`) or\n * templates (`{…}`) in the scheme or host make it unresolvable (`undefined`).\n */\nexport function originOf(value: string): string | null | undefined {\n const m = /^(https?):\\/\\/([^/?#]*)/i.exec(value.trim())\n if (!m) return null\n const host = m[2] as string\n if (!host || /[{}]/.test(host)) return undefined\n try {\n return new URL(`${(m[1] as string).toLowerCase()}://${host}`).origin\n } catch {\n return undefined\n }\n}\n\n/**\n * The origin a declared engine's `host` reaches (connector-manifest §3.1):\n * `{name}` filled from the connection's string config, the scheme dropped and\n * `https://` prepended, except an explicit `http://` on a loopback host.\n * `undefined` when a placeholder stays unfilled.\n */\nexport function engineHostOrigin(host: string, config: Record<string, unknown> | undefined): string | undefined {\n const filled = host.replace(/\\{([A-Za-z0-9_.-]+)\\}/g, (whole, name: string) => {\n const v = config?.[name]\n return typeof v === 'string' ? v : whole\n })\n if (/[{}]/.test(filled)) return undefined\n const explicitHttp = /^http:\\/\\//i.test(filled.trim())\n const bare = filled.trim().replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')\n const hostPart = bare.split(/[/?#]/)[0] ?? ''\n if (!hostPart) return undefined\n const scheme = explicitHttp && LOOPBACK.test(hostPart) ? 'http' : 'https'\n try {\n return new URL(`${scheme}://${hostPart}`).origin\n } catch {\n return undefined\n }\n}\n\n/** A prop that names where a link goes (navigation is not governed by CSP). */\nconst LINK_PROP = /^(?:href|to|link|url|.*Href|.*Link)$/\n\n/** Every absolute URL a receipt's block props carry, except links. */\nfunction mediaUrlsOf(blocks: unknown, out: string[]): void {\n if (!Array.isArray(blocks)) return\n for (const block of blocks) {\n if (!isRecord(block)) continue\n const props = block['props']\n if (isRecord(props)) collectUrls(props, out)\n mediaUrlsOf(block['children'], out)\n }\n}\n\nfunction collectUrls(value: Record<string, unknown> | unknown[], out: string[]): void {\n const entries: [string, unknown][] = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value)\n for (const [key, v] of entries) {\n if (typeof v === 'string') {\n if (!LINK_PROP.test(key) && /^https?:\\/\\//i.test(v.trim())) out.push(v.trim())\n } else if (Array.isArray(v) || isRecord(v)) {\n collectUrls(v, out)\n }\n }\n}\n\n/** `rest` data sources of a receipt's persistence capability. */\nfunction restEndpointsOf(receipt: unknown): string[] {\n if (!isRecord(receipt) || !Array.isArray(receipt['capabilities'])) return []\n const out: string[] = []\n for (const cap of receipt['capabilities']) {\n if (!isRecord(cap) || !isRecord(cap['config'])) continue\n const sources = cap['config']['dataSources']\n if (!Array.isArray(sources)) continue\n for (const s of sources) {\n if (isRecord(s) && s['kind'] === 'rest' && typeof s['endpoint'] === 'string') out.push(s['endpoint'])\n }\n }\n return out\n}\n\nconst AUTH_ADDRESS_KEYS = ['loginUrl', 'registerUrl', 'logoutUrl', 'sessionUrl', 'tokenUrl', 'issuer', 'startUrl', 'gatewaySessionUrl', 'gatewayLogoutUrl']\n\n/** Writes the directives as a policy string. */\nexport function serializeCsp(directives: CspDirectives, options: { meta?: boolean } = {}): string {\n return Object.entries(directives)\n .filter(([name, values]) => values.length > 0 && !(options.meta && HEADER_ONLY.has(name)))\n .map(([name, values]) => `${name} ${values.join(' ')}`)\n .join('; ')\n}\n\n/** Derives the app's policy from what it declares. */\nexport function deriveCsp(input: CspInputs): AppCsp {\n const unresolved: CspUnresolved[] = []\n const connect = new Set<string>()\n const media = new Set<string>()\n const style = new Set<string>()\n const font = new Set<string>()\n\n const addConnect = (value: string, source: string): void => {\n const o = originOf(value)\n if (o === undefined) unresolved.push({ source, value, reason: 'the address is a template or expression; its origin is known only at run time' })\n else if (o) connect.add(o)\n }\n\n // Data: absolute `rest` endpoints in the receipts. Relative ones are 'self'.\n for (const [key, receipt] of input.receipts) {\n for (const endpoint of restEndpointsOf(receipt)) addConnect(endpoint, `page:${key}`)\n const urls: string[] = []\n if (isRecord(receipt)) mediaUrlsOf(receipt['blocks'], urls)\n for (const url of urls) {\n const o = originOf(url)\n if (o) media.add(o)\n else if (o === undefined) unresolved.push({ source: `page:${key}`, value: url, reason: 'a media address with an expression in its origin' })\n }\n }\n\n // Connections: declared engines resolve their host; compiled ones cannot be read.\n const configOf = new Map<string, Record<string, unknown>>()\n for (const c of input.connections?.connections ?? []) {\n if (typeof c['name'] === 'string') configOf.set(c['name'], isRecord(c['config']) ? c['config'] : {})\n }\n for (const e of input.engines) {\n const host = isRecord(e.engine) && typeof e.engine['host'] === 'string' ? e.engine['host'] : undefined\n if (host !== undefined) {\n const o = engineHostOrigin(host, configOf.get(e.connection))\n if (o) connect.add(o)\n else unresolved.push({ source: `connection:${e.connection}`, value: host, reason: 'the engine host names config the connection does not provide' })\n } else if (e.moduleUrl) {\n unresolved.push({\n source: `engine:${e.atomId}`,\n value: e.moduleUrl,\n reason: `compiled engine for connection \"${e.connection}\"; the origins its code reaches are not declared`,\n })\n }\n }\n\n // Sign-in: the envelope's addresses, and an external issuer's.\n if (isRecord(input.envelopeAuth)) {\n for (const k of AUTH_ADDRESS_KEYS) {\n const v = input.envelopeAuth[k]\n if (typeof v === 'string') addConnect(v, 'auth')\n }\n }\n const issuer = isRecord(input.authDefinition) && isRecord(input.authDefinition['issuer']) ? input.authDefinition['issuer'] : undefined\n if (issuer) {\n for (const k of ['issuer', 'tokenUrl']) {\n const v = issuer[k]\n if (typeof v === 'string') addConnect(v, 'auth')\n }\n if (typeof issuer['tokenUrl'] !== 'string' && typeof issuer['issuer'] === 'string') {\n unresolved.push({\n source: 'auth',\n value: issuer['issuer'],\n reason: \"the token endpoint is discovered at run time; the policy allows the issuer's own origin only\",\n })\n }\n }\n\n // Media the app declares.\n for (const m of input.media) {\n const o = originOf(m.url)\n if (o) media.add(o)\n }\n\n // Webfonts: the stylesheet and the files it points at.\n if (input.webfontHref) {\n const o = originOf(input.webfontHref)\n if (o) style.add(o)\n if (o === 'https://fonts.googleapis.com') font.add('https://fonts.gstatic.com')\n }\n\n const sorted = (s: Set<string>): string[] => [...s].sort()\n const directives: CspDirectives = {\n 'default-src': [\"'none'\"],\n 'script-src': [\"'self'\", 'blob:', scriptHash(input.importMapText)],\n 'style-src': [\"'self'\", \"'unsafe-inline'\", ...sorted(style)],\n 'img-src': [\"'self'\", 'data:', 'blob:', ...sorted(media)],\n 'media-src': [\"'self'\", 'data:', 'blob:', ...sorted(media)],\n ...(media.size > 0 ? { 'frame-src': sorted(media) } : {}),\n 'font-src': [\"'self'\", ...sorted(font)],\n 'connect-src': [\"'self'\", ...sorted(connect)],\n 'base-uri': [\"'self'\"],\n 'object-src': [\"'none'\"],\n 'frame-ancestors': [\"'none'\"],\n }\n return {\n directives,\n header: serializeCsp(directives),\n meta: serializeCsp(directives, { meta: true }),\n unresolved,\n }\n}\n\n/** The static-host `_headers` file (`/*` and the policy). */\nexport function headersFile(policy: string): string {\n return `/*\\n Content-Security-Policy: ${policy}\\n`\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an assembly says about the folder it read: one shape for compiler
|
|
3
|
+
* output, validation findings and the assembler's own checks, so the terminal
|
|
4
|
+
* and the overlay page print the same list.
|
|
5
|
+
*/
|
|
6
|
+
export type AssembleSeverity = 'error' | 'warning' | 'info';
|
|
7
|
+
export interface AssembleDiagnostic {
|
|
8
|
+
readonly severity: AssembleSeverity;
|
|
9
|
+
/** Machine-readable reason, e.g. `PACK_NOT_CACHED` or a compiler code. */
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
/** The file the finding is about, relative to the app folder when it can be. */
|
|
13
|
+
readonly path?: string;
|
|
14
|
+
readonly line?: number;
|
|
15
|
+
readonly column?: number;
|
|
16
|
+
}
|
|
17
|
+
/** `error app/surfaces/home.receipt.json:3:7 UNKNOWN_BLOCK_TYPE …` */
|
|
18
|
+
export declare function formatDiagnostic(d: AssembleDiagnostic): string;
|
|
19
|
+
export declare function hasErrors(diagnostics: readonly AssembleDiagnostic[]): boolean;
|
|
20
|
+
/** Errors first, then warnings, then info; stable within a severity. */
|
|
21
|
+
export declare function sortDiagnostics(diagnostics: readonly AssembleDiagnostic[]): AssembleDiagnostic[];
|