codai-sdk 0.5.0 → 0.7.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
@@ -198,6 +198,52 @@ with `balance`, `required`, `currency: 'micro_eur'` and `topupUrl`. `submit` and
198
198
  `accept` are never retried. `waitFor` stops at `resolved | failed | declined | error`
199
199
  (or `until`), and throws `ResolveError` with `status: 0` on timeout.
200
200
 
201
+ ### Webhooks instead of polling
202
+
203
+ Pass `callback_url` (https, public host) on `submit`; the response carries
204
+ `callback_secret` **once** — store it. Resolve then POSTs `job.quoted`,
205
+ `job.declined`, `job.resolved`, `job.failed` and `job.error` events signed with
206
+ `x-codai-signature: t=<unix>,v1=<hex HMAC-SHA256>`. Verify against the raw body
207
+ with `verifyResolveWebhook` (server-side, uses `node:crypto`; 300 s tolerance by
208
+ default). Up to 3 attempts on `5xx`/`429`/network errors; deduplicate on
209
+ `x-codai-delivery`; delivery never changes job state — `get(id)` stays the truth.
210
+
211
+ ```ts
212
+ import { verifyResolveWebhook, type ResolveWebhookEvent } from 'codai-sdk';
213
+
214
+ const q = await codai.resolve.submit({
215
+ repo_url: 'https://github.com/psf/requests',
216
+ issue_url: 'https://github.com/psf/requests/issues/6000',
217
+ callback_url: 'https://ci.example.com/hooks/codai-resolve',
218
+ });
219
+ await saveSecret(q.id, q.callback_secret!); // shown once
220
+
221
+ // in your webhook handler (Hono / Next route / express.raw):
222
+ const raw = await req.text();
223
+ if (!verifyResolveWebhook(secret, raw, req.headers.get('x-codai-signature'))) {
224
+ return new Response('bad signature', { status: 401 });
225
+ }
226
+ const evt = JSON.parse(raw) as ResolveWebhookEvent; // evt.event, evt.job.attestation_url
227
+ ```
228
+
229
+ ### Verifying a signed attestation
230
+
231
+ Attestations created from 2026-09-27 carry `signature`: a DSSE envelope
232
+ (ECDSA P-256/SHA-256) over a canonical statement of sha256 hashes of the patch,
233
+ repro test and test outputs, plus the commands, base commit, runner digest and
234
+ timestamps. `verifyAttestation` checks the signature against
235
+ `GET /v1/resolve/keys` **and** that the statement matches the fields you were
236
+ served. Older attestations have `signature: null` → `{ ok: false, reason: 'UNSIGNED' }`.
237
+
238
+ ```ts
239
+ const proof = await codai.resolve.attestation(job.attestation_url!);
240
+ const r = await codai.resolve.verifyAttestation(proof); // or { keys: pinnedKeys }
241
+ if (!r.ok) throw new Error(`attestation not verified: ${r.reason}`);
242
+ // r.keyid — the signing key; reasons: UNSIGNED | BAD_SIGNATURE | STATEMENT_MISMATCH | BAD_PAYLOAD_TYPE
243
+ ```
244
+
245
+ Offline (no network): `verifyAttestationWithKeys(proof, keys)`. Server-side only (`node:crypto`).
246
+
201
247
  ## Shared sessions, devices and hosts
202
248
 
203
249
  Shared-sessions and hosts calls need a device UUID (`x-codai-device`). Set it once
package/dist/index.d.ts CHANGED
@@ -11024,6 +11024,30 @@ type paths = {
11024
11024
  readonly patch?: never;
11025
11025
  readonly trace?: never;
11026
11026
  };
11027
+ readonly '/v1/resolve/keys': {
11028
+ readonly parameters: {
11029
+ readonly query?: never;
11030
+ readonly header?: never;
11031
+ readonly path?: never;
11032
+ readonly cookie?: never;
11033
+ };
11034
+ /**
11035
+ * Public keys that sign attestations
11036
+ * @description Returns the public key(s) used to sign attestation envelopes (`signature` on
11037
+ * `GET /v1/resolve/attestations/{id}`). ECDSA P-256 / SHA-256 over the DSSE v1 PAE of the
11038
+ * payload; the signature is DER, base64. **Unauthenticated**, CORS-open and cacheable for
11039
+ * 1 hour. Verify with `codaid resolve verify`, `resolve.verifyAttestation` (TypeScript) or
11040
+ * `resolve.verify_attestation` (Python). `503` when signing is not configured.
11041
+ */
11042
+ readonly get: operations['getResolveKeys'];
11043
+ readonly put?: never;
11044
+ readonly post?: never;
11045
+ readonly delete?: never;
11046
+ readonly options?: never;
11047
+ readonly head?: never;
11048
+ readonly patch?: never;
11049
+ readonly trace?: never;
11050
+ };
11027
11051
  };
11028
11052
  type components = {
11029
11053
  schemas: {
@@ -11059,6 +11083,13 @@ type components = {
11059
11083
  readonly repro_test: components['schemas']['ResolveReproTest'] | null;
11060
11084
  /** @description Container image digest of the verification runner (`unknown` when not recorded). */
11061
11085
  readonly runner_image_digest: string;
11086
+ /**
11087
+ * @description DSSE envelope signing the canonical statement of this attestation (sha256 of the patch,
11088
+ * repro test, test outputs; commands, commit, runner digest, timestamps). `null` for
11089
+ * attestations created before signing was enabled (2026-09-27). Verify against
11090
+ * `GET /v1/resolve/keys`.
11091
+ */
11092
+ readonly signature: components['schemas']['ResolveAttestationEnvelope'] | null;
11062
11093
  /** Format: date-time */
11063
11094
  readonly started_at: string;
11064
11095
  /** @description FAIL_TO_PASS command that was executed. */
@@ -11070,11 +11101,39 @@ type components = {
11070
11101
  /** Format: date-time */
11071
11102
  readonly verified_at: string;
11072
11103
  };
11104
+ /**
11105
+ * Attestation envelope
11106
+ * @description DSSE v1 envelope. `payload` is base64 of the RFC 8785 (JCS) canonical statement JSON
11107
+ * `{kind: "codai.resolve.attestation.v1", attestation_id, repo_url, base_commit, patch_sha256,
11108
+ * repro_test: {path, sha256} | null, test_command, regression_command,
11109
+ * test_output_before_sha256, test_output_after_sha256, regression_output_sha256,
11110
+ * runner_image_digest, started_at, verified_at}`. Each `sig` is an ECDSA P-256/SHA-256 DER
11111
+ * signature (base64) over `PAE(payloadType, payload bytes)` =
11112
+ * `"DSSEv1 " + len(type) + " " + type + " " + len(body) + " " + body`.
11113
+ */
11114
+ readonly ResolveAttestationEnvelope: {
11115
+ /** @description Base64 of the canonical statement JSON. */
11116
+ readonly payload: string;
11117
+ /** @constant */
11118
+ readonly payloadType: 'application/vnd.codai.resolve.attestation+json;v=1';
11119
+ readonly signatures: readonly {
11120
+ /** @description Identifier of the signing key; matches `keyid` in `GET /v1/resolve/keys`. */
11121
+ readonly keyid?: string;
11122
+ /** @description DER-encoded ECDSA signature, base64. */
11123
+ readonly sig: string;
11124
+ }[];
11125
+ };
11073
11126
  /**
11074
11127
  * Declined
11075
11128
  * @description `200` body — triage declined the issue. The job is persisted with `status: declined` and is never billed.
11076
11129
  */
11077
11130
  readonly ResolveDeclined: {
11131
+ /**
11132
+ * @description Present only when the intake carried `callback_url` (a `job.declined` event is sent
11133
+ * too). HMAC-SHA256 key for webhook signatures. **Returned once** — store it now.
11134
+ * @example whsec_3q2-7wQ1d0X9mZk4Vb8yTnR5sLpA6cEe
11135
+ */
11136
+ readonly callback_secret?: string;
11078
11137
  /**
11079
11138
  * @example feature request, no testable behavior
11080
11139
  * @example triage unavailable, please retry
@@ -11132,6 +11191,15 @@ type components = {
11132
11191
  * @description Body of `POST /v1/resolve/jobs`. Provide `issue_url` or `issue_text` (at least one).
11133
11192
  */
11134
11193
  readonly ResolveIntake: {
11194
+ /**
11195
+ * Format: uri
11196
+ * @description Optional webhook target. Must be `https`, carry no credentials and point at a public
11197
+ * host (no localhost, private, link-local or metadata addresses; every DNS answer is
11198
+ * re-checked at delivery). Events are signed with the per-job `callback_secret`
11199
+ * returned in the intake response. Redirects are not followed.
11200
+ * @example https://ci.example.com/hooks/codai-resolve
11201
+ */
11202
+ readonly callback_url?: string;
11135
11203
  /** @description Inline issue description. Takes precedence over `issue_url`. Only the first 20 000 characters reach triage. */
11136
11204
  readonly issue_text?: string;
11137
11205
  /**
@@ -11198,11 +11266,37 @@ type components = {
11198
11266
  * @enum {string}
11199
11267
  */
11200
11268
  readonly ResolveJobStatus: 'triage' | 'quoted' | 'declined' | 'accepted' | 'running' | 'resolved' | 'failed' | 'error';
11269
+ /** Verification key */
11270
+ readonly ResolveKey: {
11271
+ /** @constant */
11272
+ readonly algorithm: 'ECDSA_P256_SHA256';
11273
+ /**
11274
+ * @description Envelope and signature encoding.
11275
+ * @example DSSE v1; signature DER, base64
11276
+ */
11277
+ readonly encoding: string;
11278
+ /** @description Key identifier (the Cloud KMS key version name). */
11279
+ readonly keyid: string;
11280
+ /** @constant */
11281
+ readonly payload_type: 'application/vnd.codai.resolve.attestation+json;v=1';
11282
+ /** @description SPKI public key, PEM. */
11283
+ readonly public_key_pem: string;
11284
+ };
11285
+ /** Verification keys */
11286
+ readonly ResolveKeys: {
11287
+ readonly keys: readonly components['schemas']['ResolveKey'][];
11288
+ };
11201
11289
  /**
11202
11290
  * Quote
11203
11291
  * @description `201` body — the job was quoted (`quoted`) or auto-accepted on the free tier (`accepted`).
11204
11292
  */
11205
11293
  readonly ResolveQuote: {
11294
+ /**
11295
+ * @description Present only when the intake carried `callback_url`. HMAC-SHA256 key for verifying
11296
+ * webhook signatures. **Returned once** — it is never shown again, store it now.
11297
+ * @example whsec_3q2-7wQ1d0X9mZk4Vb8yTnR5sLpA6cEe
11298
+ */
11299
+ readonly callback_secret?: string;
11206
11300
  /** @description Free resolves left for this user after this submission. */
11207
11301
  readonly free_tier_remaining: number;
11208
11302
  /** Format: uuid */
@@ -11252,6 +11346,53 @@ type components = {
11252
11346
  /** @description Errors on the whole object, e.g. `one of issue_url or issue_text is required`. */
11253
11347
  readonly formErrors: readonly string[];
11254
11348
  };
11349
+ /**
11350
+ * Webhook event payload
11351
+ * @description JSON body POSTed to `callback_url`. Verify `x-codai-signature` over the raw bytes before trusting it.
11352
+ */
11353
+ readonly ResolveWebhookEvent: {
11354
+ readonly event: components['schemas']['ResolveWebhookEventName'];
11355
+ readonly job: components['schemas']['ResolveWebhookJob'];
11356
+ /**
11357
+ * Format: date-time
11358
+ * @description When this delivery attempt was built (UTC).
11359
+ */
11360
+ readonly sent_at: string;
11361
+ };
11362
+ /**
11363
+ * Webhook event
11364
+ * @description `job.quoted` (paid quote ready), `job.declined` (triage refused), `job.resolved` (verified fix + attestation), `job.failed` (no verified fix, hold refunded), `job.error` (infrastructure failure, hold refunded).
11365
+ * @enum {string}
11366
+ */
11367
+ readonly ResolveWebhookEventName: 'job.quoted' | 'job.declined' | 'job.resolved' | 'job.failed' | 'job.error';
11368
+ /**
11369
+ * Webhook job snapshot
11370
+ * @description Snapshot of the job at send time. Nullable fields are `null` until the corresponding stage has run.
11371
+ */
11372
+ readonly ResolveWebhookJob: {
11373
+ /** Format: uuid */
11374
+ readonly attestation_id: string | null;
11375
+ /**
11376
+ * Format: uri
11377
+ * @description Absolute URL of the public attestation (unlike the relative path on `GET /v1/resolve/jobs/{id}`).
11378
+ * @example https://resolve.codai.ro/v1/resolve/attestations/1f2c1a3e-8d4b-4f8e-9a1b-0c2d3e4f5a6b
11379
+ */
11380
+ readonly attestation_url: string | null;
11381
+ /** Format: date-time */
11382
+ readonly completed_at: string | null;
11383
+ /** @description Upstream inference spend attributed to this job, USD micros (informational). */
11384
+ readonly cost_micro_usd: number;
11385
+ readonly decline_reason: string | null;
11386
+ readonly fail_reason: string | null;
11387
+ /** Format: uuid */
11388
+ readonly id: string;
11389
+ /** @description Quoted price in USD micros; `0` on the free tier. */
11390
+ readonly quote_micro_usd: number | null;
11391
+ /** Format: uri */
11392
+ readonly repo_url: string;
11393
+ readonly status: components['schemas']['ResolveJobStatus'];
11394
+ readonly tier: components['schemas']['ResolveTier'] | null;
11395
+ };
11255
11396
  };
11256
11397
  responses: {
11257
11398
  /**
@@ -11473,6 +11614,64 @@ interface operations {
11473
11614
  readonly 409: components['responses']['ResolveConflict'];
11474
11615
  };
11475
11616
  };
11617
+ readonly getResolveKeys: {
11618
+ readonly parameters: {
11619
+ readonly query?: never;
11620
+ readonly header?: never;
11621
+ readonly path?: never;
11622
+ readonly cookie?: never;
11623
+ };
11624
+ readonly requestBody?: never;
11625
+ readonly responses: {
11626
+ /** @description Current verification keys. */
11627
+ readonly 200: {
11628
+ headers: {
11629
+ readonly [name: string]: unknown;
11630
+ };
11631
+ content: {
11632
+ readonly 'application/json': components['schemas']['ResolveKeys'];
11633
+ };
11634
+ };
11635
+ /** @description Attestation signing is not configured (`attestation signing not configured`). */
11636
+ readonly 503: {
11637
+ headers: {
11638
+ readonly [name: string]: unknown;
11639
+ };
11640
+ content: {
11641
+ readonly 'application/json': components['schemas']['ResolveError'];
11642
+ };
11643
+ };
11644
+ };
11645
+ };
11646
+ readonly resolveJobWebhook: {
11647
+ readonly parameters: {
11648
+ readonly query?: never;
11649
+ readonly header: {
11650
+ /** @description Idempotency key `<jobId>:<event>`, identical on every retry. */
11651
+ readonly 'x-codai-delivery': string;
11652
+ /** @description Event name, same as `event` in the body. */
11653
+ readonly 'x-codai-event': components['schemas']['ResolveWebhookEventName'];
11654
+ /** @description `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`. */
11655
+ readonly 'x-codai-signature': string;
11656
+ };
11657
+ readonly path?: never;
11658
+ readonly cookie?: never;
11659
+ };
11660
+ readonly requestBody: {
11661
+ readonly content: {
11662
+ readonly 'application/json': components['schemas']['ResolveWebhookEvent'];
11663
+ };
11664
+ };
11665
+ readonly responses: {
11666
+ /** @description Any 2xx acknowledges the delivery; the body is ignored. */
11667
+ readonly 200: {
11668
+ headers: {
11669
+ readonly [name: string]: unknown;
11670
+ };
11671
+ content?: never;
11672
+ };
11673
+ };
11674
+ };
11476
11675
  }
11477
11676
 
11478
11677
  /**
@@ -11495,10 +11694,54 @@ type ResolveJob = ResolveSchemas['ResolveJob'];
11495
11694
  type ResolveJobStatus = ResolveSchemas['ResolveJobStatus'];
11496
11695
  type ResolveTier = ResolveSchemas['ResolveTier'];
11497
11696
  type ResolveAttestation = ResolveSchemas['ResolveAttestation'];
11697
+ /** DSSE v1 envelope in `ResolveAttestation.signature` (`null` = unsigned, pre-2026-09-27). */
11698
+ type ResolveAttestationEnvelope = ResolveSchemas['ResolveAttestationEnvelope'];
11699
+ type ResolveKeys = ResolveSchemas['ResolveKeys'];
11700
+ type ResolveKey = ResolveSchemas['ResolveKey'];
11498
11701
  type ResolveReproTest = ResolveSchemas['ResolveReproTest'];
11499
11702
  type ResolveHealth = ResolveSchemas['ResolveHealth'];
11500
11703
  type ResolveValidationDetails = ResolveSchemas['ResolveValidationDetails'];
11501
11704
  type ResolveInsufficientFunds = ResolveSchemas['ResolveInsufficientFunds'];
11705
+ /** Body POSTed to `callback_url` (verify with `verifyResolveWebhook` first). */
11706
+ type ResolveWebhookEvent = ResolveSchemas['ResolveWebhookEvent'];
11707
+ type ResolveWebhookEventName = ResolveSchemas['ResolveWebhookEventName'];
11708
+ type ResolveWebhookJob = ResolveSchemas['ResolveWebhookJob'];
11709
+ interface VerifyResolveWebhookOptions {
11710
+ /** Maximum age (and future skew) of `t`, in seconds. Default 300. */
11711
+ toleranceSeconds?: number;
11712
+ /** Current unix time in seconds (tests). Default `Date.now() / 1000`. */
11713
+ nowSeconds?: number;
11714
+ }
11715
+ /**
11716
+ * Verify a Resolve webhook delivery: `x-codai-signature: t=<unix>,v1=<hex>`
11717
+ * with `v1 = HMAC-SHA256(callback_secret, "<t>.<rawBody>")`. Pass the RAW
11718
+ * request body (before JSON parsing). Constant-time compare; rejects a
11719
+ * timestamp outside `toleranceSeconds`. Returns `false` for any bad input.
11720
+ *
11721
+ * Server-side only: uses `node:crypto` (Node 20.16+/22.3+, Bun, Deno), loaded
11722
+ * lazily so the rest of the SDK still bundles for browsers and edge runtimes.
11723
+ * Throws only when `node:crypto` is unavailable in the current runtime.
11724
+ */
11725
+ declare function verifyResolveWebhook(secret: string, rawBody: string | Uint8Array, signatureHeader: string | null | undefined, opts?: VerifyResolveWebhookOptions): boolean;
11726
+ /** `payloadType` of Resolve attestation envelopes. */
11727
+ declare const RESOLVE_ATTESTATION_PAYLOAD_TYPE = "application/vnd.codai.resolve.attestation+json;v=1";
11728
+ type ResolveAttestationVerifyResult = {
11729
+ ok: true;
11730
+ keyid: string;
11731
+ } | {
11732
+ ok: false;
11733
+ reason: 'UNSIGNED' | 'BAD_SIGNATURE' | 'STATEMENT_MISMATCH' | 'BAD_PAYLOAD_TYPE';
11734
+ };
11735
+ /** The statement the Resolve service signs, recomputed from the attestation JSON. */
11736
+ declare function resolveAttestationStatement(att: ResolveAttestation): Record<string, unknown>;
11737
+ /**
11738
+ * Verify a Resolve attestation offline against published keys: the DSSE
11739
+ * signature (ECDSA P-256/SHA-256, DER) over `PAE(payloadType, payload)` must
11740
+ * match one key, AND the signed statement must equal the one recomputed from
11741
+ * the attestation's own fields (so a valid signature over other content fails).
11742
+ * Server-side only (`node:crypto`, loaded lazily).
11743
+ */
11744
+ declare function verifyAttestationWithKeys(att: ResolveAttestation, keys: ResolveKeys): ResolveAttestationVerifyResult;
11502
11745
  /** Statuses after which a job never changes again. */
11503
11746
  declare const RESOLVE_TERMINAL_STATUSES: ReadonlySet<ResolveJobStatus>;
11504
11747
  /** Any non-2xx answer from the Resolve service (flat `{ error }` body). */
@@ -11562,6 +11805,18 @@ declare class Resolve {
11562
11805
  attestation(idOrUrl: string, opts?: CallOpts$4): Promise<ResolveAttestation>;
11563
11806
  /** `GET /health` — liveness of the Resolve service. */
11564
11807
  health(opts?: CallOpts$4): Promise<ResolveHealth>;
11808
+ /** `GET /v1/resolve/keys` — public keys that sign attestations (no auth needed server-side). */
11809
+ keys(opts?: CallOpts$4): Promise<ResolveKeys>;
11810
+ /**
11811
+ * Verify an attestation's signature and that the signed statement matches
11812
+ * its fields. Fetches `keys()` unless `opts.keys` is given (pin them to
11813
+ * avoid trusting the network). `{ ok: false, reason: 'UNSIGNED' }` for
11814
+ * attestations created before signing (2026-09-27). Server-side only.
11815
+ */
11816
+ verifyAttestation(att: ResolveAttestation, opts?: {
11817
+ keys?: ResolveKeys;
11818
+ signal?: AbortSignal;
11819
+ }): Promise<ResolveAttestationVerifyResult>;
11565
11820
  /**
11566
11821
  * Poll `get(id)` until the job reaches a terminal status (`resolved`,
11567
11822
  * `failed`, `declined`, `error`) or one of `opts.until`. A `quoted` job
@@ -12068,4 +12323,4 @@ declare class Codai {
12068
12323
  private legacyExt;
12069
12324
  }
12070
12325
 
12071
- 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, 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 components$1 as components, Codai as default, extensionHeaders, frameJson, mergeToolCall, type operations$1 as operations, type paths$1 as paths, readSse };
12326
+ 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 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 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
@@ -1220,6 +1220,145 @@ var Projects = class {
1220
1220
  };
1221
1221
 
1222
1222
  // src/resources/resolve.ts
1223
+ function verifyResolveWebhook(secret, rawBody, signatureHeader, opts = {}) {
1224
+ if (!secret || !signatureHeader) return false;
1225
+ let t;
1226
+ const v1s = [];
1227
+ for (const part of signatureHeader.split(",")) {
1228
+ const eq = part.indexOf("=");
1229
+ if (eq === -1) continue;
1230
+ const k = part.slice(0, eq).trim();
1231
+ const v = part.slice(eq + 1).trim();
1232
+ if (k === "t") t = v;
1233
+ else if (k === "v1") v1s.push(v.toLowerCase());
1234
+ }
1235
+ if (!t || !/^\d+$/.test(t) || v1s.length === 0) return false;
1236
+ const tolerance = opts.toleranceSeconds ?? 300;
1237
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
1238
+ if (Math.abs(now - Number(t)) > tolerance) return false;
1239
+ const { createHmac, timingSafeEqual } = nodeCrypto();
1240
+ const enc2 = new TextEncoder();
1241
+ const body = typeof rawBody === "string" ? enc2.encode(rawBody) : rawBody;
1242
+ const expected = createHmac("sha256", secret).update(enc2.encode(`${t}.`)).update(body).digest();
1243
+ return v1s.some((v1) => {
1244
+ if (!/^[0-9a-f]{64}$/.test(v1)) return false;
1245
+ return timingSafeEqual(hexToBytes(v1), expected);
1246
+ });
1247
+ }
1248
+ function nodeCrypto() {
1249
+ const proc = globalThis.process;
1250
+ const mod = proc?.getBuiltinModule?.("node:crypto");
1251
+ if (!mod) {
1252
+ throw new Error(
1253
+ "verifyResolveWebhook needs node:crypto (Node 20.16+/22.3+, Bun or Deno); run it server-side"
1254
+ );
1255
+ }
1256
+ return mod;
1257
+ }
1258
+ function hexToBytes(hex) {
1259
+ const out = new Uint8Array(hex.length / 2);
1260
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1261
+ return out;
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
+ }
1223
1362
  var RESOLVE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
1224
1363
  "resolved",
1225
1364
  "failed",
@@ -1307,6 +1446,21 @@ var Resolve = class {
1307
1446
  health(opts = {}) {
1308
1447
  return this.call("/health", { signal: opts.signal });
1309
1448
  }
1449
+ /** `GET /v1/resolve/keys` — public keys that sign attestations (no auth needed server-side). */
1450
+ keys(opts = {}) {
1451
+ return this.call("/v1/resolve/keys", { signal: opts.signal });
1452
+ }
1453
+ /**
1454
+ * Verify an attestation's signature and that the signed statement matches
1455
+ * its fields. Fetches `keys()` unless `opts.keys` is given (pin them to
1456
+ * avoid trusting the network). `{ ok: false, reason: 'UNSIGNED' }` for
1457
+ * attestations created before signing (2026-09-27). Server-side only.
1458
+ */
1459
+ async verifyAttestation(att, opts = {}) {
1460
+ if (!att.signature) return { ok: false, reason: "UNSIGNED" };
1461
+ const keys = opts.keys ?? await this.keys({ signal: opts.signal });
1462
+ return verifyAttestationWithKeys(att, keys);
1463
+ }
1310
1464
  /**
1311
1465
  * Poll `get(id)` until the job reaches a terminal status (`resolved`,
1312
1466
  * `failed`, `declined`, `error`) or one of `opts.until`. A `quoted` job
@@ -2019,7 +2173,8 @@ var RESOLVE_OPERATION_METHODS = {
2019
2173
  createResolveJob: "resolve.submit",
2020
2174
  getResolveJob: "resolve.get",
2021
2175
  acceptResolveJob: "resolve.accept",
2022
- getResolveAttestation: "resolve.attestation"
2176
+ getResolveAttestation: "resolve.attestation",
2177
+ getResolveKeys: "resolve.keys"
2023
2178
  };
2024
2179
 
2025
2180
  // src/index.ts
@@ -2187,6 +2342,6 @@ function allKeys(obj) {
2187
2342
  }
2188
2343
  var index_default = Codai;
2189
2344
 
2190
- 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 };
2345
+ 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 };
2191
2346
  //# sourceMappingURL=index.js.map
2192
2347
  //# sourceMappingURL=index.js.map