@north-light/api 0.3.479 → 0.3.481
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.481",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import ts from 'typescript';
|
|
7
|
-
import { errorCodes } from '../error-codes.js';
|
|
8
|
-
test('SDK and API expose one fixed code table with contract 2 tuples', () => {
|
|
9
|
-
assert.deepEqual(errorCodes.server_error, { type: 'server_error', status: 500, retryable: true });
|
|
10
|
-
assert.deepEqual(errorCodes.runtime_starting, { type: 'server_error', status: 503, retryable: true });
|
|
11
|
-
assert.deepEqual(errorCodes.payer_not_app, { type: 'permission_error', status: 403, retryable: false });
|
|
12
|
-
assert.deepEqual(errorCodes.cap_exhausted, { type: 'rate_limit_error', status: 402, retryable: false });
|
|
13
|
-
assert.deepEqual(errorCodes.concurrency_limit, { type: 'rate_limit_error', status: 429, retryable: true });
|
|
14
|
-
for (const code of ['grant_revoked', 'grant_removed', 'grant_suspended', 'refresh_token_expired', 'refresh_token_reused']) {
|
|
15
|
-
assert.deepEqual(errorCodes[code], { type: 'authentication_error', status: 401, retryable: false }, code);
|
|
16
|
-
}
|
|
17
|
-
assert.deepEqual(errorCodes.provider_setup_pending, { type: 'permission_error', status: 403, retryable: false });
|
|
18
|
-
assert.equal('provider_error' in errorCodes, false); // provider's 4xx status varies
|
|
19
|
-
assert.equal('usage' in errorCodes, false); // legacy socket-only code
|
|
20
|
-
assert.equal('stream_gap' in errorCodes, false); // stream protocol, not a fixed HTTP row
|
|
21
|
-
});
|
|
22
|
-
test('every statically named RunError and ProviderError agrees with the fixed table', () => {
|
|
23
|
-
const src = fileURLToPath(new URL('../..', import.meta.url));
|
|
24
|
-
const files = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
25
|
-
const path = join(dir, entry.name);
|
|
26
|
-
return entry.isDirectory() ? files(path) : entry.name.endsWith('.ts') && !path.includes('/__tests__/') ? [path] : [];
|
|
27
|
-
});
|
|
28
|
-
const literals = (expr) => {
|
|
29
|
-
if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr))
|
|
30
|
-
return [ts.isNumericLiteral(expr) ? Number(expr.text) : expr.text];
|
|
31
|
-
if (ts.isConditionalExpression(expr))
|
|
32
|
-
return [...literals(expr.whenTrue), ...literals(expr.whenFalse)];
|
|
33
|
-
return [];
|
|
34
|
-
};
|
|
35
|
-
let checked = 0;
|
|
36
|
-
for (const path of files(src)) {
|
|
37
|
-
const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
38
|
-
const visit = (node) => {
|
|
39
|
-
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && ['RunError', 'ProviderError'].includes(node.expression.text)) {
|
|
40
|
-
const args = node.arguments ?? [];
|
|
41
|
-
const run = node.expression.text === 'RunError';
|
|
42
|
-
const codes = args[run ? 1 : 0] ? literals(args[run ? 1 : 0]) : [];
|
|
43
|
-
const statuses = run && args[0] ? literals(args[0]) : [];
|
|
44
|
-
const options = !run && args[2] && ts.isObjectLiteralExpression(args[2]) ? args[2] : undefined;
|
|
45
|
-
const override = options?.properties.find((prop) => ts.isPropertyAssignment(prop) && prop.name.getText(source) === 'status');
|
|
46
|
-
const providerStatuses = override && ts.isPropertyAssignment(override) ? literals(override.initializer) : [];
|
|
47
|
-
for (const code of codes) {
|
|
48
|
-
if (typeof code !== 'string' || ['provider_error', 'usage', 'stream_gap', 'stream_dropped', 'stream_error'].includes(code))
|
|
49
|
-
continue;
|
|
50
|
-
const row = errorCodes[code];
|
|
51
|
-
assert.ok(row, `${path}:${source.getLineAndCharacterOfPosition(node.pos).line + 1}: ${code} missing from errorCodes`);
|
|
52
|
-
for (const status of run ? statuses : providerStatuses)
|
|
53
|
-
assert.equal(status, row.status, `${path}: ${code} status`);
|
|
54
|
-
if (run && args[6] && args[6].kind !== ts.SyntaxKind.UndefinedKeyword && args[6].kind !== ts.SyntaxKind.NullKeyword) {
|
|
55
|
-
const retry = args[6].kind === ts.SyntaxKind.TrueKeyword ? true : args[6].kind === ts.SyntaxKind.FalseKeyword ? false : undefined;
|
|
56
|
-
if (retry !== undefined)
|
|
57
|
-
assert.equal(retry, row.retryable, `${path}: ${code} retryable`);
|
|
58
|
-
}
|
|
59
|
-
else if (run && statuses.length) {
|
|
60
|
-
for (const status of statuses)
|
|
61
|
-
assert.equal(status === 503, row.retryable, `${path}: ${code} default retryable`);
|
|
62
|
-
}
|
|
63
|
-
checked++;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
ts.forEachChild(node, visit);
|
|
67
|
-
};
|
|
68
|
-
visit(source);
|
|
69
|
-
}
|
|
70
|
-
assert.ok(checked > 100, `checked only ${checked} throw sites`);
|
|
71
|
-
});
|
|
72
|
-
test('app origin belongs to the full HTTP envelope without a body status', () => {
|
|
73
|
-
const row = errorCodes.invalid_request;
|
|
74
|
-
const body = { error: { code: 'invalid_request', message: 'Missing prompt',
|
|
75
|
-
type: row.type, origin: 'app', retryable: row.retryable, request_id: 'request-1', param: 'prompt' } };
|
|
76
|
-
assert.equal(body.error.origin, 'app');
|
|
77
|
-
assert.equal('status' in body.error, false);
|
|
78
|
-
assert.equal(row.status, 400);
|
|
79
|
-
});
|