@credda/cli 0.1.2 → 0.1.4

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 CHANGED
@@ -7,157 +7,229 @@
7
7
  * looks up and offline-verifies EXISTING, already-computed trust facts.
8
8
  * `mint`/`revoke` manage a share token (a capability, not a score write).
9
9
  */
10
- export const VERSION = '0.1.2';
11
- export const HELP = `credda portable trust from the terminal
12
-
13
- Public (no API key):
14
- credda lookup <token> Trust check for a share token
15
- credda export <token> Full self-verifying trust export bundle
16
- credda verify <file|-> Offline-verify a credential: a W3C VC-JWT,
17
- a compact Trust Credential, or a saved trust
18
- export bundle (auto-detected). '-' = stdin.
19
- credda registry Federated trust registry
20
- credda did Issuer DID document
21
- credda benchmarks Cohort-benchmark catalog (dimensions + k-anonymity)
22
- credda reason-codes Adverse-action reason-code catalog (ECOA / Reg B)
23
- credda badges list Open Badges 3.0 achievements this issuer signs
24
- credda badges get <badgeId> One achievement definition
25
- credda professional-record public <token>
26
- The professional record behind a share token
27
- (the subject's own consent to present it)
28
-
29
- Platform (needs CREDDA_API_KEY):
30
- credda score <userId> Current score
31
- credda explain <userId> Factor-level score explanation
32
- credda components <userId> Six named 0-100 score components
33
- credda risk <userId> Advisory risk signals
34
- credda trust-summary <userId> [--narrative]
35
- Deterministic, evidence-based trust summary
36
- (explains; never a verdict). --narrative adds an
37
- advisory AI retelling when the server has AI on.
38
- credda benchmark <userId> [--dimension <d>]
39
- Where a subject sits within its cohort:
40
- percentile + the cohort distribution
41
- credda distribution [--dimension <d>] [--cohort <c>]
42
- Aggregate, k-anonymised cohort distribution.
43
- Omit --cohort for every cohort on the dimension.
44
- credda users [--score-min <n>] [--score-max <n>] [--band <b>] [--subject-type <PERSON|AGENT>]
45
- [--active-since <iso>] [--verified] [--min-verified <n>]
46
- [--sort <score|lastActivity|registered|externalId>] [--order <asc|desc>]
47
- [--cursor <c>] [--limit <n>]
48
- Query + export your book of subjects
49
- credda usage [days] [--from <date> --to <date>] [--csv <outfile>]
50
- Your platform's metered API usage. Either a
51
- trailing [days] window OR an inclusive
52
- --from/--to date range (YYYY-MM-DD), not both.
53
- --csv writes the flat CSV statement to a file.
54
- credda activity [--action <A>] [--from <t> --to <t>] [--cursor <c>] [--limit <n>]
55
- Your platform's own activity/audit log,
56
- newest-first, cursor-paginated
57
- credda verified-profile <userId>
58
- How much of a subject's CLAIMED record
59
- (education/skills/certifications/employment) is
60
- third-party verified. Counts whether a claim is
61
- verified, never how prestigious it is — and it
62
- can never move the Reliability Score.
63
- credda qualify <userId> --category <education|skill|certification|employment>
64
- [--label <l>] [--issuer <i>] [--verified-by <witness>]
65
- Record a qualification claim. Always recorded;
66
- counts as VERIFIED only with a genuine
67
- third-party --verified-by witness.
68
- credda professional-record get <userId>
69
- Résumé-shaped summary of a VERIFIED work record.
70
- Describes a record — not a hiring verdict, a
71
- background check, or a consumer report.
72
- credda professional-record credential <userId> [--ttl <seconds>]
73
- Mint the signed, offline-verifiable Professional
74
- Record Credential (+ an "Add to LinkedIn" link)
75
- credda mint <userId> Mint a share token for a user
76
- credda revoke <userId> Revoke a user's share token
77
-
78
- Confirmation requests the counterparty-confirmation primitive. You PROPOSE an
79
- outcome and deliver the one-time token to the counterparty over YOUR OWN channel;
80
- the event is written, verified, only when that distinct party confirms:
81
- credda confirmations create --user <externalId> --type <eventType>
82
- --counterparty <ref> [--counterparty-name <n>] [--description <d>]
83
- [--stake <HIGH|MEDIUM|LOW>] [--value <n>] [--due <iso>] [--completed <iso>]
84
- [--return-url <url>] [--expires-in <days>] [--idempotency-key <k>]
85
- Needs CREDDA_API_KEY. Token shown ONCE.
86
- credda confirmations list [--status <s>] [--cursor <c>] [--limit <n>]
87
- credda confirmations get <id>
88
- credda confirmations cancel <id>
89
- credda confirmations preview <id> --token <t>
90
- What the counterparty is asked to confirm.
91
- NO API key the token is the capability.
92
- credda confirmations respond <id> --token <t> (--confirm | --decline)
93
- The counterparty's decision. NO API key.
94
- --confirm writes the event; --decline writes
95
- nothing. Single-use either way.
96
-
97
- Threshold policies (needs CREDDA_API_KEY) declarative "tell me when this line
98
- is crossed"; delivers policy.threshold_crossed through your webhooks. Config
99
- only: a policy never reads into, blocks, or changes a score:
100
- credda policies list [--cursor <c>] [--limit <n>]
101
- credda policies get <id>
102
- credda policies create --name <n> (--user <externalId> | --all)
103
- --metric <score|component|band|verified_events>
104
- [--direction <up|down|enter|leave>] [--threshold <n>]
105
- [--component <reliability|timeliness|trustworthiness|verification|consistency|momentum>]
106
- [--band <b>]
107
- credda policies update <id> [--name <n>] [--direction <d>] [--threshold <n>]
108
- [--component <c>] [--band <b>] [--activate | --deactivate]
109
- The metric is immutable delete + recreate.
110
- credda policies delete <id>
111
-
112
- Score monitors (needs CREDDA_API_KEY) edge-triggered watches that deliver
113
- "monitor.triggered" through your webhooks; notification config only, a
114
- monitor never affects a score:
115
- credda monitors list [--cursor <c>] [--limit <n>]
116
- credda monitors get <id>
117
- credda monitors create --user <externalId> [--below <score>] [--above <score>] [--band-change]
118
- At least one condition required. --below fires
119
- on a downward crossing (and on a first score
120
- already below it), --above on an upward
121
- crossing, --band-change on any band change.
122
- credda monitors delete <id>
123
-
124
- Bulk screenings (needs CREDDA_API_KEY) async batch score reads, up to
125
- 10,000 ids per job, strictly read-only:
126
- credda screen <ids...> Submit ids (comma/space separated), or:
127
- credda screen --file <path> One id per line, or a CSV whose FIRST column
128
- is the id (a leading "id"/"userId"/
129
- "externalId" header row is skipped).
130
- [--wait] Poll until the job finishes, then print the
131
- summary (exit 1 if the job FAILED).
132
- credda screenings list [--cursor <c>] [--limit <n>]
133
- credda screenings get <id> Job status + summary
134
- credda screenings results <id> [--csv <outfile>]
135
- Per-user results (JSON; --csv writes the CSV
136
- attachment to a file instead)
137
-
138
- Webhooks (needs CREDDA_API_KEY):
139
- credda webhooks list Your webhook subscriptions
140
- credda webhooks create <url> <event..> Subscribe (secret shown ONCE)
141
- credda webhooks delete <id> Remove a webhook
142
- credda webhooks test <id> Send a synthetic signed delivery
143
- credda webhooks deliveries <id> Recent delivery attempts (incl. retries)
144
- credda webhooks recent [event..] Recent events across ALL your endpoints
145
- (sample data for automation platforms;
146
- falls back to catalog examples, flagged
147
- isExample, when nothing has fired yet)
148
-
149
- Local development:
150
- credda listen [port] Local webhook receiver: verifies each delivery's
151
- HMAC signature (CREDDA_WEBHOOK_SECRET) and
152
- pretty-prints the payload. Default port 4141.
153
- Credda delivers to public HTTPS only — expose
154
- this port with your own tunnel (e.g. cloudflared).
155
-
156
- Environment:
157
- CREDDA_API_URL API base (default https://api.credda.io)
158
- CREDDA_API_KEY Platform API key for keyed commands
159
- CREDDA_WEBHOOK_SECRET whsec_… signing secret for "credda listen"
160
-
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
+
161
233
  Exit codes: 0 ok/valid · 1 error · 2 credential failed verification`;
162
234
  function requireKey(ctx) {
163
235
  if (!ctx.apiKey) {
@@ -165,10 +237,34 @@ function requireKey(ctx) {
165
237
  }
166
238
  return ctx.apiKey;
167
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
+ }
168
264
  function requireArg(args, name) {
169
265
  const v = args[0];
170
266
  if (!v)
171
- throw new Error(`missing required argument <${name}> see "credda help"`);
267
+ throw new Error(`missing required argument <${name}>. See "credda help"`);
172
268
  return v;
173
269
  }
174
270
  function show(ctx, value) {
@@ -222,11 +318,11 @@ export function parseFlags(args, spec = {}) {
222
318
  else if (valued.has(name)) {
223
319
  const v = args[++i];
224
320
  if (v === undefined)
225
- throw new Error(`--${name} needs a value see "credda help"`);
321
+ throw new Error(`--${name} needs a value. See "credda help"`);
226
322
  flags[name] = v;
227
323
  }
228
324
  else {
229
- throw new Error(`unknown flag --${name} see "credda help"`);
325
+ throw new Error(`unknown flag --${name}. See "credda help"`);
230
326
  }
231
327
  }
232
328
  else {
@@ -260,6 +356,38 @@ function strFlag(flags, name) {
260
356
  function pageQuery(flags) {
261
357
  return { limit: intFlag(flags, 'limit'), cursor: strFlag(flags, 'cursor') };
262
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
+ }
263
391
  const ID_HEADER_NAMES = /^(id|userid|user_id|externalid|external_id)$/i;
264
392
  /**
265
393
  * Parse the ids for `credda screen`. Inline args may be comma- and/or
@@ -335,6 +463,115 @@ export async function runCli(argv, ctx) {
335
463
  case '-v':
336
464
  ctx.out(VERSION);
337
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
+ }
338
575
  case 'lookup':
339
576
  show(ctx, await ctx.client.resolveToken(requireArg(args, 'token')));
340
577
  return 0;
@@ -380,6 +617,15 @@ export async function runCli(argv, ctx) {
380
617
  // supplies the attribution only — it is not a creditor.
381
618
  show(ctx, await ctx.client.getReasonCodes());
382
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
+ }
383
629
  case 'badges': {
384
630
  // Public: the closed set of Open Badges 3.0 achievements this issuer
385
631
  // will sign. A verifier reads the criteria from the ISSUER rather than
@@ -393,7 +639,7 @@ export async function runCli(argv, ctx) {
393
639
  show(ctx, await ctx.client.getOpenBadgeAchievement(requireArg(rest, 'badgeId')));
394
640
  return 0;
395
641
  default:
396
- throw new Error(`unknown badges subcommand "${sub ?? ''}" see "credda help"`);
642
+ throw new Error(`unknown badges subcommand "${sub ?? ''}". See "credda help"`);
397
643
  }
398
644
  }
399
645
  case 'score':
@@ -429,7 +675,7 @@ export async function runCli(argv, ctx) {
429
675
  // Aggregate, k-anonymised cohort distribution (or a whole dimension).
430
676
  const { positional, flags } = parseFlags(args, { valued: ['dimension', 'cohort'] });
431
677
  if (positional.length > 0) {
432
- throw new Error(`distribution takes no positional arguments (got "${positional[0]}") see "credda help"`);
678
+ throw new Error(`distribution takes no positional arguments (got "${positional[0]}"). See "credda help"`);
433
679
  }
434
680
  show(ctx, await ctx.client.getBenchmarkDistribution(requireKey(ctx), {
435
681
  dimension: strFlag(flags, 'dimension'),
@@ -442,39 +688,48 @@ export async function runCli(argv, ctx) {
442
688
  const { positional, flags } = parseFlags(args, {
443
689
  valued: [
444
690
  'score-min', 'score-max', 'band', 'subject-type', 'active-since',
691
+ 'registered-since', 'registered-before',
445
692
  'min-verified', 'sort', 'order', 'cursor', 'limit',
446
693
  ],
447
- boolean: ['verified'],
694
+ boolean: ['verified', 'scored', 'unscored', 'frozen'],
448
695
  });
449
696
  if (positional.length > 0) {
450
- throw new Error(`users takes no positional arguments (got "${positional[0]}") see "credda help"`);
697
+ throw new Error(`users takes no positional arguments (got "${positional[0]}"). See "credda help"`);
451
698
  }
452
699
  const query = {
453
- scoreMin: numFlag(flags, 'score-min'),
454
- scoreMax: numFlag(flags, 'score-max'),
455
- band: strFlag(flags, 'band'),
456
- subjectType: strFlag(flags, 'subject-type'),
457
- activeSince: strFlag(flags, 'active-since'),
458
- minVerifiedEvents: intFlag(flags, 'min-verified'),
700
+ ...bookFilterFlags(flags),
459
701
  sort: strFlag(flags, 'sort'),
460
702
  order: strFlag(flags, 'order'),
461
703
  ...pageQuery(flags),
462
704
  };
463
- if (flags.verified === true)
464
- query.hasVerifiedEvents = true;
465
705
  show(ctx, await ctx.client.listUsers(requireKey(ctx), query));
466
706
  return 0;
467
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
+ }
468
723
  case 'usage': {
469
724
  const { positional, flags } = parseFlags(args, { valued: ['from', 'to', 'csv'] });
470
725
  const days = positional[0] ? Number(positional[0]) : undefined;
471
726
  if (days !== undefined && (!Number.isInteger(days) || days < 1)) {
472
- throw new Error('usage [days] days must be a positive integer');
727
+ throw new Error('usage [days]: days must be a positive integer');
473
728
  }
474
729
  const from = strFlag(flags, 'from');
475
730
  const to = strFlag(flags, 'to');
476
731
  if (days !== undefined && (from || to)) {
477
- throw new Error('usage use either a [days] window or --from/--to, not both');
732
+ throw new Error('usage: use either a [days] window or --from/--to, not both');
478
733
  }
479
734
  const key = requireKey(ctx);
480
735
  const outfile = strFlag(flags, 'csv');
@@ -500,7 +755,7 @@ export async function runCli(argv, ctx) {
500
755
  valued: ['action', 'from', 'to', 'cursor', 'limit'],
501
756
  });
502
757
  if (positional.length > 0) {
503
- throw new Error(`activity takes no positional arguments (got "${positional[0]}") see "credda help"`);
758
+ throw new Error(`activity takes no positional arguments (got "${positional[0]}"). See "credda help"`);
504
759
  }
505
760
  show(ctx, await ctx.client.getActivity(requireKey(ctx), {
506
761
  ...pageQuery(flags),
@@ -552,8 +807,44 @@ export async function runCli(argv, ctx) {
552
807
  show(ctx, await ctx.client.getPublicProfessionalRecord(requireArg(rest, 'token')));
553
808
  return 0;
554
809
  default:
555
- throw new Error(`unknown professional-record subcommand "${sub ?? ''}" see "credda help"`);
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)));
556
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;
557
848
  }
558
849
  case 'confirmations': {
559
850
  // ⚠️ Auth is ASYMMETRIC here: preview/respond are the COUNTERPARTY's
@@ -591,10 +882,26 @@ export async function runCli(argv, ctx) {
591
882
  ...(strFlag(flags, 'return-url') ? { returnUrl: strFlag(flags, 'return-url') } : {}),
592
883
  ...(expiresInDays !== undefined ? { expiresInDays } : {}),
593
884
  }, requireKey(ctx), { idempotencyKey: strFlag(flags, 'idempotency-key') });
594
- ctx.err('NOTE: the confirmationToken below is shown ONCE deliver it to the counterparty over your own channel.');
885
+ ctx.err('NOTE: the confirmationToken below is shown ONCE; deliver it to the counterparty over your own channel.');
595
886
  show(ctx, created);
596
887
  return 0;
597
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
+ }
598
905
  case 'list': {
599
906
  const { flags } = parseFlags(rest, { valued: ['status', 'cursor', 'limit'] });
600
907
  const status = strFlag(flags, 'status');
@@ -641,7 +948,94 @@ export async function runCli(argv, ctx) {
641
948
  return 0;
642
949
  }
643
950
  default:
644
- throw new Error(`unknown confirmations subcommand "${sub ?? ''}" see "credda help"`);
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"`);
645
1039
  }
646
1040
  }
647
1041
  case 'policies': {
@@ -698,7 +1092,7 @@ export async function runCli(argv, ctx) {
698
1092
  });
699
1093
  const id = requireArg(positional, 'id');
700
1094
  if (flags.activate === true && flags.deactivate === true) {
701
- throw new Error('policies update pass at most one of --activate / --deactivate');
1095
+ throw new Error('policies update: pass at most one of --activate / --deactivate');
702
1096
  }
703
1097
  const patch = {};
704
1098
  const name = strFlag(flags, 'name');
@@ -721,7 +1115,7 @@ export async function runCli(argv, ctx) {
721
1115
  if (flags.deactivate === true)
722
1116
  patch.isActive = false;
723
1117
  if (Object.keys(patch).length === 0) {
724
- throw new Error('policies update needs at least one field to change see "credda help"');
1118
+ throw new Error('policies update needs at least one field to change. See "credda help"');
725
1119
  }
726
1120
  show(ctx, await ctx.client.updatePolicy(id, patch, key));
727
1121
  return 0;
@@ -731,7 +1125,7 @@ export async function runCli(argv, ctx) {
731
1125
  show(ctx, { deleted: true });
732
1126
  return 0;
733
1127
  default:
734
- throw new Error(`unknown policies subcommand "${sub ?? ''}" see "credda help"`);
1128
+ throw new Error(`unknown policies subcommand "${sub ?? ''}". See "credda help"`);
735
1129
  }
736
1130
  }
737
1131
  case 'monitors': {
@@ -774,7 +1168,7 @@ export async function runCli(argv, ctx) {
774
1168
  show(ctx, { deleted: true });
775
1169
  return 0;
776
1170
  default:
777
- throw new Error(`unknown monitors subcommand "${sub ?? ''}" see "credda help"`);
1171
+ throw new Error(`unknown monitors subcommand "${sub ?? ''}". See "credda help"`);
778
1172
  }
779
1173
  }
780
1174
  case 'screen': {
@@ -784,11 +1178,11 @@ export async function runCli(argv, ctx) {
784
1178
  });
785
1179
  const file = strFlag(flags, 'file');
786
1180
  if (file && positional.length > 0) {
787
- throw new Error('screen pass ids inline OR --file <path>, not both');
1181
+ throw new Error('screen: pass ids inline OR --file <path>, not both');
788
1182
  }
789
1183
  const ids = parseIdList(file ? { fileText: await ctx.readInput(file) } : { inline: positional });
790
1184
  if (ids.length === 0) {
791
- 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)');
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)');
792
1186
  }
793
1187
  const key = requireKey(ctx);
794
1188
  let job = (await ctx.client.createScreening(ids, key)).screening;
@@ -796,7 +1190,7 @@ export async function runCli(argv, ctx) {
796
1190
  const sleep = ctx.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
797
1191
  const interval = ctx.pollIntervalMs ?? 2000;
798
1192
  while (job.status === 'QUEUED' || job.status === 'RUNNING') {
799
- ctx.err(`screening ${job.id}: ${job.status} waiting…`);
1193
+ ctx.err(`screening ${job.id}: ${job.status}, waiting…`);
800
1194
  await sleep(interval);
801
1195
  job = (await ctx.client.getScreening(job.id, key)).screening;
802
1196
  }
@@ -805,7 +1199,7 @@ export async function runCli(argv, ctx) {
805
1199
  }
806
1200
  show(ctx, { screening: job });
807
1201
  if (job.status === 'QUEUED' || job.status === 'RUNNING') {
808
- ctx.err(`still ${job.status} poll with "credda screenings get ${job.id}" (or re-run with --wait)`);
1202
+ ctx.err(`still ${job.status}; poll with "credda screenings get ${job.id}" (or re-run with --wait)`);
809
1203
  }
810
1204
  return 0;
811
1205
  }
@@ -835,7 +1229,7 @@ export async function runCli(argv, ctx) {
835
1229
  return 0;
836
1230
  }
837
1231
  default:
838
- throw new Error(`unknown screenings subcommand "${sub ?? ''}" see "credda help"`);
1232
+ throw new Error(`unknown screenings subcommand "${sub ?? ''}". See "credda help"`);
839
1233
  }
840
1234
  }
841
1235
  case 'webhooks': {
@@ -849,10 +1243,10 @@ export async function runCli(argv, ctx) {
849
1243
  const url = requireArg(rest, 'url');
850
1244
  const events = rest.slice(1);
851
1245
  if (events.length === 0) {
852
- throw new Error('webhooks create <url> <event...> provide at least one event (score.updated, score.band_changed, dispute.resolved, monitor.triggered, usage.quota_warning)');
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)');
853
1247
  }
854
1248
  const created = await ctx.client.createWebhook({ url, events: events }, key);
855
- ctx.err('NOTE: the signing secret below is shown ONCE store it now.');
1249
+ ctx.err('NOTE: the signing secret below is shown ONCE; store it now.');
856
1250
  show(ctx, created);
857
1251
  return 0;
858
1252
  }
@@ -874,7 +1268,7 @@ export async function runCli(argv, ctx) {
874
1268
  return 0;
875
1269
  }
876
1270
  default:
877
- throw new Error(`unknown webhooks subcommand "${sub ?? ''}" see "credda help"`);
1271
+ throw new Error(`unknown webhooks subcommand "${sub ?? ''}". See "credda help"`);
878
1272
  }
879
1273
  }
880
1274
  case 'listen': {
@@ -882,7 +1276,7 @@ export async function runCli(argv, ctx) {
882
1276
  throw new Error('listen is not available in this environment');
883
1277
  const port = args[0] ? Number(args[0]) : 4141;
884
1278
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
885
- throw new Error('listen [port] port must be 1-65535');
1279
+ throw new Error('listen [port]: port must be 1-65535');
886
1280
  }
887
1281
  await ctx.startListener({ port, secret: ctx.webhookSecret });
888
1282
  return 0;
@@ -895,7 +1289,7 @@ export async function runCli(argv, ctx) {
895
1289
  show(ctx, { revoked: true });
896
1290
  return 0;
897
1291
  default:
898
- throw new Error(`unknown command "${command}" see "credda help"`);
1292
+ throw new Error(`unknown command "${command}". See "credda help"`);
899
1293
  }
900
1294
  }
901
1295
  catch (e) {
@@ -907,4 +1301,3 @@ export async function runCli(argv, ctx) {
907
1301
  return 1;
908
1302
  }
909
1303
  }
910
- //# sourceMappingURL=cli.js.map