@north-light/sdk 0.3.422 → 0.3.424

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
@@ -44,9 +44,13 @@ const client = new Crouter({
44
44
  });
45
45
  ```
46
46
 
47
- ## Runtime version
47
+ ## Runtime compatibility
48
48
 
49
- The SDK talks only to a runtime at or above its own version (`SDK_VERSION`), and has no compatibility mode for older ones: its types, such as `RunObject.activity`, describe what a runtime at its release sends. Every runtime answer carries a `Crouter-Runtime-Version` header. When that version is older than the SDK, or the header is missing (every runtime released before this rule shipped), the call throws `RuntimeVersionError` before the SDK reads the response: `code` is `runtime_version_unsupported`, `status` is 426, `retryable` is false, and `sdkVersion` and `runtimeVersion` name both versions. The SDK also sends its own version on every request except `/healthz` as `Crouter-Minimum-Runtime-Version`; the hosting router and a runtime at this release or later refuse an older runtime with the same 426 *before* any handler runs, so nothing is accepted and no `Idempotency-Key` is recorded (a retry after the roll is a first send). Upgrade the runtime first, then the SDK.
49
+ Compatibility between the SDK and a runtime is a **contract level** (`MAJOR.MINOR`), not the package version. The SDK exports it as `CONTRACT_LEVEL` and its manifest carries it as `crouterContract`. The level changes only when the `/v1` surface changes in a way the release check can see or an author declared, so most releases leave it alone.
50
+
51
+ Every runtime answer carries `Crouter-Contract` (the levels it serves) and `Crouter-Runtime-Version`. The SDK sends the level it needs on every request except `/healthz` as `Crouter-Required-Contract`. A runtime satisfies it when it serves the same major and a minor at or above the required one. When it does not, or it names no level on a successful answer, the call throws `RuntimeVersionError` before the SDK reads the response: `code` is `runtime_version_unsupported`, `status` is 426, `retryable` is false, and `requiredContract` and `runtimeContract` name both levels (`sdkVersion` and `runtimeVersion` stay for logging). The hosting router and the runtime refuse the same way *before* any handler runs, so nothing is accepted and no `Idempotency-Key` is recorded (a retry after the roll is a first send). Fix it by rolling the runtime forward, or by pinning an SDK whose `crouterContract` the runtime serves.
52
+
53
+ **`@latest` always works against production.** Every SDK release is published to the `next` dist-tag first; `latest` moves only once production serves the SDK's contract level. `next` may need a newer runtime than production runs. From this contract's first release the SDK also tolerates values it does not know: an unknown run event `type` is passed through to your listener untouched, and an unknown run `status` ends a wait or watch and reports the raw status.
50
54
 
51
55
  Pin `@north-light/sdk` to an exact version. One release is one version set: `@north-light/sdk@X` depends on exactly `@north-light/api@X` and `@north-light/identity@X`, so the pin fixes all three.
52
56
 
@@ -56,7 +60,7 @@ import { RuntimeVersionError } from '@north-light/sdk';
56
60
  try {
57
61
  await client.runs.get(runId);
58
62
  } catch (error) {
59
- if (error instanceof RuntimeVersionError) console.error(`runtime ${error.runtimeVersion ?? '(unnamed)'} is older than SDK ${error.sdkVersion}`);
63
+ if (error instanceof RuntimeVersionError) console.error(`runtime serves contract ${error.runtimeContract ?? '(unnamed)'}, this SDK needs ${error.requiredContract}`);
60
64
  else throw error;
61
65
  }
62
66
  ```
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { APIError, CrtrClient, MINIMUM_RUNTIME_VERSION_HEADER, RUNTIME_VERSION_HEADER, compareReleaseVersions, routes, } from '@north-light/api';
1
+ import { APIError, CrtrClient, CONTRACT_HEADER, CONTRACT_LEVEL, MINIMUM_RUNTIME_VERSION_HEADER, REQUIRED_CONTRACT_HEADER, RUNTIME_VERSION_HEADER, contractSatisfies, routes, } from '@north-light/api';
2
2
  import { APIConnectionError, APIUserAbortError, AuthenticationError, CrouterError, mapError } from './errors.js';
3
3
  import { Bash } from './resources/bash.js';
4
4
  import { Canvas } from './resources/canvas.js';
@@ -20,7 +20,7 @@ import { Memory } from './resources/memory.js';
20
20
  import { Uploads } from './resources/uploads.js';
21
21
  import { deadline, heldReadOptions, withinDeadline } from './resources/request.js';
22
22
  import { resolveJsonSchema } from './schema.js';
23
- import { SDK_VERSION } from './version.js';
23
+ import { LEGACY_FLOOR, SDK_VERSION } from './version.js';
24
24
  const LOCAL_SOCKET_FETCH = Symbol.for('@north-light/api/local-socket-fetch');
25
25
  const DEFAULT_TIMEOUT_MS = 30_000;
26
26
  const POLL_WAIT_SECONDS = 25;
@@ -515,38 +515,43 @@ function isNodeRuntime() {
515
515
  return typeof process !== 'undefined' && process.versions?.node !== undefined;
516
516
  }
517
517
  /**
518
- * Latest only (P2): the SDK accepts a runtime at or above its own release (`SDK_VERSION`) and nothing older, so its
519
- * types never describe a field an older runtime does not send. Every runtime response names its release in
520
- * `Crouter-Runtime-Version`; a successful answer from an older runtime, or one that names no release (every runtime
521
- * before the header existed), is replaced by a 426 `runtime_version_unsupported` envelope before anything reads it,
522
- * which the client raises as `RuntimeVersionError`. A refusal without the header passes through unchanged: it can come
523
- * from a router or proxy in front of the runtime, and it carries no runtime data. `/healthz`, the unversioned
524
- * substrate probe the cold-start wait polls, is exempt.
518
+ * The contract gate. The SDK sends the one contract level it needs (`Crouter-Required-Contract`) and the frozen
519
+ * legacy floor (`Crouter-Minimum-Runtime-Version`, which a runtime from before the contract level refuses on). Every
520
+ * runtime answer lists the levels it serves in `Crouter-Contract`:
521
+ * - present and satisfying (same major, served minor at or above the required one): the answer passes;
522
+ * - present and not satisfying: replaced by a 426 `runtime_version_unsupported` envelope before anything reads it,
523
+ * which the client raises as `RuntimeVersionError`;
524
+ * - absent on a refusal (not ok): passes through unchanged, since it can come from a router or proxy and carries no
525
+ * runtime data;
526
+ * - absent on a success: a runtime from before the contract level, so the same 426.
527
+ * `/healthz`, the unversioned substrate probe the cold-start wait polls, is exempt.
525
528
  */
526
529
  function requireCurrentRuntime(inner) {
527
530
  const checked = async (input, init) => {
528
531
  const url = new URL(input instanceof Request ? input.url : String(input), 'http://localhost');
529
- // Say which runtime this SDK accepts, so the router in front of a hosted runtime, or the runtime itself, refuses
530
- // an older one before any handler runs. `/healthz` is exempt from both checks.
532
+ // Say which contract this SDK needs, so the router in front of a hosted runtime, or the runtime itself, refuses an
533
+ // incompatible one before any handler runs. `/healthz` is exempt from both checks.
531
534
  let sentInit = init;
532
535
  if (url.pathname !== '/healthz') {
533
536
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
534
- headers.set(MINIMUM_RUNTIME_VERSION_HEADER, SDK_VERSION);
537
+ headers.set(MINIMUM_RUNTIME_VERSION_HEADER, LEGACY_FLOOR);
538
+ headers.set(REQUIRED_CONTRACT_HEADER, CONTRACT_LEVEL);
535
539
  sentInit = { ...init, headers };
536
540
  }
537
541
  const response = await (inner ?? globalThis.fetch)(input, sentInit);
538
542
  if (url.pathname === '/healthz')
539
543
  return response;
540
- const runtimeVersion = response.headers.get(RUNTIME_VERSION_HEADER);
541
- if (runtimeVersion === null ? !response.ok : compareReleaseVersions(runtimeVersion, SDK_VERSION) >= 0)
544
+ const served = response.headers.get(CONTRACT_HEADER);
545
+ if (served === null ? !response.ok : contractSatisfies(CONTRACT_LEVEL, served))
542
546
  return response;
543
547
  await response.body?.cancel();
544
- const message = runtimeVersion === null
545
- ? `The runtime did not name its version, so it predates the runtime version header; @north-light/sdk ${SDK_VERSION} requires a runtime at ${SDK_VERSION} or later. Upgrade the runtime, or pin @north-light/sdk to the runtime's release.`
546
- : `The runtime is ${runtimeVersion}; @north-light/sdk ${SDK_VERSION} requires a runtime at ${SDK_VERSION} or later. Upgrade the runtime, or pin @north-light/sdk to ${runtimeVersion}.`;
548
+ const runtimeVersion = response.headers.get(RUNTIME_VERSION_HEADER);
549
+ const message = served === null
550
+ ? `The runtime did not name its contract level, so it predates the contract header; @north-light/sdk ${SDK_VERSION} requires contract ${CONTRACT_LEVEL} or later. Upgrade the runtime, or pin @north-light/sdk to an older release that matches it.`
551
+ : `The runtime serves contract ${served}; @north-light/sdk ${SDK_VERSION} requires contract ${CONTRACT_LEVEL} or later within the same major. Upgrade the runtime, or pin @north-light/sdk to a release whose crouterContract the runtime serves.`;
547
552
  return new Response(JSON.stringify({ error: {
548
553
  code: 'runtime_version_unsupported', message, type: 'invalid_request_error', retryable: false,
549
- details: { sdk_version: SDK_VERSION, runtime_version: runtimeVersion },
554
+ details: { sdk_version: SDK_VERSION, runtime_version: runtimeVersion, required_contract: CONTRACT_LEVEL, runtime_contract: served },
550
555
  } }), { status: 426, headers: { 'content-type': 'application/json' } });
551
556
  };
552
557
  if (inner !== undefined && inner[LOCAL_SOCKET_FETCH] === true) {
package/dist/errors.d.ts CHANGED
@@ -6,12 +6,16 @@ export declare class CrouterError extends APIError {
6
6
  readonly code: SdkErrorCode;
7
7
  constructor(message: string, code?: SdkErrorCode);
8
8
  }
9
- /** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
10
- * sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
9
+ /** The runtime does not serve the contract level this SDK requires (or names no level at all), so the SDK's types do not
10
+ * describe what it sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
11
11
  * runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
12
12
  export declare class RuntimeVersionError extends APIError {
13
- /** The oldest runtime this SDK accepts: its own release version. */
13
+ /** The legacy minimum-runtime release the SDK sent (or the SDK's own release when the SDK raised this itself). */
14
14
  get sdkVersion(): string;
15
+ /** The contract level the SDK requires (`Crouter-Required-Contract`), or `null` for a refusal that is not about the contract. */
16
+ get requiredContract(): string | null;
17
+ /** The contract levels the runtime serves (`Crouter-Contract`), or `null` when it named none. */
18
+ get runtimeContract(): string | null;
15
19
  /** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
16
20
  get runtimeVersion(): string | null;
17
21
  }
@@ -19,6 +23,8 @@ export declare class RuntimeVersionError extends APIError {
19
23
  export interface RuntimeVersionDetails {
20
24
  sdk_version: string;
21
25
  runtime_version: string | null;
26
+ required_contract?: string;
27
+ runtime_contract?: string | null;
22
28
  }
23
29
  /** The daemon rejected the request: HTTP 400. */
24
30
  export declare class BadRequestError extends APIError {
package/dist/errors.js CHANGED
@@ -7,12 +7,16 @@ export class CrouterError extends APIError {
7
7
  this.name = 'CrouterError';
8
8
  }
9
9
  }
10
- /** The runtime is older than this SDK (or names no version at all), so the SDK's types do not describe what it
11
- * sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
10
+ /** The runtime does not serve the contract level this SDK requires (or names no level at all), so the SDK's types do not
11
+ * describe what it sends. Raised before the SDK reads the response; never retryable. Upgrade the runtime (or pin the SDK to the
12
12
  * runtime's release). `code` is `runtime_version_unsupported`, `status` 426. */
13
13
  export class RuntimeVersionError extends APIError {
14
- /** The oldest runtime this SDK accepts: its own release version. */
14
+ /** The legacy minimum-runtime release the SDK sent (or the SDK's own release when the SDK raised this itself). */
15
15
  get sdkVersion() { return this.details.sdk_version; }
16
+ /** The contract level the SDK requires (`Crouter-Required-Contract`), or `null` for a refusal that is not about the contract. */
17
+ get requiredContract() { return this.details.required_contract ?? null; }
18
+ /** The contract levels the runtime serves (`Crouter-Contract`), or `null` when it named none. */
19
+ get runtimeContract() { return this.details.runtime_contract ?? null; }
16
20
  /** The version the runtime named in `Crouter-Runtime-Version`, or `null` when it sent none (a runtime from before the header existed). */
17
21
  get runtimeVersion() { return this.details.runtime_version; }
18
22
  }
@@ -18,6 +18,15 @@ export type RunEnded = {
18
18
  runId: string;
19
19
  ended: 'gone';
20
20
  run: null;
21
+ }
22
+ /** The run is no longer `running` but its `status` is one this SDK does not know (a runtime newer than the SDK added
23
+ * it). It is treated as at rest; `status` is the raw value and `run` the whole answer. */
24
+ | {
25
+ userId: string;
26
+ runId: string;
27
+ ended: 'unknown';
28
+ status: string;
29
+ run: RunObject;
21
30
  };
22
31
  /** One live-progress reading `RunWatcher` hands to `onActivity`: a `running` run's `activity` and the `runs.get` answer it came from. */
23
32
  export interface RunActivityEvent {
@@ -1,7 +1,7 @@
1
1
  import { APIError, CrouterError } from '../errors.js';
2
2
  import { Follower, troubleResult } from './follower.js';
3
3
  const defaultActivityWait = 2;
4
- const restingStatuses = new Set(['settled', 'idle', 'waiting_on_user']);
4
+ const knownResting = new Set(['settled', 'idle', 'waiting_on_user']);
5
5
  /**
6
6
  * Follows an app's background runs until each comes to rest (`settled`, `idle`, `waiting_on_user`, or deleted),
7
7
  * and hands each ending to `onEnded` at least once. Build it with `oauth.watchRuns(...)`, then `start()`.
@@ -90,7 +90,9 @@ export class RunWatcher {
90
90
  }
91
91
  return troubleResult(this.follower, error, run);
92
92
  }
93
- if (!restingStatuses.has(current.status)) {
93
+ // Only `running` keeps the loop going. Any other status, including one a newer runtime added that this SDK does
94
+ // not know, is at rest: looping on it would spin forever.
95
+ if (current.status === 'running') {
94
96
  plain = false;
95
97
  this.resting.delete(run.runId);
96
98
  this.progress(run, current);
@@ -105,7 +107,10 @@ export class RunWatcher {
105
107
  this.resting.add(run.runId);
106
108
  return 'park';
107
109
  }
108
- return this.deliver(run, key, { userId: run.userId, runId: run.runId, ended: current.status, run: current });
110
+ const status = current.status;
111
+ return this.deliver(run, key, knownResting.has(status)
112
+ ? { userId: run.userId, runId: run.runId, ended: status, run: current }
113
+ : { userId: run.userId, runId: run.runId, ended: 'unknown', status, run: current });
109
114
  }
110
115
  }
111
116
  /** Hand a running run's activity to `onActivity` once per distinct value, without blocking the follow loop. */
@@ -187,8 +187,8 @@ export declare class Runs {
187
187
  stream(params: RunStartParams, options?: RequestOptions & {
188
188
  idempotencyKey?: string;
189
189
  }): RunStream;
190
- /** Long-polls `runs.get` until the run is `settled`, `waiting_on_user`, or `idle`, and returns that response
191
- * unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
190
+ /** Long-polls `runs.get` until the run is no longer `running` (`settled`, `waiting_on_user`, `idle`, or a status a
191
+ * newer runtime added, which counts as at rest), and returns that response unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
192
192
  wait(runId: string, options?: RequestOptions): Promise<RunObject>;
193
193
  /** Start a run with `output_schema` and wait for it to settle; resolves with the outcome and `output_parsed`
194
194
  * typed to the schema. `onQuestion` answers each open question once; without it, a question rejects with
@@ -133,15 +133,15 @@ export class Runs {
133
133
  return this.openEvents(run.run_id, after, signal);
134
134
  }, 0, controller.signal);
135
135
  }
136
- /** Long-polls `runs.get` until the run is `settled`, `waiting_on_user`, or `idle`, and returns that response
137
- * unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
136
+ /** Long-polls `runs.get` until the run is no longer `running` (`settled`, `waiting_on_user`, `idle`, or a status a
137
+ * newer runtime added, which counts as at rest), and returns that response unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
138
138
  async wait(runId, options = {}) {
139
139
  // One deadline for the whole wait: every poll shares it instead of restarting `budgetMs`.
140
140
  const bounded = deadline(options) ?? options;
141
141
  return abortable(bounded.signal, async () => {
142
142
  for (;;) {
143
143
  const result = await this.get(runId, { ...bounded, wait: 25 });
144
- if (result.status === 'settled' || result.status === 'waiting_on_user' || result.status === 'idle')
144
+ if (result.status !== 'running')
145
145
  return result;
146
146
  }
147
147
  });
@@ -170,6 +170,9 @@ export class Runs {
170
170
  if (current.status === 'idle') {
171
171
  throw new APIError(0, 'run_idle', 'the run stopped without settling; send runs.message or runs.cancel', undefined, undefined, { retryable: false, run_id: run.run_id });
172
172
  }
173
+ if (current.status !== 'waiting_on_user') {
174
+ throw new APIError(0, 'run_status_unknown', `the run came to rest with a status this SDK does not know (${String(current.status)}); upgrade @north-light/sdk`, undefined, undefined, { retryable: false, run_id: run.run_id });
175
+ }
173
176
  const open = current.pending_question_ids.filter((id) => !answered.has(id));
174
177
  if (!onQuestion) {
175
178
  const questionId = open[0] ?? current.pending_question_ids[0];
@@ -77,6 +77,13 @@ export interface FakeCloudOptions {
77
77
  runtimeClientId?: string;
78
78
  /** The runtime's release; default `SDK_VERSION`. An older one refuses this SDK with 426. */
79
79
  runtimeVersion?: string;
80
+ /** The contract levels the runtime serves, one per major (default `[CONTRACT_LEVEL]`). A request whose
81
+ * `Crouter-Required-Contract` no served level satisfies is refused 426 by the runtime. */
82
+ contract?: string | readonly string[];
83
+ /** The level the router in front of the runtime has recorded for it (default: the same as `contract`). `null` models
84
+ * an unknown level: the router forwards and only the runtime enforces. A request the recorded level does not satisfy
85
+ * is refused 426 by the router (origin `router`, no `Crouter-Contract` header) before it reaches the runtime. */
86
+ routedContract?: string | readonly string[] | null;
80
87
  /** Idle close of a run's event stream. Default 60 000 ms. */
81
88
  streamIdleMs?: number;
82
89
  /** Discovery's `account_app_uri_template`; default `<issuer>/account/apps/{client_id}`, null omits it. */
@@ -263,7 +270,12 @@ export declare class FakeCloud {
263
270
  private state;
264
271
  private runtimeUrlFor;
265
272
  private isRuntimeOrigin;
273
+ private servedContract;
266
274
  private headers;
275
+ /** The ingress refusals, in the order production applies them, before routing, body parsing, call logging or any
276
+ * interceptor: the router's contract check against the level it recorded, then the runtime's legacy version check,
277
+ * then the runtime's contract check. `null` when the request may proceed. */
278
+ private ingressRefusal;
267
279
  private route;
268
280
  private runIntercepts;
269
281
  private directory;
@@ -1,6 +1,6 @@
1
1
  // FakeCloud: a fake crouter directory and runtime behind one `fetch`, for testing an app's real `OAuth2Client` and
2
2
  // `Crouter`. Routing, the call logs, intercepts, users and every runtime operation an app may call.
3
- import { RUNTIME_VERSION_HEADER, minimumRuntimeRefusal, MINIMUM_RUNTIME_VERSION_HEADER } from '@north-light/api';
3
+ import { CONTRACT_HEADER, CONTRACT_LEVEL, REQUIRED_CONTRACT_HEADER, RUNTIME_VERSION_HEADER, contractRefusal, minimumRuntimeRefusal, MINIMUM_RUNTIME_VERSION_HEADER } from '@north-light/api';
4
4
  import { covers } from '@north-light/identity';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { posix } from 'node:path';
@@ -416,7 +416,37 @@ export class FakeCloud {
416
416
  return true;
417
417
  return [...this.users.keys()].some((id) => new URL(option(id)).origin === origin);
418
418
  }
419
- headers() { return { [RUNTIME_VERSION_HEADER]: this.options.runtimeVersion ?? SDK_VERSION }; }
419
+ servedContract() {
420
+ const option = this.options.contract ?? CONTRACT_LEVEL;
421
+ return typeof option === 'string' ? option : option.join(', ');
422
+ }
423
+ headers() {
424
+ return { [RUNTIME_VERSION_HEADER]: this.options.runtimeVersion ?? SDK_VERSION, [CONTRACT_HEADER]: this.servedContract() };
425
+ }
426
+ /** The ingress refusals, in the order production applies them, before routing, body parsing, call logging or any
427
+ * interceptor: the router's contract check against the level it recorded, then the runtime's legacy version check,
428
+ * then the runtime's contract check. `null` when the request may proceed. */
429
+ ingressRefusal(request) {
430
+ const required = request.headers.get(REQUIRED_CONTRACT_HEADER) ?? undefined;
431
+ const legacy = request.headers.get(MINIMUM_RUNTIME_VERSION_HEADER) ?? undefined;
432
+ const runtimeVersion = this.options.runtimeVersion ?? SDK_VERSION;
433
+ const refusalResponse = (refusal, origin) => {
434
+ const error = fakeError({ status: refusal.status, code: refusal.code, message: refusal.message, ...(refusal.status === 426 ? { details: refusal.details } : { param: refusal.param }), origin, retryable: false });
435
+ return jsonAnswer(envelopeBody(error), error.status, origin === 'runtime' ? this.headers() : {});
436
+ };
437
+ const routed = this.options.routedContract === undefined ? this.servedContract() : this.options.routedContract;
438
+ if (routed !== null) {
439
+ const refusal = contractRefusal(required, typeof routed === 'string' ? routed : routed.join(', '), { legacyMinimum: legacy, runtimeVersion: null });
440
+ if (refusal !== null)
441
+ return refusalResponse(refusal, 'router');
442
+ }
443
+ const legacyRefusal = minimumRuntimeRefusal(legacy, runtimeVersion);
444
+ if (legacyRefusal !== null) {
445
+ return refusalResponse(legacyRefusal, 'runtime');
446
+ }
447
+ const refusal = contractRefusal(required, this.servedContract(), { legacyMinimum: legacy, runtimeVersion });
448
+ return refusal === null ? null : refusalResponse(refusal, 'runtime');
449
+ }
420
450
  async route(input, init) {
421
451
  const request = new Request(input, init);
422
452
  const signal = init?.signal ?? (input instanceof Request ? input.signal : null);
@@ -551,6 +581,9 @@ export class FakeCloud {
551
581
  }
552
582
  // ---- runtime -----------------------------------------------------------------------------------------------------
553
583
  async runtime(url, request, signal, claims, sub) {
584
+ const refused = this.ingressRefusal(request);
585
+ if (refused !== null)
586
+ return refused;
554
587
  const route = ROUTES.find(([method, pattern]) => method === request.method && pattern.test(url.pathname));
555
588
  if (!route)
556
589
  return jsonAnswer(envelopeBody(fakeError({ status: 404, code: 'not_found', message: 'unknown operation' })), 404, this.headers());
@@ -589,13 +622,6 @@ export class FakeCloud {
589
622
  return fail(fakeError({ status: scripted, code: scripted >= 500 ? 'server_error' : 'invalid_request', message: `scripted ${scripted}`, retryable: false }));
590
623
  return fail(scripted);
591
624
  }
592
- const minimum = minimumRuntimeRefusal(request.headers.get(MINIMUM_RUNTIME_VERSION_HEADER) ?? undefined, this.options.runtimeVersion ?? SDK_VERSION);
593
- if (minimum) {
594
- const error = fakeError({ status: minimum.status, code: minimum.code, message: minimum.message, ...(minimum.status === 426 ? { details: minimum.details } : { param: minimum.param }), origin: minimum.status === 426 ? 'router' : 'runtime', retryable: false });
595
- call.status = error.status;
596
- // A router refusal names no runtime release.
597
- return error.status === 426 ? jsonAnswer(envelopeBody(error), 426, {}) : fail(error);
598
- }
599
625
  try {
600
626
  if (!user?.refreshToken || claims === null)
601
627
  throw refuse(401, 'unauthorized', 'a directory access token is required');
package/dist/version.d.ts CHANGED
@@ -1,2 +1,5 @@
1
- /** This SDK's release version. It is also the oldest runtime the SDK accepts: one release is one version set. */
2
- export declare const SDK_VERSION = "0.3.422";
1
+ /** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
2
+ export declare const SDK_VERSION = "0.3.424";
3
+ /** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
4
+ * contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
5
+ export declare const LEGACY_FLOOR = "0.3.423";
package/dist/version.js CHANGED
@@ -1,3 +1,8 @@
1
- // Written by scripts/release-version.mjs on every release; do not edit by hand.
2
- /** This SDK's release version. It is also the oldest runtime the SDK accepts: one release is one version set. */
3
- export const SDK_VERSION = '0.3.422';
1
+ // SDK_VERSION is written by scripts/release-version.mjs on every release; do not edit by hand.
2
+ /** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
3
+ export const SDK_VERSION = '0.3.424';
4
+ // LEGACY_FLOOR is written once by `scripts/contract.mjs release`, at the first release that carries the contract level
5
+ // (the `0.0.0` placeholder is replaced there and never again); do not edit by hand.
6
+ /** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
7
+ * contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
8
+ export const LEGACY_FLOOR = '0.3.423';
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@north-light/sdk",
3
- "version": "0.3.422",
3
+ "version": "0.3.424",
4
+ "crouterContract": "1.0",
4
5
  "description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
@@ -36,11 +37,12 @@
36
37
  ],
37
38
  "sideEffects": false,
38
39
  "scripts": {
39
- "build": "tsc -p tsconfig.json && chmod 755 dist/keygen-cli.js"
40
+ "build": "tsc -p tsconfig.json && chmod 755 dist/keygen-cli.js",
41
+ "prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'next') { console.error('@north-light/sdk publishes only with --tag next; scripts/promote-sdk.mjs is the only mover of latest.'); process.exit(1); }\""
40
42
  },
41
43
  "dependencies": {
42
- "@north-light/api": "0.3.422",
43
- "@north-light/identity": "0.3.422",
44
+ "@north-light/api": "0.3.424",
45
+ "@north-light/identity": "0.3.424",
44
46
  "jose": "^6.2.1"
45
47
  },
46
48
  "peerDependencies": {