@north-light/api 0.3.422 → 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
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
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",
|