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