codai-sdk 0.6.0 → 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/README.md CHANGED
@@ -65,7 +65,7 @@ surface.
65
65
  | `receipt` | `get` | `GET /v1/receipt` |
66
66
  | `feedback` | `submit` | `POST /v1/feedback` |
67
67
  | `phoneModels` | `list` | `GET /v1/phone/models` |
68
- | `resolve` | `submit`, `get`, `accept`, `attestation`, `health`, `waitFor` | `https://resolve.codai.ro/v1/resolve/*` (own host) |
68
+ | `resolve` | `submit`, `get`, `accept`, `balance`, `attestation`, `keys`, `verifyAttestation`, `health`, `waitFor` | `https://resolve.codai.ro/v1/resolve/*` (own host) |
69
69
 
70
70
  Every method takes an optional last argument `{ ext, signal }` where `ext` is a
71
71
  typed bag of `X-Codai-*` extension headers (see below).
@@ -180,7 +180,8 @@ if (q.status === 'quoted') {
180
180
  try {
181
181
  await codai.resolve.accept(q.id); // free-tier jobs are auto-accepted
182
182
  } catch (err) {
183
- if (err instanceof ResolveInsufficientFundsError) console.log('top up:', err.topupUrl);
183
+ if (err instanceof ResolveInsufficientFundsError)
184
+ console.log('top up:', err.topupUrl, err.bundleUrl ?? ''); // bundleUrl: t7 only
184
185
  throw err;
185
186
  }
186
187
  }
@@ -198,6 +199,15 @@ with `balance`, `required`, `currency: 'micro_eur'` and `topupUrl`. `submit` and
198
199
  `accept` are never retried. `waitFor` stops at `resolved | failed | declined | error`
199
200
  (or `until`), and throws `ResolveError` with `status: 0` on timeout.
200
201
 
202
+ **Pricing.** Your first paid accept holds **€1** instead of the list price (the quote then
203
+ carries `intro_price_micro_eur` / `list_price_micro_eur`); it is used up only by a verified
204
+ fix — a failed intro job is refunded and the intro is offered again. A **fix bundle** (€49 = 10
205
+ credits, <https://pay.codai.ro/checkout?bundle=resolve10>) pays for `t7` jobs only: a credit is
206
+ taken at accept instead of a wallet hold and returned on failure. Precedence:
207
+ free > intro > credit (t7) > wallet; `accept()` reports it in `paid_with`, and a t7 `402`
208
+ carries `bundleUrl`. `codai.resolve.balance()` returns `fix_credits`, `intro_available` and
209
+ `wallet_micro_eur`.
210
+
201
211
  ### Webhooks instead of polling
202
212
 
203
213
  Pass `callback_url` (https, public host) on `submit`; the response carries
@@ -226,6 +236,24 @@ if (!verifyResolveWebhook(secret, raw, req.headers.get('x-codai-signature'))) {
226
236
  const evt = JSON.parse(raw) as ResolveWebhookEvent; // evt.event, evt.job.attestation_url
227
237
  ```
228
238
 
239
+ ### Verifying a signed attestation
240
+
241
+ Attestations created from 2026-09-27 carry `signature`: a DSSE envelope
242
+ (ECDSA P-256/SHA-256) over a canonical statement of sha256 hashes of the patch,
243
+ repro test and test outputs, plus the commands, base commit, runner digest and
244
+ timestamps. `verifyAttestation` checks the signature against
245
+ `GET /v1/resolve/keys` **and** that the statement matches the fields you were
246
+ served. Older attestations have `signature: null` → `{ ok: false, reason: 'UNSIGNED' }`.
247
+
248
+ ```ts
249
+ const proof = await codai.resolve.attestation(job.attestation_url!);
250
+ const r = await codai.resolve.verifyAttestation(proof); // or { keys: pinnedKeys }
251
+ if (!r.ok) throw new Error(`attestation not verified: ${r.reason}`);
252
+ // r.keyid — the signing key; reasons: UNSIGNED | BAD_SIGNATURE | STATEMENT_MISMATCH | BAD_PAYLOAD_TYPE
253
+ ```
254
+
255
+ Offline (no network): `verifyAttestationWithKeys(proof, keys)`. Server-side only (`node:crypto`).
256
+
229
257
  ## Shared sessions, devices and hosts
230
258
 
231
259
  Shared-sessions and hosts calls need a device UUID (`x-codai-device`). Set it once
package/dist/index.d.ts CHANGED
@@ -10940,6 +10940,28 @@ type paths = {
10940
10940
  readonly patch?: never;
10941
10941
  readonly trace?: never;
10942
10942
  };
10943
+ readonly '/v1/resolve/balance': {
10944
+ readonly parameters: {
10945
+ readonly query?: never;
10946
+ readonly header?: never;
10947
+ readonly path?: never;
10948
+ readonly cookie?: never;
10949
+ };
10950
+ /**
10951
+ * Fix credits, intro availability and wallet balance
10952
+ * @description Returns what you can pay with right now: the fix-credit balance (usable only on `t7`
10953
+ * jobs), whether the €1 intro price is still available (no verified paid job yet and no
10954
+ * other intro job in flight) and the wallet balance in EUR micros.
10955
+ */
10956
+ readonly get: operations['getResolveBalance'];
10957
+ readonly put?: never;
10958
+ readonly post?: never;
10959
+ readonly delete?: never;
10960
+ readonly options?: never;
10961
+ readonly head?: never;
10962
+ readonly patch?: never;
10963
+ readonly trace?: never;
10964
+ };
10943
10965
  readonly '/v1/resolve/jobs': {
10944
10966
  readonly parameters: {
10945
10967
  readonly query?: never;
@@ -11016,6 +11038,11 @@ type paths = {
11016
11038
  * price). The hold is kept on a verified fix and refunded in full otherwise. If the
11017
11039
  * balance is short, the answer is `402 insufficient_funds` with the shortfall and a
11018
11040
  * top-up link, and the job stays `quoted` — top up, then call `/accept` again.
11041
+ *
11042
+ * Payment precedence: `intro` (first paid job, €1 hold) > `credit` (t7 only, one fix credit,
11043
+ * no wallet hold) > `wallet` (hold of the list price). `paid_with` in the response says which
11044
+ * was used; `held_micro_eur` is the wallet amount held (`0` for a credit). For a `t7` job the
11045
+ * `402` also carries `bundle_url` (10 fixes for €49).
11019
11046
  */
11020
11047
  readonly post: operations['acceptResolveJob'];
11021
11048
  readonly delete?: never;
@@ -11024,13 +11051,43 @@ type paths = {
11024
11051
  readonly patch?: never;
11025
11052
  readonly trace?: never;
11026
11053
  };
11054
+ readonly '/v1/resolve/keys': {
11055
+ readonly parameters: {
11056
+ readonly query?: never;
11057
+ readonly header?: never;
11058
+ readonly path?: never;
11059
+ readonly cookie?: never;
11060
+ };
11061
+ /**
11062
+ * Public keys that sign attestations
11063
+ * @description Returns the public key(s) used to sign attestation envelopes (`signature` on
11064
+ * `GET /v1/resolve/attestations/{id}`). ECDSA P-256 / SHA-256 over the DSSE v1 PAE of the
11065
+ * payload; the signature is DER, base64. **Unauthenticated**, CORS-open and cacheable for
11066
+ * 1 hour. Verify with `codaid resolve verify`, `resolve.verifyAttestation` (TypeScript) or
11067
+ * `resolve.verify_attestation` (Python). `503` when signing is not configured.
11068
+ */
11069
+ readonly get: operations['getResolveKeys'];
11070
+ readonly put?: never;
11071
+ readonly post?: never;
11072
+ readonly delete?: never;
11073
+ readonly options?: never;
11074
+ readonly head?: never;
11075
+ readonly patch?: never;
11076
+ readonly trace?: never;
11077
+ };
11027
11078
  };
11028
11079
  type components = {
11029
11080
  schemas: {
11030
11081
  /** Accepted */
11031
11082
  readonly ResolveAccepted: {
11083
+ /**
11084
+ * @description Amount held on the wallet in EUR micros — `1000000` for the intro, the quote for `wallet`, `0` for `credit`.
11085
+ * @example 1000000
11086
+ */
11087
+ readonly held_micro_eur: number;
11032
11088
  /** Format: uuid */
11033
11089
  readonly id: string;
11090
+ readonly paid_with: components['schemas']['ResolvePaidWith'];
11034
11091
  /** @constant */
11035
11092
  readonly status: 'accepted';
11036
11093
  };
@@ -11059,6 +11116,13 @@ type components = {
11059
11116
  readonly repro_test: components['schemas']['ResolveReproTest'] | null;
11060
11117
  /** @description Container image digest of the verification runner (`unknown` when not recorded). */
11061
11118
  readonly runner_image_digest: string;
11119
+ /**
11120
+ * @description DSSE envelope signing the canonical statement of this attestation (sha256 of the patch,
11121
+ * repro test, test outputs; commands, commit, runner digest, timestamps). `null` for
11122
+ * attestations created before signing was enabled (2026-09-27). Verify against
11123
+ * `GET /v1/resolve/keys`.
11124
+ */
11125
+ readonly signature: components['schemas']['ResolveAttestationEnvelope'] | null;
11062
11126
  /** Format: date-time */
11063
11127
  readonly started_at: string;
11064
11128
  /** @description FAIL_TO_PASS command that was executed. */
@@ -11070,6 +11134,46 @@ type components = {
11070
11134
  /** Format: date-time */
11071
11135
  readonly verified_at: string;
11072
11136
  };
11137
+ /**
11138
+ * Attestation envelope
11139
+ * @description DSSE v1 envelope. `payload` is base64 of the RFC 8785 (JCS) canonical statement JSON
11140
+ * `{kind: "codai.resolve.attestation.v1", attestation_id, repo_url, base_commit, patch_sha256,
11141
+ * repro_test: {path, sha256} | null, test_command, regression_command,
11142
+ * test_output_before_sha256, test_output_after_sha256, regression_output_sha256,
11143
+ * runner_image_digest, started_at, verified_at}`. Each `sig` is an ECDSA P-256/SHA-256 DER
11144
+ * signature (base64) over `PAE(payloadType, payload bytes)` =
11145
+ * `"DSSEv1 " + len(type) + " " + type + " " + len(body) + " " + body`.
11146
+ */
11147
+ readonly ResolveAttestationEnvelope: {
11148
+ /** @description Base64 of the canonical statement JSON. */
11149
+ readonly payload: string;
11150
+ /** @constant */
11151
+ readonly payloadType: 'application/vnd.codai.resolve.attestation+json;v=1';
11152
+ readonly signatures: readonly {
11153
+ /** @description Identifier of the signing key; matches `keyid` in `GET /v1/resolve/keys`. */
11154
+ readonly keyid?: string;
11155
+ /** @description DER-encoded ECDSA signature, base64. */
11156
+ readonly sig: string;
11157
+ }[];
11158
+ };
11159
+ /**
11160
+ * Balance
11161
+ * @description Body of `GET /v1/resolve/balance`.
11162
+ */
11163
+ readonly ResolveBalance: {
11164
+ /**
11165
+ * @description Fix credits left (usable only on `t7` jobs). Buy 10 for €49 at https://pay.codai.ro/checkout?bundle=resolve10.
11166
+ * @example 10
11167
+ */
11168
+ readonly fix_credits: number;
11169
+ /** @description `true` while your next paid accept would hold the €1 intro price. */
11170
+ readonly intro_available: boolean;
11171
+ /**
11172
+ * @description Wallet balance in EUR micros (`5000000` = €5.00).
11173
+ * @example 5000000
11174
+ */
11175
+ readonly wallet_micro_eur: number;
11176
+ };
11073
11177
  /**
11074
11178
  * Declined
11075
11179
  * @description `200` body — triage declined the issue. The job is persisted with `status: declined` and is never billed.
@@ -11114,6 +11218,12 @@ type components = {
11114
11218
  * @example 5000000
11115
11219
  */
11116
11220
  readonly balance: number;
11221
+ /**
11222
+ * Format: uri
11223
+ * @description Present only for a `t7` job — checkout for the fix bundle (10 t7 fix credits for €49).
11224
+ * @example https://pay.codai.ro/checkout?bundle=resolve10
11225
+ */
11226
+ readonly bundle_url?: string;
11117
11227
  /**
11118
11228
  * @description Unit of `balance` and `required`.
11119
11229
  * @constant
@@ -11194,6 +11304,8 @@ type components = {
11194
11304
  readonly fail_reason: string | null;
11195
11305
  /** Format: uuid */
11196
11306
  readonly id: string;
11307
+ /** @description Present once the job was accepted (and recorded how it was paid). */
11308
+ readonly paid_with?: components['schemas']['ResolvePaidWith'];
11197
11309
  /** @description Quoted price in USD micros; `null` until quoted, `0` on the free tier. */
11198
11310
  readonly quote_micro_usd: number | null;
11199
11311
  /**
@@ -11213,6 +11325,34 @@ type components = {
11213
11325
  * @enum {string}
11214
11326
  */
11215
11327
  readonly ResolveJobStatus: 'triage' | 'quoted' | 'declined' | 'accepted' | 'running' | 'resolved' | 'failed' | 'error';
11328
+ /** Verification key */
11329
+ readonly ResolveKey: {
11330
+ /** @constant */
11331
+ readonly algorithm: 'ECDSA_P256_SHA256';
11332
+ /**
11333
+ * @description Envelope and signature encoding.
11334
+ * @example DSSE v1; signature DER, base64
11335
+ */
11336
+ readonly encoding: string;
11337
+ /** @description Key identifier (the Cloud KMS key version name). */
11338
+ readonly keyid: string;
11339
+ /** @constant */
11340
+ readonly payload_type: 'application/vnd.codai.resolve.attestation+json;v=1';
11341
+ /** @description SPKI public key, PEM. */
11342
+ readonly public_key_pem: string;
11343
+ };
11344
+ /** Verification keys */
11345
+ readonly ResolveKeys: {
11346
+ readonly keys: readonly components['schemas']['ResolveKey'][];
11347
+ };
11348
+ /**
11349
+ * Payment method
11350
+ * @description How an accepted job is paid: `free` (free tier), `intro` (first paid job, €1 hold),
11351
+ * `credit` (one t7 fix credit, no wallet hold) or `wallet` (hold of the list price).
11352
+ * Precedence: `free` > `intro` > `credit` > `wallet`.
11353
+ * @enum {string}
11354
+ */
11355
+ readonly ResolvePaidWith: 'intro' | 'credit' | 'wallet' | 'free';
11216
11356
  /**
11217
11357
  * Quote
11218
11358
  * @description `201` body — the job was quoted (`quoted`) or auto-accepted on the free tier (`accepted`).
@@ -11228,6 +11368,17 @@ type components = {
11228
11368
  readonly free_tier_remaining: number;
11229
11369
  /** Format: uuid */
11230
11370
  readonly id: string;
11371
+ /**
11372
+ * @description Present only on a `quoted` job while the intro price applies to you: accepting holds
11373
+ * this amount (EUR micros, `1000000` = €1) instead of the list price.
11374
+ * @example 1000000
11375
+ */
11376
+ readonly intro_price_micro_eur?: number;
11377
+ /**
11378
+ * @description Present together with `intro_price_micro_eur`; the regular hold in EUR micros (the quote 1:1).
11379
+ * @example 7000000
11380
+ */
11381
+ readonly list_price_micro_eur?: number;
11231
11382
  /**
11232
11383
  * @description Quoted price in USD micros (`7000000` = $7). `0` on the free tier.
11233
11384
  * @example 7000000
@@ -11348,7 +11499,8 @@ type components = {
11348
11499
  /**
11349
11500
  * @description The wallet balance does not cover the quote. Nothing was debited and the job is still
11350
11501
  * `quoted`. Open `topup_url` (pay.codai.ro checkout, shortfall rounded up to whole euros,
11351
- * minimum €10, maximum €1000), then retry `/accept`.
11502
+ * minimum €10, maximum €1000) — or, for a `t7` job, `bundle_url` (10 fix credits for €49) —
11503
+ * then retry `/accept`.
11352
11504
  */
11353
11505
  readonly ResolveInsufficientFunds: {
11354
11506
  headers: {
@@ -11452,6 +11604,27 @@ interface operations {
11452
11604
  readonly 404: components['responses']['ResolveNotFound'];
11453
11605
  };
11454
11606
  };
11607
+ readonly getResolveBalance: {
11608
+ readonly parameters: {
11609
+ readonly query?: never;
11610
+ readonly header?: never;
11611
+ readonly path?: never;
11612
+ readonly cookie?: never;
11613
+ };
11614
+ readonly requestBody?: never;
11615
+ readonly responses: {
11616
+ /** @description Current balance. */
11617
+ readonly 200: {
11618
+ headers: {
11619
+ readonly [name: string]: unknown;
11620
+ };
11621
+ content: {
11622
+ readonly 'application/json': components['schemas']['ResolveBalance'];
11623
+ };
11624
+ };
11625
+ readonly 401: components['responses']['ResolveUnauthorized'];
11626
+ };
11627
+ };
11455
11628
  readonly createResolveJob: {
11456
11629
  readonly parameters: {
11457
11630
  readonly query?: never;
@@ -11541,6 +11714,35 @@ interface operations {
11541
11714
  readonly 409: components['responses']['ResolveConflict'];
11542
11715
  };
11543
11716
  };
11717
+ readonly getResolveKeys: {
11718
+ readonly parameters: {
11719
+ readonly query?: never;
11720
+ readonly header?: never;
11721
+ readonly path?: never;
11722
+ readonly cookie?: never;
11723
+ };
11724
+ readonly requestBody?: never;
11725
+ readonly responses: {
11726
+ /** @description Current verification keys. */
11727
+ readonly 200: {
11728
+ headers: {
11729
+ readonly [name: string]: unknown;
11730
+ };
11731
+ content: {
11732
+ readonly 'application/json': components['schemas']['ResolveKeys'];
11733
+ };
11734
+ };
11735
+ /** @description Attestation signing is not configured (`attestation signing not configured`). */
11736
+ readonly 503: {
11737
+ headers: {
11738
+ readonly [name: string]: unknown;
11739
+ };
11740
+ content: {
11741
+ readonly 'application/json': components['schemas']['ResolveError'];
11742
+ };
11743
+ };
11744
+ };
11745
+ };
11544
11746
  readonly resolveJobWebhook: {
11545
11747
  readonly parameters: {
11546
11748
  readonly query?: never;
@@ -11588,10 +11790,18 @@ type ResolveDeclined = ResolveSchemas['ResolveDeclined'];
11588
11790
  /** `submit()` result: `201` quote / free-tier auto-accept, or `200` decline. */
11589
11791
  type ResolveSubmitResult = ResolveQuote | ResolveDeclined;
11590
11792
  type ResolveAccepted = ResolveSchemas['ResolveAccepted'];
11793
+ /** How an accepted job is paid: `free` > `intro` (€1) > `credit` (t7 fix credit) > `wallet`. */
11794
+ type ResolvePaidWith = ResolveSchemas['ResolvePaidWith'];
11795
+ /** `GET /v1/resolve/balance` — fix credits, intro availability, wallet (EUR micros). */
11796
+ type ResolveBalance = ResolveSchemas['ResolveBalance'];
11591
11797
  type ResolveJob = ResolveSchemas['ResolveJob'];
11592
11798
  type ResolveJobStatus = ResolveSchemas['ResolveJobStatus'];
11593
11799
  type ResolveTier = ResolveSchemas['ResolveTier'];
11594
11800
  type ResolveAttestation = ResolveSchemas['ResolveAttestation'];
11801
+ /** DSSE v1 envelope in `ResolveAttestation.signature` (`null` = unsigned, pre-2026-09-27). */
11802
+ type ResolveAttestationEnvelope = ResolveSchemas['ResolveAttestationEnvelope'];
11803
+ type ResolveKeys = ResolveSchemas['ResolveKeys'];
11804
+ type ResolveKey = ResolveSchemas['ResolveKey'];
11595
11805
  type ResolveReproTest = ResolveSchemas['ResolveReproTest'];
11596
11806
  type ResolveHealth = ResolveSchemas['ResolveHealth'];
11597
11807
  type ResolveValidationDetails = ResolveSchemas['ResolveValidationDetails'];
@@ -11617,6 +11827,25 @@ interface VerifyResolveWebhookOptions {
11617
11827
  * Throws only when `node:crypto` is unavailable in the current runtime.
11618
11828
  */
11619
11829
  declare function verifyResolveWebhook(secret: string, rawBody: string | Uint8Array, signatureHeader: string | null | undefined, opts?: VerifyResolveWebhookOptions): boolean;
11830
+ /** `payloadType` of Resolve attestation envelopes. */
11831
+ declare const RESOLVE_ATTESTATION_PAYLOAD_TYPE = "application/vnd.codai.resolve.attestation+json;v=1";
11832
+ type ResolveAttestationVerifyResult = {
11833
+ ok: true;
11834
+ keyid: string;
11835
+ } | {
11836
+ ok: false;
11837
+ reason: 'UNSIGNED' | 'BAD_SIGNATURE' | 'STATEMENT_MISMATCH' | 'BAD_PAYLOAD_TYPE';
11838
+ };
11839
+ /** The statement the Resolve service signs, recomputed from the attestation JSON. */
11840
+ declare function resolveAttestationStatement(att: ResolveAttestation): Record<string, unknown>;
11841
+ /**
11842
+ * Verify a Resolve attestation offline against published keys: the DSSE
11843
+ * signature (ECDSA P-256/SHA-256, DER) over `PAE(payloadType, payload)` must
11844
+ * match one key, AND the signed statement must equal the one recomputed from
11845
+ * the attestation's own fields (so a valid signature over other content fails).
11846
+ * Server-side only (`node:crypto`, loaded lazily).
11847
+ */
11848
+ declare function verifyAttestationWithKeys(att: ResolveAttestation, keys: ResolveKeys): ResolveAttestationVerifyResult;
11620
11849
  /** Statuses after which a job never changes again. */
11621
11850
  declare const RESOLVE_TERMINAL_STATUSES: ReadonlySet<ResolveJobStatus>;
11622
11851
  /** Any non-2xx answer from the Resolve service (flat `{ error }` body). */
@@ -11637,6 +11866,8 @@ declare class ResolveInsufficientFundsError extends ResolveError {
11637
11866
  readonly currency: 'micro_eur';
11638
11867
  /** pay.codai.ro checkout link for the shortfall — open it, then retry `accept()`. */
11639
11868
  readonly topupUrl: string;
11869
+ /** Present only for a `t7` job: fix-bundle checkout (10 t7 fix credits for €49). */
11870
+ readonly bundleUrl?: string;
11640
11871
  constructor(message: string, body: ResolveInsufficientFunds, meta?: {
11641
11872
  requestId?: string | null;
11642
11873
  });
@@ -11669,8 +11900,10 @@ declare class Resolve {
11669
11900
  get(id: string, opts?: CallOpts$4): Promise<ResolveJob>;
11670
11901
  /**
11671
11902
  * `POST /v1/resolve/jobs/{id}/accept` — accept a paid quote and start the job.
11672
- * Throws `ResolveInsufficientFundsError` (402, carries `topupUrl`) when the
11673
- * wallet is short, `ResolveError` 409 when the job is not `quoted`.
11903
+ * The result's `paid_with` says how it was paid (`intro` €1 hold, `credit`
11904
+ * t7 fix credit, or `wallet`). Throws `ResolveInsufficientFundsError` (402,
11905
+ * carries `topupUrl`, plus `bundleUrl` for t7) when the wallet is short,
11906
+ * `ResolveError` 409 when the job is not `quoted`.
11674
11907
  */
11675
11908
  accept(id: string, opts?: CallOpts$4): Promise<ResolveAccepted>;
11676
11909
  /**
@@ -11680,6 +11913,20 @@ declare class Resolve {
11680
11913
  attestation(idOrUrl: string, opts?: CallOpts$4): Promise<ResolveAttestation>;
11681
11914
  /** `GET /health` — liveness of the Resolve service. */
11682
11915
  health(opts?: CallOpts$4): Promise<ResolveHealth>;
11916
+ /** `GET /v1/resolve/balance` — fix credits (t7 only), intro availability and wallet balance. */
11917
+ balance(opts?: CallOpts$4): Promise<ResolveBalance>;
11918
+ /** `GET /v1/resolve/keys` — public keys that sign attestations (no auth needed server-side). */
11919
+ keys(opts?: CallOpts$4): Promise<ResolveKeys>;
11920
+ /**
11921
+ * Verify an attestation's signature and that the signed statement matches
11922
+ * its fields. Fetches `keys()` unless `opts.keys` is given (pin them to
11923
+ * avoid trusting the network). `{ ok: false, reason: 'UNSIGNED' }` for
11924
+ * attestations created before signing (2026-09-27). Server-side only.
11925
+ */
11926
+ verifyAttestation(att: ResolveAttestation, opts?: {
11927
+ keys?: ResolveKeys;
11928
+ signal?: AbortSignal;
11929
+ }): Promise<ResolveAttestationVerifyResult>;
11683
11930
  /**
11684
11931
  * Poll `get(id)` until the job reaches a terminal status (`resolved`,
11685
11932
  * `failed`, `declined`, `error`) or one of `opts.until`. A `quoted` job
@@ -12186,4 +12433,4 @@ declare class Codai {
12186
12433
  private legacyExt;
12187
12434
  }
12188
12435
 
12189
- export { Account, type AccountPatchRequest, type AccountPatchResponse, type AccountView, type AgentRun, type AgentRunAccepted, type AgentRunCreateRequest, type AgentRunRequest, type AgentRunResult, type AgentRunStats, type AgentRunStatus, type AgentRunStep, type AgentRunStreamEvent, AgentRuns, type AgentSyncResult, Agents, Audio, Chat, type ChatCallOptions, type ChatCompletionChunk, type ChatCompletionInput, type ChatCompletionRequest, type ChatCompletionResponse, ChatCompletions, type ChatMessage, type ChatOptions, type ChatResult, type ChatStream, type ChatStreamResult, type ChatTool, type ChatToolCall, type ChatUsage, Codai, type CodaiClientOptions, CodaiError, type CodaiRequestExtensions, type Device, type DevicePatch, type DevicePatched, Devices, type DispatchInbox, Embeddings, type EmbeddingsOptions, type EmbeddingsRequest, type EmbeddingsResponse, type EmbeddingsResult, type Environment, type EnvironmentAddMemberBody, type EnvironmentCreateBody, type EnvironmentDetail, type EnvironmentEnrollBody, type EnvironmentEnrollToken, type EnvironmentEnrolled, type EnvironmentMember, type EnvironmentMemberCreated, EnvironmentMembers, type EnvironmentPatchBody, type EnvironmentPort, type EnvironmentPortBody, EnvironmentPorts, type EnvironmentProvider, type EnvironmentRole, type EnvironmentSecret, type EnvironmentSecretSet, EnvironmentSecrets, type EnvironmentSshCa, type EnvironmentSshCert, type EnvironmentSshCertBody, type EnvironmentState, type EnvironmentTaskCreateBody, type EnvironmentTaskCreated, type EnvironmentTaskDetail, type EnvironmentTaskList, EnvironmentTasks, type EnvironmentTransition, Environments, type EphemeralScope, type EphemeralToken, type EphemeralTokenResponse, type ErrorCode, Feedback, type FeedbackRequest, type FeedbackResponse, Health, type HealthReadyResponse, type HealthResponse, type HostExecBody, type HostExecRequest, type HostExecResponse, type HostExecResultBody, type HostOp, type HostPresence, type HostStreamEvent, Hosts, HttpClient, type ListEnvironmentsQuery, type MessageCallOptions, type MessageInput, type MessageRequest, type MessageResponse, type MessageResult, type MessageStream, type MessageStreamEvent, type MessageStreamResult, Messages, type Model, type ModelList, Models, type NotificationList, Notifications, type NotificationsDeleted, type NotificationsRead, OPERATION_METHODS, type Org, type OrgAddMemberBody, type OrgMember, OrgMembers, type OrgRole, Orgs, type PhoneModel, PhoneModels, type Project, type ProjectCreateBody, type ProjectDetail, type ProjectIsolation, type ProjectPatchBody, Projects, RESOLVE_OPERATION_METHODS, RESOLVE_TERMINAL_STATUSES, type Receipt, Receipts, type RequestOptions, Resolve, type ResolveAccepted, type ResolveAttestation, type components as ResolveComponents, type ResolveDeclined, ResolveError, type ResolveHealth, type ResolveInsufficientFunds, ResolveInsufficientFundsError, type ResolveIntake, type ResolveJob, type ResolveJobStatus, type operations as ResolveOperations, type paths as ResolvePaths, type ResolveQuote, type ResolveReproTest, type ResolveSubmitResult, type ResolveTier, type ResolveValidationDetails, type ResolveWaitOptions, type ResolveWebhookEvent, type ResolveWebhookEventName, type ResolveWebhookJob, Responses, type ResponsesCallOptions, type ResponsesInput, type ResponsesRequest, type ResponsesResponse, type ResponsesResult, type ResponsesStream, type ResponsesStreamEvent, type ResponsesStreamResult, STREAMING_OPERATIONS, type Session, type SessionControl, type SessionControlAccepted, type SessionControlRequest, SessionControls, type SessionCreateRequest, type SessionDetail, type SessionDispatchRequest, type SessionDispatchResponse, type SessionEvent, SessionEvents, type SessionEventsAppendRequest, type SessionEventsAppendResponse, type SessionEventsPage, type SessionIncomingEvent, SessionLease, type SessionLeaseClaimRequest, type SessionLeaseResponse, type SessionListItem, type SessionPatchRequest, type SessionShare, type SessionShareCreateRequest, type SessionShareCreated, SessionShares, type SessionStreamEvent, Sessions, type SharedSession, type SpeechRequest, type SpeechResult, type SseFrame, type StatusResponse, type Task, type TaskConfirmRequest, type TaskConfirmResponse, type TaskListPage, type TaskListQuery, type TaskOutcome, type TaskStats, Tasks, type TokenUsage, Tokens, type ToolFetchRequest, type ToolFetchResponse, type ToolSearchRequest, type ToolSearchResponse, Tools, type TranscriptionOptions, type TranscriptionResponse, type TranscriptionResult, type TriggerCreateRequest, type TriggerCreated, type TriggerEvents, type TriggerList, type TriggerPatch, type TriggerTestRequest, type TriggerTestResult, type TriggerUpdated, Triggers, type VerifyResolveWebhookOptions, type components$1 as components, Codai as default, extensionHeaders, frameJson, mergeToolCall, type operations$1 as operations, type paths$1 as paths, readSse, verifyResolveWebhook };
12436
+ export { Account, type AccountPatchRequest, type AccountPatchResponse, type AccountView, type AgentRun, type AgentRunAccepted, type AgentRunCreateRequest, type AgentRunRequest, type AgentRunResult, type AgentRunStats, type AgentRunStatus, type AgentRunStep, type AgentRunStreamEvent, AgentRuns, type AgentSyncResult, Agents, Audio, Chat, type ChatCallOptions, type ChatCompletionChunk, type ChatCompletionInput, type ChatCompletionRequest, type ChatCompletionResponse, ChatCompletions, type ChatMessage, type ChatOptions, type ChatResult, type ChatStream, type ChatStreamResult, type ChatTool, type ChatToolCall, type ChatUsage, Codai, type CodaiClientOptions, CodaiError, type CodaiRequestExtensions, type Device, type DevicePatch, type DevicePatched, Devices, type DispatchInbox, Embeddings, type EmbeddingsOptions, type EmbeddingsRequest, type EmbeddingsResponse, type EmbeddingsResult, type Environment, type EnvironmentAddMemberBody, type EnvironmentCreateBody, type EnvironmentDetail, type EnvironmentEnrollBody, type EnvironmentEnrollToken, type EnvironmentEnrolled, type EnvironmentMember, type EnvironmentMemberCreated, EnvironmentMembers, type EnvironmentPatchBody, type EnvironmentPort, type EnvironmentPortBody, EnvironmentPorts, type EnvironmentProvider, type EnvironmentRole, type EnvironmentSecret, type EnvironmentSecretSet, EnvironmentSecrets, type EnvironmentSshCa, type EnvironmentSshCert, type EnvironmentSshCertBody, type EnvironmentState, type EnvironmentTaskCreateBody, type EnvironmentTaskCreated, type EnvironmentTaskDetail, type EnvironmentTaskList, EnvironmentTasks, type EnvironmentTransition, Environments, type EphemeralScope, type EphemeralToken, type EphemeralTokenResponse, type ErrorCode, Feedback, type FeedbackRequest, type FeedbackResponse, Health, type HealthReadyResponse, type HealthResponse, type HostExecBody, type HostExecRequest, type HostExecResponse, type HostExecResultBody, type HostOp, type HostPresence, type HostStreamEvent, Hosts, HttpClient, type ListEnvironmentsQuery, type MessageCallOptions, type MessageInput, type MessageRequest, type MessageResponse, type MessageResult, type MessageStream, type MessageStreamEvent, type MessageStreamResult, Messages, type Model, type ModelList, Models, type NotificationList, Notifications, type NotificationsDeleted, type NotificationsRead, OPERATION_METHODS, type Org, type OrgAddMemberBody, type OrgMember, OrgMembers, type OrgRole, Orgs, type PhoneModel, PhoneModels, type Project, type ProjectCreateBody, type ProjectDetail, type ProjectIsolation, type ProjectPatchBody, Projects, RESOLVE_ATTESTATION_PAYLOAD_TYPE, RESOLVE_OPERATION_METHODS, RESOLVE_TERMINAL_STATUSES, type Receipt, Receipts, type RequestOptions, Resolve, type ResolveAccepted, type ResolveAttestation, type ResolveAttestationEnvelope, type ResolveAttestationVerifyResult, type ResolveBalance, type components as ResolveComponents, type ResolveDeclined, ResolveError, type ResolveHealth, type ResolveInsufficientFunds, ResolveInsufficientFundsError, type ResolveIntake, type ResolveJob, type ResolveJobStatus, type ResolveKey, type ResolveKeys, type operations as ResolveOperations, type ResolvePaidWith, type paths as ResolvePaths, type ResolveQuote, type ResolveReproTest, type ResolveSubmitResult, type ResolveTier, type ResolveValidationDetails, type ResolveWaitOptions, type ResolveWebhookEvent, type ResolveWebhookEventName, type ResolveWebhookJob, Responses, type ResponsesCallOptions, type ResponsesInput, type ResponsesRequest, type ResponsesResponse, type ResponsesResult, type ResponsesStream, type ResponsesStreamEvent, type ResponsesStreamResult, STREAMING_OPERATIONS, type Session, type SessionControl, type SessionControlAccepted, type SessionControlRequest, SessionControls, type SessionCreateRequest, type SessionDetail, type SessionDispatchRequest, type SessionDispatchResponse, type SessionEvent, SessionEvents, type SessionEventsAppendRequest, type SessionEventsAppendResponse, type SessionEventsPage, type SessionIncomingEvent, SessionLease, type SessionLeaseClaimRequest, type SessionLeaseResponse, type SessionListItem, type SessionPatchRequest, type SessionShare, type SessionShareCreateRequest, type SessionShareCreated, SessionShares, type SessionStreamEvent, Sessions, type SharedSession, type SpeechRequest, type SpeechResult, type SseFrame, type StatusResponse, type Task, type TaskConfirmRequest, type TaskConfirmResponse, type TaskListPage, type TaskListQuery, type TaskOutcome, type TaskStats, Tasks, type TokenUsage, Tokens, type ToolFetchRequest, type ToolFetchResponse, type ToolSearchRequest, type ToolSearchResponse, Tools, type TranscriptionOptions, type TranscriptionResponse, type TranscriptionResult, type TriggerCreateRequest, type TriggerCreated, type TriggerEvents, type TriggerList, type TriggerPatch, type TriggerTestRequest, type TriggerTestResult, type TriggerUpdated, Triggers, type VerifyResolveWebhookOptions, type components$1 as components, Codai as default, extensionHeaders, frameJson, mergeToolCall, type operations$1 as operations, type paths$1 as paths, readSse, resolveAttestationStatement, verifyAttestationWithKeys, verifyResolveWebhook };
package/dist/index.js CHANGED
@@ -1260,6 +1260,105 @@ function hexToBytes(hex) {
1260
1260
  for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1261
1261
  return out;
1262
1262
  }
1263
+ var RESOLVE_ATTESTATION_PAYLOAD_TYPE = "application/vnd.codai.resolve.attestation+json;v=1";
1264
+ function verifyCrypto() {
1265
+ const proc = globalThis.process;
1266
+ const mod = proc?.getBuiltinModule?.("node:crypto");
1267
+ if (!mod) {
1268
+ throw new Error(
1269
+ "verifyAttestation needs node:crypto (Node 20.16+/22.3+, Bun or Deno); run it server-side"
1270
+ );
1271
+ }
1272
+ return mod;
1273
+ }
1274
+ function canonicalJson(v) {
1275
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
1276
+ if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
1277
+ const o = v;
1278
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
1279
+ }
1280
+ function base64ToBytes(b64) {
1281
+ const bin = atob(b64);
1282
+ const out = new Uint8Array(bin.length);
1283
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1284
+ return out;
1285
+ }
1286
+ function pae(payloadType, body) {
1287
+ const enc2 = new TextEncoder();
1288
+ const typeBytes = enc2.encode(payloadType);
1289
+ const head = enc2.encode(`DSSEv1 ${typeBytes.length} ${payloadType} ${body.length} `);
1290
+ const out = new Uint8Array(head.length + body.length);
1291
+ out.set(head, 0);
1292
+ out.set(body, head.length);
1293
+ return out;
1294
+ }
1295
+ function resolveAttestationStatement(att) {
1296
+ const { createHash } = verifyCrypto();
1297
+ const enc2 = new TextEncoder();
1298
+ const sha = (s) => s == null ? null : createHash("sha256").update(enc2.encode(s)).digest("hex");
1299
+ return {
1300
+ kind: "codai.resolve.attestation.v1",
1301
+ attestation_id: att.id,
1302
+ repo_url: att.repo_url,
1303
+ base_commit: att.base_commit,
1304
+ patch_sha256: sha(att.patch),
1305
+ repro_test: att.repro_test?.path ? { path: att.repro_test.path, sha256: sha(att.repro_test.content) } : null,
1306
+ test_command: att.test_command,
1307
+ regression_command: att.regression_command,
1308
+ test_output_before_sha256: sha(att.test_output_before),
1309
+ test_output_after_sha256: sha(att.test_output_after),
1310
+ regression_output_sha256: sha(att.regression_output),
1311
+ runner_image_digest: att.runner_image_digest,
1312
+ started_at: att.started_at,
1313
+ verified_at: att.verified_at
1314
+ };
1315
+ }
1316
+ function verifyAttestationWithKeys(att, keys) {
1317
+ const env = att.signature;
1318
+ if (!env || !Array.isArray(env.signatures) || env.signatures.length === 0)
1319
+ return { ok: false, reason: "UNSIGNED" };
1320
+ if (env.payloadType !== RESOLVE_ATTESTATION_PAYLOAD_TYPE)
1321
+ return { ok: false, reason: "BAD_PAYLOAD_TYPE" };
1322
+ const { createVerify } = verifyCrypto();
1323
+ let body;
1324
+ try {
1325
+ body = base64ToBytes(env.payload);
1326
+ } catch {
1327
+ return { ok: false, reason: "BAD_SIGNATURE" };
1328
+ }
1329
+ const msg = pae(env.payloadType, body);
1330
+ let keyid = null;
1331
+ outer: for (const s of env.signatures) {
1332
+ let sig;
1333
+ try {
1334
+ sig = base64ToBytes(s.sig);
1335
+ } catch {
1336
+ continue;
1337
+ }
1338
+ for (const k of keys.keys) {
1339
+ if (k.payload_type && k.payload_type !== env.payloadType) continue;
1340
+ try {
1341
+ const v = createVerify("SHA256");
1342
+ v.update(msg);
1343
+ if (v.verify({ key: k.public_key_pem, dsaEncoding: "der" }, sig)) {
1344
+ keyid = k.keyid;
1345
+ break outer;
1346
+ }
1347
+ } catch {
1348
+ }
1349
+ }
1350
+ }
1351
+ if (keyid === null) return { ok: false, reason: "BAD_SIGNATURE" };
1352
+ let signed;
1353
+ try {
1354
+ signed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
1355
+ } catch {
1356
+ return { ok: false, reason: "STATEMENT_MISMATCH" };
1357
+ }
1358
+ if (canonicalJson(signed) !== canonicalJson(resolveAttestationStatement(att)))
1359
+ return { ok: false, reason: "STATEMENT_MISMATCH" };
1360
+ return { ok: true, keyid };
1361
+ }
1263
1362
  var RESOLVE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1264
1363
  "resolved",
1265
1364
  "failed",
@@ -1283,6 +1382,8 @@ var ResolveInsufficientFundsError = class extends ResolveError {
1283
1382
  currency;
1284
1383
  /** pay.codai.ro checkout link for the shortfall — open it, then retry `accept()`. */
1285
1384
  topupUrl;
1385
+ /** Present only for a `t7` job: fix-bundle checkout (10 t7 fix credits for €49). */
1386
+ bundleUrl;
1286
1387
  constructor(message, body, meta = {}) {
1287
1388
  super(message, 402, body, meta);
1288
1389
  this.name = "ResolveInsufficientFundsError";
@@ -1290,6 +1391,7 @@ var ResolveInsufficientFundsError = class extends ResolveError {
1290
1391
  this.required = body.required;
1291
1392
  this.currency = body.currency;
1292
1393
  this.topupUrl = body.topup_url;
1394
+ if (typeof body.bundle_url === "string") this.bundleUrl = body.bundle_url;
1293
1395
  }
1294
1396
  };
1295
1397
  var Resolve = class {
@@ -1321,8 +1423,10 @@ var Resolve = class {
1321
1423
  }
1322
1424
  /**
1323
1425
  * `POST /v1/resolve/jobs/{id}/accept` — accept a paid quote and start the job.
1324
- * Throws `ResolveInsufficientFundsError` (402, carries `topupUrl`) when the
1325
- * wallet is short, `ResolveError` 409 when the job is not `quoted`.
1426
+ * The result's `paid_with` says how it was paid (`intro` €1 hold, `credit`
1427
+ * t7 fix credit, or `wallet`). Throws `ResolveInsufficientFundsError` (402,
1428
+ * carries `topupUrl`, plus `bundleUrl` for t7) when the wallet is short,
1429
+ * `ResolveError` 409 when the job is not `quoted`.
1326
1430
  */
1327
1431
  accept(id, opts = {}) {
1328
1432
  return this.call(`/v1/resolve/jobs/${encodeURIComponent(id)}/accept`, {
@@ -1347,6 +1451,25 @@ var Resolve = class {
1347
1451
  health(opts = {}) {
1348
1452
  return this.call("/health", { signal: opts.signal });
1349
1453
  }
1454
+ /** `GET /v1/resolve/balance` — fix credits (t7 only), intro availability and wallet balance. */
1455
+ balance(opts = {}) {
1456
+ return this.call("/v1/resolve/balance", { signal: opts.signal });
1457
+ }
1458
+ /** `GET /v1/resolve/keys` — public keys that sign attestations (no auth needed server-side). */
1459
+ keys(opts = {}) {
1460
+ return this.call("/v1/resolve/keys", { signal: opts.signal });
1461
+ }
1462
+ /**
1463
+ * Verify an attestation's signature and that the signed statement matches
1464
+ * its fields. Fetches `keys()` unless `opts.keys` is given (pin them to
1465
+ * avoid trusting the network). `{ ok: false, reason: 'UNSIGNED' }` for
1466
+ * attestations created before signing (2026-09-27). Server-side only.
1467
+ */
1468
+ async verifyAttestation(att, opts = {}) {
1469
+ if (!att.signature) return { ok: false, reason: "UNSIGNED" };
1470
+ const keys = opts.keys ?? await this.keys({ signal: opts.signal });
1471
+ return verifyAttestationWithKeys(att, keys);
1472
+ }
1350
1473
  /**
1351
1474
  * Poll `get(id)` until the job reaches a terminal status (`resolved`,
1352
1475
  * `failed`, `declined`, `error`) or one of `opts.until`. A `quoted` job
@@ -1397,7 +1520,7 @@ function toResolveError(path, err) {
1397
1520
  if (err.status === 402 && isInsufficientFunds(err.body)) {
1398
1521
  const b = err.body;
1399
1522
  return new ResolveInsufficientFundsError(
1400
- `codai-resolve ${path} \u2192 402: insufficient funds (balance ${b.balance}, required ${b.required} ${b.currency}); top up at ${b.topup_url}`,
1523
+ `codai-resolve ${path} \u2192 402: insufficient funds (balance ${b.balance}, required ${b.required} ${b.currency}); top up at ${b.topup_url}${b.bundle_url ? ` or buy 10 t7 fixes for \u20AC49 at ${b.bundle_url}` : ""}`,
1401
1524
  b,
1402
1525
  meta
1403
1526
  );
@@ -2059,7 +2182,9 @@ var RESOLVE_OPERATION_METHODS = {
2059
2182
  createResolveJob: "resolve.submit",
2060
2183
  getResolveJob: "resolve.get",
2061
2184
  acceptResolveJob: "resolve.accept",
2062
- getResolveAttestation: "resolve.attestation"
2185
+ getResolveAttestation: "resolve.attestation",
2186
+ getResolveKeys: "resolve.keys",
2187
+ getResolveBalance: "resolve.balance"
2063
2188
  };
2064
2189
 
2065
2190
  // src/index.ts
@@ -2227,6 +2352,6 @@ function allKeys(obj) {
2227
2352
  }
2228
2353
  var index_default = Codai;
2229
2354
 
2230
- export { Account, AgentRuns, Agents, Audio, Chat, ChatCompletions, Codai, CodaiError, Devices, Embeddings, EnvironmentMembers, EnvironmentPorts, EnvironmentSecrets, EnvironmentTasks, Environments, Feedback, Health, Hosts, HttpClient, Messages, Models, Notifications, OPERATION_METHODS, OrgMembers, Orgs, PhoneModels, Projects, RESOLVE_OPERATION_METHODS, RESOLVE_TERMINAL_STATUSES, Receipts, Resolve, ResolveError, ResolveInsufficientFundsError, Responses, STREAMING_OPERATIONS, SessionControls, SessionEvents, SessionLease, SessionShares, Sessions, Tasks, Tokens, Tools, Triggers, index_default as default, extensionHeaders, frameJson, mergeToolCall, readSse, verifyResolveWebhook };
2355
+ export { Account, AgentRuns, Agents, Audio, Chat, ChatCompletions, Codai, CodaiError, Devices, Embeddings, EnvironmentMembers, EnvironmentPorts, EnvironmentSecrets, EnvironmentTasks, Environments, Feedback, Health, Hosts, HttpClient, Messages, Models, Notifications, OPERATION_METHODS, OrgMembers, Orgs, PhoneModels, Projects, RESOLVE_ATTESTATION_PAYLOAD_TYPE, RESOLVE_OPERATION_METHODS, RESOLVE_TERMINAL_STATUSES, Receipts, Resolve, ResolveError, ResolveInsufficientFundsError, Responses, STREAMING_OPERATIONS, SessionControls, SessionEvents, SessionLease, SessionShares, Sessions, Tasks, Tokens, Tools, Triggers, index_default as default, extensionHeaders, frameJson, mergeToolCall, readSse, resolveAttestationStatement, verifyAttestationWithKeys, verifyResolveWebhook };
2231
2356
  //# sourceMappingURL=index.js.map
2232
2357
  //# sourceMappingURL=index.js.map