zk-credits 0.2.0 → 0.2.1
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 +52 -34
- package/dist/base-registered-trial.d.ts +33 -0
- package/dist/base-registered-trial.js +168 -0
- package/dist/base-sidecar.d.ts +14 -1
- package/dist/base-sidecar.js +82 -72
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +2 -1
- package/dist/cli.js +348 -15
- package/dist/node-sidecar-lifecycle.d.ts +2 -0
- package/dist/node-sidecar-lifecycle.js +29 -7
- package/dist/responses-bridge.d.ts +14 -0
- package/dist/responses-bridge.js +328 -0
- package/dist/setup-preflight.d.ts +2 -0
- package/dist/setup-preflight.js +12 -0
- package/dist/sidecar.js +52 -8
- package/dist/zk-credits.js +978 -111
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ are founder-provisioned test credits, and the circuit is not independently
|
|
|
7
7
|
audited.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install --global zk-credits
|
|
10
|
+
npm install --global zk-credits@latest
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Pinned pilot versions
|
|
@@ -21,9 +21,11 @@ not mix versions between the sidecar, the adapter package, and the gateway:
|
|
|
21
21
|
| `@zk-credits/x402-zk-prepaid` (adapter) | `0.1.0` |
|
|
22
22
|
| `@zk-credits/shared` | `0.1.0` |
|
|
23
23
|
|
|
24
|
-
`zk-credits@0.2.0` is
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
`zk-credits@0.2.0` is the currently published Base Sepolia pilot version. It
|
|
25
|
+
uses the non-streaming `POST /v1/chat/completions` path below. The local
|
|
26
|
+
`0.2.1` candidate adds a loopback Responses bridge for Codex and is reserved
|
|
27
|
+
for the internal trial; it is not yet published or part of the pinned pilot
|
|
28
|
+
set. Neither version carries a Stellar or evaluation path. The proving artifacts
|
|
27
29
|
(`private-credit-spend-bn254-dev-sepolia-v1`) are delivered directly through
|
|
28
30
|
the invite channel, never from a public host.
|
|
29
31
|
|
|
@@ -43,9 +45,9 @@ an arbitrary API call, and the gateway enforces the class before it reserves
|
|
|
43
45
|
the credit:
|
|
44
46
|
|
|
45
47
|
- text messages, tool definitions, and tool calls, with one generated choice;
|
|
46
|
-
- no streaming, images, files, audio, web
|
|
48
|
+
- no provider streaming, images, files, audio, web search, model fallback,
|
|
47
49
|
client-selected routing, or unknown cost-affecting fields;
|
|
48
|
-
- input capped at
|
|
50
|
+
- input capped at 128,000 UTF-8 byte units (counted conservatively, never below the
|
|
49
51
|
provider's token count), output at 4,000 tokens, request body at 256 KiB,
|
|
50
52
|
encrypted replay at 1 MiB, and upstream timeout at 120 seconds.
|
|
51
53
|
|
|
@@ -55,32 +57,27 @@ still receives the class. A rejected request consumes no credit.
|
|
|
55
57
|
|
|
56
58
|
## First run
|
|
57
59
|
|
|
58
|
-
1. Redeem an invite
|
|
59
|
-
download the password-encrypted credential backup. Credits are
|
|
60
|
+
1. Redeem an invite and download the encrypted credential. Credits are
|
|
60
61
|
founder-provisioned test credits; there is no payment step.
|
|
61
|
-
2. Install
|
|
62
|
+
2. Install `zk-credits` and ask your pilot contact for the pinned proving
|
|
63
|
+
bundle and public Base tree file. The package ships
|
|
62
64
|
`circuits/manifest.json`, which fixes the SHA-256 of the frozen
|
|
63
65
|
`private_credit_spend.wasm`, `private_credit_spend.zkey`, and
|
|
64
|
-
`verification_key_private_credit.json`. The
|
|
65
|
-
|
|
66
|
+
`verification_key_private_credit.json`. The sidecar never fetches proving
|
|
67
|
+
material at runtime. Set the paths to the downloaded files and gateway:
|
|
66
68
|
|
|
67
69
|
```bash
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
+
export ZK_CREDITS_CREDENTIAL_PATH="$HOME/Downloads/zk-credits-credential.json"
|
|
71
|
+
export ZK_CREDITS_ARTIFACT_DIR="/path/to/pinned-bundle"
|
|
72
|
+
export ZK_CREDITS_WITNESS_PATH="/path/to/base-tree.json"
|
|
73
|
+
export ZK_CREDITS_GATEWAY_URL="https://zk-credits-gateway.onrender.com"
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
A missing, relocated, symlinked-out, or altered artifact fails closed
|
|
73
77
|
before the first prove. A hash mismatch is a proof failure: no payment
|
|
74
78
|
leaves, and the local slot stays reusable.
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
export ZK_CREDITS_CREDENTIAL_PATH="$PWD/credential.json"
|
|
80
|
-
export ZK_CREDITS_CREDENTIAL_PASSWORD='use-a-long-local-password'
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Set `ZK_CREDITS_WITNESS_PATH` to a local witness artifact — either a
|
|
80
|
+
`ZK_CREDITS_WITNESS_PATH` points to a local witness artifact — either a
|
|
84
81
|
prepared witness (`root`, `pathElements`, `pathIndices`) or a public tree
|
|
85
82
|
(`leaves: [{ index, leaf, expiry? }]`, optional `root`) from which the
|
|
86
83
|
sidecar derives the depth-20 path — or configure `BASE_RPC_URL`,
|
|
@@ -89,24 +86,29 @@ still receives the class. A rejected request consumes no credit.
|
|
|
89
86
|
events and builds a local Merkle witness. The gateway never serves a path
|
|
90
87
|
for a named leaf or commitment.
|
|
91
88
|
|
|
92
|
-
|
|
89
|
+
3. Start setup. It verifies the credential, pinned artifact hashes, and
|
|
90
|
+
witness locally, then prompts for the backup password without echo:
|
|
93
91
|
|
|
94
92
|
```bash
|
|
95
|
-
zk-credits
|
|
96
|
-
zk-credits setup codex && zk-credits codex "summarize this repository"
|
|
93
|
+
zk-credits setup codex
|
|
97
94
|
```
|
|
98
95
|
|
|
96
|
+
Then run `zk-credits codex` to open Codex with the local sidecar.
|
|
97
|
+
|
|
99
98
|
The sidecar binds `127.0.0.1:3210` only. It does not modify `~/.cline` or
|
|
100
99
|
`~/.codex`.
|
|
101
100
|
|
|
102
101
|
## Supported route
|
|
103
102
|
|
|
104
|
-
The pilot serves one spend path: non-streaming
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
The pilot serves one bounded upstream spend path: non-streaming
|
|
104
|
+
`POST /v1/chat/completions`, either through this sidecar or through an
|
|
105
|
+
x402-native agent that explicitly registers the project `zk-prepaid` adapter.
|
|
106
|
+
The sidecar also accepts Codex `POST /v1/responses` requests with text input
|
|
107
|
+
and function tools, translates them to that bounded chat request, and converts
|
|
108
|
+
the committed result into Responses SSE events. It rejects unsupported fields
|
|
109
|
+
before starting a proof. Generic x402 clients, public facilitators, Bazaar,
|
|
110
|
+
MCP, and the standard `exact` rail are unsupported; the sidecar never falls
|
|
111
|
+
back to another rail.
|
|
110
112
|
|
|
111
113
|
| Route | Auth | Purpose |
|
|
112
114
|
| --- | --- | --- |
|
|
@@ -114,10 +116,14 @@ unsupported; the sidecar never falls back to another rail.
|
|
|
114
116
|
| `GET /v1/models` | local token | Codex model discovery |
|
|
115
117
|
| `GET /metrics` | local token | Aggregate proof metrics (below) |
|
|
116
118
|
| `POST /v1/chat/completions` | local token | The only proving and spend path |
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
| `POST /v1/responses` | local token | Codex bridge to the bounded chat spend path |
|
|
120
|
+
|
|
121
|
+
The chat route rejects streaming bodies (`stream`, `stream_options`) and a
|
|
122
|
+
missing `model` with a `4xx` before any proof. The Responses route requires
|
|
123
|
+
`stream: true`, text-only message/function-call input, and the known Codex
|
|
124
|
+
metadata fields; it rejects images, unsupported tools, and unknown controls
|
|
125
|
+
before any proof. Its upstream request is always `stream: false`. Anthropic
|
|
126
|
+
`/v1/messages` and unknown paths return `404 unsupported_openai_path`. The
|
|
121
127
|
sidecar never substitutes a model and never falls back to another rail.
|
|
122
128
|
|
|
123
129
|
### Gateway responses and retries
|
|
@@ -179,9 +185,21 @@ zk-credits setup codex [--model <model>]
|
|
|
179
185
|
zk-credits codex [codex arguments...]
|
|
180
186
|
zk-credits status
|
|
181
187
|
zk-credits serve [--port <port>]
|
|
188
|
+
zk-credits trial-registered-adapter
|
|
182
189
|
eval "$(zk-credits env)"
|
|
183
190
|
```
|
|
184
191
|
|
|
192
|
+
`trial-registered-adapter` is a maintainer-only internal trial command. It
|
|
193
|
+
requires the locally recovered credential and pinned proving bundle, prompts
|
|
194
|
+
for the credential password and gateway admin token without echo when they are
|
|
195
|
+
not already configured, and requires an explicit confirmation before sending
|
|
196
|
+
one billable request. It checks gateway readiness, authenticated admin status,
|
|
197
|
+
Base root agreement, Codex profile/sidecar status, and remaining local slot
|
|
198
|
+
capacity, then reports only the exchange phases and aggregate counter deltas.
|
|
199
|
+
Run it only after recovery is complete and with the same `ZK_CREDITS_HOME`
|
|
200
|
+
used by the sidecar; it registers `zk-prepaid` directly through the official
|
|
201
|
+
x402 client adapter and reuses the durable slot ledger.
|
|
202
|
+
|
|
185
203
|
Codex SDK:
|
|
186
204
|
|
|
187
205
|
```ts
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot local transport for exercising the official @x402/core adapter.
|
|
3
|
+
*
|
|
4
|
+
* @x402/core creates scheme payloads from PaymentRequired and does not pass
|
|
5
|
+
* HTTP method/body context to a scheme client. This harness captures that
|
|
6
|
+
* request context locally, then binds it into the same Base proof factory
|
|
7
|
+
* before invoking the registered `zk-prepaid` scheme. It is intended only for
|
|
8
|
+
* the internal Base Sepolia trial, not as a general HTTP client.
|
|
9
|
+
*
|
|
10
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
11
|
+
*/
|
|
12
|
+
import { type BasePrepaidClientOptions } from './base-sidecar.js';
|
|
13
|
+
export type BaseRegisteredTrialFailurePhase = 'challenge_transport' | 'challenge_status' | 'challenge_header' | 'challenge_invalid' | 'challenge_unsupported' | 'challenge_request_mismatch' | 'proof_generation' | 'payment_transport' | 'payment_rejected' | 'payment_response_missing' | 'payment_response_invalid' | 'response_not_ok';
|
|
14
|
+
export interface BaseRegisteredTrialResult {
|
|
15
|
+
challengeStatus: number | null;
|
|
16
|
+
challengeReceived: boolean;
|
|
17
|
+
proofPrepared: boolean;
|
|
18
|
+
paymentSignatureSent: boolean;
|
|
19
|
+
paymentStatus: number | null;
|
|
20
|
+
paymentResponseConfirmed: boolean;
|
|
21
|
+
responseStatus: number | null;
|
|
22
|
+
responseSucceeded: boolean;
|
|
23
|
+
committedSlotDelta: number;
|
|
24
|
+
failurePhase?: BaseRegisteredTrialFailurePhase;
|
|
25
|
+
}
|
|
26
|
+
export interface BaseRegisteredTrialOptions extends BasePrepaidClientOptions {
|
|
27
|
+
url: string;
|
|
28
|
+
body: string;
|
|
29
|
+
headers?: HeadersInit;
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
}
|
|
32
|
+
/** Runs one POST to the pilot chat route through a directly registered adapter. */
|
|
33
|
+
export declare function runBaseRegisteredTrialExchange(options: BaseRegisteredTrialOptions): Promise<BaseRegisteredTrialResult>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot local transport for exercising the official @x402/core adapter.
|
|
3
|
+
*
|
|
4
|
+
* @x402/core creates scheme payloads from PaymentRequired and does not pass
|
|
5
|
+
* HTTP method/body context to a scheme client. This harness captures that
|
|
6
|
+
* request context locally, then binds it into the same Base proof factory
|
|
7
|
+
* before invoking the registered `zk-prepaid` scheme. It is intended only for
|
|
8
|
+
* the internal Base Sepolia trial, not as a general HTTP client.
|
|
9
|
+
*
|
|
10
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
11
|
+
*/
|
|
12
|
+
import { x402Client } from '@x402/core/client';
|
|
13
|
+
import { BASE_SEPOLIA_NETWORK, decodeHeader, encodeHeader, lifecycleFailure, lifecycleStage, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, registerZkPrepaidClient, ZK_PREPAID_SCHEME, } from '@zk-credits/x402-zk-prepaid';
|
|
14
|
+
import { createBasePaymentFactory } from './base-sidecar.js';
|
|
15
|
+
function observe(options, event) {
|
|
16
|
+
try {
|
|
17
|
+
options.lifecycle?.(event);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// An observer must never change an exchange.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function withFailure(options, result, startingCommittedSlots, failurePhase, failure) {
|
|
24
|
+
result.failurePhase = failurePhase;
|
|
25
|
+
if (failure)
|
|
26
|
+
observe(options, lifecycleFailure(failure));
|
|
27
|
+
result.committedSlotDelta = (options.slotLedger?.snapshot().committed ?? startingCommittedSlots) - startingCommittedSlots;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
function requestHeaders(headers) {
|
|
31
|
+
return new Headers(headers);
|
|
32
|
+
}
|
|
33
|
+
/** Runs one POST to the pilot chat route through a directly registered adapter. */
|
|
34
|
+
export async function runBaseRegisteredTrialExchange(options) {
|
|
35
|
+
const ledger = options.slotLedger;
|
|
36
|
+
if (!ledger)
|
|
37
|
+
throw new Error('A durable slot ledger is required for the Base pilot');
|
|
38
|
+
const startingCommittedSlots = ledger.snapshot().committed;
|
|
39
|
+
const result = {
|
|
40
|
+
challengeStatus: null,
|
|
41
|
+
challengeReceived: false,
|
|
42
|
+
proofPrepared: false,
|
|
43
|
+
paymentSignatureSent: false,
|
|
44
|
+
paymentStatus: null,
|
|
45
|
+
paymentResponseConfirmed: false,
|
|
46
|
+
responseStatus: null,
|
|
47
|
+
responseSucceeded: false,
|
|
48
|
+
committedSlotDelta: 0,
|
|
49
|
+
};
|
|
50
|
+
let target;
|
|
51
|
+
try {
|
|
52
|
+
target = new URL(options.url);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_request_mismatch');
|
|
56
|
+
}
|
|
57
|
+
if (target.protocol !== 'https:'
|
|
58
|
+
|| target.username !== ''
|
|
59
|
+
|| target.password !== ''
|
|
60
|
+
|| target.search !== ''
|
|
61
|
+
|| target.hash !== ''
|
|
62
|
+
|| target.pathname !== '/v1/chat/completions') {
|
|
63
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_request_mismatch');
|
|
64
|
+
}
|
|
65
|
+
const fetcher = options.fetch ?? fetch;
|
|
66
|
+
const requestInit = {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
body: options.body,
|
|
69
|
+
headers: requestHeaders(options.headers),
|
|
70
|
+
redirect: 'error',
|
|
71
|
+
};
|
|
72
|
+
let challengeResponse;
|
|
73
|
+
try {
|
|
74
|
+
challengeResponse = await fetcher(target, requestInit);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_transport', 'transport_failed');
|
|
78
|
+
}
|
|
79
|
+
result.challengeStatus = challengeResponse.status;
|
|
80
|
+
if (challengeResponse.status !== 402) {
|
|
81
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_status', 'payment_rejected');
|
|
82
|
+
}
|
|
83
|
+
const encodedRequired = challengeResponse.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
84
|
+
if (!encodedRequired) {
|
|
85
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_header', 'challenge_unsupported');
|
|
86
|
+
}
|
|
87
|
+
let required;
|
|
88
|
+
try {
|
|
89
|
+
required = decodeHeader(encodedRequired);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_invalid', 'challenge_unreadable');
|
|
93
|
+
}
|
|
94
|
+
if (required.x402Version !== 2
|
|
95
|
+
|| !Array.isArray(required.accepts)
|
|
96
|
+
|| !required.accepts.some((accepted) => accepted.scheme === ZK_PREPAID_SCHEME && accepted.network === BASE_SEPOLIA_NETWORK)) {
|
|
97
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_unsupported', 'challenge_unsupported');
|
|
98
|
+
}
|
|
99
|
+
const challengeUrl = required.resource?.url;
|
|
100
|
+
if (challengeUrl && challengeUrl !== target.toString()) {
|
|
101
|
+
return withFailure(options, result, startingCommittedSlots, 'challenge_request_mismatch', 'challenge_unsupported');
|
|
102
|
+
}
|
|
103
|
+
result.challengeReceived = true;
|
|
104
|
+
observe(options, lifecycleStage('challenge_received'));
|
|
105
|
+
const createPayment = createBasePaymentFactory(options);
|
|
106
|
+
const directAdapter = registerZkPrepaidClient(new x402Client((_version, accepts) => {
|
|
107
|
+
const selected = accepts.find((accepted) => accepted.scheme === ZK_PREPAID_SCHEME && accepted.network === BASE_SEPOLIA_NETWORK);
|
|
108
|
+
if (!selected)
|
|
109
|
+
throw new Error('zk_prepaid_challenge_missing');
|
|
110
|
+
return selected;
|
|
111
|
+
}).setSpendControls(false), async ({ requirements }) => {
|
|
112
|
+
const prepared = await createPayment({
|
|
113
|
+
url: target.toString(),
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: options.body,
|
|
116
|
+
requirements,
|
|
117
|
+
});
|
|
118
|
+
return prepared.payment;
|
|
119
|
+
});
|
|
120
|
+
let payment;
|
|
121
|
+
try {
|
|
122
|
+
payment = await directAdapter.createPaymentPayload(required);
|
|
123
|
+
result.proofPrepared = true;
|
|
124
|
+
observe(options, lifecycleStage('payment_prepared'));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return withFailure(options, result, startingCommittedSlots, 'proof_generation', 'payment_preparation_failed');
|
|
128
|
+
}
|
|
129
|
+
const headers = requestHeaders(options.headers);
|
|
130
|
+
headers.set(PAYMENT_SIGNATURE_HEADER, encodeHeader(payment));
|
|
131
|
+
result.paymentSignatureSent = true;
|
|
132
|
+
let paidResponse;
|
|
133
|
+
try {
|
|
134
|
+
paidResponse = await fetcher(target, { ...requestInit, headers });
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return withFailure(options, result, startingCommittedSlots, 'payment_transport', 'transport_failed');
|
|
138
|
+
}
|
|
139
|
+
result.paymentStatus = paidResponse.status;
|
|
140
|
+
result.responseStatus = paidResponse.status;
|
|
141
|
+
if (paidResponse.status === 402) {
|
|
142
|
+
return withFailure(options, result, startingCommittedSlots, 'payment_rejected', 'payment_rejected');
|
|
143
|
+
}
|
|
144
|
+
const encodedPaymentResponse = paidResponse.headers.get(PAYMENT_RESPONSE_HEADER);
|
|
145
|
+
if (!encodedPaymentResponse) {
|
|
146
|
+
return withFailure(options, result, startingCommittedSlots, 'payment_response_missing', 'settlement_failed');
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const settlement = decodeHeader(encodedPaymentResponse);
|
|
150
|
+
if (settlement.success !== true
|
|
151
|
+
|| settlement.network !== BASE_SEPOLIA_NETWORK
|
|
152
|
+
|| settlement.transaction !== '') {
|
|
153
|
+
return withFailure(options, result, startingCommittedSlots, 'payment_response_invalid', 'settlement_failed');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return withFailure(options, result, startingCommittedSlots, 'payment_response_invalid', 'settlement_failed');
|
|
158
|
+
}
|
|
159
|
+
result.paymentResponseConfirmed = true;
|
|
160
|
+
observe(options, lifecycleStage('settlement_confirmed'));
|
|
161
|
+
if (!paidResponse.ok) {
|
|
162
|
+
return withFailure(options, result, startingCommittedSlots, 'response_not_ok', 'payment_rejected');
|
|
163
|
+
}
|
|
164
|
+
result.responseSucceeded = true;
|
|
165
|
+
observe(options, lifecycleStage('exchange_succeeded'));
|
|
166
|
+
result.committedSlotDelta = ledger.snapshot().committed - startingCommittedSlots;
|
|
167
|
+
return result;
|
|
168
|
+
}
|
package/dist/base-sidecar.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
9
9
|
*/
|
|
10
10
|
import { type CreditCredential } from '@zk-credits/shared/base';
|
|
11
|
-
import { type PaymentRequirements, type ZkPrepaidClient, type ZkPrepaidLifecycleObserver } from '@zk-credits/x402-zk-prepaid';
|
|
11
|
+
import { type PaymentPayload, type PaymentRequirements, type ZkPrepaidClient, type ZkPrepaidLifecycleObserver } from '@zk-credits/x402-zk-prepaid';
|
|
12
12
|
import type { BaseSlotLedger } from './slot-ledger.js';
|
|
13
13
|
export interface BaseCreditWitness {
|
|
14
14
|
root: string;
|
|
@@ -69,6 +69,19 @@ export interface BasePrepaidClient {
|
|
|
69
69
|
/** Slots committed locally; a committed slot is never reused. */
|
|
70
70
|
committedSlots(): number[];
|
|
71
71
|
}
|
|
72
|
+
export interface BasePaymentContext {
|
|
73
|
+
url: string;
|
|
74
|
+
method: string;
|
|
75
|
+
body: unknown;
|
|
76
|
+
requirements: PaymentRequirements;
|
|
77
|
+
}
|
|
78
|
+
export interface BasePreparedPayment {
|
|
79
|
+
payment: PaymentPayload;
|
|
80
|
+
responsePrivateKey: string;
|
|
81
|
+
}
|
|
82
|
+
export declare function validateBaseCreditWitness(witness: BaseCreditWitness): BaseCreditWitness;
|
|
83
|
+
/** Creates the shared local proof factory used by both client transports. */
|
|
84
|
+
export declare function createBasePaymentFactory(options: BasePrepaidClientOptions): (context: BasePaymentContext) => Promise<BasePreparedPayment>;
|
|
72
85
|
/** Builds the reusable client adapter used by the loopback proxy. */
|
|
73
86
|
export declare function createBasePrepaidClient(options: BasePrepaidClientOptions): BasePrepaidClient;
|
|
74
87
|
/**
|
package/dist/base-sidecar.js
CHANGED
|
@@ -24,7 +24,7 @@ function field(value, label) {
|
|
|
24
24
|
throw new Error(`Invalid ${label}`);
|
|
25
25
|
return parsed.toString();
|
|
26
26
|
}
|
|
27
|
-
function
|
|
27
|
+
export function validateBaseCreditWitness(witness) {
|
|
28
28
|
if (typeof witness.root !== 'string'
|
|
29
29
|
|| witness.pathElements.length !== TREE_DEPTH
|
|
30
30
|
|| witness.pathIndices.length !== TREE_DEPTH
|
|
@@ -60,6 +60,77 @@ function requestBodyBytes(body) {
|
|
|
60
60
|
return new Uint8Array();
|
|
61
61
|
throw new Error('The Base x402 sidecar requires the exact request-body bytes');
|
|
62
62
|
}
|
|
63
|
+
/** Creates the shared local proof factory used by both client transports. */
|
|
64
|
+
export function createBasePaymentFactory(options) {
|
|
65
|
+
const secret = secretFromBase64Url(options.credential.secret);
|
|
66
|
+
return async ({ method, url, body, requirements }) => {
|
|
67
|
+
if (requirements.extra.deploymentDomain !== options.credential.deploymentDomain) {
|
|
68
|
+
throw new Error('Credential deployment domain does not match the gateway challenge');
|
|
69
|
+
}
|
|
70
|
+
const slotLedger = options.slotLedger;
|
|
71
|
+
if (!slotLedger)
|
|
72
|
+
throw new Error('A durable slot ledger is required for the Base pilot');
|
|
73
|
+
const selectedSlot = slotLedger.allocateProvisional();
|
|
74
|
+
try {
|
|
75
|
+
const nonce = randomNonce();
|
|
76
|
+
const responseKeys = await responseKey();
|
|
77
|
+
const requestSignal = await deriveRequestSignal({
|
|
78
|
+
method,
|
|
79
|
+
url,
|
|
80
|
+
body: requestBodyBytes(body),
|
|
81
|
+
requirements,
|
|
82
|
+
nonce,
|
|
83
|
+
responseKey: responseKeys.publicKey,
|
|
84
|
+
});
|
|
85
|
+
const witness = validateBaseCreditWitness(await options.witnessProvider.witnessForCredential(options.credential));
|
|
86
|
+
const expiry = witness.expiry ?? options.credential.expiry;
|
|
87
|
+
if (!Number.isSafeInteger(expiry) || expiry <= 0)
|
|
88
|
+
throw new Error('Invalid Base credential expiry');
|
|
89
|
+
const slotBlinding = await computeSlotBlinding(secret, selectedSlot, options.credential.deploymentDomain);
|
|
90
|
+
const nullifier = await computeNullifier(slotBlinding);
|
|
91
|
+
const share = await computeShare(secret, requestSignal.field, slotBlinding);
|
|
92
|
+
const timestamp = String(requirements.extra.issuedAt);
|
|
93
|
+
const expectedPublicSignals = [
|
|
94
|
+
witness.root,
|
|
95
|
+
timestamp,
|
|
96
|
+
options.credential.deploymentDomain,
|
|
97
|
+
requestSignal.field,
|
|
98
|
+
nullifier,
|
|
99
|
+
share,
|
|
100
|
+
];
|
|
101
|
+
const proof = await options.prove({
|
|
102
|
+
secret: secretToField(secret),
|
|
103
|
+
tier_id: String(options.credential.tierId),
|
|
104
|
+
expiry: String(expiry),
|
|
105
|
+
slot: String(selectedSlot),
|
|
106
|
+
merkle_path_elements: witness.pathElements,
|
|
107
|
+
merkle_path_indices: witness.pathIndices.map(String),
|
|
108
|
+
root_in: witness.root,
|
|
109
|
+
timestamp_in: timestamp,
|
|
110
|
+
domain_in: options.credential.deploymentDomain,
|
|
111
|
+
request_signal_in: requestSignal.field,
|
|
112
|
+
}, { requirements, credential: options.credential, expectedPublicSignals });
|
|
113
|
+
if (!Array.isArray(proof.publicSignals)
|
|
114
|
+
|| proof.publicSignals.length !== expectedPublicSignals.length
|
|
115
|
+
|| !proof.publicSignals.every((value, index) => value === expectedPublicSignals[index])) {
|
|
116
|
+
throw new Error('Proof public signals do not match the canonical statement');
|
|
117
|
+
}
|
|
118
|
+
await slotLedger.commit(selectedSlot);
|
|
119
|
+
const payment = buildPaymentPayload({
|
|
120
|
+
requirements,
|
|
121
|
+
proof: proof.proof,
|
|
122
|
+
publicSignals: [...proof.publicSignals],
|
|
123
|
+
nonce,
|
|
124
|
+
responseKey: responseKeys.publicKey,
|
|
125
|
+
});
|
|
126
|
+
return { payment, responsePrivateKey: responseKeys.privateKey };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
slotLedger.release(selectedSlot);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
63
134
|
function decryptReplay(envelopeValue, privateKey, paymentResponse) {
|
|
64
135
|
try {
|
|
65
136
|
const envelope = JSON.parse(envelopeValue);
|
|
@@ -89,7 +160,7 @@ function decryptReplay(envelopeValue, privateKey, paymentResponse) {
|
|
|
89
160
|
}
|
|
90
161
|
/** Builds the reusable client adapter used by the loopback proxy. */
|
|
91
162
|
export function createBasePrepaidClient(options) {
|
|
92
|
-
const
|
|
163
|
+
const createPayment = createBasePaymentFactory(options);
|
|
93
164
|
const fetcher = options.fetch ?? fetch;
|
|
94
165
|
const replayKeys = new Map();
|
|
95
166
|
const pendingPayments = new Map();
|
|
@@ -244,9 +315,6 @@ export function createBasePrepaidClient(options) {
|
|
|
244
315
|
fetch: fetchWithReplay,
|
|
245
316
|
...(options.lifecycle ? { lifecycle: options.lifecycle } : {}),
|
|
246
317
|
createPayload: async ({ method, url, body, requirements }) => {
|
|
247
|
-
if (requirements.extra.deploymentDomain !== options.credential.deploymentDomain) {
|
|
248
|
-
throw new Error('Credential deployment domain does not match the gateway challenge');
|
|
249
|
-
}
|
|
250
318
|
prunePending();
|
|
251
319
|
const key = requestKey(method, url, body, requirements);
|
|
252
320
|
const cached = pendingPayments.get(key);
|
|
@@ -258,73 +326,15 @@ export function createBasePrepaidClient(options) {
|
|
|
258
326
|
if (inFlight)
|
|
259
327
|
return inFlight;
|
|
260
328
|
const paymentPromise = (async () => {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
url,
|
|
271
|
-
body: requestBodyBytes(body),
|
|
272
|
-
requirements,
|
|
273
|
-
nonce,
|
|
274
|
-
responseKey: responseKeys.publicKey,
|
|
275
|
-
});
|
|
276
|
-
const witness = assertWitness(await options.witnessProvider.witnessForCredential(options.credential));
|
|
277
|
-
const expiry = witness.expiry ?? options.credential.expiry;
|
|
278
|
-
if (!Number.isSafeInteger(expiry) || expiry <= 0)
|
|
279
|
-
throw new Error('Invalid Base credential expiry');
|
|
280
|
-
const slotBlinding = await computeSlotBlinding(secret, selectedSlot, options.credential.deploymentDomain);
|
|
281
|
-
const nullifier = await computeNullifier(slotBlinding);
|
|
282
|
-
const share = await computeShare(secret, requestSignal.field, slotBlinding);
|
|
283
|
-
const timestamp = String(requirements.extra.issuedAt);
|
|
284
|
-
const expectedPublicSignals = [
|
|
285
|
-
witness.root,
|
|
286
|
-
timestamp,
|
|
287
|
-
options.credential.deploymentDomain,
|
|
288
|
-
requestSignal.field,
|
|
289
|
-
nullifier,
|
|
290
|
-
share,
|
|
291
|
-
];
|
|
292
|
-
const proof = await options.prove({
|
|
293
|
-
secret: secretToField(secret),
|
|
294
|
-
tier_id: String(options.credential.tierId),
|
|
295
|
-
expiry: String(expiry),
|
|
296
|
-
slot: String(selectedSlot),
|
|
297
|
-
merkle_path_elements: witness.pathElements,
|
|
298
|
-
merkle_path_indices: witness.pathIndices.map(String),
|
|
299
|
-
root_in: witness.root,
|
|
300
|
-
timestamp_in: timestamp,
|
|
301
|
-
domain_in: options.credential.deploymentDomain,
|
|
302
|
-
request_signal_in: requestSignal.field,
|
|
303
|
-
}, { requirements, credential: options.credential, expectedPublicSignals });
|
|
304
|
-
if (!Array.isArray(proof.publicSignals)
|
|
305
|
-
|| proof.publicSignals.length !== expectedPublicSignals.length
|
|
306
|
-
|| !proof.publicSignals.every((value, index) => value === expectedPublicSignals[index])) {
|
|
307
|
-
throw new Error('Proof public signals do not match the canonical statement');
|
|
308
|
-
}
|
|
309
|
-
await slotLedger.commit(selectedSlot);
|
|
310
|
-
const payment = buildPaymentPayload({
|
|
311
|
-
requirements,
|
|
312
|
-
proof: proof.proof,
|
|
313
|
-
publicSignals: [...proof.publicSignals],
|
|
314
|
-
nonce,
|
|
315
|
-
responseKey: responseKeys.publicKey,
|
|
316
|
-
});
|
|
317
|
-
const expiresAt = Date.now() + REPLAY_TTL_MS;
|
|
318
|
-
replayKeys.set(nonce, { privateKey: responseKeys.privateKey, expiresAt });
|
|
319
|
-
pendingPayments.set(key, { payment, expiresAt });
|
|
320
|
-
pendingByNonce.set(nonce, key);
|
|
321
|
-
prunePending();
|
|
322
|
-
return payment;
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
slotLedger.release(selectedSlot);
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
329
|
+
const prepared = await createPayment({ method, url, body, requirements });
|
|
330
|
+
const payment = prepared.payment;
|
|
331
|
+
const nonce = payment.payload.nonce;
|
|
332
|
+
const expiresAt = Date.now() + REPLAY_TTL_MS;
|
|
333
|
+
replayKeys.set(nonce, { privateKey: prepared.responsePrivateKey, expiresAt });
|
|
334
|
+
pendingPayments.set(key, { payment, expiresAt });
|
|
335
|
+
pendingByNonce.set(nonce, key);
|
|
336
|
+
prunePending();
|
|
337
|
+
return payment;
|
|
328
338
|
})();
|
|
329
339
|
paymentPromises.set(key, paymentPromise);
|
|
330
340
|
try {
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface CliCommandDependencies {
|
|
|
3
3
|
readToken(): Promise<string>;
|
|
4
4
|
write(line: string): void;
|
|
5
5
|
isCredentialConfigured(): Promise<boolean>;
|
|
6
|
+
validateSetupPrerequisites(): Promise<void>;
|
|
6
7
|
configureCodex(model?: string): Promise<void>;
|
|
7
8
|
ensureSidecar(): Promise<string>;
|
|
8
9
|
isCodexProfileInstalled(): Promise<boolean>;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -19,8 +19,9 @@ export async function runCliCommand(args, dependencies) {
|
|
|
19
19
|
const model = setupModel(args);
|
|
20
20
|
if (!await dependencies.isCredentialConfigured())
|
|
21
21
|
throw new Error('Set ZK_CREDITS_CREDENTIAL_PATH before running zk-credits setup');
|
|
22
|
-
await dependencies.
|
|
22
|
+
await dependencies.validateSetupPrerequisites();
|
|
23
23
|
await dependencies.ensureSidecar();
|
|
24
|
+
await dependencies.configureCodex(model);
|
|
24
25
|
dependencies.write('ZK Credits is ready for Codex.');
|
|
25
26
|
dependencies.write('Run: zk-credits codex');
|
|
26
27
|
return 0;
|