@north-light/api 0.3.510 → 0.3.512

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,6 +1,6 @@
1
1
  import { ApiError } from './errors.js';
2
2
  import type { AppLimitsChangeDTO, LimitsDTO, SetAppLimitsRequest } from './dto/limits.js';
3
- import type { DaemonAdmitDTO, DaemonRestartDTO, DueDTO, HealthDTO, MigrateStateDTO, MigrateStateRequest, StatusDTO } from './dto/health.js';
3
+ import type { DaemonAdmitDTO, DaemonRestartDTO, DueDTO, HealthDTO, ReadyDTO, MigrateStateDTO, MigrateStateRequest, StatusDTO } from './dto/health.js';
4
4
  import type { BashJobStatusDTO, BashJobStopResultDTO } from './dto/bash-jobs.js';
5
5
  import type { ContextListDTO, CreateNodeRequest, ListNodesQuery, NodeDetailDTO, NodeMessagesPageDTO, NodeMessagesQuery, NodeSessionDTO, NodeSnapshotDTO, NodeSubjectDTO, NodeSummaryDTO, TranscriptDTO, TranscriptQuery } from './dto/nodes.js';
6
6
  import type { NodeOutcomeResponseDTO, OutcomeDeliveryDTO, RegisterOutcomeDeliveryRequest } from './dto/node-outcomes.js';
@@ -123,6 +123,8 @@ export declare class CrtrClient {
123
123
  /** One `/healthz` observation without cold-socket recovery. Availability
124
124
  * waiters own retry policy and pass their remaining wall-clock budget here. */
125
125
  probeHealthz(timeoutMs: number): Promise<HealthDTO>;
126
+ /** `GET /v1/ready`: 200 ready, 503 not yet; both answer a `ReadyDTO`. */
127
+ ready(): Promise<ReadyDTO>;
126
128
  status(): Promise<StatusDTO>;
127
129
  /** Busy and next-due state, the truth behind sandbox sleep and wake. */
128
130
  due(): Promise<DueDTO>;
@@ -107,6 +107,19 @@ export class CrtrClient {
107
107
  }
108
108
  return parse(response);
109
109
  }
110
+ /** `GET /v1/ready`: 200 ready, 503 not yet; both answer a `ReadyDTO`. */
111
+ async ready() {
112
+ const response = await this.transport('GET', routes.ready(), undefined, {});
113
+ if (response.status === 503) {
114
+ try {
115
+ const body = await response.clone().json();
116
+ if (body.ready === false)
117
+ return body;
118
+ }
119
+ catch { /* `parse` reports a malformed 503 normally */ }
120
+ }
121
+ return parse(response);
122
+ }
110
123
  status() {
111
124
  return this.request('GET', routes.status());
112
125
  }
@@ -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.20";
2
+ export declare const CONTRACT_LEVEL = "1.22";
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.20';
15
+ export const CONTRACT_LEVEL = '1.22';
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`. */
@@ -13,6 +13,14 @@ export interface HealthDTO {
13
13
  /** Always present; only `ready` admits normal daemon work. */
14
14
  startup_phase: StartupPhaseDTO;
15
15
  }
16
+ /** `GET /v1/ready` — whether this listener can admit ordinary work (200) or not (503, with `reason`). Reveals nothing else. */
17
+ export type ReadyDTO = {
18
+ ready: true;
19
+ } | {
20
+ ready: false;
21
+ reason: 'startup_pending' | 'jwks_pending' | 'grant_snapshot_missing';
22
+ message: string;
23
+ };
16
24
  /** `GET /v1/status` — daemon status snapshot. Backs `crtr sys daemon status`
17
25
  * state and the `crtr canvas dashboard` header. `runtime_version` is also
18
26
  * checked by Core's minimum-runtime capability gate. */
@@ -3,6 +3,7 @@ export declare const API_VERSION = "v1";
3
3
  /** Route path builders. Keys mirror the spec §6 route table. */
4
4
  export declare const routes: {
5
5
  readonly healthz: () => string;
6
+ readonly ready: () => string;
6
7
  readonly status: () => string;
7
8
  readonly due: () => string;
8
9
  readonly daemonRestart: () => string;
@@ -17,6 +17,7 @@ const V = `/${API_VERSION}`;
17
17
  export const routes = {
18
18
  // Health / status
19
19
  healthz: () => '/healthz',
20
+ ready: () => `${V}/ready`,
20
21
  status: () => `${V}/status`,
21
22
  due: () => `${V}/due`,
22
23
  daemonRestart: () => `${V}/daemon/restart`,
@@ -41,7 +41,7 @@ catch (error) {
41
41
  } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) || parsed.version !== 1 || typeof parsed.accounts !== 'object' || parsed.accounts === null || Array.isArray(parsed.accounts))
42
42
  throw new Error(`managed credential file ${path} is corrupt`); return parsed; }
43
43
  function toPublic(row, file, cooldowns = []) { const stored = file.accounts[row.id]; if (!stored || typeof stored.refresh !== 'string' || typeof stored.access !== 'string' || !Number.isFinite(stored.expires))
44
- throw new Error(`managed ${row.providerId} account ${row.id} has no credential record`); return { label: row.label, refresh: stored.refresh, access: stored.access, expires: stored.expires, rateLimitedUntil: row.rateLimitedUntil, lastAttemptAt: row.lastAttemptAt, lastRateLimitedAt: row.lastRateLimitedAt, ...(row.authFailure ? { authFailure: row.authFailure } : {}), ...(stored.accountId ? { accountId: stored.accountId } : {}), ...(stored.clientId ? { clientId: stored.clientId } : {}), ...(cooldowns.length ? { modelCooldowns: Object.fromEntries(cooldowns.map((cooldown) => [cooldown.family, cooldown.rateLimitedUntil])) } : {}) }; }
44
+ throw new Error(`managed ${row.providerId} account ${row.id} has no credential record`); return { label: row.label, refresh: stored.refresh, access: stored.access, expires: stored.expires, rateLimitedUntil: row.authFailure || !row.lastRateLimitedAt ? row.rateLimitedUntil : Math.min(row.rateLimitedUntil, row.lastRateLimitedAt + MAX_RATE_LIMIT_COOLDOWN_MS), lastAttemptAt: row.lastAttemptAt, lastRateLimitedAt: row.lastRateLimitedAt, ...(row.authFailure ? { authFailure: row.authFailure } : {}), ...(stored.accountId ? { accountId: stored.accountId } : {}), ...(stored.clientId ? { clientId: stored.clientId } : {}), ...(cooldowns.length ? { modelCooldowns: Object.fromEntries(cooldowns.map((cooldown) => [cooldown.family, cooldown.lastRateLimitedAt ? Math.min(cooldown.rateLimitedUntil, cooldown.lastRateLimitedAt + MAX_RATE_LIMIT_COOLDOWN_MS) : cooldown.rateLimitedUntil])) } : {}) }; }
45
45
  function rowsWithCredentials(provider, file) { const cooldowns = providerModelCooldownsFor(agentDir(), provider); return providerRows(agentDir(), provider).map((row) => ({ row, entry: toPublic(row, file, cooldowns.get(row.id)) })); }
46
46
  function managedCredentialFile(provider) { return new LockedJsonFile(credentialFile(provider), true, '{"version":1,"accounts":{}}\n'); }
47
47
  function readFile(provider) { return parseCredentials(credentialFile(provider), managedCredentialFile(provider).readUnlocked()); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.510",
3
+ "version": "0.3.512",
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",