@oh-my-pi/pi-ai 16.3.4 → 16.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.6] - 2026-07-04
6
+
7
+ ### Added
8
+
9
+ - Persisted credential rate-limit blocks across processes: `auth_credential_blocks` (auth schema v5) stores per-credential blocks keyed by row id + provider key + block scope with MAX-upsert semantics, `AuthStorage` merges persisted and in-memory blocks on read, and auth-broker snapshots/SSE carry per-entry blocks with `POST /v1/credential/:id/block` and `DELETE /v1/credential/:id/blocks` endpoints so gateway and sibling omp processes stop re-discovering exhausted accounts by burning a 429 each.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed Anthropic credential selection sampling Fable/Mythos-exhausted accounts on every new session: a Fable/Mythos weekly cap now proactively hard-blocks the credential when confirmed exhausted (server `exhausted` status or used fraction >= 1) with a live `resetsAt`, and a live Fable 429 extends the reactive block to the confirmed tier reset instead of the 60s default. Unconfirmed rows (missing/expired reset, below cap) remain ranking hints only, preserving the false-100% guard.
14
+ - Fixed Ollama/Ollama Cloud tool requests failing with HTTP 400 by rewriting boolean subschemas (`true`/`false`) into a value-widening `anyOf` union of primitive types, stripping boolean `additionalProperties`/`unevaluatedProperties`, and flattening nullable `type` arrays before serializing tool parameters, so unconstrained fields still advertise "any JSON value" to grammar-constrained samplers (llama.cpp) instead of collapsing to an empty object. ([#4488](https://github.com/can1357/oh-my-pi/issues/4488))
15
+
16
+ ## [16.3.5] - 2026-07-04
17
+
18
+ ### Added
19
+
20
+ - `OAuthCallbackFlow` now serves a `GET /launch` route on its loopback callback server that 302-redirects to the pending authorization URL, and exposes that short URL as `OAuthAuthInfo.launchUrl`. UIs can advertise it as a truncation-safe copy target (~30 chars) instead of the full authorize URL, so terminals narrower than the composed row cannot silently drop OAuth query parameters like `code_challenge_method=S256` ([#4418](https://github.com/can1357/oh-my-pi/issues/4418)).
21
+ - Preserved explicit `tool.strict === false` on OpenAI-family function tool payloads (openai-responses, openai-codex-responses, openai-completions) so backends that distinguish `strict: false` from an omitted flag stop over-filling optional arguments ([#4336](https://github.com/can1357/oh-my-pi/issues/4336)).
22
+
23
+ ### Fixed
24
+
25
+ - Fixed tool-call validation to strip stray trailing line terminators on schema-matching enum values and on well-known identifier fields (`path`, `paths`, `file`, `file_path`, `url`, `uri`, `title`, `label`) before dispatch, keeping ordinary trailing spaces and content-carrying fields (`content`, `input`, `code`, `command`, etc.) intact ([#4461](https://github.com/can1357/oh-my-pi/issues/4461)).
26
+
5
27
  ## [16.3.4] - 2026-07-03
6
28
 
7
29
  ### Added
@@ -1,5 +1,5 @@
1
1
  import type { AuthCredential } from "../auth-storage";
2
- import type { CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types";
2
+ import type { CredentialBlockRequest, CredentialBlockResponse, CredentialBlocksDeleteResponse, CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types";
3
3
  export interface AuthBrokerClientOptions {
4
4
  /** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
5
5
  url: string;
@@ -63,4 +63,6 @@ export declare class AuthBrokerClient {
63
63
  refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse>;
64
64
  disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
65
65
  uploadCredential(provider: string, credential: AuthCredential, signal?: AbortSignal): Promise<CredentialUploadResponse>;
66
+ upsertCredentialBlock(id: number, block: CredentialBlockRequest, signal?: AbortSignal): Promise<CredentialBlockResponse>;
67
+ deleteCredentialBlocks(id: number, signal?: AbortSignal): Promise<CredentialBlocksDeleteResponse>;
66
68
  }
@@ -1,4 +1,4 @@
1
- import { type AuthCredential, type AuthCredentialStore, type OAuthCredential, type StoredAuthCredential } from "../auth-storage";
1
+ import { type AuthCredential, type AuthCredentialStore, type OAuthCredential, type StoredAuthCredential, type StoredCredentialBlock } from "../auth-storage";
2
2
  import type { OAuthCredentials } from "../registry/oauth/types";
3
3
  import type { Provider } from "../types";
4
4
  import type { UsageReport } from "../usage";
@@ -30,6 +30,11 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
30
30
  /** Re-hydrate the in-memory snapshot from the broker. */
31
31
  refreshSnapshot(): Promise<SnapshotResponse>;
32
32
  listAuthCredentials(provider?: string): StoredAuthCredential[];
33
+ getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined;
34
+ listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[];
35
+ upsertCredentialBlock(block: StoredCredentialBlock): void;
36
+ deleteCredentialBlocks(credentialId: number): void;
37
+ cleanExpiredCredentialBlocks(nowMs: number): void;
33
38
  /**
34
39
  * In-memory update from a successful refresh through the broker. AuthStorage
35
40
  * calls this after `#replaceCredentialAt`; the broker already persisted the
@@ -5,7 +5,7 @@
5
5
  * clients use `access` tokens directly and call back to the broker when a
6
6
  * credential expires or a 401 surfaces on a supposedly-fresh credential.
7
7
  */
8
- import type { AuthCredential, AuthCredentialSnapshot, AuthCredentialSnapshotEntry } from "../auth-storage";
8
+ import type { AuthCredential, AuthCredentialSnapshot, AuthCredentialSnapshotEntry, StoredCredentialBlock } from "../auth-storage";
9
9
  import type { UsageReport } from "../usage";
10
10
  /** GET /v1/healthz response body. */
11
11
  export interface HealthzResponse {
@@ -18,8 +18,10 @@ export interface RefresherSchedule {
18
18
  skewMs: number;
19
19
  nextSweepInMs: number;
20
20
  }
21
+ export type CredentialBlockSnapshot = Omit<StoredCredentialBlock, "credentialId">;
21
22
  export type SnapshotEntry = AuthCredentialSnapshotEntry & {
22
23
  rotatesInMs: number | null;
24
+ blocks?: CredentialBlockSnapshot[];
23
25
  };
24
26
  /** GET /v1/snapshot response body. */
25
27
  export interface SnapshotResponse extends Omit<AuthCredentialSnapshot, "credentials"> {
@@ -44,6 +46,16 @@ export interface CredentialDisableRequest {
44
46
  export interface CredentialDisableResponse {
45
47
  ok: boolean;
46
48
  }
49
+ /** POST /v1/credential/:id/block request body. */
50
+ export type CredentialBlockRequest = CredentialBlockSnapshot;
51
+ /** POST /v1/credential/:id/block response body. */
52
+ export interface CredentialBlockResponse {
53
+ ok: boolean;
54
+ }
55
+ /** DELETE /v1/credential/:id/blocks response body. */
56
+ export interface CredentialBlocksDeleteResponse {
57
+ ok: boolean;
58
+ }
47
59
  /**
48
60
  * POST /v1/credential request body. The OAuth `refresh` must be the *real*
49
61
  * refresh token (not the sentinel) — the broker is the canonical writer.
@@ -75,6 +75,11 @@ export declare const credentialSnapshotEntrySchema: import("arktype/internal/var
75
75
  };
76
76
  identityKey: string | null;
77
77
  }, {}>;
78
+ export declare const credentialBlockSnapshotSchema: import("arktype/internal/variants/object.ts").ObjectType<{
79
+ providerKey: string;
80
+ blockScope: string;
81
+ blockedUntilMs: number;
82
+ }, {}>;
78
83
  export declare const snapshotEntrySchema: import("arktype/internal/variants/object.ts").ObjectType<{
79
84
  id: number;
80
85
  provider: string;
@@ -94,6 +99,11 @@ export declare const snapshotEntrySchema: import("arktype/internal/variants/obje
94
99
  };
95
100
  identityKey: string | null;
96
101
  rotatesInMs: number | null;
102
+ blocks?: {
103
+ providerKey: string;
104
+ blockScope: string;
105
+ blockedUntilMs: number;
106
+ }[] | undefined;
97
107
  }, {}>;
98
108
  export declare const refresherScheduleSchema: import("arktype/internal/variants/object.ts").ObjectType<{
99
109
  enabled: boolean;
@@ -130,6 +140,11 @@ export declare const snapshotResponseSchema: import("arktype/internal/variants/o
130
140
  };
131
141
  identityKey: string | null;
132
142
  rotatesInMs: number | null;
143
+ blocks?: {
144
+ providerKey: string;
145
+ blockScope: string;
146
+ blockedUntilMs: number;
147
+ }[] | undefined;
133
148
  }[];
134
149
  }, {}>;
135
150
  /** First frame on connect — full snapshot embedded inline with a `kind` tag. */
@@ -162,6 +177,11 @@ export declare const snapshotStreamSnapshotEventSchema: import("arktype/internal
162
177
  };
163
178
  identityKey: string | null;
164
179
  rotatesInMs: number | null;
180
+ blocks?: {
181
+ providerKey: string;
182
+ blockScope: string;
183
+ blockedUntilMs: number;
184
+ }[] | undefined;
165
185
  }[];
166
186
  kind: "snapshot";
167
187
  }, {}>;
@@ -195,6 +215,11 @@ export declare const snapshotStreamEntryEventSchema: import("arktype/internal/va
195
215
  };
196
216
  identityKey: string | null;
197
217
  rotatesInMs: number | null;
218
+ blocks?: {
219
+ providerKey: string;
220
+ blockScope: string;
221
+ blockedUntilMs: number;
222
+ }[] | undefined;
198
223
  };
199
224
  }, {}>;
200
225
  /** Per-credential delete delta. */
@@ -240,6 +265,11 @@ export declare const snapshotStreamEventSchema: import("arktype/internal/variant
240
265
  };
241
266
  identityKey: string | null;
242
267
  rotatesInMs: number | null;
268
+ blocks?: {
269
+ providerKey: string;
270
+ blockScope: string;
271
+ blockedUntilMs: number;
272
+ }[] | undefined;
243
273
  }[];
244
274
  kind: "snapshot";
245
275
  } | {
@@ -271,6 +301,11 @@ export declare const snapshotStreamEventSchema: import("arktype/internal/variant
271
301
  };
272
302
  identityKey: string | null;
273
303
  rotatesInMs: number | null;
304
+ blocks?: {
305
+ providerKey: string;
306
+ blockScope: string;
307
+ blockedUntilMs: number;
308
+ }[] | undefined;
274
309
  };
275
310
  } | {
276
311
  kind: "removed";
@@ -371,6 +406,17 @@ export declare const credentialDisableRequestSchema: import("arktype/internal/va
371
406
  export declare const credentialDisableResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
372
407
  ok: boolean;
373
408
  }, {}>;
409
+ export declare const credentialBlockRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
410
+ providerKey: string;
411
+ blockScope: string;
412
+ blockedUntilMs: number;
413
+ }, {}>;
414
+ export declare const credentialBlockResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
415
+ ok: boolean;
416
+ }, {}>;
417
+ export declare const credentialBlocksDeleteResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
418
+ ok: boolean;
419
+ }, {}>;
374
420
  export declare const credentialUploadRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
375
421
  provider: string;
376
422
  credential: {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { Database } from "bun:sqlite";
11
11
  import type { ApiKeyResolver } from "./auth-retry";
12
- import type { OAuthController, OAuthCredentials, OAuthProviderId } from "./registry/oauth/types";
12
+ import type { OAuthAuthInfo, OAuthController, OAuthCredentials, OAuthProviderId } from "./registry/oauth/types";
13
13
  import type { Provider } from "./types";
14
14
  import type { CredentialRankingStrategy, UsageCostHistoryEntry, UsageCostHistoryQuery, UsageHistoryEntry, UsageHistoryQuery, UsageLogger, UsageProvider, UsageReport } from "./usage";
15
15
  import { type CodexResetConsumeCode, type CodexResetCredit } from "./usage/openai-codex-reset";
@@ -61,6 +61,17 @@ export interface StoredAuthCredential {
61
61
  credential: AuthCredential;
62
62
  disabledCause: string | null;
63
63
  }
64
+ /** One persisted rate-limit block: credential row id + provider-type key + optional scope. */
65
+ export interface StoredCredentialBlock {
66
+ /** SQLite row id of the credential (auth_credentials.id). */
67
+ credentialId: number;
68
+ /** `${provider}:${credentialType}` — same value as AuthStorage's in-memory providerKey. */
69
+ providerKey: string;
70
+ /** Block scope (e.g. "tier:fable"); empty string = unscoped. Never NUL-delimited. */
71
+ blockScope: string;
72
+ /** Epoch milliseconds. */
73
+ blockedUntilMs: number;
74
+ }
64
75
  /**
65
76
  * Per-credential health record returned by {@link AuthStorage.checkCredentials}.
66
77
  *
@@ -227,6 +238,16 @@ export interface AuthCredentialStore {
227
238
  }): string | null;
228
239
  setCache(key: string, value: string, expiresAtSec: number): void;
229
240
  cleanExpiredCache(): void;
241
+ /** Non-expired block for one (credential, providerKey, scope) key, or undefined. */
242
+ getCredentialBlock?(credentialId: number, providerKey: string, blockScope: string): number | undefined;
243
+ /** Upsert with MAX semantics: keep the later blockedUntilMs on conflict. */
244
+ upsertCredentialBlock?(block: StoredCredentialBlock): void;
245
+ /** Drop every block row for a credential (all providerKeys/scopes). */
246
+ deleteCredentialBlocks?(credentialId: number): void;
247
+ /** Prune rows with blocked_until_ms <= nowMs. */
248
+ cleanExpiredCredentialBlocks?(nowMs: number): void;
249
+ /** List non-expired blocks for broker snapshots. */
250
+ listCredentialBlocks?(credentialIds: readonly number[]): StoredCredentialBlock[];
230
251
  /**
231
252
  * Append usage-limit snapshots for trend history. Optional: stores without
232
253
  * durable storage (e.g. the broker remote store) omit it and recording is
@@ -694,10 +715,7 @@ export declare class AuthStorage {
694
715
  */
695
716
  login(provider: OAuthProviderId, ctrl: OAuthController & {
696
717
  /** onAuth is required by auth-storage but optional in OAuthController */
697
- onAuth: (info: {
698
- url: string;
699
- instructions?: string;
700
- }) => void;
718
+ onAuth: (info: OAuthAuthInfo) => void;
701
719
  /** onPrompt is required for some providers (github-copilot, openai-codex) */
702
720
  onPrompt: (prompt: {
703
721
  message: string;
@@ -949,6 +967,18 @@ export declare class AuthStorage {
949
967
  * the existing row instead of inserting a duplicate.
950
968
  */
951
969
  upsertCredential(provider: string, credential: AuthCredential): AuthCredentialSnapshotEntry[];
970
+ /**
971
+ * Broker-server seam: list non-expired persisted blocks for snapshot entries.
972
+ */
973
+ listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[];
974
+ /**
975
+ * Broker-server seam: persist one credential block and notify snapshot waiters.
976
+ */
977
+ upsertCredentialBlock(block: StoredCredentialBlock): void;
978
+ /**
979
+ * Broker-server seam: clear all persisted blocks for one credential and notify snapshot waiters.
980
+ */
981
+ deleteCredentialBlocks(credentialId: number): void;
952
982
  /**
953
983
  * Describe where the active credential for a provider came from.
954
984
  *
@@ -1000,6 +1030,11 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
1000
1030
  }): string | null;
1001
1031
  setCache(key: string, value: string, expiresAtSec: number): void;
1002
1032
  cleanExpiredCache(): void;
1033
+ getCredentialBlock(credentialId: number, providerKey: string, blockScope: string): number | undefined;
1034
+ upsertCredentialBlock(block: StoredCredentialBlock): void;
1035
+ deleteCredentialBlocks(credentialId: number): void;
1036
+ cleanExpiredCredentialBlocks(nowMs: number): void;
1037
+ listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[];
1003
1038
  recordUsageSnapshots(entries: UsageHistoryEntry[]): void;
1004
1039
  listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[];
1005
1040
  recordUsageCosts(entries: UsageCostHistoryEntry[]): void;
@@ -18,7 +18,21 @@ export type OAuthPrompt = {
18
18
  allowEmpty?: boolean;
19
19
  };
20
20
  export type OAuthAuthInfo = {
21
+ /**
22
+ * Full authorization URL. Suitable for direct browser launch, OSC 8
23
+ * hyperlinks, and clipboard when the target UI can guarantee the full
24
+ * string reaches the user unmodified.
25
+ */
21
26
  url: string;
27
+ /**
28
+ * Short loopback URL that 302-redirects to {@link url}. Provided by flows
29
+ * that host the redirect on the same callback server they already run
30
+ * ({@link OAuthCallbackFlow}). UIs SHOULD prefer this as the copy target
31
+ * so viewport truncation cannot corrupt OAuth query parameters. Undefined
32
+ * for flows without a loopback callback server (device code, paste-code
33
+ * providers with fixed non-loopback redirects, etc.).
34
+ */
35
+ launchUrl?: string;
22
36
  instructions?: string;
23
37
  };
24
38
  export interface OAuthProviderInfo {
@@ -493,6 +493,21 @@ export interface DeveloperMessage {
493
493
  providerPayload?: ProviderPayload;
494
494
  timestamp: number;
495
495
  }
496
+ export type AssistantRetryRecoveryKind = "credential" | "model" | "wait" | "plain";
497
+ export interface AssistantRetryRecovery {
498
+ kind: "auto-retry";
499
+ status: "recovered";
500
+ attempt: number;
501
+ recoveredAt: string;
502
+ recovery: AssistantRetryRecoveryKind;
503
+ note: string;
504
+ supersededBy?: {
505
+ timestamp: number;
506
+ responseId?: string;
507
+ provider: string;
508
+ model: string;
509
+ };
510
+ }
496
511
  export interface ContextSnapshot {
497
512
  promptTokens: number;
498
513
  nonMessageTokens: number;
@@ -505,6 +520,7 @@ export interface AssistantMessage {
505
520
  provider: Provider;
506
521
  model: string;
507
522
  contextSnapshot?: ContextSnapshot;
523
+ retryRecovery?: AssistantRetryRecovery;
508
524
  responseId?: string;
509
525
  /**
510
526
  * Name of the upstream provider an aggregator routed this request to, as
@@ -66,6 +66,11 @@ export declare function normalizeSchemaForMCP(value: unknown): unknown;
66
66
  * and the depth-10 limit.
67
67
  */
68
68
  export declare function normalizeSchemaForMoonshot(value: unknown): unknown;
69
+ /**
70
+ * Rewrites standard JSON Schema forms that Ollama's Go `/api/chat` tool parser
71
+ * cannot unmarshal into its object-shaped `Schema` struct.
72
+ */
73
+ export declare function sanitizeSchemaForOllama(schema: JsonObject): JsonObject;
69
74
  /**
70
75
  * OpenAI Responses rejects `oneOf` in tool schemas even when strict mode is
71
76
  * disabled, and rejects every schema node with `type: "object"` unless it has
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.3.4",
4
+ "version": "16.3.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "16.3.4",
42
- "@oh-my-pi/pi-utils": "16.3.4",
43
- "@oh-my-pi/pi-wire": "16.3.4",
41
+ "@oh-my-pi/pi-catalog": "16.3.6",
42
+ "@oh-my-pi/pi-utils": "16.3.6",
43
+ "@oh-my-pi/pi-wire": "16.3.6",
44
44
  "arktype": "^2.2.0",
45
45
  "zod": "^4"
46
46
  },
@@ -9,6 +9,9 @@ import { readSseEvents } from "@oh-my-pi/pi-utils";
9
9
  import { type } from "arktype";
10
10
  import type { AuthCredential } from "../auth-storage";
11
11
  import type {
12
+ CredentialBlockRequest,
13
+ CredentialBlockResponse,
14
+ CredentialBlocksDeleteResponse,
12
15
  CredentialDisableRequest,
13
16
  CredentialDisableResponse,
14
17
  CredentialRefreshResponse,
@@ -20,6 +23,8 @@ import type {
20
23
  UsageResponse,
21
24
  } from "./types";
22
25
  import {
26
+ credentialBlockResponseSchema,
27
+ credentialBlocksDeleteResponseSchema,
23
28
  credentialDisableResponseSchema,
24
29
  credentialRefreshResponseSchema,
25
30
  credentialUploadResponseSchema,
@@ -106,7 +111,11 @@ export class AuthBrokerClient {
106
111
  }
107
112
 
108
113
  healthz(signal?: AbortSignal): Promise<HealthzResponse> {
109
- return this.#request("GET", "/v1/healthz", { schema: healthzResponseSchema, auth: false, signal });
114
+ return this.#request<HealthzResponse>("GET", "/v1/healthz", {
115
+ schema: healthzResponseSchema,
116
+ auth: false,
117
+ signal,
118
+ });
110
119
  }
111
120
 
112
121
  async fetchSnapshot(opts: FetchSnapshotOptions = {}): Promise<FetchSnapshotResult> {
@@ -230,19 +239,19 @@ export class AuthBrokerClient {
230
239
  // `metadata`) but leaves provider-specific extension fields permissive so
231
240
  // the broker can ship new shapes ahead of the client. `raw` is accepted
232
241
  // but normally stripped by the broker before send.
233
- return this.#request("GET", "/v1/usage", { schema: usageResponseSchema, signal }) as Promise<UsageResponse>;
242
+ return this.#request<UsageResponse>("GET", "/v1/usage", { schema: usageResponseSchema, signal });
234
243
  }
235
244
 
236
245
  async refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse> {
237
- return this.#request("POST", `/v1/credential/${id}/refresh`, {
246
+ return this.#request<CredentialRefreshResponse>("POST", `/v1/credential/${id}/refresh`, {
238
247
  schema: credentialRefreshResponseSchema,
239
248
  signal,
240
- }) as Promise<CredentialRefreshResponse>;
249
+ });
241
250
  }
242
251
 
243
252
  async disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse> {
244
253
  const body: CredentialDisableRequest = { cause };
245
- return this.#request("POST", `/v1/credential/${id}/disable`, {
254
+ return this.#request<CredentialDisableResponse>("POST", `/v1/credential/${id}/disable`, {
246
255
  body,
247
256
  schema: credentialDisableResponseSchema,
248
257
  signal,
@@ -255,18 +264,38 @@ export class AuthBrokerClient {
255
264
  signal?: AbortSignal,
256
265
  ): Promise<CredentialUploadResponse> {
257
266
  const body: CredentialUploadRequest = { provider, credential };
258
- return this.#request("POST", "/v1/credential", {
267
+ return this.#request<CredentialUploadResponse>("POST", "/v1/credential", {
259
268
  body,
260
269
  schema: credentialUploadResponseSchema,
261
270
  signal,
262
- }) as Promise<CredentialUploadResponse>;
271
+ });
272
+ }
273
+
274
+ async upsertCredentialBlock(
275
+ id: number,
276
+ block: CredentialBlockRequest,
277
+ signal?: AbortSignal,
278
+ ): Promise<CredentialBlockResponse> {
279
+ const body: CredentialBlockRequest = block;
280
+ return this.#request<CredentialBlockResponse>("POST", `/v1/credential/${id}/block`, {
281
+ body,
282
+ schema: credentialBlockResponseSchema,
283
+ signal,
284
+ });
285
+ }
286
+
287
+ async deleteCredentialBlocks(id: number, signal?: AbortSignal): Promise<CredentialBlocksDeleteResponse> {
288
+ return this.#request<CredentialBlocksDeleteResponse>("DELETE", `/v1/credential/${id}/blocks`, {
289
+ schema: credentialBlocksDeleteResponseSchema,
290
+ signal,
291
+ });
263
292
  }
264
293
 
265
- async #request(
266
- method: "GET" | "POST",
294
+ async #request<t>(
295
+ method: "GET" | "POST" | "DELETE",
267
296
  path: string,
268
297
  opts: { schema: (input: unknown) => unknown; auth?: boolean; body?: unknown; signal?: AbortSignal },
269
- ): Promise<any> {
298
+ ): Promise<t> {
270
299
  const response = await this.#fetchRaw(method, path, opts);
271
300
  const text = await response.text();
272
301
  const raw = this.#parseJson(text, response.status);
@@ -277,7 +306,7 @@ export class AuthBrokerClient {
277
306
  body: validated.summary,
278
307
  });
279
308
  }
280
- return validated;
309
+ return validated as t;
281
310
  }
282
311
 
283
312
  #parseJson(text: string, status: number): unknown {
@@ -293,7 +322,7 @@ export class AuthBrokerClient {
293
322
  }
294
323
 
295
324
  async #fetchRaw(
296
- method: "GET" | "POST",
325
+ method: "GET" | "POST" | "DELETE",
297
326
  path: string,
298
327
  opts: {
299
328
  auth?: boolean;