@credda/cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +290 -220
- package/dist/cli.d.ts +13 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +580 -186
- package/dist/cli.js.map +1 -1
- package/dist/listener.js +4 -4
- package/dist/listener.js.map +1 -1
- package/package.json +15 -4
- package/dist/cli.test.d.ts +0 -2
- package/dist/cli.test.d.ts.map +0 -1
- package/dist/cli.test.js +0 -784
- package/dist/cli.test.js.map +0 -1
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.
|
|
11
|
-
export const HELP = `credda
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
credda
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
credda
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
credda
|
|
31
|
-
credda
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
credda trust
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
credda
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
credda
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
credda
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
credda
|
|
58
|
-
|
|
59
|
-
(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
credda
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
credda
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
The
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
credda
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
credda
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
credda
|
|
140
|
-
credda
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
credda
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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}
|
|
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
|
|
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}
|
|
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 ?? ''}"
|
|
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]}")
|
|
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]}")
|
|
697
|
+
throw new Error(`users takes no positional arguments (got "${positional[0]}"). See "credda help"`);
|
|
451
698
|
}
|
|
452
699
|
const query = {
|
|
453
|
-
|
|
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]
|
|
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
|
|
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]}")
|
|
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 ?? ''}"
|
|
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
|
|
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 ?? ''}"
|
|
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
|
|
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
|
|
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 ?? ''}"
|
|
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 ?? ''}"
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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 ?? ''}"
|
|
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
|
|
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
|
|
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 ?? ''}"
|
|
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]
|
|
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}"
|
|
1292
|
+
throw new Error(`unknown command "${command}". See "credda help"`);
|
|
899
1293
|
}
|
|
900
1294
|
}
|
|
901
1295
|
catch (e) {
|