@north-light/api 0.3.421 → 0.3.423

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { CONTRACT_LEVEL, contractRefusal, contractSatisfies, parseContractLevel, parseServedContracts } from '../contract.js';
4
+ test('CONTRACT_LEVEL is MAJOR.MINOR with MAJOR equal to the API_VERSION number', async () => {
5
+ const { API_VERSION } = await import('../routes.js');
6
+ assert.deepEqual(parseContractLevel(CONTRACT_LEVEL)?.major, Number(API_VERSION.replace(/^v/, '')));
7
+ });
8
+ test('parseContractLevel accepts only MAJOR.MINOR', () => {
9
+ assert.deepEqual(parseContractLevel('1.3'), { major: 1, minor: 3 });
10
+ assert.deepEqual(parseContractLevel(' 12.0 '), { major: 12, minor: 0 });
11
+ for (const bad of ['1', '1.2.3', 'v1.2', '1.x', '', '-1.0', '1.-2'])
12
+ assert.equal(parseContractLevel(bad), null, bad);
13
+ });
14
+ test('the rule: same major, served minor at or above the required one, compared numerically', () => {
15
+ assert.equal(contractSatisfies('1.3', '1.3'), true);
16
+ assert.equal(contractSatisfies('1.3', '1.10'), true);
17
+ assert.equal(contractSatisfies('1.10', '1.3'), false);
18
+ assert.equal(contractSatisfies('1.0', '1.0'), true);
19
+ assert.equal(contractSatisfies('1.3', '2.9'), false);
20
+ assert.equal(contractSatisfies('2.0', '1.9'), false);
21
+ assert.equal(contractSatisfies('1.3', '2.0, 1.4'), true);
22
+ assert.equal(contractSatisfies('1.3', '2.0, 1.2'), false);
23
+ assert.equal(contractSatisfies('1.3', null), false);
24
+ assert.equal(contractSatisfies('1.3', ''), false);
25
+ assert.equal(contractSatisfies('1.3', 'garbage'), false);
26
+ assert.equal(contractSatisfies('garbage', '1.3'), false);
27
+ });
28
+ test('a malformed served entry is dropped without hiding the rest', () => {
29
+ assert.deepEqual(parseServedContracts('x, 1.2 ,3'), [{ major: 1, minor: 2 }]);
30
+ assert.deepEqual(parseServedContracts(null), []);
31
+ });
32
+ test('contractRefusal: absent passes, malformed is 400, unsatisfied is 426 with both levels', () => {
33
+ assert.equal(contractRefusal(undefined, '1.0'), null);
34
+ assert.equal(contractRefusal('1.0', '1.0'), null);
35
+ const bad = contractRefusal('latest', '1.0');
36
+ assert.equal(bad?.status, 400);
37
+ assert.equal(bad?.status === 400 ? bad.param : '', 'Crouter-Required-Contract');
38
+ const refused = contractRefusal('1.4', '1.3', { legacyMinimum: '0.3.500', runtimeVersion: '0.3.600' });
39
+ assert.equal(refused?.status, 426);
40
+ assert.equal(refused?.code, 'runtime_version_unsupported');
41
+ assert.deepEqual(refused?.status === 426 ? refused.details : null, { sdk_version: '0.3.500', runtime_version: '0.3.600', required_contract: '1.4', runtime_contract: '1.3' });
42
+ const unnamed = contractRefusal('1.0', null);
43
+ assert.equal(unnamed?.status === 426 ? unnamed.details.runtime_contract : 'x', null);
44
+ });
@@ -0,0 +1,45 @@
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.0";
3
+ /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
+ export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
+ /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
6
+ export declare const CONTRACT_HEADER = "Crouter-Contract";
7
+ /** A parsed `MAJOR.MINOR` level. */
8
+ export interface ContractLevel {
9
+ major: number;
10
+ minor: number;
11
+ }
12
+ /** Parses one `MAJOR.MINOR` level; `null` when it is anything else. */
13
+ export declare function parseContractLevel(value: string): ContractLevel | null;
14
+ /** Parses a `Crouter-Contract` response value into its well-formed levels, dropping anything malformed. */
15
+ export declare function parseServedContracts(header: string | null | undefined): ContractLevel[];
16
+ /** True when `required` (one `MAJOR.MINOR`) is satisfied by `served` (a `Crouter-Contract` value or its parsed levels):
17
+ * some served level has the same major and a minor at or above the required one. A malformed `required` is never
18
+ * satisfied. */
19
+ export declare function contractSatisfies(required: string, served: string | null | undefined | readonly ContractLevel[]): boolean;
20
+ /** The refusal a server answers instead of handling a request, or `null` when the request may be handled.
21
+ * - `required` absent: handled.
22
+ * - `required` not `MAJOR.MINOR`: 400 `invalid_request` (`param` is the header).
23
+ * - `required` not satisfied by `served`: 426 `runtime_version_unsupported`, details
24
+ * `{sdk_version, runtime_version, required_contract, runtime_contract}` (the SDK's `RuntimeVersionDetails`).
25
+ * `sdk_version` is the legacy minimum-runtime header value the caller sent (empty when it sent none). */
26
+ export type ContractRefusal = {
27
+ status: 400;
28
+ code: 'invalid_request';
29
+ message: string;
30
+ param: string;
31
+ } | {
32
+ status: 426;
33
+ code: 'runtime_version_unsupported';
34
+ message: string;
35
+ details: {
36
+ sdk_version: string;
37
+ runtime_version: string | null;
38
+ required_contract: string;
39
+ runtime_contract: string | null;
40
+ };
41
+ };
42
+ export declare function contractRefusal(required: string | undefined, served: string | null, context?: {
43
+ legacyMinimum?: string | undefined;
44
+ runtimeVersion?: string | null | undefined;
45
+ }): ContractRefusal | null;
@@ -0,0 +1,57 @@
1
+ // The contract level: the compatibility number an SDK and a runtime agree on (design: SDK ↔ runtime contract gate).
2
+ //
3
+ // A level is `MAJOR.MINOR`. MAJOR is the number in `API_VERSION` (`v1` → 1). MINOR rises only when the `/v1` surface
4
+ // changes in a way `scripts/contract.mjs` can see or a person declared; only that script's `release` mode writes
5
+ // `CONTRACT_LEVEL`, and the SDK package manifest carries the same value as `crouterContract`. Package versions stay
6
+ // lockstep auto-patch numbers and say nothing about compatibility.
7
+ //
8
+ // One rule, used by the runtime, the SDK, FakeCloud and the hosting router: a request requiring `M.n` is satisfied by a
9
+ // server whose served list holds `M.k` with `k >= n`. The server answers `Crouter-Contract` on every HTTP answer as a
10
+ // comma-separated list, one level per major served (today one element); the caller sends exactly one level in
11
+ // `Crouter-Required-Contract`.
12
+ //
13
+ // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
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.0';
16
+ /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
17
+ export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
18
+ /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
19
+ export const CONTRACT_HEADER = 'Crouter-Contract';
20
+ /** Parses one `MAJOR.MINOR` level; `null` when it is anything else. */
21
+ export function parseContractLevel(value) {
22
+ const match = /^(\d+)\.(\d+)$/.exec(value.trim());
23
+ return match === null ? null : { major: Number(match[1]), minor: Number(match[2]) };
24
+ }
25
+ /** Parses a `Crouter-Contract` response value into its well-formed levels, dropping anything malformed. */
26
+ export function parseServedContracts(header) {
27
+ if (header === null || header === undefined)
28
+ return [];
29
+ return header.split(',').map((part) => parseContractLevel(part)).filter((level) => level !== null);
30
+ }
31
+ /** True when `required` (one `MAJOR.MINOR`) is satisfied by `served` (a `Crouter-Contract` value or its parsed levels):
32
+ * some served level has the same major and a minor at or above the required one. A malformed `required` is never
33
+ * satisfied. */
34
+ export function contractSatisfies(required, served) {
35
+ const need = parseContractLevel(required);
36
+ if (need === null)
37
+ return false;
38
+ const levels = typeof served === 'string' || served === null || served === undefined ? parseServedContracts(served) : served;
39
+ return levels.some((level) => level.major === need.major && level.minor >= need.minor);
40
+ }
41
+ export function contractRefusal(required, served, context = {}) {
42
+ if (required === undefined)
43
+ return null;
44
+ if (parseContractLevel(required) === null) {
45
+ return { status: 400, code: 'invalid_request', param: REQUIRED_CONTRACT_HEADER, message: `${REQUIRED_CONTRACT_HEADER} must be a contract level like 1.0.` };
46
+ }
47
+ if (contractSatisfies(required, served))
48
+ return null;
49
+ const level = required.trim();
50
+ return {
51
+ status: 426, code: 'runtime_version_unsupported',
52
+ message: served === null || parseServedContracts(served).length === 0
53
+ ? `The runtime did not name its contract level; the caller requires contract ${level} or later. Roll the runtime forward.`
54
+ : `The runtime serves contract ${served}; the caller requires contract ${level} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,
55
+ details: { sdk_version: (context.legacyMinimum ?? '').trim(), runtime_version: context.runtimeVersion ?? null, required_contract: level, runtime_contract: served },
56
+ };
57
+ }
@@ -4,6 +4,11 @@ export declare const errorCodes: {
4
4
  readonly status: 401;
5
5
  readonly retryable: false;
6
6
  };
7
+ readonly machine_revoked: {
8
+ readonly type: "authentication_error";
9
+ readonly status: 401;
10
+ readonly retryable: false;
11
+ };
7
12
  readonly grant_revoked: {
8
13
  readonly type: "authentication_error";
9
14
  readonly status: 401;
@@ -79,6 +84,11 @@ export declare const errorCodes: {
79
84
  readonly status: 403;
80
85
  readonly retryable: false;
81
86
  };
87
+ readonly unsandboxed_machine: {
88
+ readonly type: "permission_error";
89
+ readonly status: 403;
90
+ readonly retryable: false;
91
+ };
82
92
  readonly isolated_run: {
83
93
  readonly type: "permission_error";
84
94
  readonly status: 403;
@@ -3,6 +3,8 @@
3
3
  // shape (provider_error, usage, stream_*) do not have a fixed tuple.
4
4
  export const errorCodes = {
5
5
  unauthorized: { type: 'authentication_error', status: 401, retryable: false },
6
+ // The directory no longer recognises this machine's runtime key: it was removed from the account.
7
+ machine_revoked: { type: 'authentication_error', status: 401, retryable: false },
6
8
  // A refusal that names no reason (runtime validation step 5, or the directory's bare invalid_grant). Not deprecated.
7
9
  grant_revoked: { type: 'authentication_error', status: 401, retryable: false },
8
10
  // The directory's reasons for refusing a refresh token (D2-ERR-49..51), carried in its invalid_grant error_description.
@@ -21,6 +23,8 @@ export const errorCodes = {
21
23
  // An app refusing its own operation because a provider in its `requires` is not `ready`; details `{ provider, state, message?, next }`, copied from `grant.get`'s `dependencies[provider]` entry (`next` = `<issuer>/connect/setup/<provider>?client_id=<grantee>`). The daemon never raises it (it only carries `dependencies`).
22
24
  provider_setup_pending: { type: 'permission_error', status: 403, retryable: false },
23
25
  question_pages_off: { type: 'permission_error', status: 403, retryable: false },
26
+ // An unsandboxed machine (a computer joined to a Northlight account) runs only the person's own terminal agents; app grantees run on the hosted machine.
27
+ unsandboxed_machine: { type: 'permission_error', status: 403, retryable: false },
24
28
  isolated_run: { type: 'permission_error', status: 403, retryable: false },
25
29
  provider_version_retired: { type: 'permission_error', status: 403, retryable: false },
26
30
  invalid_scope: { type: 'invalid_request_error', status: 400, retryable: false },
@@ -7,6 +7,8 @@ export type { ErrorCode, ErrorEnvelope, ErrorOrigin, ErrorType } from './error-c
7
7
  export { API_VERSION, routes } from './routes.js';
8
8
  export { RUNTIME_VERSION_HEADER, MINIMUM_RUNTIME_VERSION_HEADER, compareReleaseVersions, minimumRuntimeRefusal } from './runtime-version.js';
9
9
  export type { MinimumRuntimeRefusal } from './runtime-version.js';
10
+ export { CONTRACT_HEADER, CONTRACT_LEVEL, REQUIRED_CONTRACT_HEADER, contractRefusal, contractSatisfies, parseContractLevel, parseServedContracts } from './contract.js';
11
+ export type { ContractLevel, ContractRefusal } from './contract.js';
10
12
  export { LONG_POLL_MAX_WAIT_SECONDS, LONG_POLL_ROUTES, longPollHoldSeconds, longPollRoute, requestedWait } from './long-poll.js';
11
13
  export type { LongPollName, LongPollRoute } from './long-poll.js';
12
14
  export * from '../shared/generated-context.js';
package/dist/api/index.js CHANGED
@@ -6,6 +6,7 @@ export { APIError, ApiError, isErrorBody } from './errors.js';
6
6
  export { errorCodes } from './error-codes.js';
7
7
  export { API_VERSION, routes } from './routes.js';
8
8
  export { RUNTIME_VERSION_HEADER, MINIMUM_RUNTIME_VERSION_HEADER, compareReleaseVersions, minimumRuntimeRefusal } from './runtime-version.js';
9
+ export { CONTRACT_HEADER, CONTRACT_LEVEL, REQUIRED_CONTRACT_HEADER, contractRefusal, contractSatisfies, parseContractLevel, parseServedContracts } from './contract.js';
9
10
  export { LONG_POLL_MAX_WAIT_SECONDS, LONG_POLL_ROUTES, longPollHoldSeconds, longPollRoute, requestedWait } from './long-poll.js';
10
11
  export * from '../shared/generated-context.js';
11
12
  export * from './dto/common.js';
package/dist/types.d.ts CHANGED
@@ -388,10 +388,12 @@ export interface ScopeConfig {
388
388
  runtime?: {
389
389
  issuer: string;
390
390
  audience: string;
391
- listener: {
391
+ listener?: {
392
392
  address: string;
393
393
  port: number;
394
394
  };
395
+ /** Set by joining as a non-default machine: no app listener; only `app:terminal` nodes run. */
396
+ unsandboxed?: boolean;
395
397
  jwksUrl?: string;
396
398
  person_uids?: number[];
397
399
  /** Hosting file storage: `service_url` is the storage token audience and API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.421",
3
+ "version": "0.3.423",
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",