@certen.io/cli 0.7.2 → 0.8.0

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