@writerslogic/facet-cli 0.5.2
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 +201 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2153 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/config.ts
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
7
|
+
// src/util.ts
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
function printError(msg) {
|
|
10
|
+
process.stderr.write(`${pc.red(msg)}
|
|
11
|
+
`);
|
|
12
|
+
}
|
|
13
|
+
async function fetchJson(url, init) {
|
|
14
|
+
const res = await fetch(url, init);
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
let code = String(res.status);
|
|
17
|
+
try {
|
|
18
|
+
const body = await res.json();
|
|
19
|
+
if (body.error) code = body.error;
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
throw new Error(code);
|
|
23
|
+
}
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/commands/config.ts
|
|
28
|
+
var PLACEHOLDER = "PLACEHOLDER_D1_DATABASE_ID";
|
|
29
|
+
var DEFAULT_CONFIGS = ["./wrangler.jsonc", "apps/server/wrangler.jsonc"];
|
|
30
|
+
var DB_ID_RE = /("database_id"\s*:\s*")([^"]*)(")/;
|
|
31
|
+
function resolveConfigPath(flag) {
|
|
32
|
+
if (flag) return existsSync(flag) ? flag : null;
|
|
33
|
+
for (const candidate of DEFAULT_CONFIGS) {
|
|
34
|
+
if (existsSync(candidate)) return candidate;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function runConfig(args) {
|
|
39
|
+
const [sub, ...rest] = args;
|
|
40
|
+
if (sub === "set-db-id") return setDbId(rest);
|
|
41
|
+
if (sub === "check") return check(rest);
|
|
42
|
+
printError("Usage: facet config <set-db-id|check> [options]");
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
function setDbId(args) {
|
|
46
|
+
const { values } = parseArgs({
|
|
47
|
+
args,
|
|
48
|
+
options: {
|
|
49
|
+
id: { type: "string" },
|
|
50
|
+
config: { type: "string" },
|
|
51
|
+
force: { type: "boolean" }
|
|
52
|
+
},
|
|
53
|
+
allowPositionals: false
|
|
54
|
+
});
|
|
55
|
+
if (!values.id) {
|
|
56
|
+
printError("Missing required option: --id <database_id>.");
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
const path = resolveConfigPath(values.config);
|
|
60
|
+
if (!path) {
|
|
61
|
+
printError(
|
|
62
|
+
values.config ? `Config not found: ${values.config}` : `No wrangler.jsonc found (looked in ${DEFAULT_CONFIGS.join(", ")}). Pass --config <path>.`
|
|
63
|
+
);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
const source = readFileSync(path, "utf8");
|
|
67
|
+
const match = source.match(DB_ID_RE);
|
|
68
|
+
if (!match) {
|
|
69
|
+
printError(`No "database_id" field found in ${path}.`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
const current = match[2];
|
|
73
|
+
if (current && current !== PLACEHOLDER && current !== values.id && !values.force) {
|
|
74
|
+
printError(
|
|
75
|
+
`Refusing to overwrite existing database_id "${current}" in ${path}. Pass --force to override.`
|
|
76
|
+
);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const updated = source.replace(DB_ID_RE, `$1${values.id}$3`);
|
|
80
|
+
writeFileSync(path, updated);
|
|
81
|
+
process.stdout.write(`Set database_id in ${path}.
|
|
82
|
+
`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
function check(args) {
|
|
86
|
+
const { values } = parseArgs({
|
|
87
|
+
args,
|
|
88
|
+
options: { config: { type: "string" } },
|
|
89
|
+
allowPositionals: false
|
|
90
|
+
});
|
|
91
|
+
const path = resolveConfigPath(values.config);
|
|
92
|
+
if (!path) {
|
|
93
|
+
printError(
|
|
94
|
+
values.config ? `Config not found: ${values.config}` : `No wrangler.jsonc found (looked in ${DEFAULT_CONFIGS.join(", ")}). Pass --config <path>.`
|
|
95
|
+
);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
const source = readFileSync(path, "utf8");
|
|
99
|
+
const match = source.match(DB_ID_RE);
|
|
100
|
+
if (!match || !match[2]) {
|
|
101
|
+
printError(`database_id is missing or empty in ${path}.`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
if (match[2] === PLACEHOLDER) {
|
|
105
|
+
printError(
|
|
106
|
+
`database_id in ${path} is still the placeholder. Run \`facet config set-db-id --id <id>\` first.`
|
|
107
|
+
);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write(`database_id in ${path} is set.
|
|
111
|
+
`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/commands/init.ts
|
|
116
|
+
import { randomBytes } from "node:crypto";
|
|
117
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
118
|
+
import { join } from "node:path";
|
|
119
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
120
|
+
import * as p from "@clack/prompts";
|
|
121
|
+
function wranglerJsonc(name, db) {
|
|
122
|
+
return `{
|
|
123
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
124
|
+
"name": "${name}",
|
|
125
|
+
"main": "src/index.ts",
|
|
126
|
+
"compatibility_date": "2026-07-01",
|
|
127
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
128
|
+
"observability": { "enabled": true },
|
|
129
|
+
"triggers": { "crons": ["0 * * * *"] },
|
|
130
|
+
"assets": { "directory": "../dashboard/dist", "binding": "ASSETS" },
|
|
131
|
+
"d1_databases": [
|
|
132
|
+
{
|
|
133
|
+
"binding": "DB",
|
|
134
|
+
"database_name": "${db}",
|
|
135
|
+
"database_id": "PLACEHOLDER_D1_DATABASE_ID",
|
|
136
|
+
"migrations_dir": "migrations"
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"unsafe": {
|
|
140
|
+
"bindings": [
|
|
141
|
+
{
|
|
142
|
+
"name": "RATE_LIMITER",
|
|
143
|
+
"type": "ratelimit",
|
|
144
|
+
"namespace_id": "1001",
|
|
145
|
+
"simple": { "limit": 100, "period": 60 }
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"vars": { "RAW_RETENTION_DAYS": "90" }
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
async function runInit(args) {
|
|
154
|
+
const { values } = parseArgs2({
|
|
155
|
+
args,
|
|
156
|
+
options: {
|
|
157
|
+
name: { type: "string" },
|
|
158
|
+
db: { type: "string" },
|
|
159
|
+
dir: { type: "string" },
|
|
160
|
+
"dry-run": { type: "boolean" }
|
|
161
|
+
},
|
|
162
|
+
allowPositionals: false
|
|
163
|
+
});
|
|
164
|
+
const dryRun = Boolean(values["dry-run"]);
|
|
165
|
+
const dir = values.dir ?? ".";
|
|
166
|
+
const db = values.db ?? "facet";
|
|
167
|
+
let name = values.name;
|
|
168
|
+
if (!name) {
|
|
169
|
+
if (dryRun) {
|
|
170
|
+
name = "facet";
|
|
171
|
+
} else {
|
|
172
|
+
const answer = await p.text({
|
|
173
|
+
message: "Worker name",
|
|
174
|
+
placeholder: "facet",
|
|
175
|
+
defaultValue: "facet"
|
|
176
|
+
});
|
|
177
|
+
if (p.isCancel(answer)) {
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
name = String(answer);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
mkdirSync(dir, { recursive: true });
|
|
184
|
+
const adminToken = randomBytes(32).toString("hex");
|
|
185
|
+
writeFileSync2(join(dir, "wrangler.jsonc"), wranglerJsonc(name, db));
|
|
186
|
+
writeFileSync2(join(dir, ".dev.vars"), `ADMIN_TOKEN=${adminToken}
|
|
187
|
+
`);
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
`Wrote wrangler.jsonc and .dev.vars to ${dir}.
|
|
190
|
+
Next: run \`wrangler d1 create ${db}\` and put the id into wrangler.jsonc.
|
|
191
|
+
`
|
|
192
|
+
);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/commands/keyattest.ts
|
|
197
|
+
import { X509Certificate, createPublicKey } from "node:crypto";
|
|
198
|
+
import { readFile } from "node:fs/promises";
|
|
199
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
200
|
+
import pc2 from "picocolors";
|
|
201
|
+
var USAGE = `Usage: facet keyattest verify <leaf.pem> --root <root.pem> --key <file> [flags]
|
|
202
|
+
|
|
203
|
+
Verify an X.509 hardware key-attestation chain (the form real HSMs / YubiKeys / TPMs emit) and confirm
|
|
204
|
+
the leaf certifies the deployment signing key.
|
|
205
|
+
|
|
206
|
+
verify <leaf.pem> The attestation leaf certificate (the hardware-resident key's cert).
|
|
207
|
+
--root <root.pem> The CONFIGURED trust anchor (attestor/vendor root, self-signed CA). Required.
|
|
208
|
+
--key <file> The deployment signing key to bind to: a PEM public key, or a cert/PEM
|
|
209
|
+
whose SPKI is compared to the leaf's. Required.
|
|
210
|
+
--intermediate <pem> An intermediate CA PEM (repeatable) between leaf and root.
|
|
211
|
+
--now <iso8601> Verification time (default: now). Each cert must be valid at this instant.
|
|
212
|
+
|
|
213
|
+
hardware:true requires BOTH: the chain reaches the configured root AND the leaf SPKI == the deployment key.
|
|
214
|
+
`;
|
|
215
|
+
function ok(msg) {
|
|
216
|
+
process.stdout.write(`${pc2.green("\u2713")} ${msg}
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
219
|
+
async function readCert(path) {
|
|
220
|
+
try {
|
|
221
|
+
return new X509Certificate(await readFile(path));
|
|
222
|
+
} catch (err) {
|
|
223
|
+
printError(
|
|
224
|
+
`could not read certificate ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
225
|
+
);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function readSpki(path) {
|
|
230
|
+
let raw;
|
|
231
|
+
try {
|
|
232
|
+
raw = await readFile(path);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
printError(
|
|
235
|
+
`could not read key ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
236
|
+
);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return Buffer.from(
|
|
241
|
+
new X509Certificate(raw).publicKey.export({
|
|
242
|
+
type: "spki",
|
|
243
|
+
format: "der"
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
} catch {
|
|
247
|
+
try {
|
|
248
|
+
return Buffer.from(createPublicKey(raw).export({ type: "spki", format: "der" }));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
printError(
|
|
251
|
+
`--key is neither a certificate nor a public key: ${err instanceof Error ? err.message : String(err)}`
|
|
252
|
+
);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function validAt(cert, at) {
|
|
258
|
+
return at >= cert.validFromDate && at <= cert.validToDate;
|
|
259
|
+
}
|
|
260
|
+
function verifyChain(chain, root, at) {
|
|
261
|
+
for (const cert of [...chain, root]) {
|
|
262
|
+
if (!validAt(cert, at))
|
|
263
|
+
return `certificate "${cert.subject}" is not valid at ${at.toISOString()}`;
|
|
264
|
+
}
|
|
265
|
+
const ordered = [...chain, root];
|
|
266
|
+
for (let i = 0; i < ordered.length - 1; i++) {
|
|
267
|
+
const cert = ordered[i];
|
|
268
|
+
const issuer = ordered[i + 1];
|
|
269
|
+
if (!cert.checkIssued(issuer))
|
|
270
|
+
return `"${cert.subject}" was not issued by "${issuer.subject}"`;
|
|
271
|
+
if (!cert.verify(issuer.publicKey))
|
|
272
|
+
return `signature on "${cert.subject}" does not verify against "${issuer.subject}"`;
|
|
273
|
+
}
|
|
274
|
+
if (!root.checkIssued(root) || !root.verify(root.publicKey)) {
|
|
275
|
+
return `configured root "${root.subject}" is not a self-signed trust anchor`;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
async function runVerify(args) {
|
|
280
|
+
const { values, positionals } = parseArgs3({
|
|
281
|
+
args,
|
|
282
|
+
options: {
|
|
283
|
+
root: { type: "string" },
|
|
284
|
+
key: { type: "string" },
|
|
285
|
+
intermediate: { type: "string", multiple: true },
|
|
286
|
+
now: { type: "string" }
|
|
287
|
+
},
|
|
288
|
+
allowPositionals: true
|
|
289
|
+
});
|
|
290
|
+
const leafPath = positionals[0];
|
|
291
|
+
if (!leafPath) {
|
|
292
|
+
printError("missing <leaf.pem> argument");
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
if (!values.root) {
|
|
296
|
+
printError("provide the configured trust anchor with --root <root.pem>");
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
if (!values.key) {
|
|
300
|
+
printError("provide the deployment signing key with --key <file>");
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
const at = values.now ? new Date(values.now) : /* @__PURE__ */ new Date();
|
|
304
|
+
if (Number.isNaN(at.getTime())) {
|
|
305
|
+
printError(`--now is not a valid ISO-8601 date: ${values.now}`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const leaf = await readCert(leafPath);
|
|
309
|
+
const root = await readCert(values.root);
|
|
310
|
+
if (!leaf || !root) return 1;
|
|
311
|
+
const intermediates = [];
|
|
312
|
+
for (const path of values.intermediate ?? []) {
|
|
313
|
+
const cert = await readCert(path);
|
|
314
|
+
if (!cert) return 1;
|
|
315
|
+
intermediates.push(cert);
|
|
316
|
+
}
|
|
317
|
+
const deploySpki = await readSpki(values.key);
|
|
318
|
+
if (!deploySpki) return 1;
|
|
319
|
+
const chainReason = verifyChain([leaf, ...intermediates], root, at);
|
|
320
|
+
if (chainReason !== null) {
|
|
321
|
+
printError(`\u2717 attestation chain does not verify: ${chainReason}`);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
const leafSpki = Buffer.from(leaf.publicKey.export({ type: "spki", format: "der" }));
|
|
325
|
+
if (!leafSpki.equals(deploySpki)) {
|
|
326
|
+
printError(
|
|
327
|
+
"\u2717 leaf certificate does not certify the deployment signing key (SPKI mismatch)"
|
|
328
|
+
);
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
331
|
+
ok(
|
|
332
|
+
`hardware key-attestation verified (leaf="${leaf.subject}", anchored to root="${root.subject}")`
|
|
333
|
+
);
|
|
334
|
+
process.stdout.write(
|
|
335
|
+
` ${pc2.dim("leaf SPKI == deployment signing key; chain reaches the configured trust anchor.")}
|
|
336
|
+
`
|
|
337
|
+
);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
async function runKeyattest(args) {
|
|
341
|
+
const [op] = args;
|
|
342
|
+
if (op === void 0 || op === "--help" || op === "-h") {
|
|
343
|
+
process.stdout.write(USAGE);
|
|
344
|
+
return op === void 0 ? 1 : 0;
|
|
345
|
+
}
|
|
346
|
+
switch (op) {
|
|
347
|
+
case "verify":
|
|
348
|
+
return runVerify(args.slice(1));
|
|
349
|
+
default:
|
|
350
|
+
printError(`unknown keyattest op: ${op}`);
|
|
351
|
+
process.stderr.write(USAGE);
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/keys.ts
|
|
357
|
+
import { writeFile } from "node:fs/promises";
|
|
358
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
359
|
+
|
|
360
|
+
// ../trust/src/bytes.ts
|
|
361
|
+
var enc = new TextEncoder();
|
|
362
|
+
function concatBytes(...parts) {
|
|
363
|
+
let total = 0;
|
|
364
|
+
for (const p2 of parts) total += p2.length;
|
|
365
|
+
const buf = new Uint8Array(total);
|
|
366
|
+
let off = 0;
|
|
367
|
+
for (const p2 of parts) {
|
|
368
|
+
buf.set(p2, off);
|
|
369
|
+
off += p2.length;
|
|
370
|
+
}
|
|
371
|
+
return buf;
|
|
372
|
+
}
|
|
373
|
+
async function sha256(...parts) {
|
|
374
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", concatBytes(...parts)));
|
|
375
|
+
}
|
|
376
|
+
function toHex(bytes) {
|
|
377
|
+
let s = "";
|
|
378
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
379
|
+
return s;
|
|
380
|
+
}
|
|
381
|
+
function fromHex(hex) {
|
|
382
|
+
const out = new Uint8Array(hex.length / 2);
|
|
383
|
+
for (let k = 0; k < out.length; k++) out[k] = Number.parseInt(hex.slice(k * 2, k * 2 + 2), 16);
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
function bytesEqual(a, b) {
|
|
387
|
+
if (a.length !== b.length) return false;
|
|
388
|
+
for (let k = 0; k < a.length; k++) if (a[k] !== b[k]) return false;
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
function toBinaryString(bytes) {
|
|
392
|
+
let s = "";
|
|
393
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
394
|
+
return s;
|
|
395
|
+
}
|
|
396
|
+
function bytesToBase64(bytes) {
|
|
397
|
+
return btoa(toBinaryString(bytes));
|
|
398
|
+
}
|
|
399
|
+
function base64ToBytes(b64) {
|
|
400
|
+
const bin = atob(b64);
|
|
401
|
+
const out = new Uint8Array(bin.length);
|
|
402
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
function bytesToBase64url(bytes) {
|
|
406
|
+
return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
407
|
+
}
|
|
408
|
+
function base64urlToBytes(b64u) {
|
|
409
|
+
const b64 = b64u.replace(/-/g, "+").replace(/_/g, "/");
|
|
410
|
+
return base64ToBytes(b64.padEnd(Math.ceil(b64.length / 4) * 4, "="));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ../trust/src/keys.ts
|
|
414
|
+
import {
|
|
415
|
+
calculateJwkThumbprint,
|
|
416
|
+
exportJWK,
|
|
417
|
+
generateKeyPair,
|
|
418
|
+
importJWK
|
|
419
|
+
} from "jose";
|
|
420
|
+
function algForJwk(jwk) {
|
|
421
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "EdDSA";
|
|
422
|
+
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
|
|
423
|
+
throw new Error(`unsupported key type for signing: kty=${jwk.kty} crv=${jwk.crv}`);
|
|
424
|
+
}
|
|
425
|
+
async function toPublicJwk(privateJwk, alg) {
|
|
426
|
+
const { d: D, ...pub } = privateJwk;
|
|
427
|
+
const kid = await calculateJwkThumbprint(pub);
|
|
428
|
+
return { ...pub, alg, use: "sig", kid };
|
|
429
|
+
}
|
|
430
|
+
async function generateSigningJwk(alg = "EdDSA") {
|
|
431
|
+
const { publicKey, privateKey } = await generateKeyPair(alg, {
|
|
432
|
+
extractable: true
|
|
433
|
+
});
|
|
434
|
+
const privateJwk = await exportJWK(privateKey);
|
|
435
|
+
const publicJwk = await toPublicJwk(await exportJWK(publicKey), alg);
|
|
436
|
+
privateJwk.alg = alg;
|
|
437
|
+
privateJwk.kid = publicJwk.kid;
|
|
438
|
+
return { privateJwk, publicJwk };
|
|
439
|
+
}
|
|
440
|
+
async function importPublicJwk(jwk) {
|
|
441
|
+
const alg = jwk.alg === "ES256" || jwk.alg === "EdDSA" ? jwk.alg : algForJwk(jwk);
|
|
442
|
+
const key = await importJWK(jwk, alg);
|
|
443
|
+
if (key instanceof Uint8Array) throw new Error("public JWK imported as a symmetric key");
|
|
444
|
+
return { key, alg };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ../trust/src/jws.ts
|
|
448
|
+
import { CompactSign, base64url, compactVerify } from "jose";
|
|
449
|
+
|
|
450
|
+
// ../trust/src/canonicalize.ts
|
|
451
|
+
function canonicalizeJson(value) {
|
|
452
|
+
if (value === null) return "null";
|
|
453
|
+
if (typeof value === "number") {
|
|
454
|
+
if (!Number.isFinite(value)) throw new Error("cannot canonicalize a non-finite number");
|
|
455
|
+
return JSON.stringify(value);
|
|
456
|
+
}
|
|
457
|
+
if (typeof value === "boolean" || typeof value === "string") return JSON.stringify(value);
|
|
458
|
+
if (Array.isArray(value)) {
|
|
459
|
+
return `[${value.map((v) => canonicalizeJson(v)).join(",")}]`;
|
|
460
|
+
}
|
|
461
|
+
if (typeof value === "object") {
|
|
462
|
+
const obj = value;
|
|
463
|
+
const keys2 = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
464
|
+
const members = keys2.map((k) => `${JSON.stringify(k)}:${canonicalizeJson(obj[k])}`);
|
|
465
|
+
return `{${members.join(",")}}`;
|
|
466
|
+
}
|
|
467
|
+
throw new Error(`cannot canonicalize value of type ${typeof value}`);
|
|
468
|
+
}
|
|
469
|
+
function canonicalizeBytes(value) {
|
|
470
|
+
return new TextEncoder().encode(canonicalizeJson(value));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ../trust/src/jws.ts
|
|
474
|
+
async function verifyDetachedJws(detached, payload, publicJwk) {
|
|
475
|
+
const parts = detached.split(".");
|
|
476
|
+
if (parts.length !== 3 || parts[1] !== "") {
|
|
477
|
+
throw new Error("malformed detached JWS (expected `<protected>..<signature>`)");
|
|
478
|
+
}
|
|
479
|
+
const reattached = `${parts[0]}.${base64url.encode(payload)}.${parts[2]}`;
|
|
480
|
+
const { key, alg } = await importPublicJwk(publicJwk);
|
|
481
|
+
const { protectedHeader } = await compactVerify(reattached, key, {
|
|
482
|
+
algorithms: [alg]
|
|
483
|
+
});
|
|
484
|
+
return { protectedHeader };
|
|
485
|
+
}
|
|
486
|
+
async function verifyDetachedProof(proof, payload) {
|
|
487
|
+
if (proof?.type !== "DetachedJWS") return { ok: false, reason: "unsupported proof type" };
|
|
488
|
+
try {
|
|
489
|
+
const { protectedHeader } = await verifyDetachedJws(
|
|
490
|
+
proof.jws,
|
|
491
|
+
canonicalizeBytes(payload),
|
|
492
|
+
proof.publicJwk
|
|
493
|
+
);
|
|
494
|
+
if (protectedHeader.kid !== proof.kid) {
|
|
495
|
+
return {
|
|
496
|
+
ok: false,
|
|
497
|
+
reason: "protected-header kid does not match proof kid"
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return { ok: true };
|
|
501
|
+
} catch (e) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
reason: e instanceof Error ? e.message : "verification failed"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ../trust/src/cose.ts
|
|
510
|
+
import { decode as cborDecode, encode as cborEncode } from "cborg";
|
|
511
|
+
var ALG_FOR_COSE = {
|
|
512
|
+
[-8]: "EdDSA",
|
|
513
|
+
[-7]: "ES256"
|
|
514
|
+
};
|
|
515
|
+
var HDR_ALG = 1;
|
|
516
|
+
var HDR_KID = 4;
|
|
517
|
+
var COSE_SIGN1_TAG = 18;
|
|
518
|
+
var enc2 = new TextEncoder();
|
|
519
|
+
function algParams(alg) {
|
|
520
|
+
return alg === "EdDSA" ? { name: "Ed25519" } : { name: "ECDSA", hash: "SHA-256" };
|
|
521
|
+
}
|
|
522
|
+
function sigStructure(protectedBytes, payload) {
|
|
523
|
+
return cborEncode(["Signature1", protectedBytes, new Uint8Array(0), payload]);
|
|
524
|
+
}
|
|
525
|
+
var DECODE_OPTS = (() => {
|
|
526
|
+
const tags = [];
|
|
527
|
+
tags[COSE_SIGN1_TAG] = (inner) => inner;
|
|
528
|
+
return { useMaps: true, tags };
|
|
529
|
+
})();
|
|
530
|
+
function decodeCoseSign1(message) {
|
|
531
|
+
const arr = cborDecode(message, DECODE_OPTS);
|
|
532
|
+
if (!Array.isArray(arr) || arr.length !== 4) {
|
|
533
|
+
throw new Error("malformed COSE_Sign1 (expected a 4-element array)");
|
|
534
|
+
}
|
|
535
|
+
const [protectedBytes, unprotected, payload, signature] = arr;
|
|
536
|
+
if (!(protectedBytes instanceof Uint8Array) || !(payload instanceof Uint8Array) || !(signature instanceof Uint8Array)) {
|
|
537
|
+
throw new Error("malformed COSE_Sign1 (protected/payload/signature must be bstr)");
|
|
538
|
+
}
|
|
539
|
+
return [protectedBytes, unprotected, payload, signature];
|
|
540
|
+
}
|
|
541
|
+
function parseProtected(protectedBytes) {
|
|
542
|
+
const map = cborDecode(protectedBytes, DECODE_OPTS);
|
|
543
|
+
if (!(map instanceof Map)) throw new Error("COSE protected header is not a map");
|
|
544
|
+
const alg = ALG_FOR_COSE[map.get(HDR_ALG)];
|
|
545
|
+
if (!alg) throw new Error(`unsupported COSE alg ${String(map.get(HDR_ALG))}`);
|
|
546
|
+
const kidRaw = map.get(HDR_KID);
|
|
547
|
+
const kid = kidRaw instanceof Uint8Array ? new TextDecoder().decode(kidRaw) : void 0;
|
|
548
|
+
return { alg, kid };
|
|
549
|
+
}
|
|
550
|
+
async function verifyCoseSign1(message, publicJwk) {
|
|
551
|
+
const [protectedBytes, , payload, signature] = decodeCoseSign1(message);
|
|
552
|
+
const header = parseProtected(protectedBytes);
|
|
553
|
+
const { key, alg } = await importPublicJwk(publicJwk);
|
|
554
|
+
if (alg !== header.alg) throw new Error("COSE alg does not match the verification key");
|
|
555
|
+
const toVerify = sigStructure(protectedBytes, payload);
|
|
556
|
+
const ok4 = await crypto.subtle.verify(algParams(alg), key, signature, toVerify);
|
|
557
|
+
if (!ok4) throw new Error("COSE_Sign1 signature did not verify");
|
|
558
|
+
return { protectedHeader: header, payload };
|
|
559
|
+
}
|
|
560
|
+
function coseFromBase64url(b64u) {
|
|
561
|
+
return base64urlToBytes(b64u);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ../trust/src/signed-export.ts
|
|
565
|
+
var SIGNED_EXPORT_TYPE = "facet-signed-export/1";
|
|
566
|
+
async function verifySignedExport(env) {
|
|
567
|
+
const { proof, payload } = env;
|
|
568
|
+
const kid = proof?.kid ?? "";
|
|
569
|
+
const alg = proof?.alg ?? "EdDSA";
|
|
570
|
+
const jwksUrl = proof?.jwksUrl;
|
|
571
|
+
const fail = (reason) => ({
|
|
572
|
+
valid: false,
|
|
573
|
+
kid,
|
|
574
|
+
alg,
|
|
575
|
+
jwksUrl,
|
|
576
|
+
reason
|
|
577
|
+
});
|
|
578
|
+
if (env.facet !== SIGNED_EXPORT_TYPE) return fail("unrecognized envelope type");
|
|
579
|
+
const check2 = await verifyDetachedProof(proof, payload);
|
|
580
|
+
return check2.ok ? { valid: true, kid, alg, jwksUrl } : fail(check2.reason ?? "verification failed");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ../trust/src/base58.ts
|
|
584
|
+
var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
585
|
+
var BASE = 58n;
|
|
586
|
+
var INDEX = {};
|
|
587
|
+
for (let i = 0; i < ALPHABET.length; i++) INDEX[ALPHABET[i]] = i;
|
|
588
|
+
function base58decode(str) {
|
|
589
|
+
let leading = 0;
|
|
590
|
+
while (leading < str.length && str[leading] === "1") leading++;
|
|
591
|
+
let num = 0n;
|
|
592
|
+
for (const ch of str) {
|
|
593
|
+
const v = INDEX[ch];
|
|
594
|
+
if (v === void 0) throw new Error(`invalid base58 character: ${ch}`);
|
|
595
|
+
num = num * BASE + BigInt(v);
|
|
596
|
+
}
|
|
597
|
+
const bytes = [];
|
|
598
|
+
while (num > 0n) {
|
|
599
|
+
bytes.unshift(Number(num % 256n));
|
|
600
|
+
num = num / 256n;
|
|
601
|
+
}
|
|
602
|
+
return new Uint8Array([...new Array(leading).fill(0), ...bytes]);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ../trust/src/multikey.ts
|
|
606
|
+
var ED25519_PREFIX = new Uint8Array([237, 1]);
|
|
607
|
+
function ed25519RawFromJwk(jwk) {
|
|
608
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) {
|
|
609
|
+
throw new Error("not an Ed25519 (OKP) public JWK");
|
|
610
|
+
}
|
|
611
|
+
const raw = base64urlToBytes(jwk.x);
|
|
612
|
+
if (raw.length !== 32) throw new Error("Ed25519 public key must be 32 bytes");
|
|
613
|
+
return raw;
|
|
614
|
+
}
|
|
615
|
+
function publicKeyMultibaseToRaw(multibase) {
|
|
616
|
+
if (!multibase.startsWith("z")) throw new Error("expected base58btc multibase (z-prefix)");
|
|
617
|
+
const decoded = base58decode(multibase.slice(1));
|
|
618
|
+
if (decoded[0] !== ED25519_PREFIX[0] || decoded[1] !== ED25519_PREFIX[1]) {
|
|
619
|
+
throw new Error("not an Ed25519 multikey (missing 0xed01 prefix)");
|
|
620
|
+
}
|
|
621
|
+
const raw = decoded.slice(2);
|
|
622
|
+
if (raw.length !== 32) throw new Error("Ed25519 public key must be 32 bytes");
|
|
623
|
+
return raw;
|
|
624
|
+
}
|
|
625
|
+
function rawToEd25519Jwk(raw) {
|
|
626
|
+
return { kty: "OKP", crv: "Ed25519", x: bytesToBase64url(raw) };
|
|
627
|
+
}
|
|
628
|
+
function publicKeyMultibaseToJwk(multibase) {
|
|
629
|
+
return rawToEd25519Jwk(publicKeyMultibaseToRaw(multibase));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ../trust/src/vc.ts
|
|
633
|
+
var CRYPTOSUITE = "eddsa-jcs-2022";
|
|
634
|
+
async function hashData(unsecured, proofConfig) {
|
|
635
|
+
const proofConfigHash = await sha256(canonicalizeBytes(proofConfig));
|
|
636
|
+
const documentHash = await sha256(canonicalizeBytes(unsecured));
|
|
637
|
+
const out = new Uint8Array(64);
|
|
638
|
+
out.set(proofConfigHash, 0);
|
|
639
|
+
out.set(documentHash, 32);
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
async function importEd25519Verify(jwk) {
|
|
643
|
+
return crypto.subtle.importKey("jwk", jwk, { name: "Ed25519" }, false, ["verify"]);
|
|
644
|
+
}
|
|
645
|
+
async function verifyCredential(credential, opts) {
|
|
646
|
+
const proof = credential.proof;
|
|
647
|
+
const issuer = typeof credential.issuer === "string" ? credential.issuer : credential.issuer?.id;
|
|
648
|
+
const fail = (reason) => ({
|
|
649
|
+
valid: false,
|
|
650
|
+
issuer,
|
|
651
|
+
verificationMethod: proof?.verificationMethod,
|
|
652
|
+
reason
|
|
653
|
+
});
|
|
654
|
+
if (!proof) return fail("missing proof");
|
|
655
|
+
if (proof.type !== "DataIntegrityProof" || proof.cryptosuite !== CRYPTOSUITE) {
|
|
656
|
+
return fail(`unsupported proof (${proof.type}/${proof.cryptosuite})`);
|
|
657
|
+
}
|
|
658
|
+
if (!proof.proofValue?.startsWith("z")) return fail("proofValue is not base58btc multibase");
|
|
659
|
+
let jwk;
|
|
660
|
+
try {
|
|
661
|
+
if (opts.publicKeyMultibase) jwk = publicKeyMultibaseToJwk(opts.publicKeyMultibase);
|
|
662
|
+
else if (opts.publicJwk) jwk = rawToEd25519Jwk(ed25519RawFromJwk(opts.publicJwk));
|
|
663
|
+
else return fail("no verification key supplied");
|
|
664
|
+
} catch (e) {
|
|
665
|
+
return fail(e instanceof Error ? e.message : "invalid verification key");
|
|
666
|
+
}
|
|
667
|
+
const { proof: P, ...unsecured } = credential;
|
|
668
|
+
const config = {
|
|
669
|
+
"@context": credential["@context"],
|
|
670
|
+
type: proof.type,
|
|
671
|
+
cryptosuite: proof.cryptosuite,
|
|
672
|
+
created: proof.created,
|
|
673
|
+
verificationMethod: proof.verificationMethod,
|
|
674
|
+
proofPurpose: proof.proofPurpose
|
|
675
|
+
};
|
|
676
|
+
try {
|
|
677
|
+
const data = await hashData(unsecured, config);
|
|
678
|
+
const signature = base58decode(proof.proofValue.slice(1));
|
|
679
|
+
const key = await importEd25519Verify(jwk);
|
|
680
|
+
const ok4 = await crypto.subtle.verify({ name: "Ed25519" }, key, signature, data);
|
|
681
|
+
return ok4 ? {
|
|
682
|
+
valid: true,
|
|
683
|
+
issuer,
|
|
684
|
+
verificationMethod: proof.verificationMethod
|
|
685
|
+
} : fail("signature did not verify");
|
|
686
|
+
} catch (e) {
|
|
687
|
+
return fail(e instanceof Error ? e.message : "verification error");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ../trust/src/did-web.ts
|
|
692
|
+
function publicKeyMultibaseFor(doc, vmId) {
|
|
693
|
+
const vm = doc.verificationMethod.find((m) => m.id === vmId);
|
|
694
|
+
return vm?.publicKeyMultibase ?? null;
|
|
695
|
+
}
|
|
696
|
+
async function verifyDidConfiguration(config, didDoc, expectedOrigin) {
|
|
697
|
+
const did = didDoc.id;
|
|
698
|
+
const credential = config.linked_dids?.find((c) => {
|
|
699
|
+
const subject2 = c.credentialSubject;
|
|
700
|
+
return subject2?.id === did;
|
|
701
|
+
});
|
|
702
|
+
if (!credential)
|
|
703
|
+
return {
|
|
704
|
+
valid: false,
|
|
705
|
+
did,
|
|
706
|
+
reason: "no linked credential for this DID"
|
|
707
|
+
};
|
|
708
|
+
const subject = credential.credentialSubject;
|
|
709
|
+
if (subject.origin !== expectedOrigin) {
|
|
710
|
+
return {
|
|
711
|
+
valid: false,
|
|
712
|
+
did,
|
|
713
|
+
origin: subject.origin,
|
|
714
|
+
reason: "origin mismatch"
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const vmId = credential.proof?.verificationMethod ?? "";
|
|
718
|
+
const publicKeyMultibase = publicKeyMultibaseFor(didDoc, vmId);
|
|
719
|
+
if (!publicKeyMultibase) {
|
|
720
|
+
return {
|
|
721
|
+
valid: false,
|
|
722
|
+
did,
|
|
723
|
+
reason: "verification method not found in DID document"
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const result = await verifyCredential(credential, { publicKeyMultibase });
|
|
727
|
+
if (!result.valid)
|
|
728
|
+
return {
|
|
729
|
+
valid: false,
|
|
730
|
+
did,
|
|
731
|
+
origin: subject.origin,
|
|
732
|
+
reason: result.reason
|
|
733
|
+
};
|
|
734
|
+
return { valid: true, did, origin: subject.origin };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ../trust/src/mmr.ts
|
|
738
|
+
function u64be(n) {
|
|
739
|
+
const out = new Uint8Array(8);
|
|
740
|
+
new DataView(out.buffer).setBigUint64(0, BigInt(n), false);
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
743
|
+
function hashPosPair(pos, left, right) {
|
|
744
|
+
return sha256(u64be(pos), left, right);
|
|
745
|
+
}
|
|
746
|
+
function allOnes(n) {
|
|
747
|
+
return (n & n + 1) === 0 && n !== 0;
|
|
748
|
+
}
|
|
749
|
+
function mostSigBit(n) {
|
|
750
|
+
return 1 << 31 - Math.clz32(n);
|
|
751
|
+
}
|
|
752
|
+
function indexHeight(i) {
|
|
753
|
+
let pos = i + 1;
|
|
754
|
+
while (!allOnes(pos)) {
|
|
755
|
+
pos = pos - mostSigBit(pos) + 1;
|
|
756
|
+
}
|
|
757
|
+
return 31 - Math.clz32(pos);
|
|
758
|
+
}
|
|
759
|
+
async function includedRoot(i, nodeHash, proof) {
|
|
760
|
+
let root = nodeHash;
|
|
761
|
+
let g = indexHeight(i);
|
|
762
|
+
let idx = i;
|
|
763
|
+
for (const sibling of proof) {
|
|
764
|
+
if (indexHeight(idx + 1) > g) {
|
|
765
|
+
idx += 1;
|
|
766
|
+
root = await hashPosPair(idx + 1, sibling, root);
|
|
767
|
+
} else {
|
|
768
|
+
idx += 2 << g;
|
|
769
|
+
root = await hashPosPair(idx + 1, root, sibling);
|
|
770
|
+
}
|
|
771
|
+
g += 1;
|
|
772
|
+
}
|
|
773
|
+
return root;
|
|
774
|
+
}
|
|
775
|
+
async function baggedRoot(count, peaks) {
|
|
776
|
+
return sha256(u64be(count), ...peaks);
|
|
777
|
+
}
|
|
778
|
+
async function verifyInclusion(proof, root) {
|
|
779
|
+
const peak = await includedRoot(proof.index, proof.leaf, proof.path);
|
|
780
|
+
if (!proof.peaks.some((p2) => bytesEqual(p2, peak))) return false;
|
|
781
|
+
return bytesEqual(await baggedRoot(proof.size, proof.peaks), root);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ../trust/src/statement.ts
|
|
785
|
+
async function verifyCoseProof(proof, payload) {
|
|
786
|
+
try {
|
|
787
|
+
const { protectedHeader, payload: signed } = await verifyCoseSign1(
|
|
788
|
+
coseFromBase64url(proof.cose),
|
|
789
|
+
proof.publicJwk
|
|
790
|
+
);
|
|
791
|
+
if (protectedHeader.kid !== proof.kid) {
|
|
792
|
+
return {
|
|
793
|
+
ok: false,
|
|
794
|
+
reason: "COSE protected-header kid does not match proof kid"
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const expected = canonicalizeBytes(payload);
|
|
798
|
+
if (signed.length !== expected.length || !signed.every((b, i) => b === expected[i])) {
|
|
799
|
+
return {
|
|
800
|
+
ok: false,
|
|
801
|
+
reason: "COSE payload does not match the statement payload"
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
return { ok: true };
|
|
805
|
+
} catch (e) {
|
|
806
|
+
return {
|
|
807
|
+
ok: false,
|
|
808
|
+
reason: e instanceof Error ? e.message : "COSE verification failed"
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function verifyStatement(stmt, expectedType) {
|
|
813
|
+
const { proof, payload, statement } = stmt;
|
|
814
|
+
const base = { statement, kid: proof?.kid };
|
|
815
|
+
if (expectedType && statement !== expectedType) {
|
|
816
|
+
return {
|
|
817
|
+
valid: false,
|
|
818
|
+
...base,
|
|
819
|
+
reason: `expected statement type ${expectedType}`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const check2 = proof?.type === "COSE_Sign1" ? await verifyCoseProof(proof, payload) : await verifyDetachedProof(proof, payload);
|
|
823
|
+
return check2.ok ? { valid: true, ...base } : { valid: false, ...base, reason: check2.reason };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ../trust/src/receipt.ts
|
|
827
|
+
function receiptToInclusion(r) {
|
|
828
|
+
return {
|
|
829
|
+
index: r.index,
|
|
830
|
+
leaf: fromHex(r.leaf),
|
|
831
|
+
path: r.path.map(fromHex),
|
|
832
|
+
size: r.size,
|
|
833
|
+
peaks: r.peaks.map(fromHex)
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function verifyInclusionReceipt(r, rootHex) {
|
|
837
|
+
return verifyInclusion(receiptToInclusion(r), fromHex(rootHex));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ../trust/src/scitt.ts
|
|
841
|
+
var SCITT_RECEIPT = "scitt-receipt/1";
|
|
842
|
+
async function verifyScittReceipt(receipt) {
|
|
843
|
+
const sig = await verifyStatement(receipt, SCITT_RECEIPT);
|
|
844
|
+
const p2 = receipt.payload;
|
|
845
|
+
if (!sig.valid)
|
|
846
|
+
return {
|
|
847
|
+
valid: false,
|
|
848
|
+
logId: p2?.logId,
|
|
849
|
+
entryId: p2?.entryId,
|
|
850
|
+
reason: sig.reason
|
|
851
|
+
};
|
|
852
|
+
const included = await verifyInclusionReceipt(p2.inclusion, p2.root);
|
|
853
|
+
if (!included) {
|
|
854
|
+
return {
|
|
855
|
+
valid: false,
|
|
856
|
+
logId: p2.logId,
|
|
857
|
+
entryId: p2.entryId,
|
|
858
|
+
reason: "inclusion proof failed"
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
return { valid: true, logId: p2.logId, entryId: p2.entryId };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ../trust/src/rats.ts
|
|
865
|
+
import { calculateJwkThumbprint as calculateJwkThumbprint3 } from "jose";
|
|
866
|
+
|
|
867
|
+
// ../trust/src/keyattest.ts
|
|
868
|
+
import { calculateJwkThumbprint as calculateJwkThumbprint2 } from "jose";
|
|
869
|
+
var KEY_ATTESTATION_TYPE = "facet-key-attestation/1";
|
|
870
|
+
async function verifyKeyAttestation(attestation, opts) {
|
|
871
|
+
const sig = await verifyStatement(attestation, KEY_ATTESTATION_TYPE);
|
|
872
|
+
if (!sig.valid) {
|
|
873
|
+
return {
|
|
874
|
+
valid: false,
|
|
875
|
+
hardware: false,
|
|
876
|
+
reason: sig.reason ?? "attestation signature invalid"
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
const claims = attestation.payload;
|
|
880
|
+
const recomputed = await calculateJwkThumbprint2(claims.subjectPublicJwk);
|
|
881
|
+
if (recomputed !== claims.subjectThumbprint) {
|
|
882
|
+
return {
|
|
883
|
+
valid: true,
|
|
884
|
+
hardware: false,
|
|
885
|
+
subjectThumbprint: claims.subjectThumbprint,
|
|
886
|
+
reason: "subject thumbprint does not match the attested subject key"
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
if (opts.expectedThumbprint !== void 0 && opts.expectedThumbprint !== claims.subjectThumbprint) {
|
|
890
|
+
return {
|
|
891
|
+
valid: true,
|
|
892
|
+
hardware: false,
|
|
893
|
+
subjectThumbprint: claims.subjectThumbprint,
|
|
894
|
+
vendor: claims.vendor,
|
|
895
|
+
reason: "attested subject thumbprint does not match the expected key"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const signerThumbprint = await signerThumbprintOf(attestation);
|
|
899
|
+
const anchorThumbprints = await Promise.all(
|
|
900
|
+
opts.trustAnchors.map((a) => calculateJwkThumbprint2(a))
|
|
901
|
+
);
|
|
902
|
+
const anchored = signerThumbprint !== void 0 && anchorThumbprints.includes(signerThumbprint);
|
|
903
|
+
if (!anchored) {
|
|
904
|
+
return {
|
|
905
|
+
valid: true,
|
|
906
|
+
hardware: false,
|
|
907
|
+
subjectThumbprint: claims.subjectThumbprint,
|
|
908
|
+
vendor: claims.vendor,
|
|
909
|
+
reason: "attestor is not a configured trust anchor"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
valid: true,
|
|
914
|
+
hardware: true,
|
|
915
|
+
deviceClass: claims.deviceClass,
|
|
916
|
+
...claims.fipsLevel !== void 0 ? { fipsLevel: claims.fipsLevel } : {},
|
|
917
|
+
subjectThumbprint: claims.subjectThumbprint,
|
|
918
|
+
vendor: claims.vendor
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
async function signerThumbprintOf(attestation) {
|
|
922
|
+
const jwk = attestation.proof?.publicJwk;
|
|
923
|
+
if (!jwk) return void 0;
|
|
924
|
+
return calculateJwkThumbprint2(jwk);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// ../trust/src/rats.ts
|
|
928
|
+
var PROCESS_EVIDENCE_TYPE = "rats-process-evidence/1";
|
|
929
|
+
async function verifyEmbeddedHardwareRoot(claims, trustAnchors) {
|
|
930
|
+
const ref = claims["key-attestation"];
|
|
931
|
+
if (!ref || !trustAnchors || trustAnchors.length === 0) return false;
|
|
932
|
+
const cnfJwk = claims.cnf?.jwk;
|
|
933
|
+
if (!cnfJwk) return false;
|
|
934
|
+
const expectedThumbprint = await calculateJwkThumbprint3(cnfJwk);
|
|
935
|
+
const result = await verifyKeyAttestation(ref.attestation, {
|
|
936
|
+
trustAnchors,
|
|
937
|
+
now: Date.now(),
|
|
938
|
+
expectedThumbprint
|
|
939
|
+
});
|
|
940
|
+
return result.hardware;
|
|
941
|
+
}
|
|
942
|
+
async function verifyProcessEvidence(stmt, opts = {}) {
|
|
943
|
+
const sig = await verifyStatement(stmt, PROCESS_EVIDENCE_TYPE);
|
|
944
|
+
if (!sig.valid)
|
|
945
|
+
return {
|
|
946
|
+
valid: false,
|
|
947
|
+
keyBound: false,
|
|
948
|
+
hardwareRootOfTrust: false,
|
|
949
|
+
reason: sig.reason
|
|
950
|
+
};
|
|
951
|
+
const claims = stmt.payload;
|
|
952
|
+
const expectedDigest = toHex(await sha256(canonicalizeBytes(claims["process-evidence"])));
|
|
953
|
+
if (claims["content-ref"]?.digest !== expectedDigest) {
|
|
954
|
+
return {
|
|
955
|
+
valid: false,
|
|
956
|
+
keyBound: false,
|
|
957
|
+
hardwareRootOfTrust: false,
|
|
958
|
+
reason: "content-ref digest does not match evidence"
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const cnfKid = claims.cnf?.jwk?.kid;
|
|
962
|
+
const keyBound = cnfKid !== void 0 && cnfKid === stmt.proof.kid;
|
|
963
|
+
if (!keyBound) {
|
|
964
|
+
return {
|
|
965
|
+
valid: false,
|
|
966
|
+
keyBound: false,
|
|
967
|
+
hardwareRootOfTrust: false,
|
|
968
|
+
reason: "cnf subject key is not bound to the signing key"
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (opts.nonce !== void 0 && claims.eat_nonce !== opts.nonce) {
|
|
972
|
+
return {
|
|
973
|
+
valid: false,
|
|
974
|
+
keyBound,
|
|
975
|
+
hardwareRootOfTrust: false,
|
|
976
|
+
reason: "eat_nonce does not match the expected nonce"
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const hardwareRootOfTrust = await verifyEmbeddedHardwareRoot(claims, opts.trustAnchors);
|
|
980
|
+
return {
|
|
981
|
+
valid: true,
|
|
982
|
+
keyBound,
|
|
983
|
+
hardwareRootOfTrust,
|
|
984
|
+
evidence: claims["process-evidence"]
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/commands/keys.ts
|
|
989
|
+
import pc3 from "picocolors";
|
|
990
|
+
var USAGE2 = `Usage: facet keys generate [flags]
|
|
991
|
+
|
|
992
|
+
Generate a deployment signing keypair for signed attestations, credentials, and exports.
|
|
993
|
+
|
|
994
|
+
generate
|
|
995
|
+
--alg <EdDSA|ES256> Signature algorithm (default: EdDSA \u2014 required for eddsa-jcs-2022 VCs).
|
|
996
|
+
--out <file> Write the PRIVATE JWK to this file (default: print to stdout).
|
|
997
|
+
|
|
998
|
+
Store the PRIVATE JWK as the FACET_SIGNING_JWK Worker secret:
|
|
999
|
+
facet keys generate --out signing.jwk && wrangler secret put FACET_SIGNING_JWK < signing.jwk
|
|
1000
|
+
`;
|
|
1001
|
+
async function runGenerate(args) {
|
|
1002
|
+
const { values } = parseArgs4({
|
|
1003
|
+
args,
|
|
1004
|
+
options: {
|
|
1005
|
+
alg: { type: "string" },
|
|
1006
|
+
out: { type: "string" }
|
|
1007
|
+
},
|
|
1008
|
+
allowPositionals: false
|
|
1009
|
+
});
|
|
1010
|
+
const alg = values.alg ?? "EdDSA";
|
|
1011
|
+
if (alg !== "EdDSA" && alg !== "ES256") {
|
|
1012
|
+
printError(`--alg must be EdDSA or ES256 (got: ${values.alg})`);
|
|
1013
|
+
return 1;
|
|
1014
|
+
}
|
|
1015
|
+
const { privateJwk, publicJwk } = await generateSigningJwk(alg);
|
|
1016
|
+
const privateJson = JSON.stringify(privateJwk);
|
|
1017
|
+
if (values.out) {
|
|
1018
|
+
await writeFile(values.out, `${privateJson}
|
|
1019
|
+
`, { mode: 384 });
|
|
1020
|
+
process.stdout.write(`${pc3.green("\u2713")} wrote private signing JWK \u2192 ${values.out}
|
|
1021
|
+
`);
|
|
1022
|
+
process.stdout.write(
|
|
1023
|
+
` ${pc3.dim(`kid ${publicJwk.kid} (${alg}). Store it as a secret: wrangler secret put FACET_SIGNING_JWK < ${values.out}`)}
|
|
1024
|
+
`
|
|
1025
|
+
);
|
|
1026
|
+
process.stdout.write(
|
|
1027
|
+
` ${pc3.yellow("Keep this file secret; delete it after uploading.")}
|
|
1028
|
+
`
|
|
1029
|
+
);
|
|
1030
|
+
} else {
|
|
1031
|
+
process.stdout.write(`${privateJson}
|
|
1032
|
+
`);
|
|
1033
|
+
process.stderr.write(
|
|
1034
|
+
`${pc3.dim(`kid ${publicJwk.kid} (${alg}) \u2014 pipe stdout into: wrangler secret put FACET_SIGNING_JWK`)}
|
|
1035
|
+
`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
return 0;
|
|
1039
|
+
}
|
|
1040
|
+
async function runKeys(args) {
|
|
1041
|
+
const [op] = args;
|
|
1042
|
+
if (op === void 0 || op === "--help" || op === "-h") {
|
|
1043
|
+
process.stdout.write(USAGE2);
|
|
1044
|
+
return op === void 0 ? 1 : 0;
|
|
1045
|
+
}
|
|
1046
|
+
switch (op) {
|
|
1047
|
+
case "generate":
|
|
1048
|
+
return runGenerate(args.slice(1));
|
|
1049
|
+
default:
|
|
1050
|
+
printError(`unknown keys op: ${op}`);
|
|
1051
|
+
process.stderr.write(USAGE2);
|
|
1052
|
+
return 1;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/commands/migrate.ts
|
|
1057
|
+
import { spawn } from "node:child_process";
|
|
1058
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
1059
|
+
function runMigrate(args, spawnImpl = spawn) {
|
|
1060
|
+
const { values } = parseArgs5({
|
|
1061
|
+
args,
|
|
1062
|
+
options: {
|
|
1063
|
+
db: { type: "string" },
|
|
1064
|
+
remote: { type: "boolean" }
|
|
1065
|
+
},
|
|
1066
|
+
allowPositionals: false
|
|
1067
|
+
});
|
|
1068
|
+
const db = values.db ?? "facet";
|
|
1069
|
+
const argv = ["d1", "migrations", "apply", db, values.remote ? "--remote" : "--local"];
|
|
1070
|
+
return new Promise((resolve) => {
|
|
1071
|
+
const child = spawnImpl("wrangler", argv, { stdio: "inherit" });
|
|
1072
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/commands/resources.ts
|
|
1077
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
1078
|
+
import pc4 from "picocolors";
|
|
1079
|
+
|
|
1080
|
+
// src/admin.ts
|
|
1081
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1082
|
+
function isUuid(value) {
|
|
1083
|
+
return UUID_RE.test(value);
|
|
1084
|
+
}
|
|
1085
|
+
var UsageError = class extends Error {
|
|
1086
|
+
};
|
|
1087
|
+
function resolveHost(flag) {
|
|
1088
|
+
const host = flag ?? process.env.FACET_HOST;
|
|
1089
|
+
if (!host) {
|
|
1090
|
+
throw new UsageError("Missing deployment host: pass --host <url> or set FACET_HOST.");
|
|
1091
|
+
}
|
|
1092
|
+
return host.replace(/\/$/, "");
|
|
1093
|
+
}
|
|
1094
|
+
function resolveAdminToken(flag) {
|
|
1095
|
+
const token = flag ?? process.env.FACET_ADMIN_TOKEN;
|
|
1096
|
+
if (!token) {
|
|
1097
|
+
throw new UsageError(
|
|
1098
|
+
"Missing admin token: pass --admin-token <t> or set FACET_ADMIN_TOKEN."
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
return token;
|
|
1102
|
+
}
|
|
1103
|
+
function adminClient(host, token, fetchImpl = fetchJson) {
|
|
1104
|
+
const authHeader = { Authorization: `Bearer ${token}` };
|
|
1105
|
+
return {
|
|
1106
|
+
get(path) {
|
|
1107
|
+
return fetchImpl(`${host}${path}`, {
|
|
1108
|
+
headers: { ...authHeader }
|
|
1109
|
+
});
|
|
1110
|
+
},
|
|
1111
|
+
post(path, body) {
|
|
1112
|
+
return fetchImpl(`${host}${path}`, {
|
|
1113
|
+
method: "POST",
|
|
1114
|
+
headers: { ...authHeader, "content-type": "application/json" },
|
|
1115
|
+
body: JSON.stringify(body)
|
|
1116
|
+
});
|
|
1117
|
+
},
|
|
1118
|
+
delete(path) {
|
|
1119
|
+
return fetchImpl(`${host}${path}`, {
|
|
1120
|
+
method: "DELETE",
|
|
1121
|
+
headers: { ...authHeader }
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function requireUuid(flag, value) {
|
|
1127
|
+
if (!value) {
|
|
1128
|
+
throw new UsageError(`Missing required option: --${flag} <uuid>.`);
|
|
1129
|
+
}
|
|
1130
|
+
if (!isUuid(value)) {
|
|
1131
|
+
throw new UsageError(`Invalid --${flag}: "${value}" is not a valid UUID.`);
|
|
1132
|
+
}
|
|
1133
|
+
return value;
|
|
1134
|
+
}
|
|
1135
|
+
function requireString(flag, value) {
|
|
1136
|
+
if (value === void 0 || value === "") {
|
|
1137
|
+
throw new UsageError(`Missing required option: --${flag}.`);
|
|
1138
|
+
}
|
|
1139
|
+
return value;
|
|
1140
|
+
}
|
|
1141
|
+
function renderTable(headers, rows) {
|
|
1142
|
+
const widths = headers.map(
|
|
1143
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
1144
|
+
);
|
|
1145
|
+
const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
|
|
1146
|
+
const out = [line(headers)];
|
|
1147
|
+
for (const row of rows) out.push(line(row));
|
|
1148
|
+
return `${out.join("\n")}
|
|
1149
|
+
`;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/commands/resources.ts
|
|
1153
|
+
var COMMON = {
|
|
1154
|
+
host: { type: "string" },
|
|
1155
|
+
"admin-token": { type: "string" },
|
|
1156
|
+
json: { type: "boolean" }
|
|
1157
|
+
};
|
|
1158
|
+
function client(values, fetchImpl) {
|
|
1159
|
+
const host = resolveHost(values.host);
|
|
1160
|
+
const token = resolveAdminToken(values["admin-token"]);
|
|
1161
|
+
return adminClient(host, token, fetchImpl);
|
|
1162
|
+
}
|
|
1163
|
+
function emitJson(value) {
|
|
1164
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
1165
|
+
`);
|
|
1166
|
+
return 0;
|
|
1167
|
+
}
|
|
1168
|
+
function fmtTime(ms) {
|
|
1169
|
+
return new Date(ms).toISOString();
|
|
1170
|
+
}
|
|
1171
|
+
async function sites(argv, fetchImpl) {
|
|
1172
|
+
const [sub, ...rest] = argv;
|
|
1173
|
+
if (sub === "list") {
|
|
1174
|
+
const { values } = parseArgs6({
|
|
1175
|
+
args: rest,
|
|
1176
|
+
options: { ...COMMON },
|
|
1177
|
+
allowPositionals: false
|
|
1178
|
+
});
|
|
1179
|
+
const api = client(values, fetchImpl);
|
|
1180
|
+
const { sites: sites2 } = await api.get("/api/sites");
|
|
1181
|
+
if (values.json) return emitJson(sites2);
|
|
1182
|
+
process.stdout.write(
|
|
1183
|
+
renderTable(
|
|
1184
|
+
["ID", "NAME", "DOMAIN", "CREATED"],
|
|
1185
|
+
sites2.map((s) => [s.id, s.name, s.domain, fmtTime(s.created_at)])
|
|
1186
|
+
)
|
|
1187
|
+
);
|
|
1188
|
+
return 0;
|
|
1189
|
+
}
|
|
1190
|
+
if (sub === "create") {
|
|
1191
|
+
const { values } = parseArgs6({
|
|
1192
|
+
args: rest,
|
|
1193
|
+
options: {
|
|
1194
|
+
...COMMON,
|
|
1195
|
+
name: { type: "string" },
|
|
1196
|
+
domain: { type: "string" }
|
|
1197
|
+
},
|
|
1198
|
+
allowPositionals: false
|
|
1199
|
+
});
|
|
1200
|
+
const name = requireString("name", values.name);
|
|
1201
|
+
const domain = requireString("domain", values.domain);
|
|
1202
|
+
const api = client(values, fetchImpl);
|
|
1203
|
+
const { site } = await api.post("/api/sites", {
|
|
1204
|
+
name,
|
|
1205
|
+
domain
|
|
1206
|
+
});
|
|
1207
|
+
if (values.json) return emitJson(site);
|
|
1208
|
+
process.stdout.write(`Created site ${site.id} (${site.name} / ${site.domain}).
|
|
1209
|
+
`);
|
|
1210
|
+
return 0;
|
|
1211
|
+
}
|
|
1212
|
+
throw new UsageError("Usage: facet sites <list|create> [options]");
|
|
1213
|
+
}
|
|
1214
|
+
async function keys(argv, fetchImpl) {
|
|
1215
|
+
const [sub, ...rest] = argv;
|
|
1216
|
+
if (sub === "list") {
|
|
1217
|
+
const { values } = parseArgs6({
|
|
1218
|
+
args: rest,
|
|
1219
|
+
options: { ...COMMON, site: { type: "string" } },
|
|
1220
|
+
allowPositionals: false
|
|
1221
|
+
});
|
|
1222
|
+
const site = requireUuid("site", values.site);
|
|
1223
|
+
const api = client(values, fetchImpl);
|
|
1224
|
+
const { keys: keys2 } = await api.get(`/api/keys?site_id=${site}`);
|
|
1225
|
+
if (values.json) return emitJson(keys2);
|
|
1226
|
+
process.stdout.write(
|
|
1227
|
+
renderTable(
|
|
1228
|
+
["ID", "LABEL", "CREATED", "LAST USED"],
|
|
1229
|
+
keys2.map((k) => [
|
|
1230
|
+
k.id,
|
|
1231
|
+
k.label ?? "",
|
|
1232
|
+
fmtTime(k.created_at),
|
|
1233
|
+
k.last_used ? fmtTime(k.last_used) : "never"
|
|
1234
|
+
])
|
|
1235
|
+
)
|
|
1236
|
+
);
|
|
1237
|
+
return 0;
|
|
1238
|
+
}
|
|
1239
|
+
if (sub === "issue") {
|
|
1240
|
+
const { values } = parseArgs6({
|
|
1241
|
+
args: rest,
|
|
1242
|
+
options: {
|
|
1243
|
+
...COMMON,
|
|
1244
|
+
site: { type: "string" },
|
|
1245
|
+
label: { type: "string" }
|
|
1246
|
+
},
|
|
1247
|
+
allowPositionals: false
|
|
1248
|
+
});
|
|
1249
|
+
const site = requireUuid("site", values.site);
|
|
1250
|
+
const api = client(values, fetchImpl);
|
|
1251
|
+
const body = { site_id: site };
|
|
1252
|
+
if (values.label) body.label = values.label;
|
|
1253
|
+
const issued = await api.post("/api/keys", body);
|
|
1254
|
+
if (values.json) return emitJson(issued);
|
|
1255
|
+
process.stdout.write(`Issued key ${issued.id}.
|
|
1256
|
+
`);
|
|
1257
|
+
process.stdout.write(
|
|
1258
|
+
`${pc4.yellow("This key is shown once and cannot be retrieved again:")}
|
|
1259
|
+
`
|
|
1260
|
+
);
|
|
1261
|
+
process.stdout.write(` ${issued.key}
|
|
1262
|
+
`);
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
1265
|
+
if (sub === "revoke") {
|
|
1266
|
+
const { values } = parseArgs6({
|
|
1267
|
+
args: rest,
|
|
1268
|
+
options: {
|
|
1269
|
+
...COMMON,
|
|
1270
|
+
id: { type: "string" },
|
|
1271
|
+
site: { type: "string" }
|
|
1272
|
+
},
|
|
1273
|
+
allowPositionals: false
|
|
1274
|
+
});
|
|
1275
|
+
const id = requireUuid("id", values.id);
|
|
1276
|
+
const site = requireUuid("site", values.site);
|
|
1277
|
+
const api = client(values, fetchImpl);
|
|
1278
|
+
await api.delete(`/api/keys/${id}?site_id=${site}`);
|
|
1279
|
+
if (values.json) return emitJson({ deleted: true, id });
|
|
1280
|
+
process.stdout.write(`Revoked key ${id}.
|
|
1281
|
+
`);
|
|
1282
|
+
return 0;
|
|
1283
|
+
}
|
|
1284
|
+
throw new UsageError("Usage: facet keys <list|issue|revoke> [options]");
|
|
1285
|
+
}
|
|
1286
|
+
async function goals(argv, fetchImpl) {
|
|
1287
|
+
const [sub, ...rest] = argv;
|
|
1288
|
+
if (sub === "list") {
|
|
1289
|
+
const { values } = parseArgs6({
|
|
1290
|
+
args: rest,
|
|
1291
|
+
options: { ...COMMON, site: { type: "string" } },
|
|
1292
|
+
allowPositionals: false
|
|
1293
|
+
});
|
|
1294
|
+
const site = requireUuid("site", values.site);
|
|
1295
|
+
const api = client(values, fetchImpl);
|
|
1296
|
+
const { goals: goals2 } = await api.get(`/api/goals?site_id=${site}`);
|
|
1297
|
+
if (values.json) return emitJson(goals2);
|
|
1298
|
+
process.stdout.write(
|
|
1299
|
+
renderTable(
|
|
1300
|
+
["ID", "NAME", "TYPE", "MATCH"],
|
|
1301
|
+
goals2.map((g) => [g.id, g.name, g.type, g.match_value])
|
|
1302
|
+
)
|
|
1303
|
+
);
|
|
1304
|
+
return 0;
|
|
1305
|
+
}
|
|
1306
|
+
if (sub === "create") {
|
|
1307
|
+
const { values } = parseArgs6({
|
|
1308
|
+
args: rest,
|
|
1309
|
+
options: {
|
|
1310
|
+
...COMMON,
|
|
1311
|
+
site: { type: "string" },
|
|
1312
|
+
name: { type: "string" },
|
|
1313
|
+
type: { type: "string" },
|
|
1314
|
+
match: { type: "string" }
|
|
1315
|
+
},
|
|
1316
|
+
allowPositionals: false
|
|
1317
|
+
});
|
|
1318
|
+
const site = requireUuid("site", values.site);
|
|
1319
|
+
const name = requireString("name", values.name);
|
|
1320
|
+
const type = requireString("type", values.type);
|
|
1321
|
+
if (type !== "event" && type !== "path") {
|
|
1322
|
+
throw new UsageError('Invalid --type: expected "event" or "path".');
|
|
1323
|
+
}
|
|
1324
|
+
const match = requireString("match", values.match);
|
|
1325
|
+
const api = client(values, fetchImpl);
|
|
1326
|
+
const { goal } = await api.post("/api/goals", {
|
|
1327
|
+
site_id: site,
|
|
1328
|
+
name,
|
|
1329
|
+
type,
|
|
1330
|
+
match_value: match
|
|
1331
|
+
});
|
|
1332
|
+
if (values.json) return emitJson(goal);
|
|
1333
|
+
process.stdout.write(`Created goal ${goal.id} (${goal.name}).
|
|
1334
|
+
`);
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
if (sub === "delete") {
|
|
1338
|
+
const { values } = parseArgs6({
|
|
1339
|
+
args: rest,
|
|
1340
|
+
options: {
|
|
1341
|
+
...COMMON,
|
|
1342
|
+
id: { type: "string" },
|
|
1343
|
+
site: { type: "string" }
|
|
1344
|
+
},
|
|
1345
|
+
allowPositionals: false
|
|
1346
|
+
});
|
|
1347
|
+
const id = requireUuid("id", values.id);
|
|
1348
|
+
const site = requireUuid("site", values.site);
|
|
1349
|
+
const api = client(values, fetchImpl);
|
|
1350
|
+
await api.delete(`/api/goals/${id}?site_id=${site}`);
|
|
1351
|
+
if (values.json) return emitJson({ deleted: true, id });
|
|
1352
|
+
process.stdout.write(`Deleted goal ${id}.
|
|
1353
|
+
`);
|
|
1354
|
+
return 0;
|
|
1355
|
+
}
|
|
1356
|
+
throw new UsageError("Usage: facet goals <list|create|delete> [options]");
|
|
1357
|
+
}
|
|
1358
|
+
function parseJsonFlag(flag, raw) {
|
|
1359
|
+
try {
|
|
1360
|
+
return JSON.parse(raw);
|
|
1361
|
+
} catch {
|
|
1362
|
+
throw new UsageError(`Invalid --${flag}: not valid JSON.`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async function funnels(argv, fetchImpl) {
|
|
1366
|
+
const [sub, ...rest] = argv;
|
|
1367
|
+
if (sub === "list") {
|
|
1368
|
+
const { values } = parseArgs6({
|
|
1369
|
+
args: rest,
|
|
1370
|
+
options: { ...COMMON, site: { type: "string" } },
|
|
1371
|
+
allowPositionals: false
|
|
1372
|
+
});
|
|
1373
|
+
const site = requireUuid("site", values.site);
|
|
1374
|
+
const api = client(values, fetchImpl);
|
|
1375
|
+
const { funnels: funnels2 } = await api.get(`/api/funnels?site_id=${site}`);
|
|
1376
|
+
if (values.json) return emitJson(funnels2);
|
|
1377
|
+
process.stdout.write(
|
|
1378
|
+
renderTable(
|
|
1379
|
+
["ID", "NAME", "STEPS"],
|
|
1380
|
+
funnels2.map((f) => [f.id, f.name, String(f.steps.length)])
|
|
1381
|
+
)
|
|
1382
|
+
);
|
|
1383
|
+
return 0;
|
|
1384
|
+
}
|
|
1385
|
+
if (sub === "create") {
|
|
1386
|
+
const { values } = parseArgs6({
|
|
1387
|
+
args: rest,
|
|
1388
|
+
options: {
|
|
1389
|
+
...COMMON,
|
|
1390
|
+
site: { type: "string" },
|
|
1391
|
+
name: { type: "string" },
|
|
1392
|
+
steps: { type: "string" }
|
|
1393
|
+
},
|
|
1394
|
+
allowPositionals: false
|
|
1395
|
+
});
|
|
1396
|
+
const site = requireUuid("site", values.site);
|
|
1397
|
+
const name = requireString("name", values.name);
|
|
1398
|
+
const stepsRaw = requireString("steps", values.steps);
|
|
1399
|
+
const steps = parseJsonFlag("steps", stepsRaw);
|
|
1400
|
+
if (!Array.isArray(steps) || steps.length < 2) {
|
|
1401
|
+
throw new UsageError("Invalid --steps: expected a JSON array of at least 2 steps.");
|
|
1402
|
+
}
|
|
1403
|
+
const api = client(values, fetchImpl);
|
|
1404
|
+
const { funnel } = await api.post("/api/funnels", {
|
|
1405
|
+
site_id: site,
|
|
1406
|
+
name,
|
|
1407
|
+
steps
|
|
1408
|
+
});
|
|
1409
|
+
if (values.json) return emitJson(funnel);
|
|
1410
|
+
process.stdout.write(`Created funnel ${funnel.id} (${funnel.name}).
|
|
1411
|
+
`);
|
|
1412
|
+
return 0;
|
|
1413
|
+
}
|
|
1414
|
+
if (sub === "delete") {
|
|
1415
|
+
const { values } = parseArgs6({
|
|
1416
|
+
args: rest,
|
|
1417
|
+
options: {
|
|
1418
|
+
...COMMON,
|
|
1419
|
+
id: { type: "string" },
|
|
1420
|
+
site: { type: "string" }
|
|
1421
|
+
},
|
|
1422
|
+
allowPositionals: false
|
|
1423
|
+
});
|
|
1424
|
+
const id = requireUuid("id", values.id);
|
|
1425
|
+
const site = requireUuid("site", values.site);
|
|
1426
|
+
const api = client(values, fetchImpl);
|
|
1427
|
+
await api.delete(`/api/funnels/${id}?site_id=${site}`);
|
|
1428
|
+
if (values.json) return emitJson({ deleted: true, id });
|
|
1429
|
+
process.stdout.write(`Deleted funnel ${id}.
|
|
1430
|
+
`);
|
|
1431
|
+
return 0;
|
|
1432
|
+
}
|
|
1433
|
+
throw new UsageError("Usage: facet funnels <list|create|delete> [options]");
|
|
1434
|
+
}
|
|
1435
|
+
async function experiments(argv, fetchImpl) {
|
|
1436
|
+
const [sub, ...rest] = argv;
|
|
1437
|
+
if (sub === "list") {
|
|
1438
|
+
const { values } = parseArgs6({
|
|
1439
|
+
args: rest,
|
|
1440
|
+
options: { ...COMMON, site: { type: "string" } },
|
|
1441
|
+
allowPositionals: false
|
|
1442
|
+
});
|
|
1443
|
+
const site = requireUuid("site", values.site);
|
|
1444
|
+
const api = client(values, fetchImpl);
|
|
1445
|
+
const { experiments: experiments2 } = await api.get(
|
|
1446
|
+
`/api/experiments?site_id=${site}`
|
|
1447
|
+
);
|
|
1448
|
+
if (values.json) return emitJson(experiments2);
|
|
1449
|
+
process.stdout.write(
|
|
1450
|
+
renderTable(
|
|
1451
|
+
["ID", "NAME", "FLAG", "VARIANTS", "ACTIVE"],
|
|
1452
|
+
experiments2.map((e) => [
|
|
1453
|
+
e.id,
|
|
1454
|
+
e.name,
|
|
1455
|
+
e.flag_key,
|
|
1456
|
+
e.variants.map((v) => v.key).join(","),
|
|
1457
|
+
e.active ? "yes" : "no"
|
|
1458
|
+
])
|
|
1459
|
+
)
|
|
1460
|
+
);
|
|
1461
|
+
return 0;
|
|
1462
|
+
}
|
|
1463
|
+
if (sub === "create") {
|
|
1464
|
+
const { values } = parseArgs6({
|
|
1465
|
+
args: rest,
|
|
1466
|
+
options: {
|
|
1467
|
+
...COMMON,
|
|
1468
|
+
site: { type: "string" },
|
|
1469
|
+
name: { type: "string" },
|
|
1470
|
+
flag: { type: "string" },
|
|
1471
|
+
variants: { type: "string" }
|
|
1472
|
+
},
|
|
1473
|
+
allowPositionals: false
|
|
1474
|
+
});
|
|
1475
|
+
const site = requireUuid("site", values.site);
|
|
1476
|
+
const name = requireString("name", values.name);
|
|
1477
|
+
const flag = requireString("flag", values.flag);
|
|
1478
|
+
const variantsRaw = requireString("variants", values.variants);
|
|
1479
|
+
const variants = parseJsonFlag("variants", variantsRaw);
|
|
1480
|
+
if (!Array.isArray(variants) || variants.length < 2) {
|
|
1481
|
+
throw new UsageError(
|
|
1482
|
+
"Invalid --variants: expected a JSON array of at least 2 variants."
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
const api = client(values, fetchImpl);
|
|
1486
|
+
const { experiment } = await api.post("/api/experiments", {
|
|
1487
|
+
site_id: site,
|
|
1488
|
+
name,
|
|
1489
|
+
flag_key: flag,
|
|
1490
|
+
variants
|
|
1491
|
+
});
|
|
1492
|
+
if (values.json) return emitJson(experiment);
|
|
1493
|
+
process.stdout.write(`Created experiment ${experiment.id} (${experiment.name}).
|
|
1494
|
+
`);
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1497
|
+
if (sub === "delete") {
|
|
1498
|
+
const { values } = parseArgs6({
|
|
1499
|
+
args: rest,
|
|
1500
|
+
options: {
|
|
1501
|
+
...COMMON,
|
|
1502
|
+
id: { type: "string" },
|
|
1503
|
+
site: { type: "string" }
|
|
1504
|
+
},
|
|
1505
|
+
allowPositionals: false
|
|
1506
|
+
});
|
|
1507
|
+
const id = requireUuid("id", values.id);
|
|
1508
|
+
const site = requireUuid("site", values.site);
|
|
1509
|
+
const api = client(values, fetchImpl);
|
|
1510
|
+
await api.delete(`/api/experiments/${id}?site_id=${site}`);
|
|
1511
|
+
if (values.json) return emitJson({ deleted: true, id });
|
|
1512
|
+
process.stdout.write(`Deleted experiment ${id}.
|
|
1513
|
+
`);
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|
|
1516
|
+
throw new UsageError("Usage: facet experiments <list|create|delete> [options]");
|
|
1517
|
+
}
|
|
1518
|
+
var GROUPS = {
|
|
1519
|
+
sites,
|
|
1520
|
+
keys,
|
|
1521
|
+
goals,
|
|
1522
|
+
funnels,
|
|
1523
|
+
experiments
|
|
1524
|
+
};
|
|
1525
|
+
function isResourceCommand(command) {
|
|
1526
|
+
return command in GROUPS;
|
|
1527
|
+
}
|
|
1528
|
+
async function runResource(command, argv, fetchImpl = fetchJson) {
|
|
1529
|
+
const handler = GROUPS[command];
|
|
1530
|
+
if (!handler) {
|
|
1531
|
+
printError(`Unknown resource command: ${command}`);
|
|
1532
|
+
return 1;
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
return await handler(argv, fetchImpl);
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
if (err instanceof UsageError) {
|
|
1538
|
+
printError(err.message);
|
|
1539
|
+
return 1;
|
|
1540
|
+
}
|
|
1541
|
+
printError(
|
|
1542
|
+
`${command} request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1543
|
+
);
|
|
1544
|
+
return 1;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/commands/sd.ts
|
|
1549
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1550
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
1551
|
+
import pc5 from "picocolors";
|
|
1552
|
+
|
|
1553
|
+
// src/lib/cryptosuites.ts
|
|
1554
|
+
import * as bbs2023 from "@digitalbazaar/bbs-2023-cryptosuite";
|
|
1555
|
+
import * as Bls12381Multikey from "@digitalbazaar/bls12-381-multikey";
|
|
1556
|
+
import * as credentialsContext from "@digitalbazaar/credentials-context";
|
|
1557
|
+
import { DataIntegrityProof } from "@digitalbazaar/data-integrity";
|
|
1558
|
+
import * as dataIntegrityContext from "@digitalbazaar/data-integrity-context";
|
|
1559
|
+
import * as EcdsaMultikey from "@digitalbazaar/ecdsa-multikey";
|
|
1560
|
+
import * as ecdsaSd2023 from "@digitalbazaar/ecdsa-sd-2023-cryptosuite";
|
|
1561
|
+
import * as multikeyContext from "@digitalbazaar/multikey-context";
|
|
1562
|
+
import { JsonLdDocumentLoader } from "jsonld-document-loader";
|
|
1563
|
+
import jsigs from "jsonld-signatures";
|
|
1564
|
+
var { purposes } = jsigs;
|
|
1565
|
+
var DID_V1_URL = "https://www.w3.org/ns/did/v1";
|
|
1566
|
+
var DID_V1_CONTEXT = {
|
|
1567
|
+
"@context": {
|
|
1568
|
+
"@version": 1.1,
|
|
1569
|
+
id: "@id",
|
|
1570
|
+
type: "@type",
|
|
1571
|
+
assertionMethod: {
|
|
1572
|
+
"@id": "https://w3id.org/security#assertionMethod",
|
|
1573
|
+
"@type": "@id",
|
|
1574
|
+
"@container": "@set"
|
|
1575
|
+
},
|
|
1576
|
+
verificationMethod: {
|
|
1577
|
+
"@id": "https://w3id.org/security#verificationMethod",
|
|
1578
|
+
"@type": "@id"
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
function baseLoader() {
|
|
1583
|
+
const jdl = new JsonLdDocumentLoader();
|
|
1584
|
+
for (const [url, ctx] of credentialsContext.contexts) jdl.addStatic(url, ctx);
|
|
1585
|
+
jdl.addStatic(dataIntegrityContext.CONTEXT_URL, dataIntegrityContext.CONTEXT);
|
|
1586
|
+
jdl.addStatic(multikeyContext.CONTEXT_URL, multikeyContext.CONTEXT);
|
|
1587
|
+
jdl.addStatic(DID_V1_URL, DID_V1_CONTEXT);
|
|
1588
|
+
return jdl.build();
|
|
1589
|
+
}
|
|
1590
|
+
function loaderWithController(base, controller, vmId, publicKeyDoc) {
|
|
1591
|
+
const controllerDoc = {
|
|
1592
|
+
"@context": [DID_V1_URL, multikeyContext.CONTEXT_URL],
|
|
1593
|
+
id: controller,
|
|
1594
|
+
assertionMethod: [publicKeyDoc]
|
|
1595
|
+
};
|
|
1596
|
+
return async (url) => {
|
|
1597
|
+
if (url === controller)
|
|
1598
|
+
return {
|
|
1599
|
+
contextUrl: null,
|
|
1600
|
+
documentUrl: url,
|
|
1601
|
+
document: controllerDoc
|
|
1602
|
+
};
|
|
1603
|
+
if (url === vmId)
|
|
1604
|
+
return {
|
|
1605
|
+
contextUrl: null,
|
|
1606
|
+
documentUrl: url,
|
|
1607
|
+
document: publicKeyDoc
|
|
1608
|
+
};
|
|
1609
|
+
return base(url);
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
async function generateIssuerKey(suite, controller = "did:key:facet-issuer") {
|
|
1613
|
+
const verificationMethod = `${controller}#${suite === "bbs-2023" ? "bbs" : "k1"}`;
|
|
1614
|
+
const keyPair = suite === "bbs-2023" ? await Bls12381Multikey.generateBbsKeyPair({
|
|
1615
|
+
algorithm: "BBS-BLS12-381-SHA-256",
|
|
1616
|
+
id: verificationMethod,
|
|
1617
|
+
controller
|
|
1618
|
+
}) : await EcdsaMultikey.generate({
|
|
1619
|
+
curve: "P-256",
|
|
1620
|
+
id: verificationMethod,
|
|
1621
|
+
controller
|
|
1622
|
+
});
|
|
1623
|
+
const publicKeyDoc = await keyPair.export({
|
|
1624
|
+
publicKey: true,
|
|
1625
|
+
includeContext: true
|
|
1626
|
+
});
|
|
1627
|
+
return { suite, controller, verificationMethod, keyPair, publicKeyDoc };
|
|
1628
|
+
}
|
|
1629
|
+
async function exportIssuerKey(key) {
|
|
1630
|
+
const secret = await key.keyPair.export({
|
|
1631
|
+
secretKey: true,
|
|
1632
|
+
publicKey: true,
|
|
1633
|
+
includeContext: true
|
|
1634
|
+
});
|
|
1635
|
+
return { suite: key.suite, controller: key.controller, secretKey: secret };
|
|
1636
|
+
}
|
|
1637
|
+
async function importIssuerKey(doc) {
|
|
1638
|
+
const suite = doc.suite;
|
|
1639
|
+
const secret = doc.secretKey;
|
|
1640
|
+
const keyPair = suite === "bbs-2023" ? await Bls12381Multikey.from(secret) : await EcdsaMultikey.from(secret);
|
|
1641
|
+
const publicKeyDoc = await keyPair.export({
|
|
1642
|
+
publicKey: true,
|
|
1643
|
+
includeContext: true
|
|
1644
|
+
});
|
|
1645
|
+
return {
|
|
1646
|
+
suite,
|
|
1647
|
+
controller: secret.controller,
|
|
1648
|
+
verificationMethod: secret.id,
|
|
1649
|
+
keyPair,
|
|
1650
|
+
publicKeyDoc
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
function signSuiteFor(suite, key, mandatoryPointers) {
|
|
1654
|
+
const factory = suite === "bbs-2023" ? bbs2023 : ecdsaSd2023;
|
|
1655
|
+
return new DataIntegrityProof({
|
|
1656
|
+
signer: key.keyPair.signer(),
|
|
1657
|
+
cryptosuite: factory.createSignCryptosuite({ mandatoryPointers })
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
async function issueSelective(suite, credential, key, mandatoryPointers = ["/issuer"]) {
|
|
1661
|
+
const documentLoader = loaderWithController(
|
|
1662
|
+
baseLoader(),
|
|
1663
|
+
key.controller,
|
|
1664
|
+
key.verificationMethod,
|
|
1665
|
+
key.publicKeyDoc
|
|
1666
|
+
);
|
|
1667
|
+
return jsigs.sign(credential, {
|
|
1668
|
+
suite: signSuiteFor(suite, key, mandatoryPointers),
|
|
1669
|
+
purpose: new purposes.AssertionProofPurpose(),
|
|
1670
|
+
documentLoader
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
async function deriveSelective(suite, signedCredential, key, selectivePointers) {
|
|
1674
|
+
const factory = suite === "bbs-2023" ? bbs2023 : ecdsaSd2023;
|
|
1675
|
+
const documentLoader = loaderWithController(
|
|
1676
|
+
baseLoader(),
|
|
1677
|
+
key.controller,
|
|
1678
|
+
key.verificationMethod,
|
|
1679
|
+
key.publicKeyDoc
|
|
1680
|
+
);
|
|
1681
|
+
return jsigs.derive(signedCredential, {
|
|
1682
|
+
suite: new DataIntegrityProof({
|
|
1683
|
+
cryptosuite: factory.createDiscloseCryptosuite({
|
|
1684
|
+
selectivePointers
|
|
1685
|
+
})
|
|
1686
|
+
}),
|
|
1687
|
+
purpose: new purposes.AssertionProofPurpose(),
|
|
1688
|
+
documentLoader
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
async function verifySelective(suite, presentation, key) {
|
|
1692
|
+
const factory = suite === "bbs-2023" ? bbs2023 : ecdsaSd2023;
|
|
1693
|
+
const documentLoader = loaderWithController(
|
|
1694
|
+
baseLoader(),
|
|
1695
|
+
key.controller,
|
|
1696
|
+
key.verificationMethod,
|
|
1697
|
+
key.publicKeyDoc
|
|
1698
|
+
);
|
|
1699
|
+
const result = await jsigs.verify(presentation, {
|
|
1700
|
+
suite: new DataIntegrityProof({
|
|
1701
|
+
cryptosuite: factory.createVerifyCryptosuite()
|
|
1702
|
+
}),
|
|
1703
|
+
purpose: new purposes.AssertionProofPurpose(),
|
|
1704
|
+
documentLoader
|
|
1705
|
+
});
|
|
1706
|
+
if (result.verified) return { verified: true };
|
|
1707
|
+
const err = result.error;
|
|
1708
|
+
const reason = err?.errors?.map((e) => e.message).join("; ") ?? err?.message ?? "verification failed";
|
|
1709
|
+
return { verified: false, reason };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/commands/sd.ts
|
|
1713
|
+
var USAGE3 = `Usage: facet sd <op> [flags] (Node-only W3C selective disclosure)
|
|
1714
|
+
|
|
1715
|
+
Suites: ecdsa-sd-2023 | bbs-2023
|
|
1716
|
+
|
|
1717
|
+
Operations:
|
|
1718
|
+
keygen --suite <s> --out <keyfile>
|
|
1719
|
+
issue --suite <s> --credential <file> --key <keyfile> [--mandatory </a,/b>] --out <file>
|
|
1720
|
+
derive --suite <s> --credential <signed> --key <keyfile> --reveal </a,/b> --out <file>
|
|
1721
|
+
verify --suite <s> --presentation <file> --key <keyfile>
|
|
1722
|
+
`;
|
|
1723
|
+
function ok2(msg) {
|
|
1724
|
+
process.stdout.write(`${pc5.green("\u2713")} ${msg}
|
|
1725
|
+
`);
|
|
1726
|
+
}
|
|
1727
|
+
async function readJson(path) {
|
|
1728
|
+
try {
|
|
1729
|
+
return JSON.parse(await readFile2(path, "utf8"));
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
printError(`could not read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function requireSuite(flags) {
|
|
1736
|
+
const s = flags.suite;
|
|
1737
|
+
if (s === "ecdsa-sd-2023" || s === "bbs-2023") return s;
|
|
1738
|
+
printError("provide --suite ecdsa-sd-2023 | bbs-2023");
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
function pointers(v) {
|
|
1742
|
+
return v ? v.split(",").map((p2) => p2.trim()).filter(Boolean) : [];
|
|
1743
|
+
}
|
|
1744
|
+
async function runSd(args) {
|
|
1745
|
+
const [op] = args;
|
|
1746
|
+
if (op === void 0 || op === "--help" || op === "-h") {
|
|
1747
|
+
process.stdout.write(USAGE3);
|
|
1748
|
+
return op === void 0 ? 1 : 0;
|
|
1749
|
+
}
|
|
1750
|
+
const { values } = parseArgs7({
|
|
1751
|
+
args: args.slice(1),
|
|
1752
|
+
options: {
|
|
1753
|
+
suite: { type: "string" },
|
|
1754
|
+
credential: { type: "string" },
|
|
1755
|
+
presentation: { type: "string" },
|
|
1756
|
+
key: { type: "string" },
|
|
1757
|
+
out: { type: "string" },
|
|
1758
|
+
mandatory: { type: "string" },
|
|
1759
|
+
reveal: { type: "string" }
|
|
1760
|
+
},
|
|
1761
|
+
allowPositionals: true
|
|
1762
|
+
});
|
|
1763
|
+
const flags = values;
|
|
1764
|
+
switch (op) {
|
|
1765
|
+
case "keygen": {
|
|
1766
|
+
const suite = requireSuite(flags);
|
|
1767
|
+
if (!suite || !flags.out) {
|
|
1768
|
+
if (suite) printError("provide --out <keyfile>");
|
|
1769
|
+
return 1;
|
|
1770
|
+
}
|
|
1771
|
+
const key = await generateIssuerKey(suite);
|
|
1772
|
+
await writeFile2(flags.out, JSON.stringify(await exportIssuerKey(key), null, 2));
|
|
1773
|
+
ok2(`generated ${suite} issuer key \u2192 ${flags.out} (controller ${key.controller})`);
|
|
1774
|
+
return 0;
|
|
1775
|
+
}
|
|
1776
|
+
case "issue": {
|
|
1777
|
+
const suite = requireSuite(flags);
|
|
1778
|
+
if (!suite || !flags.credential || !flags.key || !flags.out) {
|
|
1779
|
+
if (suite) printError("provide --credential <file> --key <keyfile> --out <file>");
|
|
1780
|
+
return 1;
|
|
1781
|
+
}
|
|
1782
|
+
const cred = await readJson(flags.credential);
|
|
1783
|
+
const keyDoc = await readJson(flags.key);
|
|
1784
|
+
if (cred === null || keyDoc === null) return 1;
|
|
1785
|
+
const key = await importIssuerKey(keyDoc);
|
|
1786
|
+
const signed = await issueSelective(
|
|
1787
|
+
suite,
|
|
1788
|
+
cred,
|
|
1789
|
+
key,
|
|
1790
|
+
pointers(flags.mandatory).length ? pointers(flags.mandatory) : ["/issuer"]
|
|
1791
|
+
);
|
|
1792
|
+
await writeFile2(flags.out, JSON.stringify(signed, null, 2));
|
|
1793
|
+
ok2(`issued ${suite} credential \u2192 ${flags.out}`);
|
|
1794
|
+
return 0;
|
|
1795
|
+
}
|
|
1796
|
+
case "derive": {
|
|
1797
|
+
const suite = requireSuite(flags);
|
|
1798
|
+
if (!suite || !flags.credential || !flags.key || !flags.reveal || !flags.out) {
|
|
1799
|
+
if (suite)
|
|
1800
|
+
printError(
|
|
1801
|
+
"provide --credential <signed> --key <keyfile> --reveal <ptrs> --out <file>"
|
|
1802
|
+
);
|
|
1803
|
+
return 1;
|
|
1804
|
+
}
|
|
1805
|
+
const signed = await readJson(flags.credential);
|
|
1806
|
+
const keyDoc = await readJson(flags.key);
|
|
1807
|
+
if (signed === null || keyDoc === null) return 1;
|
|
1808
|
+
const key = await importIssuerKey(keyDoc);
|
|
1809
|
+
const derived = await deriveSelective(
|
|
1810
|
+
suite,
|
|
1811
|
+
signed,
|
|
1812
|
+
key,
|
|
1813
|
+
pointers(flags.reveal)
|
|
1814
|
+
);
|
|
1815
|
+
await writeFile2(flags.out, JSON.stringify(derived, null, 2));
|
|
1816
|
+
ok2(`derived ${suite} presentation \u2192 ${flags.out}`);
|
|
1817
|
+
return 0;
|
|
1818
|
+
}
|
|
1819
|
+
case "verify": {
|
|
1820
|
+
const suite = requireSuite(flags);
|
|
1821
|
+
if (!suite || !flags.presentation || !flags.key) {
|
|
1822
|
+
if (suite) printError("provide --presentation <file> --key <keyfile>");
|
|
1823
|
+
return 1;
|
|
1824
|
+
}
|
|
1825
|
+
const pres = await readJson(flags.presentation);
|
|
1826
|
+
const keyDoc = await readJson(flags.key);
|
|
1827
|
+
if (pres === null || keyDoc === null) return 1;
|
|
1828
|
+
const key = await importIssuerKey(keyDoc);
|
|
1829
|
+
const result = await verifySelective(suite, pres, key);
|
|
1830
|
+
if (result.verified) {
|
|
1831
|
+
ok2(`valid ${suite} selective-disclosure proof`);
|
|
1832
|
+
return 0;
|
|
1833
|
+
}
|
|
1834
|
+
printError(`\u2717 invalid ${suite} proof: ${result.reason ?? "verification failed"}`);
|
|
1835
|
+
return 1;
|
|
1836
|
+
}
|
|
1837
|
+
default:
|
|
1838
|
+
printError(`unknown sd op: ${op}`);
|
|
1839
|
+
process.stderr.write(USAGE3);
|
|
1840
|
+
return 1;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/commands/stats.ts
|
|
1845
|
+
import { parseArgs as parseArgs8 } from "node:util";
|
|
1846
|
+
import pc6 from "picocolors";
|
|
1847
|
+
var RANGE_DAYS = {
|
|
1848
|
+
"24h": 1,
|
|
1849
|
+
"7d": 7,
|
|
1850
|
+
"30d": 30,
|
|
1851
|
+
"90d": 90
|
|
1852
|
+
};
|
|
1853
|
+
var DAY_MS = 864e5;
|
|
1854
|
+
async function runStats(args, fetchImpl = fetchJson) {
|
|
1855
|
+
const { values } = parseArgs8({
|
|
1856
|
+
args,
|
|
1857
|
+
options: {
|
|
1858
|
+
host: { type: "string" },
|
|
1859
|
+
key: { type: "string" },
|
|
1860
|
+
site: { type: "string" },
|
|
1861
|
+
range: { type: "string" }
|
|
1862
|
+
},
|
|
1863
|
+
allowPositionals: false
|
|
1864
|
+
});
|
|
1865
|
+
const host = values.host;
|
|
1866
|
+
const key = values.key;
|
|
1867
|
+
const site = values.site;
|
|
1868
|
+
if (!host || !key || !site) {
|
|
1869
|
+
printError("Missing required option: --host, --key, and --site are all required.");
|
|
1870
|
+
return 1;
|
|
1871
|
+
}
|
|
1872
|
+
const range = values.range ?? "7d";
|
|
1873
|
+
const days = RANGE_DAYS[range] ?? 7;
|
|
1874
|
+
const end = Date.now();
|
|
1875
|
+
const start = end - days * DAY_MS;
|
|
1876
|
+
const url = `${host.replace(/\/$/, "")}/api/stats?site_id=${site}&start=${start}&end=${end}`;
|
|
1877
|
+
try {
|
|
1878
|
+
const data = await fetchImpl(url, {
|
|
1879
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
1880
|
+
});
|
|
1881
|
+
const { pageviews, visitors, events } = data.summary;
|
|
1882
|
+
process.stdout.write(`${pc6.bold("Facet stats")} (${range})
|
|
1883
|
+
`);
|
|
1884
|
+
process.stdout.write(` Pageviews: ${pageviews}
|
|
1885
|
+
`);
|
|
1886
|
+
process.stdout.write(` Visitors: ${visitors}
|
|
1887
|
+
`);
|
|
1888
|
+
process.stdout.write(` Events: ${events}
|
|
1889
|
+
`);
|
|
1890
|
+
if (data.top_paths.length > 0) {
|
|
1891
|
+
process.stdout.write(`
|
|
1892
|
+
${pc6.bold("Top paths")}
|
|
1893
|
+
`);
|
|
1894
|
+
for (const row of data.top_paths.slice(0, 5)) {
|
|
1895
|
+
process.stdout.write(` ${row.count} ${row.key}
|
|
1896
|
+
`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return 0;
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
printError(`stats request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1902
|
+
return 1;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/commands/verify.ts
|
|
1907
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1908
|
+
import { parseArgs as parseArgs9 } from "node:util";
|
|
1909
|
+
import pc7 from "picocolors";
|
|
1910
|
+
var USAGE4 = `Usage: facet verify <target> [file] [flags]
|
|
1911
|
+
|
|
1912
|
+
Targets:
|
|
1913
|
+
export <file> Verify a signed stats export envelope (offline).
|
|
1914
|
+
credential <file> (--key <z\u2026> | --jwk <file>) Verify a VC's eddsa-jcs-2022 proof.
|
|
1915
|
+
did-configuration <file> --did-doc <file> [--origin <origin>]
|
|
1916
|
+
Verify a DIF domain-linkage against a DID document.
|
|
1917
|
+
receipt <file> Verify a SCITT receipt (signature + MMR inclusion).
|
|
1918
|
+
attestation <file> [--nonce <n>] Verify a RATS process-evidence EAT (software only).
|
|
1919
|
+
`;
|
|
1920
|
+
async function readJson2(path) {
|
|
1921
|
+
try {
|
|
1922
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
printError(`could not read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function ok3(msg) {
|
|
1929
|
+
process.stdout.write(`${pc7.green("\u2713")} ${msg}
|
|
1930
|
+
`);
|
|
1931
|
+
}
|
|
1932
|
+
async function verifyExport(path) {
|
|
1933
|
+
const doc = await readJson2(path);
|
|
1934
|
+
if (doc === null) return 1;
|
|
1935
|
+
const result = await verifySignedExport(doc);
|
|
1936
|
+
if (result.valid) {
|
|
1937
|
+
ok3(`valid signed export (alg=${result.alg}, kid=${result.kid})`);
|
|
1938
|
+
if (result.jwksUrl) {
|
|
1939
|
+
process.stdout.write(` key: ${result.jwksUrl}
|
|
1940
|
+
`);
|
|
1941
|
+
process.stdout.write(
|
|
1942
|
+
` ${pc7.dim("note: for full trust, confirm this kid appears in the deployment JWKS above.")}
|
|
1943
|
+
`
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
return 0;
|
|
1947
|
+
}
|
|
1948
|
+
printError(`\u2717 invalid signed export: ${result.reason ?? "signature did not verify"}`);
|
|
1949
|
+
return 1;
|
|
1950
|
+
}
|
|
1951
|
+
async function verifyCredentialCmd(file, flags) {
|
|
1952
|
+
const doc = await readJson2(file);
|
|
1953
|
+
if (doc === null) return 1;
|
|
1954
|
+
const publicKeyMultibase = flags.key;
|
|
1955
|
+
if (!publicKeyMultibase && flags.jwk) {
|
|
1956
|
+
const jwk = await readJson2(flags.jwk);
|
|
1957
|
+
if (jwk === null) return 1;
|
|
1958
|
+
const result2 = await verifyCredential(doc, {
|
|
1959
|
+
publicJwk: jwk
|
|
1960
|
+
});
|
|
1961
|
+
return report(result2);
|
|
1962
|
+
}
|
|
1963
|
+
if (!publicKeyMultibase) {
|
|
1964
|
+
printError("provide the verification key with --key <publicKeyMultibase> or --jwk <file>");
|
|
1965
|
+
return 1;
|
|
1966
|
+
}
|
|
1967
|
+
const result = await verifyCredential(doc, {
|
|
1968
|
+
publicKeyMultibase
|
|
1969
|
+
});
|
|
1970
|
+
return report(result);
|
|
1971
|
+
}
|
|
1972
|
+
function report(result) {
|
|
1973
|
+
if (result.valid) {
|
|
1974
|
+
ok3(`valid credential (issuer=${result.issuer ?? "unknown"})`);
|
|
1975
|
+
return 0;
|
|
1976
|
+
}
|
|
1977
|
+
printError(`\u2717 invalid credential: ${result.reason ?? "signature did not verify"}`);
|
|
1978
|
+
return 1;
|
|
1979
|
+
}
|
|
1980
|
+
async function verifyDidConfigurationCmd(file, flags) {
|
|
1981
|
+
if (!flags["did-doc"]) {
|
|
1982
|
+
printError("provide the DID document with --did-doc <file>");
|
|
1983
|
+
return 1;
|
|
1984
|
+
}
|
|
1985
|
+
const config = await readJson2(file);
|
|
1986
|
+
const didDoc = await readJson2(flags["did-doc"]);
|
|
1987
|
+
if (config === null || didDoc === null) return 1;
|
|
1988
|
+
const doc = didDoc;
|
|
1989
|
+
const origin = flags.origin ?? new URL(doc.id.replace(/^did:web:/, "https://")).origin;
|
|
1990
|
+
const result = await verifyDidConfiguration(config, doc, origin);
|
|
1991
|
+
if (result.valid) {
|
|
1992
|
+
ok3(`valid domain linkage (did=${result.did}, origin=${result.origin})`);
|
|
1993
|
+
return 0;
|
|
1994
|
+
}
|
|
1995
|
+
printError(`\u2717 invalid domain linkage: ${result.reason ?? "verification failed"}`);
|
|
1996
|
+
return 1;
|
|
1997
|
+
}
|
|
1998
|
+
async function verifyReceiptCmd(file) {
|
|
1999
|
+
const doc = await readJson2(file);
|
|
2000
|
+
if (doc === null) return 1;
|
|
2001
|
+
const result = await verifyScittReceipt(doc);
|
|
2002
|
+
if (result.valid) {
|
|
2003
|
+
ok3(`valid SCITT receipt (log=${result.logId}, entry=${result.entryId})`);
|
|
2004
|
+
return 0;
|
|
2005
|
+
}
|
|
2006
|
+
printError(`\u2717 invalid SCITT receipt: ${result.reason ?? "verification failed"}`);
|
|
2007
|
+
return 1;
|
|
2008
|
+
}
|
|
2009
|
+
async function verifyAttestationCmd(file, flags) {
|
|
2010
|
+
const doc = await readJson2(file);
|
|
2011
|
+
if (doc === null) return 1;
|
|
2012
|
+
const result = await verifyProcessEvidence(doc, {
|
|
2013
|
+
nonce: flags.nonce
|
|
2014
|
+
});
|
|
2015
|
+
if (result.valid) {
|
|
2016
|
+
ok3(`valid RATS process evidence (key-bound, build=${result.evidence?.buildId})`);
|
|
2017
|
+
process.stdout.write(
|
|
2018
|
+
` ${pc7.dim("software attestation only \u2014 no hardware root of trust")}
|
|
2019
|
+
`
|
|
2020
|
+
);
|
|
2021
|
+
return 0;
|
|
2022
|
+
}
|
|
2023
|
+
printError(`\u2717 invalid attestation: ${result.reason ?? "verification failed"}`);
|
|
2024
|
+
return 1;
|
|
2025
|
+
}
|
|
2026
|
+
async function runVerify2(args) {
|
|
2027
|
+
const [what] = args;
|
|
2028
|
+
if (what === "--help" || what === "-h" || what === void 0) {
|
|
2029
|
+
process.stdout.write(USAGE4);
|
|
2030
|
+
return what === void 0 ? 1 : 0;
|
|
2031
|
+
}
|
|
2032
|
+
const { values, positionals } = parseArgs9({
|
|
2033
|
+
args: args.slice(1),
|
|
2034
|
+
options: {
|
|
2035
|
+
key: { type: "string" },
|
|
2036
|
+
jwk: { type: "string" },
|
|
2037
|
+
"did-doc": { type: "string" },
|
|
2038
|
+
origin: { type: "string" },
|
|
2039
|
+
nonce: { type: "string" }
|
|
2040
|
+
},
|
|
2041
|
+
allowPositionals: true
|
|
2042
|
+
});
|
|
2043
|
+
const file = positionals[0];
|
|
2044
|
+
const flags = values;
|
|
2045
|
+
if (!file) {
|
|
2046
|
+
printError("missing <file> argument");
|
|
2047
|
+
return 1;
|
|
2048
|
+
}
|
|
2049
|
+
switch (what) {
|
|
2050
|
+
case "export":
|
|
2051
|
+
return verifyExport(file);
|
|
2052
|
+
case "credential":
|
|
2053
|
+
return verifyCredentialCmd(file, flags);
|
|
2054
|
+
case "did-configuration":
|
|
2055
|
+
return verifyDidConfigurationCmd(file, flags);
|
|
2056
|
+
case "receipt":
|
|
2057
|
+
return verifyReceiptCmd(file);
|
|
2058
|
+
case "attestation":
|
|
2059
|
+
return verifyAttestationCmd(file, flags);
|
|
2060
|
+
default:
|
|
2061
|
+
printError(`unknown verify target: ${what}`);
|
|
2062
|
+
process.stderr.write(USAGE4);
|
|
2063
|
+
return 1;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// src/index.ts
|
|
2068
|
+
var USAGE5 = `Usage: facet <command> [options]
|
|
2069
|
+
|
|
2070
|
+
Setup:
|
|
2071
|
+
init Scaffold wrangler.jsonc + .dev.vars
|
|
2072
|
+
migrate [--remote] Apply D1 migrations via wrangler
|
|
2073
|
+
config set-db-id --id <id> Write the D1 database_id into wrangler.jsonc
|
|
2074
|
+
config check Verify the D1 database_id is set (not the placeholder)
|
|
2075
|
+
keys generate [--alg <a>] Generate a deployment signing keypair (FACET_SIGNING_JWK)
|
|
2076
|
+
|
|
2077
|
+
Reporting:
|
|
2078
|
+
stats --host <url> --key <k> --site <uuid> Print summary stats
|
|
2079
|
+
|
|
2080
|
+
Verify (offline):
|
|
2081
|
+
verify export <file> Verify a signed stats export envelope
|
|
2082
|
+
verify credential <file> --key <z\u2026>|--jwk <f> Verify a VC (eddsa-jcs-2022)
|
|
2083
|
+
verify did-configuration <file> --did-doc <f> Verify a DIF domain linkage
|
|
2084
|
+
|
|
2085
|
+
Selective disclosure (Node-only W3C cryptosuites):
|
|
2086
|
+
sd keygen --suite <ecdsa-sd-2023|bbs-2023> --out <keyfile>
|
|
2087
|
+
sd issue --suite <s> --credential <f> --key <keyfile> [--mandatory </a,/b>] --out <f>
|
|
2088
|
+
sd derive --suite <s> --credential <signed> --key <keyfile> --reveal </a,/b> --out <f>
|
|
2089
|
+
sd verify --suite <s> --presentation <f> --key <keyfile>
|
|
2090
|
+
|
|
2091
|
+
Hardware key-attestation (Node-only X.509 chain via node:crypto):
|
|
2092
|
+
keyattest verify <leaf.pem> --root <root.pem> --key <deploy-pub|.crt> [--intermediate <pem>] [--now <iso>]
|
|
2093
|
+
|
|
2094
|
+
Resources (admin API \u2014 needs --host + --admin-token, or FACET_HOST/FACET_ADMIN_TOKEN):
|
|
2095
|
+
sites list | create --name <n> --domain <d>
|
|
2096
|
+
keys list --site <uuid> | issue --site <uuid> [--label <l>] | revoke --id <uuid> --site <uuid>
|
|
2097
|
+
goals list --site <uuid> | create --site <uuid> --name <n> --type <event|path> --match <v>
|
|
2098
|
+
| delete --id <uuid> --site <uuid>
|
|
2099
|
+
funnels list --site <uuid> | create --site <uuid> --name <n> --steps <json>
|
|
2100
|
+
| delete --id <uuid> --site <uuid>
|
|
2101
|
+
experiments list --site <uuid> | create --site <uuid> --name <n> --flag <key> --variants <json>
|
|
2102
|
+
| delete --id <uuid> --site <uuid>
|
|
2103
|
+
|
|
2104
|
+
All resource commands support --json for machine-readable output.
|
|
2105
|
+
|
|
2106
|
+
Examples:
|
|
2107
|
+
facet config set-db-id --id 1a2b3c4d-... --config apps/server/wrangler.jsonc
|
|
2108
|
+
facet sites create --host https://a.example.com --admin-token $TOKEN --name Blog --domain blog.dev
|
|
2109
|
+
FACET_HOST=https://a.example.com FACET_ADMIN_TOKEN=$TOKEN facet keys issue --site <uuid> --label ci
|
|
2110
|
+
facet funnels create --site <uuid> --name Signup \\
|
|
2111
|
+
--steps '[{"type":"path","match_value":"/"},{"type":"path","match_value":"/done"}]'
|
|
2112
|
+
`;
|
|
2113
|
+
async function main(argv) {
|
|
2114
|
+
const [command] = argv;
|
|
2115
|
+
switch (command) {
|
|
2116
|
+
case "init":
|
|
2117
|
+
return runInit(argv.slice(1));
|
|
2118
|
+
case "migrate":
|
|
2119
|
+
return runMigrate(argv.slice(1));
|
|
2120
|
+
case "stats":
|
|
2121
|
+
return runStats(argv.slice(1));
|
|
2122
|
+
case "config":
|
|
2123
|
+
return runConfig(argv.slice(1));
|
|
2124
|
+
case "keys":
|
|
2125
|
+
return runKeys(argv.slice(1));
|
|
2126
|
+
case "verify":
|
|
2127
|
+
return runVerify2(argv.slice(1));
|
|
2128
|
+
case "sd":
|
|
2129
|
+
return runSd(argv.slice(1));
|
|
2130
|
+
case "keyattest":
|
|
2131
|
+
return runKeyattest(argv.slice(1));
|
|
2132
|
+
case "--help":
|
|
2133
|
+
case "-h":
|
|
2134
|
+
process.stdout.write(USAGE5);
|
|
2135
|
+
return 0;
|
|
2136
|
+
case void 0:
|
|
2137
|
+
process.stdout.write(USAGE5);
|
|
2138
|
+
return 0;
|
|
2139
|
+
default:
|
|
2140
|
+
if (isResourceCommand(command)) {
|
|
2141
|
+
return runResource(command, argv.slice(1));
|
|
2142
|
+
}
|
|
2143
|
+
process.stderr.write(USAGE5);
|
|
2144
|
+
return 1;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
var isMain = typeof process.argv[1] === "string" && import.meta.url === new URL(process.argv[1], "file://").href;
|
|
2148
|
+
if (isMain) {
|
|
2149
|
+
void main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
2150
|
+
}
|
|
2151
|
+
export {
|
|
2152
|
+
main
|
|
2153
|
+
};
|