@certen.io/cli 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,519 +1,553 @@
1
- # Changelog — @certen.io/cli
2
-
3
- ## 0.7.1sign a pending transaction by hash
4
-
5
- ### Added`certen pending sign <target>` accepts what people actually have
6
-
7
- A pending transaction was signable only by its inbox UUID. But the id a person is holding usually
8
- comes from the explorer, from `queryTx`, or from a `/v1/sign` response and that is a transaction
9
- hash or a TxID, not an inbox id. The gap forced a lookup to translate one into the other, and got
10
- an opaque gateway 400 when they guessed.
11
-
12
- All four forms now resolve, with no `--type` flag to get wrong:
13
-
14
- ```
15
- certen pending sign 4f3c…-…-… # inbox id (UUID) -> pending_action
16
- certen pending sign 882d1793…b1ad811 # 64-hex hash -> pending_tx
17
- certen pending sign 0x882d1793…b1ad811 # the same, 0x-prefixed
18
- certen pending sign acc://882d…@panel.acme # TxID, as pasted
19
- ```
20
-
21
- The forms are disjoint, so the type is inferred rather than declared. Resolution lives in
22
- `@certen.io/sdk` (`resolveSignTarget`) and is shared with the MCP server two copies would drift,
23
- and a drifted resolver signs the wrong preimage: a valid signature attached to nothing.
24
-
25
- An unrecognised target is refused with `INVALID_SIGN_TARGET` and never guessed at.
26
-
27
- ## 0.7.0one response shape
28
-
29
- **Breaking for `--json` consumers.** Requires a gateway from 2026-08 or later.
30
- ### Added `certen fund` closes the money path
31
-
32
- Four changes to the last step of onboarding, which is the only one where a mistake costs money
33
- rather than time.
34
-
35
- **A link a wallet can open.** `--uri` emits an EIP-681 request carrying the token contract, the
36
- numeric chain, the recipient and the amount in the token's smallest unit — so the transfer stops
37
- being four values transcribed by hand. **A mistyped recipient is the one error in this product that
38
- loses real money irreversibly.** The amount is converted with string and BigInt arithmetic, never
39
- floats: `25.10 * 1e6` is `25099999.999999996` in IEEE-754, which truncates to one unit short of the
40
- amount attribution matches on — a deposit that arrives and is never credited, with nothing on either
41
- side saying why. It is also in the machine payload unconditionally, so a script building a deep link
42
- never repeats that arithmetic.
43
-
44
- **Register the payer when you know which wallet you are sending from.** `--payer 0x…` registers it
45
- inline, so future deposits credit on sight with no exact-amount match to beat. `init --payer` existed
46
- but runs before anyone has chosen a wallet. A payer failure can never break the payment: the deposit
47
- target is valid regardless, and reporting a payer problem as a payment problem would send someone
48
- hunting for a transfer that was never made.
49
-
50
- **How long it will take.** `Credited after 3 confirmation(s)` became `— about 6 seconds on
51
- base-sepolia`, computed from block time and labelled an estimate. A confirmation count alone gives no
52
- way to tell a slow chain from a broken command, which is when people interrupt and send twice.
53
-
54
- **One payload per reader.** `fund` printed the raw table *and* the readable instructions, the same
55
- defect fixed in `balance`. It mattered more here once the table carried the payment URI: a person saw
56
- the long link whether or not they asked, which buried the deposit address.
57
-
58
- ### Changed — `certen quote` says how long the price is good for
59
-
60
- `--id` reported a status and an expiry timestamp, leaving the reader to subtract against a clock to
61
- answer the only question they had. Now `Valid for another 4m 12s`, with `seconds_remaining` on the
62
- SDK response so a caller can branch without touching a date.
63
-
64
- ### Changed — `certen balance` answers the question instead of listing the figures
65
-
66
- Three things, all on the one command someone runs to find out whether they can keep working.
67
-
68
- **It printed everything twice.** The raw key/value table rendered alongside the readable summary, so
69
- every figure appeared once as `available_usd -72.355716` and again as `Available -$72.35` — with
70
- `credit`, a nested object, showing as a line of raw JSON in between. The useful rendering came
71
- second, under eleven lines of noise. Machine consumers now get the payload and a person gets the
72
- summary, which is what every other command in this group already did.
73
-
74
- **A negative balance is a drawdown, not a fault.** `Available -$72.35` was the first line a credit
75
- account saw, with nothing to say that drawing on a credit line is how the account is meant to work.
76
- It now reads `Drawn on credit $72.36`.
77
-
78
- **The thresholds are stated as a distance.** `Warning at $125.00 drawn · service stops at $250.00`
79
- published the limits without the number they are measured against, so the reader had to find
80
- `available_usd`, negate it, and compare by hand to answer whether their service is about to stop.
81
- It now reads `Drawn $72.36 of $250.00 (first warning at $125.00)`, and past the warning threshold it
82
- leads with the headroom and the command to fix it.
83
-
84
- ### Fixed money rendered two different ways
85
-
86
- There were two copies of the currency formatter. `billing.ts` handled a leading minus and
87
- `whoami.ts` did not, so a drawn-down account rendered `-$72.35` in one command and the malformed
88
- `$-72.35` in the other. Both are replaced by one exported helper, which also rounds the cents
89
- half-up rather than truncating the old one displayed `-72.355716` as `-$72.35`, a cent kinder to
90
- the account than the truth.
91
-
92
- ### Added — `certen signup --with-key`
93
-
94
- Onboarding with nobody in the loop at all.
95
-
96
- ```
97
- certen keys create dev # a keypair, never leaves this machine
98
- certen signup --with-key dev # an organization, in one step
99
- certen init --key dev # identity, chains, verified
100
- ```
101
-
102
- No browser, no email, no waiting for anyone to approve anything. The CLI signs a server-issued nonce
103
- with a key it already has; CERTEN sees a public key and a signature and never the private half —
104
- which is what non-custodial should mean at signup, not only afterwards.
105
-
106
- ### Added `certen orgs` and `certen signup --token`
107
-
108
- Onboarding an organization without a browser.
109
-
110
- ```
111
- certen orgs invite --name "Acme" --expires 7d # a human, once, in advance
112
- certen signup --token crt_... # the machine, later, alone
113
- ```
114
-
115
- `certen signup` previously printed a code and waited indefinitely for somebody to open a
116
- browser and approve it. That is right for a person at a terminal and a wall for CI, for a platform
117
- provisioning its customers, and for an agent starting up. `--token` is the same command with the
118
- human moved earlier in time.
119
-
120
- `certen orgs list` shows what each token became, which is the link between a decision someone made
121
- and an organization now on their bill. `certen orgs revoke` stops an unredeemed one; a token that
122
- has already been redeemed cannot be revoked, and says so rather than implying the organization can
123
- be un-created.
124
-
125
- ### Added — `certen identity mnemonic`
126
-
127
- Collect the mnemonic from a `signing_mode: "provider"` identity. There was no way to do this from
128
- the CLI at all, and the retrieval token is consumed on first read and expires in about ten minutes —
129
- so the seed was lost by default unless someone hand-rolled the request.
130
-
131
- **Writes to a file, mode 0600, rather than printing.** Stdout is scrollback, CI logs, and whatever
132
- is recording the session; a seed phrase outlives its usefulness there by years. `--print` is there
133
- for anyone who genuinely wants it on screen, and `--json` gives the value to a script that asked.
134
-
135
- Takes the `mnemonic_retrieval.url` straight from the create response, or an id and token separately.
136
- An unparseable target fails before any request is issued, because a half-parsed target would spend a
137
- one-shot token for nothing.
138
-
139
- ### Added — `certen oauth-clients`
140
-
141
- `list`, `create`, `rotate-secret`, `remove`. A deployment could authenticate with client credentials
142
- and could not create the client those credentials belong to, which is where "automated" stopped
143
- being true.
144
-
145
- `rotate-secret --grace <seconds>` is the one worth knowing: the previous secret keeps working while
146
- the fleet picks up the new one, so changing a credential is not an outage. `remove` is the opposite
147
- and says so — it revokes every live token immediately.
148
-
149
- ### Added`certen errors`
150
-
151
- `certen errors` lists every code the gateway you are talking to can return; `certen errors <CODE>`
152
- explains one. Needs no API key. This is the command to run when a code shows up in a log and the
153
- question is whether to retry, pay, or wake someone — `retryable` answers "can this exact request
154
- ever work", and a `platform` audience says plainly that there is nothing on your side to change.
155
-
156
- ### Added `certen quote --id`
157
-
158
- Read back a quote you already hold instead of guessing whether it is still good. Same command as
159
- issuing one, because it is the same question; it answers "still usable" outright rather than leaving
160
- a status string and a timestamp to compare by hand.
161
-
162
- ### Added `certen auth revoke-token`
163
-
164
- Revoke a leaked OAuth2 token from the terminal. **Works with no API key configured** — the gateway
165
- authenticates the request with the token itself, and requiring the credential you are trying to
166
- contain would be backwards.
167
-
168
- Reads from stdin or a prompt when no argument is given, so a live token does not land in shell
169
- history or a process listing. `--refresh` marks it as a refresh token, whose revocation also kills
170
- every access token descended from it.
171
-
172
- ### Fixed — the CLI test suite no longer flakes (properly, this time)
173
-
174
- Every test in that package spawns the CLI as a real subprocess, against vitest's 5s default — a
175
- budget sized for in-process tests. The slowest case measured **4353ms**, about 13% headroom, so
176
- ordinary scheduling load tipped a passing test over; `doctor.test.ts` failed in a full run and
177
- passed alone. Raised to 20s, roughly 4.5x the measured worst case.
178
-
179
- **That fix was inert for a whole phase.** It was written into
180
- `packages/cli/vitest.config.ts`, and a per-package config only applies when vitest is invoked with
181
- that package as its root while `scripts/test-all.mjs` runs a single process from the repository
182
- root. Its own comment claimed "the root config already globs them"; no root config existed. So every
183
- run kept using 5s, and the suite flaked twice in one session, both times at exactly 5000ms.
184
-
185
- The timeout now lives in a root `vitest.config.ts`, which sets **only** the timeouts
186
- declaring `include` there would change discovery, and a glob subtly narrower than the default drops
187
- test files with no failure anywhere. `packages/sdk/test/test-config.test.ts` asserts the root config
188
- exists, that its timeout is a real number rather than a present key, that discovery is left alone,
189
- and that all three packages are still found.
190
-
191
- ### Added — `certen webhooks`
192
-
193
- `list`, `add`, `remove`, `verify`, `rotate-secret`, `deliveries`, `redeliver`.
194
-
195
- `certen webhooks deliveries` is the command that earns the group: it shows the status, HTTP code and
196
- error for each attempt, and prints the exact `redeliver` command for anything that failed. Without
197
- it a dropped delivery was indistinguishable from an event that never fired. `--failed` narrows to
198
- just the ones that did not arrive.
199
-
200
- Registering prints the signing secret with a plain warning that it is shown once — the only
201
- recovery is rotating, which invalidates whatever the previous secret was already signing.
202
-
203
- ### Changed `certen receipts get <id>` replaces `certen receipt <id>`
204
-
205
- Two top-level commands differing by one character, sitting adjacent in help. Now one group, matching
206
- `tx` and `identity`: `certen receipts` lists, `certen receipts get <id>` fetches one. `certen verify`
207
- stays top-level — it is the command that answers "can I prove this charge", and burying it would
208
- hide it.
209
-
210
- Both commands were added in this same release and neither has shipped, so nothing external breaks.
211
-
212
- ### Changed — commands fetch only the identity data they read
213
-
214
- `certen call`, `certen init` and `certen identity retire` all fetched an identity with every
215
- enrichment: on-chain governance, per-chain balances, and pending counts. `call` reads the balances
216
- (for the unfunded-account guard) and never touches governance or pending; the other two need none of
217
- it. Each unused enrichment is a live query governance is a network round trip, balances runs once
218
- per linked chain — and they sat on the critical path of the flagship command.
219
-
220
- ### Added `scripts/measure-onboarding.mjs`
221
-
222
- Records round trips, wall-clock and endpoints touched for each step of the first-run journey, by
223
- running the real CLI through a counting proxy. Onboarding had never been measured end to end, so
224
- there was no way to tell whether any of this work reduced friction or moved it.
225
-
226
- First run: 12 requests, 10.7s. After bounding the gateway health probe: **12 requests, 4.9s** — both
227
- measured against a local gateway with every downstream absent, so they are a floor for the read path
228
- and nothing more.
229
-
230
- Against production (`https://gateway.kompendium.co`, 2026-08-16): **14 requests, 5.5s**. Two of the
231
- eight steps exit non-zero there `certen pricing` and `certen scopes` not because of anything in
232
- the CLI, but because `GET /v1/pricing` and `GET /v1/scopes` are not deployed yet. The measurement
233
- found the same gap `npm run check:gateway` reports, from the other direction.
234
-
235
- ### Changed `certen whoami` reports your organization and scopes instead of guessing
236
-
237
- It printed `organization: "not exposed to API keys see the portal"`, and reported permissions as
238
- `scopes_observed` — a guess assembled from which probe calls happened to return 200 rather than 403,
239
- which can only ever describe the scopes it thought to test for.
240
-
241
- It now reads `GET /v1/me`: the real organization name, the granted scopes, the key id and its rate
242
- limit. Still two requests, but the third call to `/v1/admin/usage` is gone — it existed only to
243
- infer whether the key held `admin:read` — so `whoami` no longer needs that scope at all.
244
-
245
- **Breaking for `--json` consumers:** `scopes_observed` (an object of booleans) is replaced by
246
- `scopes` (an array of granted scope names), and `organization` is now `{ id, name }` rather than an
247
- explanatory sentence.
248
-
249
- ### Added `certen proof open <link>`
250
-
251
- Read a proof someone shared with you. **Runs with no API key configured** the recipient of a
252
- share link has no CERTEN account, and every other share command (`share`, `shares`, `shares revoke`)
253
- serves the sender.
254
-
255
- ```
256
- certen proof open https://gateway.kompendium.co/v1/proof/shared/<token>
257
- certen proof open <link> --out proof.json
258
- ```
259
-
260
- An expired or revoked link exits non-zero saying to ask for a fresh one, rather than reporting that
261
- the proof does not exist.
262
-
263
- ### Added — `certen verify <receipt-id>`
264
-
265
- Confirm a charge instead of being told it is fine.
266
-
267
- ```
268
- PASS digest sha256(canonical_json(body)) matches the stated digest.
269
- PASS signature ed25519 signature verifies against published key bd4a7a92f29958b9.
270
- PASS inclusion This receipt is leaf 1254 of 1269.
271
- PASS root Audit path folds to the root of the independently fetched signed head at 1269.
272
- SKIP anchor No anchored tree head covers this receipt yet.
273
- ```
274
-
275
- The receipt already carried a `verification` block; it is CERTEN checking CERTEN. This recomputes
276
- everything from published data and compares the folded root against a tree head fetched separately.
277
-
278
- **It exits non-zero when it did not fully verify — including when checks were merely SKIPPED.** An
279
- incomplete run is not a pass, and exiting 0 would let a script report an unverified receipt as
280
- verified. The report survives the failure under `error.details`, matching `certen doctor`.
281
-
282
- ### Added — `certen ledger`, `certen receipts`
283
-
284
- Where the money went, and proof of what you were charged — neither was reachable from the terminal.
285
-
286
- ```
287
- certen ledger --all # every balance change, paged for you
288
- certen receipts # NUMBER, WHEN, TYPE, AMOUNT, EVIDENCE
289
- certen receipts get <id> --proof # signature, and the inclusion proof
290
- ```
291
-
292
- `EVIDENCE` shows `signed + logged`, because `logged` is what decides whether an inclusion proof
293
- exists — without it you ask for one, get a 404, and cannot tell "not yet" from "wrong id".
294
-
295
- The anchor line reads `covering_head`, not `head`: a receipt whose own tree head is unanchored is
296
- still anchored by any later root that commits to it, and reporting `head` would call a perfectly
297
- good receipt unproven for every gap between anchors. An unattested anchor time is labelled a loose
298
- upper bound rather than presented as the block time.
299
-
300
- `--all` pages for you and refuses `--offset` alongside it; a non-numeric `--limit` and
301
- `--tree-size` without `--proof` are both rejected before any network call.
302
-
303
- ### Added `certen payers` and `certen payers add`
304
-
305
- Register a wallet you send from, so deposits credit on sight instead of needing a one-time payment
306
- opened for the exact amount before every send. This is what a 402 already told you to do, at an
307
- endpoint no command could reach.
308
-
309
- ```
310
- certen payers add 0xAbC… --chain base-sepolia --label treasury
311
- certen payers # what is registered
312
- ```
313
-
314
- A malformed address is rejected before the network call, an empty list says so plainly rather than
315
- looking like success, and a 409 (the address belongs to another organization on that chain) exits
316
- non-zeroa funding script must not read it as "attribution is set up".
317
-
318
- ### Changed `certen balance` makes one request instead of two
319
-
320
- It fetched the balance and `/v1/billing/obligations` concurrently, because the balance alone could
321
- not say how much was actually left to commit. The gateway now reports that with the balance, so the
322
- second request is gone. Against an older gateway the command still falls back to it — printing
323
- `spendable_usd` in the "Left to commit" slot would report committed money as available.
324
-
325
- ### Added — `certen pricing`, and `--sku` on `certen quote`
326
-
327
- There was no way to ask the CLI what anything costs. `certen quote` prices one operation and takes
328
- its sku from a vocabulary nothing published — and it had no `--sku` flag at all, so it could only
329
- ever price the default. Asking "what does CERTEN cost" meant reading the gateway's refusals.
330
-
331
- ```
332
- $ certen pricing --chain base-sepolia
333
-
334
- SKU CHAIN PRICE
335
- identity.provision * $5.00
336
- proof.execute base-sepolia $0.35 + gas
337
- ```
338
-
339
- `+ gas` marks prices that are completed at execution, so a floor is not read as a total. `*` is the
340
- entry that applies to any chain without one of its own, and is kept when filtering by chain
341
- dropping it would report identity provisioning as unpriced on a chain where it costs $5.
342
-
343
- `certen quote --sku <sku>` now prices any of them, and exits non-zero when pricing is not
344
- configured rather than reporting an empty catalogue.
345
-
346
- ### Changed — `certen call` no longer reads the same balances twice
347
-
348
- It fetched the identity (it needs `can_sign` before prompting for a passphrase), then fetched
349
- `/v1/portfolio` for balances the identity response had already returned a round trip on the
350
- critical path of the main flow, for numbers it was holding.
351
-
352
- The guard now takes those balances when the caller has them, and still reads the portfolio when the
353
- gateway sends none, so an older gateway does not silently lose the guard.
354
-
355
- ### Added `certen tx list --all`
356
-
357
- Fetches every page instead of the first. Answering "how many intents failed this month" previously
358
- meant a shell loop incrementing `--offset` and knowing when to stop; getting that wrong reads as
359
- "there were none".
360
-
361
- With `--all`, `--limit` is the page size rather than a cap, and `--offset` is rejected `--all`
362
- starts from the beginning, so the two together have no coherent meaning.
363
-
364
- ### Breaking — `certen identity` JSON output is no longer wrapped
365
-
366
- `certen --json identity get <id>` and `certen --json identity create` returned the identity nested
367
- under an `identity` key. It is now at the top level, matching every other command and the API.
368
-
369
- ```bash
370
- # before
371
- certen --json identity get "$ID" | jq -r '.data.identity.can_sign'
372
- # after
373
- certen --json identity get "$ID" | jq -r '.data.can_sign'
374
- ```
375
-
376
- The envelope itself (`{ ok, data }` / `{ ok, error }`) and the exit codes are unchanged, as is table
377
- output, which was never a contract. See `docs/CLI-CONTRACT.md`.
378
-
379
- ### Fixed — `certen tx status --json` reports absent fields as `null`
380
-
381
- `proof_id`, `proof_bundle_url`, `accum_tx_hash` and `error_message` came back as `""` when the
382
- gateway had no value, so `.proof_id != null` was true for a transaction with no proof. They are now
383
- `null`. A script testing `if .proof_id then` is unaffected; one testing `!= null` was wrong before
384
- and is right now.
385
-
386
- ### Fixed `can_sign` distinguishes "cannot sign" from "could not check"
387
-
388
- An unreadable key page reported `can_sign: false`. It now reports `null`, and the table prints
389
- `unknown`. The two have different fixes: one is repairable with `certen identity update
390
- --public-key`, the other is a retry.
391
-
392
- ## 0.6.0 from eighteen steps to four
393
-
394
- ### Added — `certen login` / `certen signup`
395
-
396
- The device authorization grant. The CLI prints a short code, you approve it in a portal session you
397
- already trust, and the key arrives over the CLI's own channel. **The secret is never displayed and
398
- never passes through a clipboard or shell history.** Requires a gateway that serves
399
- `/v1/portal/device`; against an older one it says so and points at the portal.
400
-
401
- ### Added `certen init`, `call`, `proof`, `chains`, `whoami`, `doctor`, `identity retire`
402
-
403
- `init` creates only what is missing, waits until the identity can actually sign, and records the id
404
- so a later run reuses it rather than burning org quota. `call` is a proof-gated contract call in one
405
- command it derives the ADI URL, the abstract account and the numeric chain id from the identity,
406
- and type-checks `--arg` against the Solidity signature before anything is sent. `proof` retrieves,
407
- bundles, shares and verifies. `doctor` names the one thing blocking you and the command that fixes
408
- it.
409
-
410
- ### Behavioural change usage errors now exit 2
411
-
412
- A wrong invocation used to exit 1, indistinguishable from a rejected request. Several commands threw
413
- untyped errors; they now exit 2 as the contract always specified. Scripts branching on any non-zero
414
- exit are unaffected; scripts that treated 1 as "the gateway said no" should re-check.
415
-
416
- ### Behavioural change `auth login` verifies the key before saving it
417
-
418
- A typo'd or revoked key used to be written and then surface as an opaque 401 at whatever command ran
419
- next. It is now checked first and **not saved if rejected**. A 403 means the key is real but
420
- unscoped and is accepted with a note. `--api-key -` reads from stdin; omitting it prompts.
421
-
422
- ### Behavioural change human mode waits by default
423
-
424
- `identity create` and `tx create` poll to a usable state. `--json` keeps the old fire-and-forget
425
- default so existing scripts do not silently start blocking.
426
-
427
- ### Added — the unfunded-account guard, and `error.details`
428
-
429
- A value transfer from an empty abstract account is refused before submitting, naming the faucet;
430
- `--force` overrides. And a failure that still produced a result carries it under `error.details` —
431
- `certen --json doctor` returns every check that way, so signalling the failure never costs you the
432
- diagnosis. See docs/CLI-CONTRACT.md.
433
-
434
- ## 0.5.0 — money commands, and a refusal that tells you how to fix it
435
-
436
- ### Added — `certen balance` and `certen fund`
437
-
438
- `balance` prints available, held, credit line, spendable, and **left to commit** spendable minus
439
- what pending intents have already claimed. Showing only the balance would tell you that you can
440
- afford work that is already spoken for.
441
-
442
- `fund <amount> --chain <chain>` prints where to send stablecoin and waits until it is credited.
443
- It never touches a wallet or a key: signing and sending stay with you. `--no-wait` prints the
444
- details and exits; `--poll-interval` and `--timeout` control the wait. An uncredited or expired
445
- payment exits non-zero, because a funding script must not read one as paid.
446
-
447
- Every option is validated before the network call a typo in `--timeout` used to open a real
448
- payment intent first.
449
-
450
- ### Changeda 402 now prints the way out
451
-
452
- A refusal for lack of funds shows the shortfall, the address, the exact amount, the reference, both
453
- `certen fund …` and the portal link, and the quote id to retry with then states plainly that
454
- nothing was charged and no work was started. All on stderr; stdout is untouched.
455
-
456
- In `--json`, the failure envelope gains `shortfall_usd`, `quote_id` and `resolve` on payment
457
- failures only. They are absent on every other error rather than present as nulls. See
458
- docs/CLI-CONTRACT.md.
459
-
460
- ### Fixed — the error reporter no longer flattens SDK errors
461
-
462
- `handleError` copied a `CertenError` into an object literal before reporting it. The values
463
- survived; the class identity did not so a payment refusal could not be recognised and its payment
464
- target was silently dropped. The fields it copied are all readable on the instance, `isRetryable`
465
- getter included, so nothing was gained by the copy.
466
-
467
- ## 0.4.0 — `--json` is a machine contract
468
-
469
- Adds a stable, tested output contract for scripts and AI agents. **Human output is unchanged**: if
470
- you do not pass `--json`, this release behaves exactly as 0.3.1 did, with one exception noted under
471
- Breaking.
472
-
473
- Before this, every failure exited `1` and explained itself in English on stderr. An automated caller
474
- could not distinguish "you passed a malformed address" from "the gateway is down" without parsing
475
- prose — and those want opposite responses. One is a bug to fix; the other is worth retrying. For a
476
- CLI that authorizes cross-chain execution against real funds, guessing wrong is expensive in a way
477
- that is not recoverable.
478
-
479
- The full specification is [docs/CLI-CONTRACT.md](../../docs/CLI-CONTRACT.md), enforced by
480
- `test/conformance.test.ts`, which runs the built binary as a subprocess and checks the real process's
481
- stdout and exit code.
482
-
483
- ### Added
484
-
485
- - **Global `--json`**, accepted anywhere in the argument list — `certen --json tx status X` and
486
- `certen tx status X --json` are identical. It is resolved before argument parsing, so it applies
487
- even to failures that occur while resolving credentials.
488
- - **One JSON envelope on stdout and nothing else.** `{"ok":true,"data":…}` or
489
- `{"ok":false,"error":{"code","message","retryable","status?","requestId?"}}`. A command producing
490
- several payloads emits an array in `data`; one producing none emits `"data":null`. stdout is never
491
- empty and never carries two concatenated objects. All human-facing text moves to stderr.
492
- - **Meaningful exit codes:** `0` ok · `1` operation failed · `2` usage error · `3` gateway
493
- unreachable. `3` guarantees nothing was submitted, so a retry cannot double-execute.
494
- - **`error.retryable`**, taken from the SDK's own `CertenError.isRetryable`, so the CLI and the SDK
495
- hand an automated caller the identical retry decision.
496
- - **`certen --help --json`** returns the entire command tree every command, argument, flag and exit
497
- codein one call, instead of scraping help text once per subcommand.
498
-
499
- ### Fixed
500
-
501
- - **Usage errors on subcommands bypassed error handling entirely.** `exitOverride()` is not inherited
502
- by commander subcommands, so a missing required flag (`certen identity create` with no `--name`)
503
- called `process.exit(1)` inside commander: no envelope was emitted, stdout stayed empty, and a
504
- usage error was indistinguishable from a failed request. It is now applied to every command in the
505
- tree.
506
-
507
- ### Breaking
508
-
509
- - **"No API key configured" now exits `2` instead of `1`.** It is a usage error: nothing was sent,
510
- and retrying cannot help. The same applies to a config file with unsafe permissions and to a
511
- missing keyring backend. Scripts treating any non-zero exit as failure are unaffected; scripts
512
- testing specifically for `-eq 1` need updating.
513
-
514
- ### Note
515
-
516
- `output: "json"` in `~/.certen/config.json` is **not** this contract. That setting predates the
517
- envelope and makes commands print their raw payload — no `ok`, no `error`, no exit-code guarantees.
518
- It is kept for backward compatibility. Automated callers should pass `--json` explicitly rather than
519
- depend on a machine's local config, which they cannot see.
1
+ # Changelog — @certen.io/cli
2
+
3
+ ## 0.7.2the CLI runs when you install it
4
+
5
+ ### Fixedthe binary did nothing at all on Linux and macOS
6
+
7
+ `npx @certen.io/cli signup ...` printed nothing, sent nothing, and exited **0**. So did every other
8
+ command. Not a failure anyone could act on: no error, no output, no request reaching the gateway,
9
+ and therefore nothing in the gateway's logs to find either. From outside it looked like a CLI that
10
+ installed correctly and had simply decided not to speak.
11
+
12
+ The last statement in the entrypoint decides whether the file was run as a program or imported by
13
+ a test:
14
+
15
+ ```js
16
+ import.meta.url === pathToFileURL(process.argv[1]).href // 0.7.1
17
+ ```
18
+
19
+ Node resolves symlinks before it records `import.meta.url`, and does not for `process.argv[1]` —
20
+ that stays the path as typed. Every POSIX install exposes this package as a **symlink** at
21
+ `node_modules/.bin/certen` pointing into `dist/`, and `npx` is no exception. So the two strings
22
+ were never equal, the guard was false, and `run()` was never called. The whole CLI was inside an
23
+ `if` that could not be true for a real user.
24
+
25
+ Windows is why it shipped: npm writes a `.cmd` shim there rather than a symlink, and the shim
26
+ invokes node on the real path, so `argv[1]` arrived already resolved and the comparison passed.
27
+ The test suite is why it stayed: all of it conformance included — spawns `node dist/index.js`
28
+ directly, which is the one invocation path with no symlink in it, and the one no user takes.
29
+
30
+ `argv[1]` is now resolved with `realpathSync` before the comparison, so both sides are the real
31
+ path. A new test, `test/installed-bin.test.ts`, runs the built binary **through a symlink** and
32
+ asserts it produces output the case the suite could not previously express.
33
+
34
+ Nothing else changed. There is no behaviour difference for anyone on Windows, and none for anyone
35
+ who was invoking `dist/index.js` by path.
36
+
37
+ ## 0.7.1 sign a pending transaction by hash
38
+
39
+ ### Added `certen pending sign <target>` accepts what people actually have
40
+
41
+ A pending transaction was signable only by its inbox UUID. But the id a person is holding usually
42
+ comes from the explorer, from `queryTx`, or from a `/v1/sign` response — and that is a transaction
43
+ hash or a TxID, not an inbox id. The gap forced a lookup to translate one into the other, and got
44
+ an opaque gateway 400 when they guessed.
45
+
46
+ All four forms now resolve, with no `--type` flag to get wrong:
47
+
48
+ ```
49
+ certen pending sign 4f3c…-…-… # inbox id (UUID) -> pending_action
50
+ certen pending sign 882d1793…b1ad811 # 64-hex hash -> pending_tx
51
+ certen pending sign 0x882d1793…b1ad811 # the same, 0x-prefixed
52
+ certen pending sign acc://882d…@panel.acme # TxID, as pasted
53
+ ```
54
+
55
+ The forms are disjoint, so the type is inferred rather than declared. Resolution lives in
56
+ `@certen.io/sdk` (`resolveSignTarget`) and is shared with the MCP server two copies would drift,
57
+ and a drifted resolver signs the wrong preimage: a valid signature attached to nothing.
58
+
59
+ An unrecognised target is refused with `INVALID_SIGN_TARGET` and never guessed at.
60
+
61
+ ## 0.7.0 one response shape
62
+
63
+ **Breaking for `--json` consumers.** Requires a gateway from 2026-08 or later.
64
+ ### Added — `certen fund` closes the money path
65
+
66
+ Four changes to the last step of onboarding, which is the only one where a mistake costs money
67
+ rather than time.
68
+
69
+ **A link a wallet can open.** `--uri` emits an EIP-681 request carrying the token contract, the
70
+ numeric chain, the recipient and the amount in the token's smallest unit so the transfer stops
71
+ being four values transcribed by hand. **A mistyped recipient is the one error in this product that
72
+ loses real money irreversibly.** The amount is converted with string and BigInt arithmetic, never
73
+ floats: `25.10 * 1e6` is `25099999.999999996` in IEEE-754, which truncates to one unit short of the
74
+ amount attribution matches on a deposit that arrives and is never credited, with nothing on either
75
+ side saying why. It is also in the machine payload unconditionally, so a script building a deep link
76
+ never repeats that arithmetic.
77
+
78
+ **Register the payer when you know which wallet you are sending from.** `--payer 0x…` registers it
79
+ inline, so future deposits credit on sight with no exact-amount match to beat. `init --payer` existed
80
+ but runs before anyone has chosen a wallet. A payer failure can never break the payment: the deposit
81
+ target is valid regardless, and reporting a payer problem as a payment problem would send someone
82
+ hunting for a transfer that was never made.
83
+
84
+ **How long it will take.** `Credited after 3 confirmation(s)` became `— about 6 seconds on
85
+ base-sepolia`, computed from block time and labelled an estimate. A confirmation count alone gives no
86
+ way to tell a slow chain from a broken command, which is when people interrupt and send twice.
87
+
88
+ **One payload per reader.** `fund` printed the raw table *and* the readable instructions, the same
89
+ defect fixed in `balance`. It mattered more here once the table carried the payment URI: a person saw
90
+ the long link whether or not they asked, which buried the deposit address.
91
+
92
+ ### Changed — `certen quote` says how long the price is good for
93
+
94
+ `--id` reported a status and an expiry timestamp, leaving the reader to subtract against a clock to
95
+ answer the only question they had. Now `Valid for another 4m 12s`, with `seconds_remaining` on the
96
+ SDK response so a caller can branch without touching a date.
97
+
98
+ ### Changed — `certen balance` answers the question instead of listing the figures
99
+
100
+ Three things, all on the one command someone runs to find out whether they can keep working.
101
+
102
+ **It printed everything twice.** The raw key/value table rendered alongside the readable summary, so
103
+ every figure appeared once as `available_usd -72.355716` and again as `Available -$72.35` with
104
+ `credit`, a nested object, showing as a line of raw JSON in between. The useful rendering came
105
+ second, under eleven lines of noise. Machine consumers now get the payload and a person gets the
106
+ summary, which is what every other command in this group already did.
107
+
108
+ **A negative balance is a drawdown, not a fault.** `Available -$72.35` was the first line a credit
109
+ account saw, with nothing to say that drawing on a credit line is how the account is meant to work.
110
+ It now reads `Drawn on credit $72.36`.
111
+
112
+ **The thresholds are stated as a distance.** `Warning at $125.00 drawn · service stops at $250.00`
113
+ published the limits without the number they are measured against, so the reader had to find
114
+ `available_usd`, negate it, and compare by hand — to answer whether their service is about to stop.
115
+ It now reads `Drawn $72.36 of $250.00 (first warning at $125.00)`, and past the warning threshold it
116
+ leads with the headroom and the command to fix it.
117
+
118
+ ### Fixed money rendered two different ways
119
+
120
+ There were two copies of the currency formatter. `billing.ts` handled a leading minus and
121
+ `whoami.ts` did not, so a drawn-down account rendered `-$72.35` in one command and the malformed
122
+ `$-72.35` in the other. Both are replaced by one exported helper, which also rounds the cents
123
+ half-up rather than truncating — the old one displayed `-72.355716` as `-$72.35`, a cent kinder to
124
+ the account than the truth.
125
+
126
+ ### Added — `certen signup --with-key`
127
+
128
+ Onboarding with nobody in the loop at all.
129
+
130
+ ```
131
+ certen keys create dev # a keypair, never leaves this machine
132
+ certen signup --with-key dev # an organization, in one step
133
+ certen init --key dev # identity, chains, verified
134
+ ```
135
+
136
+ No browser, no email, no waiting for anyone to approve anything. The CLI signs a server-issued nonce
137
+ with a key it already has; CERTEN sees a public key and a signature and never the private half —
138
+ which is what non-custodial should mean at signup, not only afterwards.
139
+
140
+ ### Added — `certen orgs` and `certen signup --token`
141
+
142
+ Onboarding an organization without a browser.
143
+
144
+ ```
145
+ certen orgs invite --name "Acme" --expires 7d # a human, once, in advance
146
+ certen signup --token crt_... # the machine, later, alone
147
+ ```
148
+
149
+ `certen signup` previously printed a code and waited indefinitely — for somebody to open a
150
+ browser and approve it. That is right for a person at a terminal and a wall for CI, for a platform
151
+ provisioning its customers, and for an agent starting up. `--token` is the same command with the
152
+ human moved earlier in time.
153
+
154
+ `certen orgs list` shows what each token became, which is the link between a decision someone made
155
+ and an organization now on their bill. `certen orgs revoke` stops an unredeemed one; a token that
156
+ has already been redeemed cannot be revoked, and says so rather than implying the organization can
157
+ be un-created.
158
+
159
+ ### Added `certen identity mnemonic`
160
+
161
+ Collect the mnemonic from a `signing_mode: "provider"` identity. There was no way to do this from
162
+ the CLI at all, and the retrieval token is consumed on first read and expires in about ten minutes —
163
+ so the seed was lost by default unless someone hand-rolled the request.
164
+
165
+ **Writes to a file, mode 0600, rather than printing.** Stdout is scrollback, CI logs, and whatever
166
+ is recording the session; a seed phrase outlives its usefulness there by years. `--print` is there
167
+ for anyone who genuinely wants it on screen, and `--json` gives the value to a script that asked.
168
+
169
+ Takes the `mnemonic_retrieval.url` straight from the create response, or an id and token separately.
170
+ An unparseable target fails before any request is issued, because a half-parsed target would spend a
171
+ one-shot token for nothing.
172
+
173
+ ### Added — `certen oauth-clients`
174
+
175
+ `list`, `create`, `rotate-secret`, `remove`. A deployment could authenticate with client credentials
176
+ and could not create the client those credentials belong to, which is where "automated" stopped
177
+ being true.
178
+
179
+ `rotate-secret --grace <seconds>` is the one worth knowing: the previous secret keeps working while
180
+ the fleet picks up the new one, so changing a credential is not an outage. `remove` is the opposite
181
+ and says soit revokes every live token immediately.
182
+
183
+ ### Added `certen errors`
184
+
185
+ `certen errors` lists every code the gateway you are talking to can return; `certen errors <CODE>`
186
+ explains one. Needs no API key. This is the command to run when a code shows up in a log and the
187
+ question is whether to retry, pay, or wake someone — `retryable` answers "can this exact request
188
+ ever work", and a `platform` audience says plainly that there is nothing on your side to change.
189
+
190
+ ### Added — `certen quote --id`
191
+
192
+ Read back a quote you already hold instead of guessing whether it is still good. Same command as
193
+ issuing one, because it is the same question; it answers "still usable" outright rather than leaving
194
+ a status string and a timestamp to compare by hand.
195
+
196
+ ### Added `certen auth revoke-token`
197
+
198
+ Revoke a leaked OAuth2 token from the terminal. **Works with no API key configured** — the gateway
199
+ authenticates the request with the token itself, and requiring the credential you are trying to
200
+ contain would be backwards.
201
+
202
+ Reads from stdin or a prompt when no argument is given, so a live token does not land in shell
203
+ history or a process listing. `--refresh` marks it as a refresh token, whose revocation also kills
204
+ every access token descended from it.
205
+
206
+ ### Fixed the CLI test suite no longer flakes (properly, this time)
207
+
208
+ Every test in that package spawns the CLI as a real subprocess, against vitest's 5s default — a
209
+ budget sized for in-process tests. The slowest case measured **4353ms**, about 13% headroom, so
210
+ ordinary scheduling load tipped a passing test over; `doctor.test.ts` failed in a full run and
211
+ passed alone. Raised to 20s, roughly 4.5x the measured worst case.
212
+
213
+ **That fix was inert for a whole phase.** It was written into
214
+ `packages/cli/vitest.config.ts`, and a per-package config only applies when vitest is invoked with
215
+ that package as its root while `scripts/test-all.mjs` runs a single process from the repository
216
+ root. Its own comment claimed "the root config already globs them"; no root config existed. So every
217
+ run kept using 5s, and the suite flaked twice in one session, both times at exactly 5000ms.
218
+
219
+ The timeout now lives in a root `vitest.config.ts`, which sets **only** the timeouts —
220
+ declaring `include` there would change discovery, and a glob subtly narrower than the default drops
221
+ test files with no failure anywhere. `packages/sdk/test/test-config.test.ts` asserts the root config
222
+ exists, that its timeout is a real number rather than a present key, that discovery is left alone,
223
+ and that all three packages are still found.
224
+
225
+ ### Added — `certen webhooks`
226
+
227
+ `list`, `add`, `remove`, `verify`, `rotate-secret`, `deliveries`, `redeliver`.
228
+
229
+ `certen webhooks deliveries` is the command that earns the group: it shows the status, HTTP code and
230
+ error for each attempt, and prints the exact `redeliver` command for anything that failed. Without
231
+ it a dropped delivery was indistinguishable from an event that never fired. `--failed` narrows to
232
+ just the ones that did not arrive.
233
+
234
+ Registering prints the signing secret with a plain warning that it is shown once — the only
235
+ recovery is rotating, which invalidates whatever the previous secret was already signing.
236
+
237
+ ### Changed `certen receipts get <id>` replaces `certen receipt <id>`
238
+
239
+ Two top-level commands differing by one character, sitting adjacent in help. Now one group, matching
240
+ `tx` and `identity`: `certen receipts` lists, `certen receipts get <id>` fetches one. `certen verify`
241
+ stays top-level it is the command that answers "can I prove this charge", and burying it would
242
+ hide it.
243
+
244
+ Both commands were added in this same release and neither has shipped, so nothing external breaks.
245
+
246
+ ### Changed commands fetch only the identity data they read
247
+
248
+ `certen call`, `certen init` and `certen identity retire` all fetched an identity with every
249
+ enrichment: on-chain governance, per-chain balances, and pending counts. `call` reads the balances
250
+ (for the unfunded-account guard) and never touches governance or pending; the other two need none of
251
+ it. Each unused enrichment is a live query governance is a network round trip, balances runs once
252
+ per linked chain and they sat on the critical path of the flagship command.
253
+
254
+ ### Added — `scripts/measure-onboarding.mjs`
255
+
256
+ Records round trips, wall-clock and endpoints touched for each step of the first-run journey, by
257
+ running the real CLI through a counting proxy. Onboarding had never been measured end to end, so
258
+ there was no way to tell whether any of this work reduced friction or moved it.
259
+
260
+ First run: 12 requests, 10.7s. After bounding the gateway health probe: **12 requests, 4.9s** both
261
+ measured against a local gateway with every downstream absent, so they are a floor for the read path
262
+ and nothing more.
263
+
264
+ Against production (`https://gateway.kompendium.co`, 2026-08-16): **14 requests, 5.5s**. Two of the
265
+ eight steps exit non-zero there `certen pricing` and `certen scopes` — not because of anything in
266
+ the CLI, but because `GET /v1/pricing` and `GET /v1/scopes` are not deployed yet. The measurement
267
+ found the same gap `npm run check:gateway` reports, from the other direction.
268
+
269
+ ### Changed `certen whoami` reports your organization and scopes instead of guessing
270
+
271
+ It printed `organization: "not exposed to API keys see the portal"`, and reported permissions as
272
+ `scopes_observed` a guess assembled from which probe calls happened to return 200 rather than 403,
273
+ which can only ever describe the scopes it thought to test for.
274
+
275
+ It now reads `GET /v1/me`: the real organization name, the granted scopes, the key id and its rate
276
+ limit. Still two requests, but the third call to `/v1/admin/usage` is gone it existed only to
277
+ infer whether the key held `admin:read` — so `whoami` no longer needs that scope at all.
278
+
279
+ **Breaking for `--json` consumers:** `scopes_observed` (an object of booleans) is replaced by
280
+ `scopes` (an array of granted scope names), and `organization` is now `{ id, name }` rather than an
281
+ explanatory sentence.
282
+
283
+ ### Added — `certen proof open <link>`
284
+
285
+ Read a proof someone shared with you. **Runs with no API key configured** — the recipient of a
286
+ share link has no CERTEN account, and every other share command (`share`, `shares`, `shares revoke`)
287
+ serves the sender.
288
+
289
+ ```
290
+ certen proof open https://gateway.kompendium.co/v1/proof/shared/<token>
291
+ certen proof open <link> --out proof.json
292
+ ```
293
+
294
+ An expired or revoked link exits non-zero saying to ask for a fresh one, rather than reporting that
295
+ the proof does not exist.
296
+
297
+ ### Added `certen verify <receipt-id>`
298
+
299
+ Confirm a charge instead of being told it is fine.
300
+
301
+ ```
302
+ PASS digest sha256(canonical_json(body)) matches the stated digest.
303
+ PASS signature ed25519 signature verifies against published key bd4a7a92f29958b9.
304
+ PASS inclusion This receipt is leaf 1254 of 1269.
305
+ PASS root Audit path folds to the root of the independently fetched signed head at 1269.
306
+ SKIP anchor No anchored tree head covers this receipt yet.
307
+ ```
308
+
309
+ The receipt already carried a `verification` block; it is CERTEN checking CERTEN. This recomputes
310
+ everything from published data and compares the folded root against a tree head fetched separately.
311
+
312
+ **It exits non-zero when it did not fully verify — including when checks were merely SKIPPED.** An
313
+ incomplete run is not a pass, and exiting 0 would let a script report an unverified receipt as
314
+ verified. The report survives the failure under `error.details`, matching `certen doctor`.
315
+
316
+ ### Added `certen ledger`, `certen receipts`
317
+
318
+ Where the money went, and proof of what you were charged — neither was reachable from the terminal.
319
+
320
+ ```
321
+ certen ledger --all # every balance change, paged for you
322
+ certen receipts # NUMBER, WHEN, TYPE, AMOUNT, EVIDENCE
323
+ certen receipts get <id> --proof # signature, and the inclusion proof
324
+ ```
325
+
326
+ `EVIDENCE` shows `signed + logged`, because `logged` is what decides whether an inclusion proof
327
+ exists without it you ask for one, get a 404, and cannot tell "not yet" from "wrong id".
328
+
329
+ The anchor line reads `covering_head`, not `head`: a receipt whose own tree head is unanchored is
330
+ still anchored by any later root that commits to it, and reporting `head` would call a perfectly
331
+ good receipt unproven for every gap between anchors. An unattested anchor time is labelled a loose
332
+ upper bound rather than presented as the block time.
333
+
334
+ `--all` pages for you and refuses `--offset` alongside it; a non-numeric `--limit` and
335
+ `--tree-size` without `--proof` are both rejected before any network call.
336
+
337
+ ### Added — `certen payers` and `certen payers add`
338
+
339
+ Register a wallet you send from, so deposits credit on sight instead of needing a one-time payment
340
+ opened for the exact amount before every send. This is what a 402 already told you to do, at an
341
+ endpoint no command could reach.
342
+
343
+ ```
344
+ certen payers add 0xAbC… --chain base-sepolia --label treasury
345
+ certen payers # what is registered
346
+ ```
347
+
348
+ A malformed address is rejected before the network call, an empty list says so plainly rather than
349
+ looking like success, and a 409 (the address belongs to another organization on that chain) exits
350
+ non-zero a funding script must not read it as "attribution is set up".
351
+
352
+ ### Changed `certen balance` makes one request instead of two
353
+
354
+ It fetched the balance and `/v1/billing/obligations` concurrently, because the balance alone could
355
+ not say how much was actually left to commit. The gateway now reports that with the balance, so the
356
+ second request is gone. Against an older gateway the command still falls back to it — printing
357
+ `spendable_usd` in the "Left to commit" slot would report committed money as available.
358
+
359
+ ### Added — `certen pricing`, and `--sku` on `certen quote`
360
+
361
+ There was no way to ask the CLI what anything costs. `certen quote` prices one operation and takes
362
+ its sku from a vocabulary nothing published and it had no `--sku` flag at all, so it could only
363
+ ever price the default. Asking "what does CERTEN cost" meant reading the gateway's refusals.
364
+
365
+ ```
366
+ $ certen pricing --chain base-sepolia
367
+
368
+ SKU CHAIN PRICE
369
+ identity.provision * $5.00
370
+ proof.execute base-sepolia $0.35 + gas
371
+ ```
372
+
373
+ `+ gas` marks prices that are completed at execution, so a floor is not read as a total. `*` is the
374
+ entry that applies to any chain without one of its own, and is kept when filtering by chain —
375
+ dropping it would report identity provisioning as unpriced on a chain where it costs $5.
376
+
377
+ `certen quote --sku <sku>` now prices any of them, and exits non-zero when pricing is not
378
+ configured rather than reporting an empty catalogue.
379
+
380
+ ### Changed — `certen call` no longer reads the same balances twice
381
+
382
+ It fetched the identity (it needs `can_sign` before prompting for a passphrase), then fetched
383
+ `/v1/portfolio` for balances the identity response had already returned a round trip on the
384
+ critical path of the main flow, for numbers it was holding.
385
+
386
+ The guard now takes those balances when the caller has them, and still reads the portfolio when the
387
+ gateway sends none, so an older gateway does not silently lose the guard.
388
+
389
+ ### Added `certen tx list --all`
390
+
391
+ Fetches every page instead of the first. Answering "how many intents failed this month" previously
392
+ meant a shell loop incrementing `--offset` and knowing when to stop; getting that wrong reads as
393
+ "there were none".
394
+
395
+ With `--all`, `--limit` is the page size rather than a cap, and `--offset` is rejected — `--all`
396
+ starts from the beginning, so the two together have no coherent meaning.
397
+
398
+ ### Breaking `certen identity` JSON output is no longer wrapped
399
+
400
+ `certen --json identity get <id>` and `certen --json identity create` returned the identity nested
401
+ under an `identity` key. It is now at the top level, matching every other command and the API.
402
+
403
+ ```bash
404
+ # before
405
+ certen --json identity get "$ID" | jq -r '.data.identity.can_sign'
406
+ # after
407
+ certen --json identity get "$ID" | jq -r '.data.can_sign'
408
+ ```
409
+
410
+ The envelope itself (`{ ok, data }` / `{ ok, error }`) and the exit codes are unchanged, as is table
411
+ output, which was never a contract. See `docs/CLI-CONTRACT.md`.
412
+
413
+ ### Fixed `certen tx status --json` reports absent fields as `null`
414
+
415
+ `proof_id`, `proof_bundle_url`, `accum_tx_hash` and `error_message` came back as `""` when the
416
+ gateway had no value, so `.proof_id != null` was true for a transaction with no proof. They are now
417
+ `null`. A script testing `if .proof_id then` is unaffected; one testing `!= null` was wrong before
418
+ and is right now.
419
+
420
+ ### Fixed `can_sign` distinguishes "cannot sign" from "could not check"
421
+
422
+ An unreadable key page reported `can_sign: false`. It now reports `null`, and the table prints
423
+ `unknown`. The two have different fixes: one is repairable with `certen identity update
424
+ --public-key`, the other is a retry.
425
+
426
+ ## 0.6.0 — from eighteen steps to four
427
+
428
+ ### Added — `certen login` / `certen signup`
429
+
430
+ The device authorization grant. The CLI prints a short code, you approve it in a portal session you
431
+ already trust, and the key arrives over the CLI's own channel. **The secret is never displayed and
432
+ never passes through a clipboard or shell history.** Requires a gateway that serves
433
+ `/v1/portal/device`; against an older one it says so and points at the portal.
434
+
435
+ ### Added — `certen init`, `call`, `proof`, `chains`, `whoami`, `doctor`, `identity retire`
436
+
437
+ `init` creates only what is missing, waits until the identity can actually sign, and records the id
438
+ so a later run reuses it rather than burning org quota. `call` is a proof-gated contract call in one
439
+ command it derives the ADI URL, the abstract account and the numeric chain id from the identity,
440
+ and type-checks `--arg` against the Solidity signature before anything is sent. `proof` retrieves,
441
+ bundles, shares and verifies. `doctor` names the one thing blocking you and the command that fixes
442
+ it.
443
+
444
+ ### Behavioural change usage errors now exit 2
445
+
446
+ A wrong invocation used to exit 1, indistinguishable from a rejected request. Several commands threw
447
+ untyped errors; they now exit 2 as the contract always specified. Scripts branching on any non-zero
448
+ exit are unaffected; scripts that treated 1 as "the gateway said no" should re-check.
449
+
450
+ ### Behavioural change `auth login` verifies the key before saving it
451
+
452
+ A typo'd or revoked key used to be written and then surface as an opaque 401 at whatever command ran
453
+ next. It is now checked first and **not saved if rejected**. A 403 means the key is real but
454
+ unscoped and is accepted with a note. `--api-key -` reads from stdin; omitting it prompts.
455
+
456
+ ### Behavioural change human mode waits by default
457
+
458
+ `identity create` and `tx create` poll to a usable state. `--json` keeps the old fire-and-forget
459
+ default so existing scripts do not silently start blocking.
460
+
461
+ ### Added — the unfunded-account guard, and `error.details`
462
+
463
+ A value transfer from an empty abstract account is refused before submitting, naming the faucet;
464
+ `--force` overrides. And a failure that still produced a result carries it under `error.details`
465
+ `certen --json doctor` returns every check that way, so signalling the failure never costs you the
466
+ diagnosis. See docs/CLI-CONTRACT.md.
467
+
468
+ ## 0.5.0 — money commands, and a refusal that tells you how to fix it
469
+
470
+ ### Added `certen balance` and `certen fund`
471
+
472
+ `balance` prints available, held, credit line, spendable, and **left to commit** — spendable minus
473
+ what pending intents have already claimed. Showing only the balance would tell you that you can
474
+ afford work that is already spoken for.
475
+
476
+ `fund <amount> --chain <chain>` prints where to send stablecoin and waits until it is credited.
477
+ It never touches a wallet or a key: signing and sending stay with you. `--no-wait` prints the
478
+ details and exits; `--poll-interval` and `--timeout` control the wait. An uncredited or expired
479
+ payment exits non-zero, because a funding script must not read one as paid.
480
+
481
+ Every option is validated before the network call — a typo in `--timeout` used to open a real
482
+ payment intent first.
483
+
484
+ ### Changed — a 402 now prints the way out
485
+
486
+ A refusal for lack of funds shows the shortfall, the address, the exact amount, the reference, both
487
+ `certen fund …` and the portal link, and the quote id to retry with then states plainly that
488
+ nothing was charged and no work was started. All on stderr; stdout is untouched.
489
+
490
+ In `--json`, the failure envelope gains `shortfall_usd`, `quote_id` and `resolve` on payment
491
+ failures only. They are absent on every other error rather than present as nulls. See
492
+ docs/CLI-CONTRACT.md.
493
+
494
+ ### Fixed the error reporter no longer flattens SDK errors
495
+
496
+ `handleError` copied a `CertenError` into an object literal before reporting it. The values
497
+ survived; the class identity did not so a payment refusal could not be recognised and its payment
498
+ target was silently dropped. The fields it copied are all readable on the instance, `isRetryable`
499
+ getter included, so nothing was gained by the copy.
500
+
501
+ ## 0.4.0 `--json` is a machine contract
502
+
503
+ Adds a stable, tested output contract for scripts and AI agents. **Human output is unchanged**: if
504
+ you do not pass `--json`, this release behaves exactly as 0.3.1 did, with one exception noted under
505
+ Breaking.
506
+
507
+ Before this, every failure exited `1` and explained itself in English on stderr. An automated caller
508
+ could not distinguish "you passed a malformed address" from "the gateway is down" without parsing
509
+ prose and those want opposite responses. One is a bug to fix; the other is worth retrying. For a
510
+ CLI that authorizes cross-chain execution against real funds, guessing wrong is expensive in a way
511
+ that is not recoverable.
512
+
513
+ The full specification is [docs/CLI-CONTRACT.md](../../docs/CLI-CONTRACT.md), enforced by
514
+ `test/conformance.test.ts`, which runs the built binary as a subprocess and checks the real process's
515
+ stdout and exit code.
516
+
517
+ ### Added
518
+
519
+ - **Global `--json`**, accepted anywhere in the argument list — `certen --json tx status X` and
520
+ `certen tx status X --json` are identical. It is resolved before argument parsing, so it applies
521
+ even to failures that occur while resolving credentials.
522
+ - **One JSON envelope on stdout and nothing else.** `{"ok":true,"data":…}` or
523
+ `{"ok":false,"error":{"code","message","retryable","status?","requestId?"}}`. A command producing
524
+ several payloads emits an array in `data`; one producing none emits `"data":null`. stdout is never
525
+ empty and never carries two concatenated objects. All human-facing text moves to stderr.
526
+ - **Meaningful exit codes:** `0` ok · `1` operation failed · `2` usage error · `3` gateway
527
+ unreachable. `3` guarantees nothing was submitted, so a retry cannot double-execute.
528
+ - **`error.retryable`**, taken from the SDK's own `CertenError.isRetryable`, so the CLI and the SDK
529
+ hand an automated caller the identical retry decision.
530
+ - **`certen --help --json`** returns the entire command tree — every command, argument, flag and exit
531
+ code — in one call, instead of scraping help text once per subcommand.
532
+
533
+ ### Fixed
534
+
535
+ - **Usage errors on subcommands bypassed error handling entirely.** `exitOverride()` is not inherited
536
+ by commander subcommands, so a missing required flag (`certen identity create` with no `--name`)
537
+ called `process.exit(1)` inside commander: no envelope was emitted, stdout stayed empty, and a
538
+ usage error was indistinguishable from a failed request. It is now applied to every command in the
539
+ tree.
540
+
541
+ ### Breaking
542
+
543
+ - **"No API key configured" now exits `2` instead of `1`.** It is a usage error: nothing was sent,
544
+ and retrying cannot help. The same applies to a config file with unsafe permissions and to a
545
+ missing keyring backend. Scripts treating any non-zero exit as failure are unaffected; scripts
546
+ testing specifically for `-eq 1` need updating.
547
+
548
+ ### Note
549
+
550
+ `output: "json"` in `~/.certen/config.json` is **not** this contract. That setting predates the
551
+ envelope and makes commands print their raw payload — no `ok`, no `error`, no exit-code guarantees.
552
+ It is kept for backward compatibility. Automated callers should pass `--json` explicitly rather than
553
+ depend on a machine's local config, which they cannot see.