@credda/cli 0.1.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js DELETED
@@ -1,1303 +0,0 @@
1
- /**
2
- * Command router — pure of process/env/fs so it's testable with a mocked
3
- * context (same pattern as packages/mcp's tools.ts).
4
- *
5
- * Every command is READ-ONLY against Credda's deterministic score. Nothing
6
- * here writes an Event, adjusts a score, or makes a trust decision — the CLI
7
- * looks up and offline-verifies EXISTING, already-computed trust facts.
8
- * `mint`/`revoke` manage a share token (a capability, not a score write).
9
- */
10
- export const VERSION = '0.1.3';
11
- export const HELP = `credda: portable trust from the terminal
12
-
13
- Start here (needs a sandbox CREDDA_API_KEY, the crd_test_ kind):
14
- credda quickstart Seed your sandbox with synthetic subjects, print
15
- their real scores, read one back, and then close
16
- the counterparty-confirmation loop end to end so
17
- you finish holding a real VERIFIED event, not
18
- just a number you read. One command, nothing
19
- written outside the sandbox.
20
- --no-confirm Stop after the seed; skip the confirmation loop.
21
-
22
- Sandbox (crd_test_ keys only; a live key is refused before anything happens):
23
- credda sandbox seed Populate the sandbox with synthetic subjects.
24
- Idempotent: an already-seeded subject is left
25
- alone, never doubled.
26
- credda sandbox reset Wipe the sandbox and start over.
27
-
28
- Public (no API key):
29
- credda lookup <token> Trust check for a share token
30
- credda export <token> Full self-verifying trust export bundle
31
- credda verify <file|-> Offline-verify a credential: a W3C VC-JWT,
32
- a compact Trust Credential, or a saved trust
33
- export bundle (auto-detected). '-' = stdin.
34
- credda registry Federated trust registry
35
- credda did Issuer DID document
36
- credda benchmarks Cohort-benchmark catalog (dimensions + k-anonymity)
37
- credda reason-codes Adverse-action reason-code catalog (ECOA / Reg B)
38
- credda outcome-templates [industry]
39
- How a real-world business maps its work to
40
- Credda events, and WHO confirms each outcome.
41
- Guidance only. Optional industry slug filters.
42
- credda badges list Open Badges 3.0 achievements this issuer signs
43
- credda badges get <badgeId> One achievement definition
44
- credda professional-record public <token>
45
- The professional record behind a share token
46
- (the subject's own consent to present it)
47
- credda career-export --token <token>
48
- The subject's whole verified record as a JSON
49
- Resume document, behind a share token (the
50
- subject's own consent, no API key sent)
51
-
52
- Platform (needs CREDDA_API_KEY):
53
- credda score <userId> Current score
54
- credda explain <userId> Factor-level score explanation
55
- credda components <userId> Six named 0-100 score components
56
- credda risk <userId> Advisory risk signals
57
- credda trust-summary <userId> [--narrative]
58
- Deterministic, evidence-based trust summary
59
- (explains; never a verdict). --narrative adds an
60
- advisory AI retelling when the server has AI on.
61
- credda benchmark <userId> [--dimension <d>]
62
- Where a subject sits within its cohort:
63
- percentile + the cohort distribution
64
- credda distribution [--dimension <d>] [--cohort <c>]
65
- Aggregate, k-anonymised cohort distribution.
66
- Omit --cohort for every cohort on the dimension.
67
- credda users [--score-min <n>] [--score-max <n>] [--band <b>]
68
- [--subject-type <PERSON|AGENT|ORGANIZATION>] [--scored|--unscored] [--frozen]
69
- [--active-since <iso>] [--registered-since <iso>] [--registered-before <iso>]
70
- [--verified] [--min-verified <n>]
71
- [--sort <score|lastActivity|registered|externalId>] [--order <asc|desc>]
72
- [--cursor <c>] [--limit <n>]
73
- Query + export your book of subjects.
74
- A subject with no score yet reports null,
75
- never a placeholder; list those with
76
- --unscored.
77
- credda book-summary [same filters as "users"]
78
- Size a segment WITHOUT paging it: how many
79
- match, how many are scored, band mix and
80
- median/mean. Null (not 0) when nothing in the
81
- segment is scored.
82
- credda usage [days] [--from <date> --to <date>] [--csv <outfile>]
83
- Your platform's metered API usage. Either a
84
- trailing [days] window OR an inclusive
85
- --from/--to date range (YYYY-MM-DD), not both.
86
- --csv writes the flat CSV statement to a file.
87
- credda activity [--action <A>] [--from <t> --to <t>] [--cursor <c>] [--limit <n>]
88
- Your platform's own activity/audit log,
89
- newest-first, cursor-paginated
90
- credda verified-profile <userId>
91
- How much of a subject's CLAIMED record
92
- (education/skills/certifications/employment) is
93
- third-party verified. Counts whether a claim is
94
- verified, never how prestigious it is, and it
95
- can never move the Reliability Score.
96
- credda qualify <userId> --category <education|skill|certification|employment>
97
- [--label <l>] [--issuer <i>] [--verified-by <witness>]
98
- Record a qualification claim. Always recorded;
99
- counts as VERIFIED only with a genuine
100
- third-party --verified-by witness.
101
- credda professional-record get <userId>
102
- Résumé-shaped summary of a VERIFIED work record.
103
- Describes a record, not a hiring verdict, a
104
- background check, or a consumer report.
105
- credda professional-record credential <userId> [--ttl <seconds>]
106
- Mint the signed, offline-verifiable Professional
107
- Record Credential (+ an "Add to LinkedIn" link)
108
- credda reliability-report <userId> [--recent <n>] [--benchmark]
109
- The consolidated worker reliability report a
110
- staffing agency or employer weighs: reliability,
111
- metrics, verified experience, tenure, ranked
112
- drivers, recent outcomes. EVIDENCE, not a hire /
113
- place / rank verdict, a background check, or a
114
- consumer report. Use --token <token> for the
115
- public worker-consent route (NO API key).
116
- credda career-export <userId> The subject's whole verified record as an open
117
- JSON Resume document (jsonresume.org). Describes
118
- a record, not a hiring verdict or a consumer
119
- report. Use --token <token> for the public route.
120
- credda mint <userId> Mint a share token for a user
121
- credda revoke <userId> Revoke a user's share token
122
-
123
- Confirmation requests: the counterparty-confirmation primitive. You PROPOSE an
124
- outcome and deliver the one-time token to the counterparty over YOUR OWN channel;
125
- the event is written, verified, only when that distinct party confirms:
126
- credda confirmations create --user <externalId> --type <eventType>
127
- --counterparty <ref> [--counterparty-name <n>] [--description <d>]
128
- [--stake <HIGH|MEDIUM|LOW>] [--value <n>] [--due <iso>] [--completed <iso>]
129
- [--return-url <url>] [--expires-in <days>] [--idempotency-key <k>]
130
- Needs CREDDA_API_KEY. Token shown ONCE.
131
- credda confirmations batch <file.json> [--idempotency-key <k>]
132
- The ACTIVATION ENGINE: bulk-create up to 100
133
- requests from a JSON file (an array of request
134
- bodies, or { "requests": [...] }). Warms a cold
135
- ledger from your book. Needs CREDDA_API_KEY;
136
- each ok item's token is shown ONCE.
137
- credda confirmations list [--status <s>] [--cursor <c>] [--limit <n>]
138
- credda confirmations get <id>
139
- credda confirmations cancel <id>
140
- credda confirmations preview <id> --token <t>
141
- What the counterparty is asked to confirm.
142
- NO API key; the token is the capability.
143
- credda confirmations respond <id> --token <t> (--confirm | --decline)
144
- The counterparty's decision. NO API key.
145
- --confirm writes the event; --decline writes
146
- nothing. Single-use either way.
147
-
148
- Reference requests: the qualifications-half sibling of confirmations. A résumé
149
- claim (employment / education / certification / skill) becomes VERIFIED when the
150
- named third party who was there confirms it. Records no qualification and never
151
- moves the reliability score:
152
- credda references create --user <externalId>
153
- --category <employment|education|certification|skill>
154
- --counterparty <ref> [--label <l>] [--issuer <i>] [--jurisdiction <j>]
155
- [--reference <r>] [--counterparty-name <n>] [--description <d>]
156
- [--return-url <url>] [--expires-in <days>] [--idempotency-key <k>]
157
- Needs CREDDA_API_KEY. Token shown ONCE.
158
- credda references list [--status <s>] [--cursor <c>] [--limit <n>]
159
- credda references get <id>
160
- credda references cancel <id>
161
- credda references preview <id> --token <t>
162
- What the reference is asked to confirm.
163
- NO API key; the token is the capability.
164
- credda references respond <id> --token <t> (--confirm | --decline)
165
- The reference's decision. NO API key.
166
- --confirm records the qualification; --decline
167
- writes nothing. Single-use either way.
168
-
169
- Threshold policies (needs CREDDA_API_KEY): declarative "tell me when this line
170
- is crossed"; delivers policy.threshold_crossed through your webhooks. Config
171
- only: a policy never reads into, blocks, or changes a score:
172
- credda policies list [--cursor <c>] [--limit <n>]
173
- credda policies get <id>
174
- credda policies create --name <n> (--user <externalId> | --all)
175
- --metric <score|component|band|verified_events>
176
- [--direction <up|down|enter|leave>] [--threshold <n>]
177
- [--component <reliability|timeliness|trustworthiness|verification|consistency|momentum>]
178
- [--band <b>]
179
- credda policies update <id> [--name <n>] [--direction <d>] [--threshold <n>]
180
- [--component <c>] [--band <b>] [--activate | --deactivate]
181
- The metric is immutable; delete + recreate.
182
- credda policies delete <id>
183
-
184
- Score monitors (needs CREDDA_API_KEY): edge-triggered watches that deliver
185
- "monitor.triggered" through your webhooks; notification config only, a
186
- monitor never affects a score:
187
- credda monitors list [--cursor <c>] [--limit <n>]
188
- credda monitors get <id>
189
- credda monitors create --user <externalId> [--below <score>] [--above <score>] [--band-change]
190
- At least one condition required. --below fires
191
- on a downward crossing (and on a first score
192
- already below it), --above on an upward
193
- crossing, --band-change on any band change.
194
- credda monitors delete <id>
195
-
196
- Bulk screenings (needs CREDDA_API_KEY): async batch score reads, up to
197
- 10,000 ids per job, strictly read-only:
198
- credda screen <ids...> Submit ids (comma/space separated), or:
199
- credda screen --file <path> One id per line, or a CSV whose FIRST column
200
- is the id (a leading "id"/"userId"/
201
- "externalId" header row is skipped).
202
- [--wait] Poll until the job finishes, then print the
203
- summary (exit 1 if the job FAILED).
204
- credda screenings list [--cursor <c>] [--limit <n>]
205
- credda screenings get <id> Job status + summary
206
- credda screenings results <id> [--csv <outfile>]
207
- Per-user results (JSON; --csv writes the CSV
208
- attachment to a file instead)
209
-
210
- Webhooks (needs CREDDA_API_KEY):
211
- credda webhooks list Your webhook subscriptions
212
- credda webhooks create <url> <event..> Subscribe (secret shown ONCE)
213
- credda webhooks delete <id> Remove a webhook
214
- credda webhooks test <id> Send a synthetic signed delivery
215
- credda webhooks deliveries <id> Recent delivery attempts (incl. retries)
216
- credda webhooks recent [event..] Recent events across ALL your endpoints
217
- (sample data for automation platforms;
218
- falls back to catalog examples, flagged
219
- isExample, when nothing has fired yet)
220
-
221
- Local development:
222
- credda listen [port] Local webhook receiver: verifies each delivery's
223
- HMAC signature (CREDDA_WEBHOOK_SECRET) and
224
- pretty-prints the payload. Default port 4141.
225
- Credda delivers to public HTTPS only; expose
226
- this port with your own tunnel (e.g. cloudflared).
227
-
228
- Environment:
229
- CREDDA_API_URL API base (default https://api.credda.io)
230
- CREDDA_API_KEY Platform API key for keyed commands
231
- CREDDA_WEBHOOK_SECRET whsec_… signing secret for "credda listen"
232
-
233
- Exit codes: 0 ok/valid · 1 error · 2 credential failed verification`;
234
- function requireKey(ctx) {
235
- if (!ctx.apiKey) {
236
- throw new Error('this command needs CREDDA_API_KEY set (a platform API key)');
237
- }
238
- return ctx.apiKey;
239
- }
240
- /** Raw-key prefix the API stamps on a sandbox key (lib/testMode.ts). */
241
- export const TEST_KEY_PREFIX = 'crd_test_';
242
- /**
243
- * A sandbox key, or an error that says exactly what to do next.
244
- *
245
- * The server refuses a live key anyway (`403 TEST_MODE_ONLY`), but a first-run
246
- * user does not deserve a 403 to interpret — the prefix is visible locally, so
247
- * the actionable message costs one string comparison. This is the "better
248
- * first-run errors" rule applied to the single most likely first mistake.
249
- */
250
- export function requireSandboxKey(ctx) {
251
- const key = requireKey(ctx);
252
- if (!key.startsWith(TEST_KEY_PREFIX)) {
253
- throw new Error(`this command only runs against a SANDBOX key, and CREDDA_API_KEY looks like a live key.\n` +
254
- ` Create one at https://api.credda.io/console (the "Sandbox key" button, free on every plan,\n` +
255
- ` and it does not consume a production key slot), then:\n` +
256
- ` export CREDDA_API_KEY=${TEST_KEY_PREFIX}…`);
257
- }
258
- return key;
259
- }
260
- /** Right-pad for the quickstart table. Pure so the router stays testable. */
261
- function pad(value, width) {
262
- return value.length >= width ? value : value + ' '.repeat(width - value.length);
263
- }
264
- function requireArg(args, name) {
265
- const v = args[0];
266
- if (!v)
267
- throw new Error(`missing required argument <${name}>. See "credda help"`);
268
- return v;
269
- }
270
- function show(ctx, value) {
271
- ctx.out(JSON.stringify(value, null, 2));
272
- }
273
- /**
274
- * Extra stderr lines for a failed command.
275
- *
276
- * The important one is the **request id**: it is the single fastest way for
277
- * Credda to diagnose a failure, and a CLI user has nowhere else to find it.
278
- * Also surfaces the machine code (so it can be looked up in
279
- * `GET /api/v1/errors`) and any `Retry-After` the server asked for.
280
- *
281
- * Duck-typed rather than `instanceof CreddaError` on purpose — the router
282
- * imports only TYPES from the SDK, so it stays pure and trivially mockable.
283
- * Pure and exported for testing.
284
- */
285
- export function errorHints(e) {
286
- if (!e || typeof e !== 'object')
287
- return [];
288
- const err = e;
289
- const lines = [];
290
- if (typeof err.code === 'string' && err.code) {
291
- lines.push(` code: ${err.code} (see https://api.credda.io/api/v1/errors)`);
292
- }
293
- if (typeof err.requestId === 'string' && err.requestId) {
294
- lines.push(` requestId: ${err.requestId} (quote this to support)`);
295
- }
296
- if (typeof err.retryAfterMs === 'number' && err.retryAfterMs > 0) {
297
- lines.push(` retry in: ${Math.ceil(err.retryAfterMs / 1000)}s`);
298
- }
299
- return lines;
300
- }
301
- /**
302
- * Tiny flag parser: `--name value` for valued flags, bare `--name` for
303
- * booleans, everything else positional. Unknown `--flags` are an error rather
304
- * than silently becoming positionals.
305
- */
306
- export function parseFlags(args, spec = {}) {
307
- const valued = new Set(spec.valued ?? []);
308
- const bools = new Set(spec.boolean ?? []);
309
- const positional = [];
310
- const flags = {};
311
- for (let i = 0; i < args.length; i++) {
312
- const a = args[i];
313
- if (a.startsWith('--')) {
314
- const name = a.slice(2);
315
- if (bools.has(name)) {
316
- flags[name] = true;
317
- }
318
- else if (valued.has(name)) {
319
- const v = args[++i];
320
- if (v === undefined)
321
- throw new Error(`--${name} needs a value. See "credda help"`);
322
- flags[name] = v;
323
- }
324
- else {
325
- throw new Error(`unknown flag --${name}. See "credda help"`);
326
- }
327
- }
328
- else {
329
- positional.push(a);
330
- }
331
- }
332
- return { positional, flags };
333
- }
334
- function numFlag(flags, name) {
335
- const v = flags[name];
336
- if (v === undefined)
337
- return undefined;
338
- const n = Number(v);
339
- if (typeof v !== 'string' || v === '' || !Number.isFinite(n)) {
340
- throw new Error(`--${name} must be a number`);
341
- }
342
- return n;
343
- }
344
- function intFlag(flags, name) {
345
- const n = numFlag(flags, name);
346
- if (n !== undefined && (!Number.isInteger(n) || n < 1)) {
347
- throw new Error(`--${name} must be a positive integer`);
348
- }
349
- return n;
350
- }
351
- function strFlag(flags, name) {
352
- const v = flags[name];
353
- return typeof v === 'string' ? v : undefined;
354
- }
355
- /** `--cursor` / `--limit` shared by the paginated list commands. */
356
- function pageQuery(flags) {
357
- return { limit: intFlag(flags, 'limit'), cursor: strFlag(flags, 'cursor') };
358
- }
359
- /**
360
- * The CLOSED book filter set, shared by `credda users` and
361
- * `credda book-summary` so the two can never offer different filters.
362
- *
363
- * `--scored` / `--unscored` are surfaced as two opposing switches rather than a
364
- * `--has-score=true|false` value because that reads better at a prompt; passing
365
- * both is a contradiction and is refused rather than silently resolved.
366
- */
367
- function bookFilterFlags(flags) {
368
- if (flags.scored === true && flags.unscored === true) {
369
- throw new Error('--scored and --unscored are opposites; pass at most one');
370
- }
371
- const query = {
372
- scoreMin: numFlag(flags, 'score-min'),
373
- scoreMax: numFlag(flags, 'score-max'),
374
- band: strFlag(flags, 'band'),
375
- subjectType: strFlag(flags, 'subject-type'),
376
- activeSince: strFlag(flags, 'active-since'),
377
- registeredSince: strFlag(flags, 'registered-since'),
378
- registeredBefore: strFlag(flags, 'registered-before'),
379
- minVerifiedEvents: intFlag(flags, 'min-verified'),
380
- };
381
- if (flags.verified === true)
382
- query.hasVerifiedEvents = true;
383
- if (flags.frozen === true)
384
- query.scoreFrozen = true;
385
- if (flags.scored === true)
386
- query.hasScore = true;
387
- if (flags.unscored === true)
388
- query.hasScore = false;
389
- return query;
390
- }
391
- const ID_HEADER_NAMES = /^(id|userid|user_id|externalid|external_id)$/i;
392
- /**
393
- * Parse the ids for `credda screen`. Inline args may be comma- and/or
394
- * space-separated. A file is one id per line — or a CSV, in which case only
395
- * the FIRST column is read (a leading header row named id/userId/externalId
396
- * is skipped). Deduped, order-preserving. Deliberately simple: no quoted-CSV
397
- * handling — an id containing a comma isn't a valid external id anyway.
398
- */
399
- export function parseIdList(input) {
400
- const raw = [];
401
- if (input.inline) {
402
- for (const chunk of input.inline)
403
- raw.push(...chunk.split(/[\s,]+/));
404
- }
405
- if (input.fileText !== undefined) {
406
- const lines = input.fileText.split(/\r?\n/);
407
- for (let i = 0; i < lines.length; i++) {
408
- const cell = lines[i].split(',')[0].trim();
409
- if (!cell)
410
- continue;
411
- if (i === 0 && ID_HEADER_NAMES.test(cell))
412
- continue; // CSV header row
413
- raw.push(cell);
414
- }
415
- }
416
- const seen = new Set();
417
- const ids = [];
418
- for (const id of raw) {
419
- const trimmed = id.trim();
420
- if (trimmed && !seen.has(trimmed)) {
421
- seen.add(trimmed);
422
- ids.push(trimmed);
423
- }
424
- }
425
- return ids;
426
- }
427
- function requireCsvIo(ctx) {
428
- if (!ctx.fetchCsv || !ctx.writeFile) {
429
- throw new Error('CSV output is not available in this environment');
430
- }
431
- return { fetchCsv: ctx.fetchCsv, writeFile: ctx.writeFile };
432
- }
433
- /** Classify verify input: trust-export bundle JSON, VC-JWT, or compact credential. */
434
- export function classifyCredentialInput(raw) {
435
- const trimmed = raw.trim();
436
- try {
437
- const parsed = JSON.parse(trimmed);
438
- if (parsed && typeof parsed === 'object' && parsed.format === 'credda-trust-export/1') {
439
- return { kind: 'export', bundle: parsed };
440
- }
441
- }
442
- catch {
443
- // not JSON — fall through to string formats
444
- }
445
- if (/^eyJ[\w-]*\.[\w-]+\.[\w-]+$/.test(trimmed)) {
446
- return { kind: 'vc-jwt', jwt: trimmed };
447
- }
448
- return { kind: 'compact', credential: trimmed };
449
- }
450
- /** Run one CLI invocation. Returns the process exit code. */
451
- export async function runCli(argv, ctx) {
452
- const [command, ...args] = argv;
453
- try {
454
- switch (command) {
455
- case undefined:
456
- case 'help':
457
- case '--help':
458
- case '-h':
459
- ctx.out(HELP);
460
- return 0;
461
- case 'version':
462
- case '--version':
463
- case '-v':
464
- ctx.out(VERSION);
465
- return 0;
466
- /**
467
- * The one-command start. Seeds the sandbox, prints what the real formula
468
- * scored each synthetic subject, then reads one back through the ordinary
469
- * score endpoint so the developer has SEEN a round trip work.
470
- *
471
- * Deliberately human-readable rather than JSON: every other command prints
472
- * JSON because it is a client for a program, but this one's entire job is
473
- * legibility to a person in their first two minutes.
474
- */
475
- case 'quickstart': {
476
- const key = requireSandboxKey(ctx);
477
- // Parsed up front so an unknown flag fails before anything is written.
478
- const { flags: qsFlags } = parseFlags(args, { boolean: ['no-confirm'] });
479
- ctx.out('Seeding your Credda sandbox…');
480
- const seed = await ctx.client.seedSandbox(key);
481
- ctx.out('');
482
- ctx.out(seed.subjectsSkipped > 0
483
- ? `${seed.subjectsCreated} subject(s) created, ${seed.subjectsSkipped} already present (left untouched), ${seed.eventsWritten} event(s) written.`
484
- : `${seed.subjectsCreated} subject(s), ${seed.eventsWritten} event(s) written.`);
485
- ctx.out('');
486
- const idWidth = Math.max(...seed.subjects.map((s) => s.userId.length), 7);
487
- ctx.out(`${pad('SUBJECT', idWidth)} SCORE BAND`);
488
- for (const s of seed.subjects) {
489
- const score = s.finalScore === null ? '—' : String(s.finalScore);
490
- ctx.out(`${pad(s.userId, idWidth)} ${pad(score, 5)} ${s.scoreBand ?? '—'}`);
491
- }
492
- ctx.out('');
493
- for (const s of seed.subjects)
494
- ctx.out(`${s.userId}: ${s.record}`);
495
- // Prove a plain read works — this is the call their integration makes.
496
- const first = seed.subjects[0];
497
- if (first) {
498
- ctx.out('');
499
- ctx.out(`Reading it back: GET /api/v1/users/${first.userId}/score`);
500
- const score = await ctx.client.getScore(first.userId, key);
501
- ctx.out(JSON.stringify(score, null, 2));
502
- }
503
- // ── The half that was missing ──────────────────────────────────────
504
- // Everything above is a READ of data we handed them. The product is
505
- // counterparty-CONFIRMED evidence, and a developer used to be able to
506
- // finish the entire on-ramp without meeting POST /confirmations. So
507
- // the quickstart now closes the loop for real: propose an outcome,
508
- // then respond as the counterparty (legitimate here — the create
509
- // response hands the raw token to whoever made the request, and this
510
- // is their own disposable sandbox), and print the verified event id.
511
- //
512
- // Sandbox-only by construction: `requireSandboxKey` above already
513
- // refused a live key. Skippable with --no-confirm for a pure read run.
514
- if (qsFlags['no-confirm'] !== true) {
515
- ctx.out('');
516
- ctx.out('Now the part that matters: earning a verified event.');
517
- ctx.out('A score you can read is wiring. An outcome someone OTHER than the subject');
518
- ctx.out('confirmed is evidence, and it is the only thing that moves a record into the');
519
- ctx.out('earned bands. Three calls, and only the first needs your key:');
520
- ctx.out('');
521
- const subject = 'sbx_confirmation_demo';
522
- const counterpartyRef = `client-${Date.now()}@example.test`;
523
- ctx.out(` 1. POST /api/v1/confirmations (subject ${subject})`);
524
- const created = await ctx.client.createConfirmationRequest({
525
- userId: subject,
526
- eventType: 'CONTRACT_FULFILLED',
527
- stakeLevel: 'MEDIUM',
528
- transactionValue: 1200,
529
- counterpartyRef,
530
- counterpartyName: 'Northside Property Group',
531
- description: 'Kitchen refit at 14 Bridge St, completed 12 March.',
532
- }, key);
533
- ctx.out(` → ${created.confirmation.id} (${created.confirmation.status}); nothing on the ledger yet.`);
534
- ctx.out(` In production you send this to the counterparty over YOUR channel:`);
535
- ctx.out(` ${created.confirmUrl}`);
536
- ctx.out(` Credda sends nothing and never learns their address.`);
537
- ctx.out('');
538
- ctx.out(' 2. GET /api/v1/confirmations/{id}/preview (NO api key, token only)');
539
- const preview = await ctx.client.previewConfirmation(created.confirmation.id, created.confirmationToken);
540
- ctx.out(` They are shown: "${preview.confirmation.description ?? ''}"`);
541
- ctx.out(' Note what they are NOT shown: the subject id. It is not in that projection.');
542
- ctx.out('');
543
- ctx.out(' 3. POST /api/v1/confirmations/{id}/respond (NO api key, token only)');
544
- const decided = await ctx.client.respondToConfirmation(created.confirmation.id, created.confirmationToken, 'confirm');
545
- ctx.out(` → ${decided.status}, ledger event ${decided.eventId ?? '—'}`);
546
- ctx.out(' isVerified: true, earned, because a distinct token-holder acted. Declining');
547
- ctx.out(' would have written nothing at all: no confirmation is never read as a bad outcome.');
548
- ctx.out('');
549
- ctx.out(`The score for ${subject} recomputes asynchronously; read it in a moment with ` +
550
- `"credda score ${subject}". Re-running quickstart adds another confirmed outcome to it.`);
551
- }
552
- ctx.out('');
553
- ctx.out('Next:');
554
- for (const step of seed.nextSteps)
555
- ctx.out(` ${step}`);
556
- ctx.out(' credda sandbox reset wipe it and start over');
557
- ctx.out('');
558
- ctx.out('All of the above is synthetic sandbox data: invisible to live keys, unable to hold a share ' +
559
- 'token or credential, and scored by the identical deterministic formula.');
560
- return 0;
561
- }
562
- case 'sandbox': {
563
- const [sub] = args;
564
- switch (sub) {
565
- case 'seed':
566
- show(ctx, await ctx.client.seedSandbox(requireSandboxKey(ctx)));
567
- return 0;
568
- case 'reset':
569
- show(ctx, await ctx.client.resetSandbox(requireSandboxKey(ctx)));
570
- return 0;
571
- default:
572
- throw new Error(`unknown sandbox subcommand "${sub ?? ''}". See "credda help"`);
573
- }
574
- }
575
- case 'lookup':
576
- show(ctx, await ctx.client.resolveToken(requireArg(args, 'token')));
577
- return 0;
578
- case 'export':
579
- show(ctx, await ctx.client.getTrustExport(requireArg(args, 'token')));
580
- return 0;
581
- case 'verify': {
582
- const raw = await ctx.readInput(requireArg(args, 'file'));
583
- const input = classifyCredentialInput(raw);
584
- try {
585
- if (input.kind === 'export') {
586
- const verified = await ctx.verifiers.trustExport(input.bundle);
587
- show(ctx, { valid: true, kind: 'trust-export', facts: verified.credential.cred });
588
- }
589
- else if (input.kind === 'vc-jwt') {
590
- const verified = await ctx.verifiers.verifiableCredential(input.jwt);
591
- show(ctx, { valid: true, kind: 'w3c-vc', facts: verified.cred });
592
- }
593
- else {
594
- const verified = await ctx.verifiers.trustCredential(input.credential);
595
- show(ctx, { valid: true, kind: 'trust-credential', facts: verified.cred });
596
- }
597
- return 0;
598
- }
599
- catch (e) {
600
- show(ctx, { valid: false, kind: input.kind, reason: e instanceof Error ? e.message : String(e) });
601
- return 2;
602
- }
603
- }
604
- case 'registry':
605
- show(ctx, await ctx.client.getTrustRegistry());
606
- return 0;
607
- case 'did':
608
- show(ctx, await ctx.client.getDidDocument());
609
- return 0;
610
- case 'benchmarks':
611
- // Public catalog: cohort dimensions + the k-anonymity floor. A benchmark
612
- // is a distribution fact, never a verdict.
613
- show(ctx, await ctx.client.getBenchmarks());
614
- return 0;
615
- case 'reason-codes':
616
- // Public adverse-action reason-code catalog (ECOA / Reg B). Credda
617
- // supplies the attribution only — it is not a creditor.
618
- show(ctx, await ctx.client.getReasonCodes());
619
- return 0;
620
- case 'outcome-templates': {
621
- // Public catalog: how a real-world business maps its work to Credda
622
- // events, and WHO the third-party witness is for each outcome. Guidance
623
- // only — nothing here scores, writes, or ranks anyone. Optional
624
- // positional industry slug filters to one set.
625
- const [industry] = args;
626
- show(ctx, await ctx.client.getOutcomeTemplates(industry));
627
- return 0;
628
- }
629
- case 'badges': {
630
- // Public: the closed set of Open Badges 3.0 achievements this issuer
631
- // will sign. A verifier reads the criteria from the ISSUER rather than
632
- // trusting the narrative inside the document it is checking.
633
- const [sub, ...rest] = args;
634
- switch (sub) {
635
- case 'list':
636
- show(ctx, await ctx.client.getOpenBadgeAchievements());
637
- return 0;
638
- case 'get':
639
- show(ctx, await ctx.client.getOpenBadgeAchievement(requireArg(rest, 'badgeId')));
640
- return 0;
641
- default:
642
- throw new Error(`unknown badges subcommand "${sub ?? ''}". See "credda help"`);
643
- }
644
- }
645
- case 'score':
646
- show(ctx, await ctx.client.getScore(requireArg(args, 'userId'), requireKey(ctx)));
647
- return 0;
648
- case 'explain':
649
- show(ctx, await ctx.client.getScoreExplain(requireArg(args, 'userId'), requireKey(ctx)));
650
- return 0;
651
- case 'components':
652
- show(ctx, await ctx.client.getScoreComponents(requireArg(args, 'userId'), requireKey(ctx)));
653
- return 0;
654
- case 'risk':
655
- show(ctx, await ctx.client.getRisk(requireArg(args, 'userId'), requireKey(ctx)));
656
- return 0;
657
- case 'trust-summary': {
658
- const { positional, flags } = parseFlags(args, { boolean: ['narrative'] });
659
- const userId = requireArg(positional, 'userId');
660
- show(ctx, await ctx.client.getTrustSummary(userId, requireKey(ctx), {
661
- narrative: flags.narrative === true,
662
- }));
663
- return 0;
664
- }
665
- case 'benchmark': {
666
- // Where one subject sits within a cohort — percentile + distribution.
667
- const { positional, flags } = parseFlags(args, { valued: ['dimension'] });
668
- const userId = requireArg(positional, 'userId');
669
- show(ctx, await ctx.client.getUserBenchmark(userId, requireKey(ctx), {
670
- dimension: strFlag(flags, 'dimension'),
671
- }));
672
- return 0;
673
- }
674
- case 'distribution': {
675
- // Aggregate, k-anonymised cohort distribution (or a whole dimension).
676
- const { positional, flags } = parseFlags(args, { valued: ['dimension', 'cohort'] });
677
- if (positional.length > 0) {
678
- throw new Error(`distribution takes no positional arguments (got "${positional[0]}"). See "credda help"`);
679
- }
680
- show(ctx, await ctx.client.getBenchmarkDistribution(requireKey(ctx), {
681
- dimension: strFlag(flags, 'dimension'),
682
- cohort: strFlag(flags, 'cohort'),
683
- }));
684
- return 0;
685
- }
686
- case 'users': {
687
- // Query + export your book of subjects (closed filter set).
688
- const { positional, flags } = parseFlags(args, {
689
- valued: [
690
- 'score-min', 'score-max', 'band', 'subject-type', 'active-since',
691
- 'registered-since', 'registered-before',
692
- 'min-verified', 'sort', 'order', 'cursor', 'limit',
693
- ],
694
- boolean: ['verified', 'scored', 'unscored', 'frozen'],
695
- });
696
- if (positional.length > 0) {
697
- throw new Error(`users takes no positional arguments (got "${positional[0]}"). See "credda help"`);
698
- }
699
- const query = {
700
- ...bookFilterFlags(flags),
701
- sort: strFlag(flags, 'sort'),
702
- order: strFlag(flags, 'order'),
703
- ...pageQuery(flags),
704
- };
705
- show(ctx, await ctx.client.listUsers(requireKey(ctx), query));
706
- return 0;
707
- }
708
- case 'book-summary': {
709
- // Size a segment of the book without paging it — same closed filter set.
710
- const { positional, flags } = parseFlags(args, {
711
- valued: [
712
- 'score-min', 'score-max', 'band', 'subject-type', 'active-since',
713
- 'registered-since', 'registered-before', 'min-verified',
714
- ],
715
- boolean: ['verified', 'scored', 'unscored', 'frozen'],
716
- });
717
- if (positional.length > 0) {
718
- throw new Error(`book-summary takes no positional arguments (got "${positional[0]}"). See "credda help"`);
719
- }
720
- show(ctx, await ctx.client.getBookSummary(requireKey(ctx), bookFilterFlags(flags)));
721
- return 0;
722
- }
723
- case 'usage': {
724
- const { positional, flags } = parseFlags(args, { valued: ['from', 'to', 'csv'] });
725
- const days = positional[0] ? Number(positional[0]) : undefined;
726
- if (days !== undefined && (!Number.isInteger(days) || days < 1)) {
727
- throw new Error('usage [days]: days must be a positive integer');
728
- }
729
- const from = strFlag(flags, 'from');
730
- const to = strFlag(flags, 'to');
731
- if (days !== undefined && (from || to)) {
732
- throw new Error('usage: use either a [days] window or --from/--to, not both');
733
- }
734
- const key = requireKey(ctx);
735
- const outfile = strFlag(flags, 'csv');
736
- if (outfile) {
737
- const { fetchCsv, writeFile } = requireCsvIo(ctx);
738
- const qs = new URLSearchParams({ format: 'csv' });
739
- if (days !== undefined)
740
- qs.set('days', String(days));
741
- if (from)
742
- qs.set('from', from);
743
- if (to)
744
- qs.set('to', to);
745
- await writeFile(outfile, await fetchCsv(`/usage?${qs.toString()}`, key));
746
- show(ctx, { written: outfile });
747
- return 0;
748
- }
749
- const window = from || to ? { from, to } : days;
750
- show(ctx, await ctx.client.getUsage(key, window));
751
- return 0;
752
- }
753
- case 'activity': {
754
- const { positional, flags } = parseFlags(args, {
755
- valued: ['action', 'from', 'to', 'cursor', 'limit'],
756
- });
757
- if (positional.length > 0) {
758
- throw new Error(`activity takes no positional arguments (got "${positional[0]}"). See "credda help"`);
759
- }
760
- show(ctx, await ctx.client.getActivity(requireKey(ctx), {
761
- ...pageQuery(flags),
762
- action: strFlag(flags, 'action'),
763
- from: strFlag(flags, 'from'),
764
- to: strFlag(flags, 'to'),
765
- }));
766
- return 0;
767
- }
768
- case 'verified-profile':
769
- // A SECOND measure over the same ledger — it can never move a score.
770
- show(ctx, await ctx.client.getVerifiedProfile(requireArg(args, 'userId'), requireKey(ctx)));
771
- return 0;
772
- case 'qualify': {
773
- // The claim is ALWAYS recorded; --verified-by decides whether it counts
774
- // as verified. Never assert it yourself — name the witness.
775
- const { positional, flags } = parseFlags(args, {
776
- valued: ['category', 'label', 'issuer', 'verified-by'],
777
- });
778
- const userId = requireArg(positional, 'userId');
779
- const category = strFlag(flags, 'category');
780
- if (!category) {
781
- throw new Error('qualify needs --category <education|skill|certification|employment>');
782
- }
783
- show(ctx, await ctx.client.recordQualification(userId, {
784
- category: category,
785
- label: strFlag(flags, 'label'),
786
- issuer: strFlag(flags, 'issuer'),
787
- verifiedBy: strFlag(flags, 'verified-by'),
788
- }, requireKey(ctx)));
789
- return 0;
790
- }
791
- case 'professional-record': {
792
- const [sub, ...rest] = args;
793
- switch (sub) {
794
- case 'get':
795
- show(ctx, await ctx.client.getProfessionalRecord(requireArg(rest, 'userId'), requireKey(ctx)));
796
- return 0;
797
- case 'credential': {
798
- const { positional, flags } = parseFlags(rest, { valued: ['ttl'] });
799
- const userId = requireArg(positional, 'userId');
800
- const ttlSeconds = intFlag(flags, 'ttl');
801
- const minted = await ctx.client.mintProfessionalRecordCredential(userId, requireKey(ctx), ttlSeconds !== undefined ? { ttlSeconds } : {});
802
- show(ctx, minted);
803
- return 0;
804
- }
805
- case 'public':
806
- // Public: the token IS the subject's consent to present the record.
807
- show(ctx, await ctx.client.getPublicProfessionalRecord(requireArg(rest, 'token')));
808
- return 0;
809
- default:
810
- throw new Error(`unknown professional-record subcommand "${sub ?? ''}". See "credda help"`);
811
- }
812
- }
813
- case 'career-export': {
814
- // The subject's whole verified record as a JSON Resume document.
815
- // Keyed by default (your own subject). With --token it's the PUBLIC
816
- // route: the token is the subject's own consent, so NO API key is sent.
817
- const { positional, flags } = parseFlags(args, { valued: ['token'] });
818
- const token = strFlag(flags, 'token');
819
- if (token) {
820
- show(ctx, await ctx.client.getPublicCareerExport(token));
821
- }
822
- else {
823
- show(ctx, await ctx.client.getCareerExport(requireArg(positional, 'userId'), requireKey(ctx)));
824
- }
825
- return 0;
826
- }
827
- case 'reliability-report': {
828
- // The buy-trigger read. Keyed by default (a userId you can look up).
829
- // With --token it's the PUBLIC worker-consent route: the token is the
830
- // worker's own consent, so NO API key is sent.
831
- const { positional, flags } = parseFlags(args, {
832
- valued: ['token', 'recent'],
833
- boolean: ['benchmark'],
834
- });
835
- const token = strFlag(flags, 'token');
836
- const recent = intFlag(flags, 'recent');
837
- const opts = {
838
- ...(recent !== undefined ? { recent } : {}),
839
- ...(flags.benchmark === true ? { benchmark: true } : {}),
840
- };
841
- if (token) {
842
- show(ctx, await ctx.client.getPublicReliabilityReport(token, opts));
843
- }
844
- else {
845
- show(ctx, await ctx.client.getReliabilityReport(requireArg(positional, 'userId'), requireKey(ctx), opts));
846
- }
847
- return 0;
848
- }
849
- case 'confirmations': {
850
- // ⚠️ Auth is ASYMMETRIC here: preview/respond are the COUNTERPARTY's
851
- // calls and take no API key at all, so requireKey() is deliberately
852
- // scoped per-subcommand rather than hoisted for the whole group.
853
- const [sub, ...rest] = args;
854
- switch (sub) {
855
- case 'create': {
856
- const { flags } = parseFlags(rest, {
857
- valued: [
858
- 'user', 'type', 'counterparty', 'counterparty-name', 'description',
859
- 'stake', 'value', 'due', 'completed', 'return-url', 'expires-in',
860
- 'idempotency-key',
861
- ],
862
- });
863
- const userId = strFlag(flags, 'user');
864
- const eventType = strFlag(flags, 'type');
865
- const counterpartyRef = strFlag(flags, 'counterparty');
866
- if (!userId || !eventType || !counterpartyRef) {
867
- throw new Error('confirmations create needs --user <externalId> (the subject), --type <eventType> and --counterparty <ref> (your key for the party being asked to confirm)');
868
- }
869
- const stake = strFlag(flags, 'stake');
870
- const value = numFlag(flags, 'value');
871
- const expiresInDays = intFlag(flags, 'expires-in');
872
- const created = await ctx.client.createConfirmationRequest({
873
- userId,
874
- eventType: eventType,
875
- counterpartyRef,
876
- ...(strFlag(flags, 'counterparty-name') ? { counterpartyName: strFlag(flags, 'counterparty-name') } : {}),
877
- ...(strFlag(flags, 'description') ? { description: strFlag(flags, 'description') } : {}),
878
- ...(stake ? { stakeLevel: stake } : {}),
879
- ...(value !== undefined ? { transactionValue: value } : {}),
880
- ...(strFlag(flags, 'due') ? { dueDate: strFlag(flags, 'due') } : {}),
881
- ...(strFlag(flags, 'completed') ? { completedAt: strFlag(flags, 'completed') } : {}),
882
- ...(strFlag(flags, 'return-url') ? { returnUrl: strFlag(flags, 'return-url') } : {}),
883
- ...(expiresInDays !== undefined ? { expiresInDays } : {}),
884
- }, requireKey(ctx), { idempotencyKey: strFlag(flags, 'idempotency-key') });
885
- ctx.err('NOTE: the confirmationToken below is shown ONCE; deliver it to the counterparty over your own channel.');
886
- show(ctx, created);
887
- return 0;
888
- }
889
- case 'batch': {
890
- // The ACTIVATION ENGINE — bulk-create up to 100 requests from a
891
- // JSON file: either an array of request bodies, or { requests: [...] }.
892
- const { positional, flags } = parseFlags(rest, { valued: ['idempotency-key'] });
893
- const file = requireArg(positional, 'file');
894
- const parsed = JSON.parse(await ctx.readInput(file));
895
- const requests = (Array.isArray(parsed) ? parsed : parsed.requests);
896
- if (!Array.isArray(requests)) {
897
- throw new Error('confirmations batch <file.json>: the file must be a JSON array of request bodies, or an object with a "requests" array');
898
- }
899
- const idempotencyKey = strFlag(flags, 'idempotency-key');
900
- const result = await ctx.client.createConfirmationBatch(requests, requireKey(ctx), idempotencyKey ? { idempotencyKey } : {});
901
- ctx.err('NOTE: each ok item below carries a confirmationToken shown ONCE; deliver it to that counterparty over your own channel.');
902
- show(ctx, result);
903
- return 0;
904
- }
905
- case 'list': {
906
- const { flags } = parseFlags(rest, { valued: ['status', 'cursor', 'limit'] });
907
- const status = strFlag(flags, 'status');
908
- show(ctx, await ctx.client.listConfirmations(requireKey(ctx), {
909
- ...pageQuery(flags),
910
- ...(status ? { status: status.toUpperCase() } : {}),
911
- }));
912
- return 0;
913
- }
914
- case 'get':
915
- show(ctx, await ctx.client.getConfirmation(requireArg(rest, 'id'), requireKey(ctx)));
916
- return 0;
917
- case 'cancel':
918
- show(ctx, await ctx.client.cancelConfirmation(requireArg(rest, 'id'), requireKey(ctx)));
919
- return 0;
920
- case 'preview': {
921
- // KEYLESS on purpose — the counterparty holds a token, not a key.
922
- const { positional, flags } = parseFlags(rest, { valued: ['token'] });
923
- const id = requireArg(positional, 'id');
924
- const token = strFlag(flags, 'token');
925
- if (!token)
926
- throw new Error('confirmations preview needs --token <t> (the one-time token you were sent)');
927
- show(ctx, await ctx.client.previewConfirmation(id, token));
928
- return 0;
929
- }
930
- case 'respond': {
931
- // KEYLESS on purpose. The decision is explicit: there is no default,
932
- // because confirming an outcome you did not witness is the one thing
933
- // this primitive exists to prevent.
934
- const { positional, flags } = parseFlags(rest, {
935
- valued: ['token'],
936
- boolean: ['confirm', 'decline'],
937
- });
938
- const id = requireArg(positional, 'id');
939
- const token = strFlag(flags, 'token');
940
- if (!token)
941
- throw new Error('confirmations respond needs --token <t> (the one-time token you were sent)');
942
- const confirm = flags.confirm === true;
943
- const decline = flags.decline === true;
944
- if (confirm === decline) {
945
- throw new Error('confirmations respond needs exactly one of --confirm or --decline');
946
- }
947
- show(ctx, await ctx.client.respondToConfirmation(id, token, confirm ? 'confirm' : 'decline'));
948
- return 0;
949
- }
950
- default:
951
- throw new Error(`unknown confirmations subcommand "${sub ?? ''}". See "credda help"`);
952
- }
953
- }
954
- case 'references': {
955
- // ⚠️ Same ASYMMETRIC auth as confirmations: preview/respond are the
956
- // reference's own calls and take no API key, so requireKey() is scoped
957
- // per-subcommand rather than hoisted for the whole group.
958
- const [sub, ...rest] = args;
959
- switch (sub) {
960
- case 'create': {
961
- const { flags } = parseFlags(rest, {
962
- valued: [
963
- 'user', 'category', 'counterparty', 'counterparty-name', 'description',
964
- 'label', 'issuer', 'jurisdiction', 'reference', 'return-url', 'expires-in',
965
- 'idempotency-key',
966
- ],
967
- });
968
- const userId = strFlag(flags, 'user');
969
- const category = strFlag(flags, 'category');
970
- const counterpartyRef = strFlag(flags, 'counterparty');
971
- if (!userId || !category || !counterpartyRef) {
972
- throw new Error('references create needs --user <externalId> (the subject), --category <employment|education|certification|skill> and --counterparty <ref> (your key for the party being asked to confirm)');
973
- }
974
- const expiresInDays = intFlag(flags, 'expires-in');
975
- const created = await ctx.client.createReferenceRequest({
976
- userId,
977
- category: category,
978
- counterpartyRef,
979
- ...(strFlag(flags, 'label') ? { label: strFlag(flags, 'label') } : {}),
980
- ...(strFlag(flags, 'issuer') ? { issuer: strFlag(flags, 'issuer') } : {}),
981
- ...(strFlag(flags, 'jurisdiction') ? { jurisdiction: strFlag(flags, 'jurisdiction') } : {}),
982
- ...(strFlag(flags, 'reference') ? { reference: strFlag(flags, 'reference') } : {}),
983
- ...(strFlag(flags, 'counterparty-name') ? { counterpartyName: strFlag(flags, 'counterparty-name') } : {}),
984
- ...(strFlag(flags, 'description') ? { description: strFlag(flags, 'description') } : {}),
985
- ...(strFlag(flags, 'return-url') ? { returnUrl: strFlag(flags, 'return-url') } : {}),
986
- ...(expiresInDays !== undefined ? { expiresInDays } : {}),
987
- }, requireKey(ctx), { idempotencyKey: strFlag(flags, 'idempotency-key') });
988
- ctx.err('NOTE: the referenceToken below is shown ONCE; deliver it to the reference over your own channel.');
989
- show(ctx, created);
990
- return 0;
991
- }
992
- case 'list': {
993
- const { flags } = parseFlags(rest, { valued: ['status', 'cursor', 'limit'] });
994
- const status = strFlag(flags, 'status');
995
- show(ctx, await ctx.client.listReferences(requireKey(ctx), {
996
- ...pageQuery(flags),
997
- ...(status ? { status: status.toUpperCase() } : {}),
998
- }));
999
- return 0;
1000
- }
1001
- case 'get':
1002
- show(ctx, await ctx.client.getReference(requireArg(rest, 'id'), requireKey(ctx)));
1003
- return 0;
1004
- case 'cancel':
1005
- show(ctx, await ctx.client.cancelReference(requireArg(rest, 'id'), requireKey(ctx)));
1006
- return 0;
1007
- case 'preview': {
1008
- // KEYLESS on purpose — the reference holds a token, not a key.
1009
- const { positional, flags } = parseFlags(rest, { valued: ['token'] });
1010
- const id = requireArg(positional, 'id');
1011
- const token = strFlag(flags, 'token');
1012
- if (!token)
1013
- throw new Error('references preview needs --token <t> (the one-time token you were sent)');
1014
- show(ctx, await ctx.client.previewReference(id, token));
1015
- return 0;
1016
- }
1017
- case 'respond': {
1018
- // KEYLESS on purpose. The decision is explicit: there is no default,
1019
- // because confirming a claim you cannot vouch for is the one thing
1020
- // this primitive exists to prevent.
1021
- const { positional, flags } = parseFlags(rest, {
1022
- valued: ['token'],
1023
- boolean: ['confirm', 'decline'],
1024
- });
1025
- const id = requireArg(positional, 'id');
1026
- const token = strFlag(flags, 'token');
1027
- if (!token)
1028
- throw new Error('references respond needs --token <t> (the one-time token you were sent)');
1029
- const confirm = flags.confirm === true;
1030
- const decline = flags.decline === true;
1031
- if (confirm === decline) {
1032
- throw new Error('references respond needs exactly one of --confirm or --decline');
1033
- }
1034
- show(ctx, await ctx.client.respondToReference(id, token, confirm ? 'confirm' : 'decline'));
1035
- return 0;
1036
- }
1037
- default:
1038
- throw new Error(`unknown references subcommand "${sub ?? ''}". See "credda help"`);
1039
- }
1040
- }
1041
- case 'policies': {
1042
- const [sub, ...rest] = args;
1043
- const key = requireKey(ctx);
1044
- switch (sub) {
1045
- case 'list': {
1046
- const { flags } = parseFlags(rest, { valued: ['cursor', 'limit'] });
1047
- show(ctx, await ctx.client.listPolicies(key, pageQuery(flags)));
1048
- return 0;
1049
- }
1050
- case 'get':
1051
- show(ctx, await ctx.client.getPolicy(requireArg(rest, 'id'), key));
1052
- return 0;
1053
- case 'create': {
1054
- const { flags } = parseFlags(rest, {
1055
- valued: ['name', 'user', 'metric', 'direction', 'threshold', 'component', 'band'],
1056
- boolean: ['all'],
1057
- });
1058
- const name = strFlag(flags, 'name');
1059
- const metric = strFlag(flags, 'metric');
1060
- if (!name || !metric) {
1061
- throw new Error('policies create needs --name <n> and --metric <score|component|band|verified_events>');
1062
- }
1063
- const userId = strFlag(flags, 'user');
1064
- const appliesToAll = flags.all === true;
1065
- if (Boolean(userId) === appliesToAll) {
1066
- throw new Error('policies create needs exactly one of --user <externalId> (watch one subject) or --all (watch all your subjects)');
1067
- }
1068
- const threshold = numFlag(flags, 'threshold');
1069
- const input = {
1070
- name,
1071
- metric: metric,
1072
- ...(userId ? { userId } : { appliesToAll: true }),
1073
- };
1074
- const direction = strFlag(flags, 'direction');
1075
- if (direction)
1076
- input.direction = direction;
1077
- if (threshold !== undefined)
1078
- input.threshold = threshold;
1079
- const component = strFlag(flags, 'component');
1080
- if (component)
1081
- input.component = component;
1082
- const band = strFlag(flags, 'band');
1083
- if (band)
1084
- input.band = band;
1085
- show(ctx, await ctx.client.createPolicy(input, key));
1086
- return 0;
1087
- }
1088
- case 'update': {
1089
- const { positional, flags } = parseFlags(rest, {
1090
- valued: ['name', 'direction', 'threshold', 'component', 'band'],
1091
- boolean: ['activate', 'deactivate'],
1092
- });
1093
- const id = requireArg(positional, 'id');
1094
- if (flags.activate === true && flags.deactivate === true) {
1095
- throw new Error('policies update: pass at most one of --activate / --deactivate');
1096
- }
1097
- const patch = {};
1098
- const name = strFlag(flags, 'name');
1099
- if (name)
1100
- patch.name = name;
1101
- const direction = strFlag(flags, 'direction');
1102
- if (direction)
1103
- patch.direction = direction;
1104
- const threshold = numFlag(flags, 'threshold');
1105
- if (threshold !== undefined)
1106
- patch.threshold = threshold;
1107
- const component = strFlag(flags, 'component');
1108
- if (component)
1109
- patch.component = component;
1110
- const band = strFlag(flags, 'band');
1111
- if (band)
1112
- patch.band = band;
1113
- if (flags.activate === true)
1114
- patch.isActive = true;
1115
- if (flags.deactivate === true)
1116
- patch.isActive = false;
1117
- if (Object.keys(patch).length === 0) {
1118
- throw new Error('policies update needs at least one field to change. See "credda help"');
1119
- }
1120
- show(ctx, await ctx.client.updatePolicy(id, patch, key));
1121
- return 0;
1122
- }
1123
- case 'delete':
1124
- await ctx.client.deletePolicy(requireArg(rest, 'id'), key);
1125
- show(ctx, { deleted: true });
1126
- return 0;
1127
- default:
1128
- throw new Error(`unknown policies subcommand "${sub ?? ''}". See "credda help"`);
1129
- }
1130
- }
1131
- case 'monitors': {
1132
- const [sub, ...rest] = args;
1133
- const key = requireKey(ctx);
1134
- switch (sub) {
1135
- case 'list': {
1136
- const { flags } = parseFlags(rest, { valued: ['cursor', 'limit'] });
1137
- show(ctx, await ctx.client.listMonitors(key, pageQuery(flags)));
1138
- return 0;
1139
- }
1140
- case 'get':
1141
- show(ctx, await ctx.client.getMonitor(requireArg(rest, 'id'), key));
1142
- return 0;
1143
- case 'create': {
1144
- const { flags } = parseFlags(rest, {
1145
- valued: ['user', 'below', 'above'],
1146
- boolean: ['band-change'],
1147
- });
1148
- const userId = strFlag(flags, 'user');
1149
- if (!userId) {
1150
- throw new Error('monitors create needs --user <externalId> (the user to watch)');
1151
- }
1152
- const belowScore = numFlag(flags, 'below');
1153
- const aboveScore = numFlag(flags, 'above');
1154
- const onBandChange = flags['band-change'] === true;
1155
- if (belowScore === undefined && aboveScore === undefined && !onBandChange) {
1156
- throw new Error('monitors create needs at least one condition: --below <score> (fires when the score crosses DOWN through it), --above <score> (crosses UP), or --band-change (any band change)');
1157
- }
1158
- show(ctx, await ctx.client.createMonitor({
1159
- userId,
1160
- ...(belowScore !== undefined ? { belowScore } : {}),
1161
- ...(aboveScore !== undefined ? { aboveScore } : {}),
1162
- ...(onBandChange ? { onBandChange } : {}),
1163
- }, key));
1164
- return 0;
1165
- }
1166
- case 'delete':
1167
- await ctx.client.deleteMonitor(requireArg(rest, 'id'), key);
1168
- show(ctx, { deleted: true });
1169
- return 0;
1170
- default:
1171
- throw new Error(`unknown monitors subcommand "${sub ?? ''}". See "credda help"`);
1172
- }
1173
- }
1174
- case 'screen': {
1175
- const { positional, flags } = parseFlags(args, {
1176
- valued: ['file'],
1177
- boolean: ['wait'],
1178
- });
1179
- const file = strFlag(flags, 'file');
1180
- if (file && positional.length > 0) {
1181
- throw new Error('screen: pass ids inline OR --file <path>, not both');
1182
- }
1183
- const ids = parseIdList(file ? { fileText: await ctx.readInput(file) } : { inline: positional });
1184
- if (ids.length === 0) {
1185
- throw new Error('screen: no ids found. Pass ids (comma/space separated) or --file <path> (one id per line, or a CSV whose first column is the id)');
1186
- }
1187
- const key = requireKey(ctx);
1188
- let job = (await ctx.client.createScreening(ids, key)).screening;
1189
- if (flags.wait === true) {
1190
- const sleep = ctx.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1191
- const interval = ctx.pollIntervalMs ?? 2000;
1192
- while (job.status === 'QUEUED' || job.status === 'RUNNING') {
1193
- ctx.err(`screening ${job.id}: ${job.status}, waiting…`);
1194
- await sleep(interval);
1195
- job = (await ctx.client.getScreening(job.id, key)).screening;
1196
- }
1197
- show(ctx, { screening: job });
1198
- return job.status === 'COMPLETED' ? 0 : 1;
1199
- }
1200
- show(ctx, { screening: job });
1201
- if (job.status === 'QUEUED' || job.status === 'RUNNING') {
1202
- ctx.err(`still ${job.status}; poll with "credda screenings get ${job.id}" (or re-run with --wait)`);
1203
- }
1204
- return 0;
1205
- }
1206
- case 'screenings': {
1207
- const [sub, ...rest] = args;
1208
- const key = requireKey(ctx);
1209
- switch (sub) {
1210
- case 'list': {
1211
- const { flags } = parseFlags(rest, { valued: ['cursor', 'limit'] });
1212
- show(ctx, await ctx.client.listScreenings(key, pageQuery(flags)));
1213
- return 0;
1214
- }
1215
- case 'get':
1216
- show(ctx, await ctx.client.getScreening(requireArg(rest, 'id'), key));
1217
- return 0;
1218
- case 'results': {
1219
- const { positional, flags } = parseFlags(rest, { valued: ['csv'] });
1220
- const id = requireArg(positional, 'id');
1221
- const outfile = strFlag(flags, 'csv');
1222
- if (outfile) {
1223
- const { fetchCsv, writeFile } = requireCsvIo(ctx);
1224
- await writeFile(outfile, await fetchCsv(`/screenings/${encodeURIComponent(id)}/results?format=csv`, key));
1225
- show(ctx, { written: outfile });
1226
- return 0;
1227
- }
1228
- show(ctx, await ctx.client.getScreeningResults(id, key));
1229
- return 0;
1230
- }
1231
- default:
1232
- throw new Error(`unknown screenings subcommand "${sub ?? ''}". See "credda help"`);
1233
- }
1234
- }
1235
- case 'webhooks': {
1236
- const [sub, ...rest] = args;
1237
- const key = requireKey(ctx);
1238
- switch (sub) {
1239
- case 'list':
1240
- show(ctx, await ctx.client.listWebhooks(key));
1241
- return 0;
1242
- case 'create': {
1243
- const url = requireArg(rest, 'url');
1244
- const events = rest.slice(1);
1245
- if (events.length === 0) {
1246
- throw new Error('webhooks create <url> <event...>: provide at least one event (score.updated, score.band_changed, dispute.resolved, monitor.triggered, usage.quota_warning)');
1247
- }
1248
- const created = await ctx.client.createWebhook({ url, events: events }, key);
1249
- ctx.err('NOTE: the signing secret below is shown ONCE; store it now.');
1250
- show(ctx, created);
1251
- return 0;
1252
- }
1253
- case 'delete':
1254
- await ctx.client.deleteWebhook(requireArg(rest, 'id'), key);
1255
- show(ctx, { deleted: true });
1256
- return 0;
1257
- case 'test':
1258
- show(ctx, await ctx.client.testWebhook(requireArg(rest, 'id'), key));
1259
- return 0;
1260
- case 'deliveries':
1261
- show(ctx, await ctx.client.getWebhookDeliveries(requireArg(rest, 'id'), key, 25));
1262
- return 0;
1263
- case 'recent': {
1264
- // Sample data across ALL endpoints — falls back to the event
1265
- // catalog's examples (isExample:true) when nothing has fired yet.
1266
- const eventType = rest.length > 0 ? rest : undefined;
1267
- show(ctx, await ctx.client.getRecentWebhookEvents(key, { limit: 25, eventType }));
1268
- return 0;
1269
- }
1270
- default:
1271
- throw new Error(`unknown webhooks subcommand "${sub ?? ''}". See "credda help"`);
1272
- }
1273
- }
1274
- case 'listen': {
1275
- if (!ctx.startListener)
1276
- throw new Error('listen is not available in this environment');
1277
- const port = args[0] ? Number(args[0]) : 4141;
1278
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
1279
- throw new Error('listen [port]: port must be 1-65535');
1280
- }
1281
- await ctx.startListener({ port, secret: ctx.webhookSecret });
1282
- return 0;
1283
- }
1284
- case 'mint':
1285
- show(ctx, await ctx.client.mintShareToken(requireArg(args, 'userId'), requireKey(ctx)));
1286
- return 0;
1287
- case 'revoke':
1288
- await ctx.client.revokeShareToken(requireArg(args, 'userId'), requireKey(ctx));
1289
- show(ctx, { revoked: true });
1290
- return 0;
1291
- default:
1292
- throw new Error(`unknown command "${command}". See "credda help"`);
1293
- }
1294
- }
1295
- catch (e) {
1296
- const message = e instanceof Error ? e.message : String(e);
1297
- // SDK errors already carry a "credda:" prefix — don't double it.
1298
- ctx.err(message.startsWith('credda:') ? message : `credda: ${message}`);
1299
- for (const line of errorHints(e))
1300
- ctx.err(line);
1301
- return 1;
1302
- }
1303
- }