zk-credits 0.2.10 → 0.2.12
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/README.md +101 -230
- package/dist/base-sidecar.d.ts +24 -0
- package/dist/base-sidecar.js +74 -6
- package/dist/cli.js +216 -198
- package/dist/codex-launcher.d.ts +2 -1
- package/dist/codex-launcher.js +2 -2
- package/dist/codex-sdk-options.js +3 -0
- package/dist/credential-store.js +12 -7
- package/dist/investigator.mjs +2638 -0
- package/dist/local-x402-agent.d.ts +10 -2
- package/dist/local-x402-agent.js +1422 -1015
- package/dist/responses-bridge.js +10 -1
- package/dist/rpc-config.d.ts +1 -1
- package/dist/rpc-config.js +2 -2
- package/dist/runtime-owner.d.ts +17 -0
- package/dist/runtime-owner.js +72 -0
- package/dist/setup-config.js +2 -2
- package/dist/sidecar-diagnostics.d.ts +7 -0
- package/dist/sidecar-diagnostics.js +103 -0
- package/dist/sidecar-lifecycle.js +1 -1
- package/dist/sidecar-runtime.d.ts +47 -0
- package/dist/sidecar-runtime.js +122 -0
- package/dist/sidecar.d.ts +2 -0
- package/dist/sidecar.js +21 -0
- package/dist/slot-ledger.js +8 -3
- package/dist/task-evidence.d.ts +20 -0
- package/dist/task-evidence.js +41 -0
- package/dist/zk-credits.js +1749 -1322
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,248 +1,119 @@
|
|
|
1
|
-
> Release candidate `0.2.9` is prepared for owner publication. Registry commands
|
|
2
|
-
> below apply after publication; the existing `0.2.8` lacks this flow.
|
|
3
|
-
|
|
4
1
|
# zk-credits
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Built for the invite-only experimental Base Sepolia pilot. Product calls use
|
|
9
|
-
founder-provisioned test credits and have no product payment step; the circuit
|
|
10
|
-
is not independently audited. External research participants in slots A and C
|
|
11
|
-
receive $25 for a 30-minute session regardless of setup success. That payment
|
|
12
|
-
is not product revenue or willingness-to-pay evidence.
|
|
3
|
+
API credits for Codex, Cline, and x402 agents. The local sidecar proves each
|
|
4
|
+
spend and forwards your request to the gateway.
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
npm install --global zk-credits@0.2.9
|
|
16
|
-
```
|
|
6
|
+
Requires Node.js 20 or later. Runs on Base Sepolia.
|
|
17
7
|
|
|
18
|
-
##
|
|
8
|
+
## Quick start
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
1. [Activate your credits](https://hazecredits.vercel.app) and download your activated credential.
|
|
11
|
+
2. Install the package and configure a Base Sepolia RPC endpoint:
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| `@zk-credits/x402-zk-prepaid` (adapter) | `0.1.0` |
|
|
27
|
-
| `@zk-credits/shared` | `0.1.0` |
|
|
28
|
-
|
|
29
|
-
The pinned Base Sepolia trial version is `0.2.9`. It includes passwordless
|
|
30
|
-
OS credential storage, the hash-pinned proving archive, public Base Sepolia RPC
|
|
31
|
-
by default, a narrow Codex Responses bridge, and the request-aware x402 adapter.
|
|
32
|
-
Setup validates the public chain witness and gateway-known root. No private
|
|
33
|
-
GitHub repository access or recovery password is required for the new flow.
|
|
34
|
-
|
|
35
|
-
Verify the shipped artifacts before your first prove. The package's
|
|
36
|
-
`circuits/manifest.json` fixes the SHA-256 of the frozen
|
|
37
|
-
`private_credit_spend.wasm`, `private_credit_spend.zkey`, and
|
|
38
|
-
`verification_key_private_credit.json`; compute the digest of each delivered
|
|
39
|
-
file and compare it to the manifest. A mismatch is a proof failure, and the
|
|
40
|
-
sidecar refuses to prove rather than sending a payment. The founder compares
|
|
41
|
-
the same digests against the delivered set before inviting you.
|
|
42
|
-
|
|
43
|
-
## What one credit buys
|
|
44
|
-
|
|
45
|
-
One credit buys one successfully committed response from the single service
|
|
46
|
-
class `coding-deepseek-v4-flash-v1` (`deepseek/deepseek-v4-flash`). It is not
|
|
47
|
-
an arbitrary API call, and the gateway enforces the class before it reserves
|
|
48
|
-
the credit:
|
|
49
|
-
|
|
50
|
-
- text messages, tool definitions, and tool calls, with one generated choice;
|
|
51
|
-
- no provider streaming, images, files, audio, web search, model fallback,
|
|
52
|
-
client-selected routing, or unknown cost-affecting fields;
|
|
53
|
-
- input capped at 128,000 UTF-8 byte units (counted conservatively, never below the
|
|
54
|
-
provider's token count), output at 4,000 tokens, request body at 256 KiB,
|
|
55
|
-
encrypted replay at 1 MiB, and upstream timeout at 120 seconds.
|
|
56
|
-
|
|
57
|
-
The requested `model` field is ignored: the gateway always dispatches the
|
|
58
|
-
class model, so an OpenAI-compatible client configured with any model name
|
|
59
|
-
still receives the class. A rejected request consumes no credit.
|
|
60
|
-
|
|
61
|
-
## First run
|
|
62
|
-
|
|
63
|
-
1. Sign in at https://hazecredits.vercel.app and redeem your invite. Save and
|
|
64
|
-
reimport the passwordless recovery capsule, fund your pilot credential, then
|
|
65
|
-
download the activated credential. Anyone possessing these files can spend
|
|
66
|
-
their credits; keep them private.
|
|
67
|
-
2. Import the activated file and validate setup:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
export ZK_CREDITS_CREDENTIAL_PATH="$HOME/Downloads/zk-credits-credential.json"
|
|
71
|
-
zk-credits setup codex
|
|
13
|
+
```sh
|
|
14
|
+
npm install --global zk-credits
|
|
15
|
+
zk-credits config rpc
|
|
72
16
|
```
|
|
73
17
|
|
|
74
|
-
|
|
75
|
-
and verifies the packaged archive and artifact hashes, synchronizes public
|
|
76
|
-
Base events from the manifest-pinned bond and deployment block, and checks
|
|
77
|
-
the gateway-known root. Credentials enter OS secure storage: macOS Keychain,
|
|
78
|
-
Windows Credential Manager, or persistent Linux Secret Service. Storage
|
|
79
|
-
access approval may be needed; unavailable or denied storage fails closed.
|
|
80
|
-
3. Review consent, choose your task, and run `zk-credits codex` or
|
|
81
|
-
`zk-credits x402-agent`. Subsequent launches use OS storage without a password
|
|
82
|
-
or import file. The activated download remains a private recovery backup.
|
|
83
|
-
|
|
84
|
-
The RPC defaults to the launch endpoint baked into the package. If that endpoint is limited,
|
|
85
|
-
run `zk-credits config rpc` and enter your own Base Sepolia endpoint in the hidden
|
|
86
|
-
local prompt. Saved settings stay owner-only; `BASE_RPC_URL` takes precedence.
|
|
87
|
-
Provider keys must not appear in chat or evidence. No gateway admin token is
|
|
88
|
-
needed for operator setup.
|
|
89
|
-
|
|
90
|
-
Existing encrypted exports require `zk-credits setup codex --legacy` (or
|
|
91
|
-
`setup x402 --legacy`) and their original password in a hidden local prompt.
|
|
92
|
-
A missing or altered proving artifact fails closed before payment creation.
|
|
93
|
-
Setup and proving stay local; no supplied witness file is accepted for the
|
|
94
|
-
manifest-pinned V2 deployment.
|
|
95
|
-
|
|
96
|
-
The sidecar binds `127.0.0.1:3210` only. It does not modify `~/.cline` or
|
|
97
|
-
`~/.codex`.
|
|
98
|
-
|
|
99
|
-
## Supported route
|
|
100
|
-
|
|
101
|
-
The pilot serves one bounded upstream spend path: non-streaming
|
|
102
|
-
`POST /v1/chat/completions`, either through this sidecar or through an
|
|
103
|
-
x402-native agent that explicitly registers the project `zk-prepaid` adapter.
|
|
104
|
-
The sidecar also accepts Codex `POST /v1/responses` requests with text input
|
|
105
|
-
and function tools, translates them to that bounded chat request, and converts
|
|
106
|
-
the committed result into Responses SSE events. It rejects unsupported fields
|
|
107
|
-
before starting a proof. Generic x402 clients, public facilitators, Bazaar,
|
|
108
|
-
MCP, and the standard `exact` rail are unsupported; the sidecar never falls
|
|
109
|
-
back to another rail.
|
|
110
|
-
|
|
111
|
-
| Route | Auth | Purpose |
|
|
112
|
-
| --- | --- | --- |
|
|
113
|
-
| `GET /health` | none | Liveness for the local launcher |
|
|
114
|
-
| `GET /v1/models` | local token | Codex model discovery |
|
|
115
|
-
| `GET /metrics` | local token | Aggregate proof metrics (below) |
|
|
116
|
-
| `POST /v1/chat/completions` | local token | The only proving and spend path |
|
|
117
|
-
| `POST /v1/responses` | local token | Codex bridge to the bounded chat spend path |
|
|
118
|
-
|
|
119
|
-
The chat route rejects streaming bodies (`stream`, `stream_options`) and a
|
|
120
|
-
missing `model` with a `4xx` before any proof. The Responses route requires
|
|
121
|
-
`stream: true`, text-only message/function-call input, and the known Codex
|
|
122
|
-
metadata fields; it rejects images, unsupported tools, and unknown controls
|
|
123
|
-
before any proof. Its upstream request is always `stream: false`. Anthropic
|
|
124
|
-
`/v1/messages` and unknown paths return `404 unsupported_openai_path`. The
|
|
125
|
-
sidecar never substitutes a model and never falls back to another rail.
|
|
126
|
-
|
|
127
|
-
### Gateway responses and retries
|
|
128
|
-
|
|
129
|
-
| Response | Meaning | What to do |
|
|
130
|
-
| --- | --- | --- |
|
|
131
|
-
| `402` | No usable authorization, or a stale or invalid one | Re-prove against the fresh challenge |
|
|
132
|
-
| `409 claim_already_committed` | Your exact request already succeeded | Read `encryptedReplay` from the response, or fetch it from `POST /x402/replay`; do not re-prove |
|
|
133
|
-
| `409 claim_commit_ambiguous` | The commit outcome is unknown | Retry the same request; never re-prove a different one |
|
|
134
|
-
| `503 pilot_paused` | The operator paused the pilot | Wait. This is retryable, consumes no credit, and is not a proof failure |
|
|
135
|
-
| `503 provider_spend_cap_exhausted` | The provider-spend cap paused the pilot | Wait for the operator to review. No credit was consumed |
|
|
136
|
-
| `503 claim_store_unavailable` | Transient gateway fault | Retry with backoff |
|
|
137
|
-
|
|
138
|
-
The gateway never asks you to re-prove for its own account. A `503` is
|
|
139
|
-
retryable and free; a proof failure is local and never reaches the gateway.
|
|
140
|
-
|
|
141
|
-
## Proof path
|
|
142
|
-
|
|
143
|
-
- Local artifacts only, hash-pinned against the shipped manifest.
|
|
144
|
-
- One prove at a time per sidecar process, in a terminable child-process
|
|
145
|
-
worker with a 10-second deadline. (snarkjs cannot run inside a Node
|
|
146
|
-
`worker_threads` worker: its `web-worker` polyfill re-enters itself there.)
|
|
147
|
-
- Every proof is self-verified locally with the pinned verification key
|
|
148
|
-
before `PAYMENT-SIGNATURE` can be emitted, and its six public signals must
|
|
149
|
-
exactly match `[root, timestamp, domain, requestSignal, nullifier, share]`.
|
|
150
|
-
- A failed attempt retries with the same slot, request signal, nonce,
|
|
151
|
-
response key, and gateway `issuedAt` while more than ten seconds remain in
|
|
152
|
-
the challenge window.
|
|
153
|
-
- A slot is provisional during proving and recorded in the durable local
|
|
154
|
-
ledger (`$ZK_CREDITS_HOME/base-slots.json`) only after self-verification.
|
|
155
|
-
Proof misses, timeouts, hash failures, and verification failures release
|
|
156
|
-
it.
|
|
157
|
-
|
|
158
|
-
## `GET /metrics`
|
|
159
|
-
|
|
160
|
-
Aggregate only, authenticated with the same local token:
|
|
161
|
-
|
|
162
|
-
```json
|
|
163
|
-
{
|
|
164
|
-
"attempts": 12,
|
|
165
|
-
"successes": 11,
|
|
166
|
-
"failures": 1,
|
|
167
|
-
"retries": 1,
|
|
168
|
-
"failuresByCategory": { "timeout": 1, "artifact_hash_mismatch": 0, "...": 0 },
|
|
169
|
-
"hotProve": { "samples": 11, "p50Ms": 1180.4, "p95Ms": 1902.7 },
|
|
170
|
-
"updatedAt": "2026-09-20T14:00:00.000Z"
|
|
171
|
-
}
|
|
172
|
-
```
|
|
18
|
+
3. Import your credential and connect Codex:
|
|
173
19
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
20
|
+
```sh
|
|
21
|
+
ZK_CREDITS_CREDENTIAL_PATH=/path/to/activated-credential.json zk-credits setup codex
|
|
22
|
+
zk-credits codex
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Setup stores your credential in OS secure storage. Keep your recovery file
|
|
26
|
+
private: anyone with it can spend your credits.
|
|
177
27
|
|
|
178
28
|
## Commands
|
|
179
29
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
zk-credits setup codex
|
|
183
|
-
zk-credits setup x402
|
|
184
|
-
zk-credits
|
|
185
|
-
zk-credits codex
|
|
186
|
-
zk-credits
|
|
187
|
-
zk-credits
|
|
188
|
-
zk-credits x402-agent
|
|
189
|
-
zk-credits
|
|
190
|
-
zk-credits
|
|
191
|
-
|
|
30
|
+
| Command | Purpose |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `zk-credits setup codex` | Import a credential and configure Codex |
|
|
33
|
+
| `zk-credits setup x402` | Prepare an x402 agent |
|
|
34
|
+
| `zk-credits codex [args...]` | Start Codex with the local sidecar |
|
|
35
|
+
| `zk-credits codex --task-evidence` | Run a coding task and show its aggregate payment evidence on exit |
|
|
36
|
+
| `zk-credits investigate --chain base <transaction-hash>` | Investigate a failed Base transaction using prepaid inference |
|
|
37
|
+
| `zk-credits cline [args...]` | Start Cline with the local sidecar |
|
|
38
|
+
| `zk-credits x402-agent` | Run the included x402 agent |
|
|
39
|
+
| `zk-credits serve` | Run the sidecar in the foreground |
|
|
40
|
+
| `zk-credits doctor` | Inspect the service, PID, setup, RPC source, and log location |
|
|
41
|
+
| `zk-credits stop` | Stop the local sidecar |
|
|
42
|
+
| `zk-credits status` | Check the Codex configuration and sidecar |
|
|
43
|
+
| `zk-credits config rpc` | Save or change your Base Sepolia RPC endpoint |
|
|
44
|
+
| `zk-credits --help` | Show command and troubleshooting guidance |
|
|
45
|
+
|
|
46
|
+
`doctor` and `stop` are available in version 0.2.11. They work before setup.
|
|
47
|
+
The default port is `3210`; set `ZK_CREDITS_SIDECAR_PORT` to select another port.
|
|
48
|
+
Automatic startup logs are saved to `~/.zk-credits/sidecar.log`, or
|
|
49
|
+
`$ZK_CREDITS_HOME/sidecar.log` when set.
|
|
50
|
+
|
|
51
|
+
## Troubleshooting
|
|
52
|
+
|
|
53
|
+
**Sidecar stopped or setup missing:** Run `zk-credits doctor`. If setup is
|
|
54
|
+
missing, import your activated credential with the setup command above.
|
|
55
|
+
Otherwise run `zk-credits serve` and read its output.
|
|
56
|
+
|
|
57
|
+
**Port occupied or “Server is not running”:** `x402-agent` starts its own local
|
|
58
|
+
server. Run `zk-credits stop` before switching from `serve` to `x402-agent`.
|
|
59
|
+
For older versions, inspect the listener before stopping it:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
lsof -nP -iTCP:3210 -sTCP:LISTEN
|
|
63
|
+
ps -p <PID> -o pid,ppid,etime,command
|
|
64
|
+
kill -TERM <PID>
|
|
192
65
|
```
|
|
193
66
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
attempt one proof. A later valid request receives HTTP 409 with
|
|
198
|
-
`internal_trial_limit_reached`; ordinary `serve` behavior is unchanged without
|
|
199
|
-
the flag.
|
|
200
|
-
|
|
201
|
-
`trial-registered-adapter` is a maintainer-only internal trial command. It
|
|
202
|
-
requires the locally recovered credential and pinned proving bundle, prompts
|
|
203
|
-
for the credential password and gateway admin token without echo when they are
|
|
204
|
-
not already configured, and requires an explicit confirmation before sending
|
|
205
|
-
one billable request. It checks gateway readiness, authenticated admin status,
|
|
206
|
-
Base root agreement, Codex profile/sidecar status, and remaining local slot
|
|
207
|
-
capacity, then reports only the exchange phases and aggregate counter deltas.
|
|
208
|
-
Run it only after recovery is complete and with the same `ZK_CREDITS_HOME`
|
|
209
|
-
used by the sidecar; it registers `zk-prepaid` directly through the official
|
|
210
|
-
x402 client adapter and reuses the durable slot ledger.
|
|
211
|
-
|
|
212
|
-
Codex SDK:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
import { buildCodexSdkOptions, buildCodexThreadOptions } from 'zk-credits/codex';
|
|
216
|
-
```
|
|
67
|
+
Check the PID and command to confirm it is your sidecar. For a foreground
|
|
68
|
+
process, press Ctrl+C. `stop` authenticates the service and lets active requests
|
|
69
|
+
finish before exit.
|
|
217
70
|
|
|
218
|
-
`zk-credits
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
71
|
+
**Base witness synchronization failed:** Run `zk-credits config rpc` and enter
|
|
72
|
+
a Base Sepolia endpoint (chain ID `84532`). Stop and restart the sidecar, or
|
|
73
|
+
retry setup if it failed. `BASE_RPC_URL` overrides the saved endpoint.
|
|
74
|
+
A missing funded credential or rejected root can require activation checks;
|
|
75
|
+
changing the RPC may not resolve it.
|
|
76
|
+
|
|
77
|
+
## Clients and privacy
|
|
78
|
+
|
|
79
|
+
Codex and Cline use the local OpenAI-compatible transport. Your own x402 agent
|
|
80
|
+
must register the custom `zk-prepaid` adapter; generic x402 clients do not
|
|
81
|
+
support this scheme automatically. See the [x402 integration guide](https://github.com/mangekyou-labs/haze-api/blob/main/docs/onboarding/base-zk-credits-x402-agent.md).
|
|
82
|
+
|
|
83
|
+
Your credential stays on your device. Proofs are sent to authorize each spend. The gateway and model provider receive request content and
|
|
84
|
+
can observe traffic metadata. This does not hide your IP address.
|
|
85
|
+
|
|
86
|
+
Base Sepolia test credits only. The circuit is not independently audited.
|
|
87
|
+
|
|
88
|
+
### Coding-task evidence (0.2.12 candidate)
|
|
89
|
+
|
|
90
|
+
After setup, `zk-credits codex --task-evidence` launches Codex against a fresh,
|
|
91
|
+
dedicated local sidecar and prints aggregate payment counters when Codex exits.
|
|
92
|
+
The provider and profile are fixed for attribution. The summary contains no
|
|
93
|
+
prompts, bearer credentials, proofs, or payment headers. `exchange_verified`
|
|
94
|
+
confirms a successful agent exit and an observed settled response; inspect the
|
|
95
|
+
code diff and tests separately to judge the coding task. The supported model and
|
|
96
|
+
reasoning defaults are selected automatically, including with older setups:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
zk-credits codex --task-evidence
|
|
240
100
|
```
|
|
241
101
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
102
|
+
Enter your task, review the change and tests, then exit Codex to see the summary.
|
|
103
|
+
Version 0.2.12 is prepared for owner publication; 0.2.11 lacks this command.
|
|
104
|
+
See the [indexer sample](https://github.com/mangekyou-labs/haze-api/tree/main/examples/base-usdc-indexer).
|
|
105
|
+
|
|
106
|
+
### Transaction investigator (0.2.12 candidate)
|
|
107
|
+
|
|
108
|
+
After closing Codex, run `zk-credits investigate --chain base <transaction-hash>`
|
|
109
|
+
with the same configured credential. It fetches public execution evidence and
|
|
110
|
+
uses the custom x402 `zk-prepaid` adapter for model inference. Base mainnet is
|
|
111
|
+
the investigated chain; credit funding remains on Base Sepolia and consumption
|
|
112
|
+
is offchain. It does not purchase external traces. Keep missing-evidence notices
|
|
113
|
+
visible and distinguish observed errors from model hypotheses.
|
|
245
114
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
115
|
+
Repeating a transaction hash recovers its existing local task; it need not make
|
|
116
|
+
a new paid call or return a complete report. Do not delete task journals or reset
|
|
117
|
+
credential accounting to force a fresh exchange. Unknown payment outcomes must
|
|
118
|
+
be reconciled before another paid task. A saved report must be labeled as a prior
|
|
119
|
+
run, with its own payment summary.
|
package/dist/base-sidecar.d.ts
CHANGED
|
@@ -65,6 +65,28 @@ export interface BasePrepaidClientOptions {
|
|
|
65
65
|
* a public signal, or a credential identifier.
|
|
66
66
|
*/
|
|
67
67
|
lifecycle?: ZkPrepaidLifecycleObserver;
|
|
68
|
+
/** Caller-owned durable policy and accounting boundary; failures propagate. */
|
|
69
|
+
authorization?: BaseAuthorizationBridge;
|
|
70
|
+
}
|
|
71
|
+
export interface BaseAuthorizationRequest extends BasePaymentContext {
|
|
72
|
+
attemptId: string;
|
|
73
|
+
body: Uint8Array;
|
|
74
|
+
}
|
|
75
|
+
export type BaseUsageObservation = {
|
|
76
|
+
attemptId: string;
|
|
77
|
+
slot: number;
|
|
78
|
+
} & ({
|
|
79
|
+
type: 'provisional' | 'released' | 'retired';
|
|
80
|
+
} | {
|
|
81
|
+
type: 'settlement';
|
|
82
|
+
outcome: 'confirmed' | 'failed' | 'unknown';
|
|
83
|
+
reservationId?: string;
|
|
84
|
+
});
|
|
85
|
+
export interface BaseAuthorizationBridge {
|
|
86
|
+
/** Persist reservation and validate actual terms before any fresh proof. */
|
|
87
|
+
beforeProof(request: BaseAuthorizationRequest): Promise<void>;
|
|
88
|
+
/** Contains no request content, credential, proof, signature or replay key. */
|
|
89
|
+
observe(event: BaseUsageObservation): Promise<void>;
|
|
68
90
|
}
|
|
69
91
|
export interface BasePrepaidClient {
|
|
70
92
|
client: ZkPrepaidClient;
|
|
@@ -80,6 +102,8 @@ export interface BasePaymentContext {
|
|
|
80
102
|
export interface BasePreparedPayment {
|
|
81
103
|
payment: PaymentPayload;
|
|
82
104
|
responsePrivateKey: string;
|
|
105
|
+
attemptId: string;
|
|
106
|
+
slot: number;
|
|
83
107
|
}
|
|
84
108
|
export declare function validateBaseCreditWitness(witness: BaseCreditWitness): BaseCreditWitness;
|
|
85
109
|
/** Creates the shared local proof factory used by both client transports. */
|
package/dist/base-sidecar.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
9
9
|
*/
|
|
10
|
-
import { createDecipheriv, generateKeyPair as generateKeyPairCallback, privateDecrypt, randomBytes } from 'node:crypto';
|
|
10
|
+
import { createDecipheriv, generateKeyPair as generateKeyPairCallback, privateDecrypt, randomBytes, randomUUID } from 'node:crypto';
|
|
11
11
|
import { promisify } from 'node:util';
|
|
12
12
|
import { BASE_MEMBERSHIP_TREE_CAPACITY, canonicalizeBaseJson, computeCreditLeaf, computeNullifier, computeShare, computeSlotBlinding, deriveRequestSignal, deriveSparseCreditWitness, secretFromBase64Url, secretToField, } from '@zk-credits/shared/base';
|
|
13
13
|
import { buildPaymentPayload, createZkPrepaidClient, decodeHeader, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, } from '@zk-credits/x402-zk-prepaid';
|
|
@@ -77,8 +77,13 @@ export function createBasePaymentFactory(options) {
|
|
|
77
77
|
const slotLedger = options.slotLedger;
|
|
78
78
|
if (!slotLedger)
|
|
79
79
|
throw new Error('A durable slot ledger is required for the Base pilot');
|
|
80
|
+
const attemptId = randomUUID();
|
|
81
|
+
const bytes = new Uint8Array(requestBodyBytes(body));
|
|
82
|
+
await options.authorization?.beforeProof({ attemptId, method, url, body: bytes, requirements: structuredClone(requirements) });
|
|
80
83
|
const selectedSlot = slotLedger.allocateProvisional();
|
|
84
|
+
let retired = false;
|
|
81
85
|
try {
|
|
86
|
+
await options.authorization?.observe({ type: 'provisional', attemptId, slot: selectedSlot });
|
|
82
87
|
const nonce = randomNonce();
|
|
83
88
|
const responseKeys = await responseKey();
|
|
84
89
|
const requestSignal = await deriveRequestSignal({
|
|
@@ -128,6 +133,8 @@ export function createBasePaymentFactory(options) {
|
|
|
128
133
|
throw new Error('Proof public signals do not match the canonical statement');
|
|
129
134
|
}
|
|
130
135
|
await slotLedger.commit(selectedSlot);
|
|
136
|
+
retired = true;
|
|
137
|
+
await options.authorization?.observe({ type: 'retired', attemptId, slot: selectedSlot });
|
|
131
138
|
const payment = buildPaymentPayload({
|
|
132
139
|
requirements,
|
|
133
140
|
proof: proofForPayment(proof.proof),
|
|
@@ -135,10 +142,13 @@ export function createBasePaymentFactory(options) {
|
|
|
135
142
|
nonce,
|
|
136
143
|
responseKey: responseKeys.publicKey,
|
|
137
144
|
});
|
|
138
|
-
return { payment, responsePrivateKey: responseKeys.privateKey };
|
|
145
|
+
return { payment, responsePrivateKey: responseKeys.privateKey, attemptId, slot: selectedSlot };
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
|
141
|
-
|
|
148
|
+
if (!retired) {
|
|
149
|
+
slotLedger.release(selectedSlot);
|
|
150
|
+
await options.authorization?.observe({ type: 'released', attemptId, slot: selectedSlot });
|
|
151
|
+
}
|
|
142
152
|
throw error;
|
|
143
153
|
}
|
|
144
154
|
};
|
|
@@ -266,9 +276,49 @@ export function createBasePrepaidClient(options) {
|
|
|
266
276
|
headers: new Headers(response.headers),
|
|
267
277
|
});
|
|
268
278
|
};
|
|
279
|
+
const observeSettlement = async (response, init) => {
|
|
280
|
+
const header = new Headers(init?.headers).get(PAYMENT_SIGNATURE_HEADER);
|
|
281
|
+
if (!header || !options.authorization)
|
|
282
|
+
return;
|
|
283
|
+
let payment;
|
|
284
|
+
try {
|
|
285
|
+
payment = decodeHeader(header);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const key = pendingByNonce.get(payment.payload.nonce);
|
|
291
|
+
const pending = key ? pendingPayments.get(key) : undefined;
|
|
292
|
+
if (!pending)
|
|
293
|
+
return;
|
|
294
|
+
let outcome = 'unknown';
|
|
295
|
+
let reservationId;
|
|
296
|
+
try {
|
|
297
|
+
const encoded = response.headers.get(PAYMENT_RESPONSE_HEADER);
|
|
298
|
+
const result = encoded ? decodeHeader(encoded) : undefined;
|
|
299
|
+
if (result && result.network === pending.payment.accepted.network && result.transaction === ''
|
|
300
|
+
&& (result.reservationId === undefined || (typeof result.reservationId === 'string' && result.reservationId.length > 0 && result.reservationId.length <= 256))
|
|
301
|
+
&& typeof result.success === 'boolean' && !('payer' in result)) {
|
|
302
|
+
// A failed response can describe an ambiguous ready/commit failure.
|
|
303
|
+
outcome = result.success ? 'confirmed' : 'unknown';
|
|
304
|
+
reservationId = result.reservationId;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch { /* Missing or malformed settlement leaves consumption unknown. */ }
|
|
308
|
+
await options.authorization.observe({ type: 'settlement', attemptId: pending.attemptId, slot: pending.slot, outcome,
|
|
309
|
+
...(reservationId === undefined ? {} : { reservationId }) });
|
|
310
|
+
};
|
|
269
311
|
const fetchWithReplay = async (input, init) => {
|
|
270
312
|
prunePending();
|
|
271
|
-
|
|
313
|
+
let response;
|
|
314
|
+
try {
|
|
315
|
+
response = await fetcher(input, { ...init, redirect: 'error' });
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
await observeSettlement(new Response(null, { status: 503 }), init);
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
await observeSettlement(response, init);
|
|
272
322
|
if (response.status !== 409)
|
|
273
323
|
return trackPaymentResponse(response, init);
|
|
274
324
|
let replayAvailable = false;
|
|
@@ -343,7 +393,7 @@ export function createBasePrepaidClient(options) {
|
|
|
343
393
|
const nonce = payment.payload.nonce;
|
|
344
394
|
const expiresAt = Date.now() + REPLAY_TTL_MS;
|
|
345
395
|
replayKeys.set(nonce, { privateKey: prepared.responsePrivateKey, expiresAt });
|
|
346
|
-
pendingPayments.set(key, { payment, expiresAt });
|
|
396
|
+
pendingPayments.set(key, { payment, expiresAt, attemptId: prepared.attemptId, slot: prepared.slot });
|
|
347
397
|
pendingByNonce.set(nonce, key);
|
|
348
398
|
prunePending();
|
|
349
399
|
return payment;
|
|
@@ -358,7 +408,25 @@ export function createBasePrepaidClient(options) {
|
|
|
358
408
|
}
|
|
359
409
|
},
|
|
360
410
|
});
|
|
361
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
client: {
|
|
413
|
+
clearRequirements: client.clearRequirements,
|
|
414
|
+
async fetch(input, init) {
|
|
415
|
+
const source = input instanceof Request ? input : undefined;
|
|
416
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
417
|
+
const body = init?.body ?? (source?.body ? new Uint8Array(await source.clone().arrayBuffer()) : undefined);
|
|
418
|
+
const snapshot = body instanceof ArrayBuffer ? new Uint8Array(body.slice(0))
|
|
419
|
+
: body instanceof Uint8Array ? new Uint8Array(body) : body;
|
|
420
|
+
return client.fetch(url, {
|
|
421
|
+
...(source ? { method: source.method, headers: new Headers(source.headers), signal: source.signal } : {}),
|
|
422
|
+
...init,
|
|
423
|
+
headers: new Headers(init?.headers ?? source?.headers),
|
|
424
|
+
...(snapshot === undefined ? {} : { body: snapshot }),
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
committedSlots: () => options.slotLedger?.committedSlots() ?? [],
|
|
429
|
+
};
|
|
362
430
|
}
|
|
363
431
|
function parsePublicTreeArtifact(value) {
|
|
364
432
|
if (!value || typeof value !== 'object')
|