@credda/cli 0.1.6 → 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/CHANGELOG.md +71 -0
- package/README.md +219 -279
- package/dist/args.d.ts +99 -0
- package/dist/args.js +222 -0
- package/dist/commands.d.ts +153 -0
- package/dist/commands.js +1192 -0
- package/dist/index.d.ts +41 -4
- package/dist/index.js +41 -74
- package/package.json +18 -17
- package/dist/cli.d.ts +0 -108
- package/dist/cli.js +0 -1303
- package/dist/listener.d.ts +0 -17
- package/dist/listener.js +0 -73
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.6';
|
|
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 ? 'none' : String(s.finalScore);
|
|
490
|
-
ctx.out(`${pad(s.userId, idWidth)} ${pad(score, 5)} ${s.scoreBand ?? 'none'}`);
|
|
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 ?? '(none)'}`);
|
|
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
|
-
}
|