@north-light/api 0.3.540 → 0.3.542

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.
@@ -1,5 +1,5 @@
1
1
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
2
- export declare const CONTRACT_LEVEL = "1.28";
2
+ export declare const CONTRACT_LEVEL = "1.29";
3
3
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
4
  export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
5
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -12,7 +12,7 @@
12
12
  //
13
13
  // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
14
14
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
15
- export const CONTRACT_LEVEL = '1.28';
15
+ export const CONTRACT_LEVEL = '1.29';
16
16
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
17
17
  export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
18
18
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -77,18 +77,24 @@ export declare class DuplicateSubscriptionError extends Error {
77
77
  constructor(message: string);
78
78
  }
79
79
  export declare function readSubscriptionPool(provider: ManagedProviderId): SubscriptionCredential[];
80
+ /** The pool with each account's row id: the stable handle a caller outside the process removes an account by. */
81
+ export declare function readSubscriptionPoolRows(provider: ManagedProviderId): Array<{
82
+ id: string;
83
+ entry: SubscriptionCredential;
84
+ }>;
80
85
  export declare function writeSubscriptionPool(provider: ManagedProviderId, next: SubscriptionCredential[]): Promise<SubscriptionCredential[]>;
81
86
  export declare function addSubscription(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
82
87
  export declare function upsertSubscription(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
83
88
  export declare function upsertSubscriptionWithUniqueAccount(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
84
89
  export declare function renameSubscription(provider: ManagedProviderId, ref: string, nextLabel: string): Promise<SubscriptionCredential[]>;
85
90
  export declare function removeManagedAccount(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
91
+ /** Remove the account whose row id is `id` (see `readSubscriptionPoolRows`); throws when no account has it. */
92
+ export declare function removeManagedAccountById(provider: ManagedProviderId, id: string): Promise<SubscriptionCredential[]>;
86
93
  export declare function removeSubscription(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
87
94
  export declare function removeAllManagedAccounts(provider: ManagedProviderId): Promise<number>;
88
95
  export declare function removeManagedProvider(provider: ManagedProviderId, label?: string): Promise<SubscriptionCredential[]>;
89
96
  export declare function promoteSubscription(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
90
97
  export declare function markSubscriptionAttempt(provider: ManagedProviderId, ref: string, attemptAt?: number): Promise<SubscriptionCredential[]>;
91
- export declare function markSubscriptionAuthenticationFailed(provider: ManagedProviderId, ref: string, attempted: Pick<SubscriptionCredential, 'refresh' | 'access'>, retryAfterMs: number, attemptAt?: number): Promise<SubscriptionCredential[]>;
92
98
  /** Record a 429 on an account. `scope` defaults to the whole account; a model scope cools only that family on this account, and a 429 older than the newest one already recorded for the same scope is ignored. */
93
99
  export declare function markSubscriptionRateLimited(provider: ManagedProviderId, ref: string, retryAfterMs: number, attemptAt?: number, rateLimitedAt?: number, scope?: RateLimitScope): Promise<SubscriptionCredential[]>;
94
100
  /** A call on `modelId` succeeded: clear the account-wide cooldown and auth failure, and that model family's cooldown, unless a newer 429 for the same scope landed after `attemptAt` began. */
@@ -87,6 +87,8 @@ export class DuplicateSubscriptionError extends Error {
87
87
  constructor(message) { super(message); this.name = 'DuplicateSubscriptionError'; }
88
88
  }
89
89
  export function readSubscriptionPool(provider) { return rowsWithCredentials(provider, readFile(provider)).map(({ entry }) => entry); }
90
+ /** The pool with each account's row id: the stable handle a caller outside the process removes an account by. */
91
+ export function readSubscriptionPoolRows(provider) { return rowsWithCredentials(provider, readFile(provider)).map(({ row, entry }) => ({ id: row.id, entry })); }
90
92
  async function locked(provider, run) { const path = credentialFile(provider); return managedCredentialFile(provider).withLockAsync(async (current, _held, publish) => { const file = parseCredentials(path, current); return { result: await run(file, rowsWithCredentials(provider, file), (next) => publish(`${JSON.stringify(next)}\n`)) }; }); }
91
93
  function insert(db, provider, row) { cachedStatement(db, 'INSERT INTO managed_accounts(provider_id,id,label,label_key,position,last_attempt_at,last_rate_limited_at,rate_limited_until,auth_failure) VALUES(?,?,?,?,?,?,?,?,?)').run(provider, row.id, row.label, row.labelKey, row.position, row.lastAttemptAt, row.lastRateLimitedAt, row.rateLimitedUntil, row.authFailure ?? null); }
92
94
  function update(db, row) { cachedStatement(db, 'UPDATE managed_accounts SET label=?,label_key=?,position=?,last_attempt_at=?,last_rate_limited_at=?,rate_limited_until=?,auth_failure=? WHERE provider_id=? AND id=?').run(row.label, row.labelKey, row.position, row.lastAttemptAt, row.lastRateLimitedAt, row.rateLimitedUntil, row.authFailure ?? null, row.providerId, row.id); }
@@ -130,7 +132,7 @@ export async function upsertSubscriptionWithUniqueAccount(provider, entry) {
130
132
  const cooldownIncoming = checked.lastRateLimitedAt >= row.lastRateLimitedAt;
131
133
  file.accounts[row.id] = stored(checked);
132
134
  publish(file);
133
- writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { update(db, { ...row, label: checked.label, labelKey: key(checked.label), lastAttemptAt: Math.max(row.lastAttemptAt, checked.lastAttemptAt), lastRateLimitedAt: cooldownIncoming ? checked.lastRateLimitedAt : row.lastRateLimitedAt, rateLimitedUntil: cooldownIncoming ? checked.rateLimitedUntil : row.rateLimitedUntil }); if (differentAccount(existing.entry, checked))
135
+ writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { update(db, { ...row, authFailure: undefined, label: checked.label, labelKey: key(checked.label), lastAttemptAt: Math.max(row.lastAttemptAt, checked.lastAttemptAt), lastRateLimitedAt: cooldownIncoming ? checked.lastRateLimitedAt : row.lastRateLimitedAt, rateLimitedUntil: cooldownIncoming ? checked.rateLimitedUntil : row.rateLimitedUntil }); if (differentAccount(existing.entry, checked))
134
136
  deleteModelCooldowns(db, provider, row.id); }));
135
137
  return publicPool(provider, file);
136
138
  });
@@ -139,25 +141,24 @@ export async function renameSubscription(provider, ref, nextLabel) { return lock
139
141
  throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const label = nextLabel.trim(); if (!label)
140
142
  throw new Error(`${getProviderLabel(provider)} subscription label cannot be empty`); if (rows.some((value, i) => i !== index && key(value.entry.label) === key(label)))
141
143
  throw new DuplicateSubscriptionError(`${getProviderLabel(provider)} subscription already exists: ${label}`); providerWrite(agentDir(), (db) => update(db, { ...rows[index].row, label, labelKey: key(label) })); return publicPool(provider, file); }); }
142
- async function remove(provider, ref) { return locked(provider, async (file, rows, publish) => { const index = resolve(rows, ref); if (index < 0)
143
- throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const removed = rows[index].row; providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=? AND id=?').run(provider, removed.id); deleteModelCooldowns(db, provider, removed.id); }); delete file.accounts[removed.id]; publish(file); return publicPool(provider, file); }); }
144
- export async function removeManagedAccount(provider, ref) { return remove(provider, ref); }
145
- export async function removeSubscription(provider, ref) { return remove(provider, ref); }
144
+ async function remove(provider, find, missing) { return locked(provider, async (file, rows, publish) => { const index = find(rows); if (index < 0)
145
+ throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${missing}`); const removed = rows[index].row; providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=? AND id=?').run(provider, removed.id); deleteModelCooldowns(db, provider, removed.id); }); delete file.accounts[removed.id]; publish(file); return publicPool(provider, file); }); }
146
+ export async function removeManagedAccount(provider, ref) { return remove(provider, (rows) => resolve(rows, ref), ref); }
147
+ /** Remove the account whose row id is `id` (see `readSubscriptionPoolRows`); throws when no account has it. */
148
+ export async function removeManagedAccountById(provider, id) { return remove(provider, (rows) => rows.findIndex(({ row }) => row.id === id), id); }
149
+ export async function removeSubscription(provider, ref) { return remove(provider, (rows) => resolve(rows, ref), ref); }
146
150
  export async function removeAllManagedAccounts(provider) { return locked(provider, async (file, rows, publish) => { providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=?').run(provider); deleteModelCooldowns(db, provider); }); for (const { row } of rows)
147
151
  delete file.accounts[row.id]; publish(file); return rows.length; }); }
148
152
  export async function removeManagedProvider(provider, label) { if (label === undefined) {
149
153
  await removeAllManagedAccounts(provider);
150
154
  return [];
151
- } return remove(provider, label); }
155
+ } return removeManagedAccount(provider, label); }
152
156
  export async function promoteSubscription(provider, ref) { return locked(provider, async (file, rows) => { const index = resolve(rows, ref); if (index < 0)
153
157
  throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); providerWrite(agentDir(), (db) => { const reordered = [rows[index], ...rows.filter((_, i) => i !== index)]; reordered.forEach((item, position) => update(db, { ...item.row, position })); }); return publicPool(provider, file); }); }
154
158
  async function mutateHealth(provider, ref, mutate) { providerWrite(agentDir(), (db) => { const row = providerRowByReference(db, provider, ref); if (!row)
155
159
  throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const next = mutate(row); if (next)
156
160
  update(db, next); }); return readSubscriptionPool(provider); }
157
161
  export async function markSubscriptionAttempt(provider, ref, attemptAt = Date.now()) { return mutateHealth(provider, ref, (row) => ({ ...row, lastAttemptAt: Math.max(row.lastAttemptAt, attemptAt) })); }
158
- export async function markSubscriptionAuthenticationFailed(provider, ref, attempted, retryAfterMs, attemptAt = Date.now()) { return locked(provider, async (file, rows) => { const current = rows.find(({ entry }) => key(entry.label) === key(ref)); if (!current)
159
- throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); if (current.entry.refresh !== attempted.refresh || current.entry.access !== attempted.access)
160
- return publicPool(provider, file); providerWrite(agentDir(), (db) => update(db, { ...current.row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(current.row.lastAttemptAt, attemptAt), rateLimitedUntil: Math.max(current.row.rateLimitedUntil, attemptAt + Math.max(0, retryAfterMs)) })); return publicPool(provider, file); }); }
161
162
  /** Record a 429 on an account. `scope` defaults to the whole account; a model scope cools only that family on this account, and a 429 older than the newest one already recorded for the same scope is ignored. */
162
163
  export async function markSubscriptionRateLimited(provider, ref, retryAfterMs, attemptAt = Date.now(), rateLimitedAt = Date.now(), scope = { kind: 'account' }) {
163
164
  retryAfterMs = Math.min(Math.max(0, retryAfterMs), MAX_RATE_LIMIT_COOLDOWN_MS);
@@ -192,9 +193,18 @@ export async function markSubscriptionSuccess(provider, ref, attemptAt = Date.no
192
193
  });
193
194
  return readSubscriptionPool(provider);
194
195
  }
196
+ /** The vendor refused this account's refresh token: flag the row so every pool reader skips it until a sign-in replaces it. Both refresh paths (pi's `modify` and the daemon's /v1/llm) record it here. */
197
+ function recordInvalidGrant(row) { const at = Date.now(); providerWrite(agentDir(), (db) => update(db, { ...row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(row.lastAttemptAt, at), rateLimitedUntil: Math.max(row.rateLimitedUntil, at + INVALID_REFRESH_TOKEN_BACKOFF_MS) })); }
195
198
  export async function refreshSubscriptionCredential(provider, label, exchange) { return locked(provider, async (file, rows, publish) => { const index = resolve(rows, label); if (index < 0)
196
199
  throw new Error(`${getProviderLabel(provider)} subscription "${label}" no longer exists`); const current = rows[index]; if (current.entry.expires > Date.now())
197
- return current.entry; const next = await exchange(current.entry.refresh, current.entry.clientId); const tokens = withClientId(assertTokens(provider, current.entry.label, next), next.clientId ?? current.entry.clientId); file.accounts[current.row.id] = { ...file.accounts[current.row.id], ...tokens }; publish(file); return { ...current.entry, ...tokens }; }); }
200
+ return current.entry; let next; try {
201
+ next = await exchange(current.entry.refresh, current.entry.clientId);
202
+ }
203
+ catch (error) {
204
+ if (isInvalidRefreshTokenError(error))
205
+ recordInvalidGrant(current.row);
206
+ throw error;
207
+ } const tokens = withClientId(assertTokens(provider, current.entry.label, next), next.clientId ?? current.entry.clientId); file.accounts[current.row.id] = { ...file.accounts[current.row.id], ...tokens }; publish(file); return { ...current.entry, ...tokens }; }); }
198
208
  export function isInvalidRefreshTokenError(error) { const value = error; const message = [value?.errorMessage, value?.message, error instanceof Error ? error.message : ''].filter((part) => typeof part === 'string').join(' '); const status = typeof value?.status === 'number' ? value.status : Number(message.match(/\bstatus[=:]\s*(\d+)/i)?.[1]); return status === 400 && /invalid_grant|refresh token .*invalid|refresh token .*not found/i.test(message); }
199
209
  export async function modifyElectedSubscription(provider, elect, apply) { return locked(provider, async (file, rows, publish) => { const attempted = new Set(); let lastError; while (attempted.size < rows.length) {
200
210
  const chosen = elect(rows.filter(({ entry }) => !attempted.has(entry.label)).map(({ entry }) => entry));
@@ -216,8 +226,7 @@ export async function modifyElectedSubscription(provider, elect, apply) { return
216
226
  if (!isInvalidRefreshTokenError(error))
217
227
  throw error;
218
228
  lastError = error;
219
- const at = Date.now();
220
- providerWrite(agentDir(), (db) => update(db, { ...current.row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(current.row.lastAttemptAt, at), rateLimitedUntil: Math.max(current.row.rateLimitedUntil, at + INVALID_REFRESH_TOKEN_BACKOFF_MS) }));
229
+ recordInvalidGrant(current.row);
221
230
  }
222
231
  } if (lastError)
223
232
  throw lastError; return undefined; }); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.540",
3
+ "version": "0.3.542",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",