mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-11 23:00:58 +00:00
12e22c258a
A stable release without a dialect of its own now ships the one on main only while that one has 90 days left; dialect 0 or less stops the run before anything is committed, unless allow_stale_dialect is ticked. Fetching and checking moved into registration-dialect.yml, which never fails its caller and also type-checks the module under the strict config and lints it with --max-warnings 0, since the bump commit skips CI. The bump and everything after it run whatever became of the dialect job. Prereleases ask the issuer for a dialect of their -pre.N version and build it into the image without committing it. The release check reads the module token by token against an allowlist of keywords, globals and member names, refuses any assignment to a property, computed access and regular expressions, and names Object, JSON and defineProperty among the forbidden. The issuer's kitchen-sink sample is gone; the tests use a hand-written dialect instead.
138 lines
5.9 KiB
JavaScript
138 lines
5.9 KiB
JavaScript
import { checkDialect, staticProblems, tokenize, tokenProblems } from './check-registration-dialect.mjs';
|
|
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { after, describe, it } from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// A dialect written by hand for TREK's tests, for version 9.9.9. It is none of
|
|
// the issuer's; the issuer checks its own modules against these rules too.
|
|
const EXAMPLE = fileURLToPath(
|
|
new URL('../../server/tests/fixtures/service-identity/registration-dialect.example.ts', import.meta.url),
|
|
);
|
|
const source = readFileSync(EXAMPLE, 'utf8');
|
|
|
|
const scratch = mkdtempSync(join(tmpdir(), 'registration-dialect-check-'));
|
|
after(() => rmSync(scratch, { recursive: true, force: true }));
|
|
|
|
let written = 0;
|
|
function variant(text) {
|
|
written += 1;
|
|
const file = join(scratch, `variant-${written}.ts`);
|
|
writeFileSync(file, text);
|
|
return file;
|
|
}
|
|
|
|
function replaced(from, to) {
|
|
assert.ok(source.includes(from), `the example no longer holds ${from}`);
|
|
return source.replace(from, to);
|
|
}
|
|
|
|
// The example with `line` added in front of its exports.
|
|
const withLine = (line) => replaced('export const registrationDialect', `${line}\nexport const registrationDialect`);
|
|
|
|
describe('check-registration-dialect', () => {
|
|
it('passes the example for its own version', async () => {
|
|
assert.deepEqual(await checkDialect(EXAMPLE, '9.9.9'), []);
|
|
});
|
|
|
|
it('refuses a dialect written for another version', async () => {
|
|
const problems = await checkDialect(EXAMPLE, '9.9.10');
|
|
assert.ok(problems.some((p) => p.includes('not written for 9.9.10')));
|
|
});
|
|
|
|
it('refuses an import from anywhere but node:crypto, node:zlib and the types', () => {
|
|
const text = replaced(
|
|
"import { crc32 } from 'node:zlib';",
|
|
"import { crc32 } from 'node:zlib';\nimport { readFileSync } from 'node:fs';",
|
|
);
|
|
assert.ok(staticProblems(text, '9.9.9').some((p) => p.includes("from 'node:fs'")));
|
|
const runtimeTypes = replaced('import type {', 'import {');
|
|
assert.ok(staticProblems(runtimeTypes, '9.9.9').some((p) => p.includes('registration-dialect.types')));
|
|
});
|
|
|
|
it('refuses what reaches the process, the network or code from strings, before running any of it', async () => {
|
|
for (const line of ['const env = process.env;', 'void fetch("https://example.org");', 'const run = eval;']) {
|
|
const file = variant(withLine(line));
|
|
const problems = await checkDialect(file, '9.9.9');
|
|
assert.ok(
|
|
problems.some((p) => p.includes('names what a dialect does not need')),
|
|
line,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('refuses changing a global or anything else that is not its own name', () => {
|
|
for (const line of [
|
|
'Math.random = () => 0;',
|
|
'Buffer.from = Buffer.alloc;',
|
|
'const m = Math;\nm.floor = Math.max;',
|
|
'const b = Buffer;\nb.poolSize += 1;',
|
|
'(Buffer).concat = Buffer.alloc;',
|
|
'PEPPER.c++;',
|
|
'++PEPPER.c;',
|
|
]) {
|
|
const problems = staticProblems(withLine(line), '9.9.9');
|
|
assert.ok(
|
|
problems.some((p) => p.includes('other than a name of its own') || p.includes('reads .')),
|
|
line,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('refuses names and members it does not know, however they are reached', () => {
|
|
const cases = [
|
|
['Object.defineProperty(JSON, "parse", {});', 'names what a dialect does not need'],
|
|
['const pick = Math.random();', 'reads .random'],
|
|
['const big = Buffer.alloc(4).buffer;', 'reads .buffer'],
|
|
['const own = Array.isArray;', 'reads .isArray'],
|
|
['const later = Promise.resolve(1);', 'names Promise'],
|
|
['const date = new Date();', 'names new'],
|
|
['delete PEPPER.c;', 'names delete'],
|
|
['const kind = typeof PEPPER;', 'names typeof'],
|
|
['const at = PEPPER?.length;', 'the character'],
|
|
];
|
|
for (const [line, expected] of cases) {
|
|
const problems = staticProblems(withLine(line), '9.9.9');
|
|
assert.ok(
|
|
problems.some((p) => p.includes(expected)),
|
|
`${line}: ${problems.join('; ')}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('refuses a computed property name, a regular expression and any escape that could spell a name', () => {
|
|
const computed = withLine("const name = PEPPER['length'];");
|
|
assert.ok(staticProblems(computed, '9.9.9').some((p) => p.includes('computed name')));
|
|
const pattern = withLine('const test = /process/.source;');
|
|
assert.ok(staticProblems(pattern, '9.9.9').some((p) => p.includes('regular expression')));
|
|
const escaped = withLine("const name = 'pro\\u0063ess';");
|
|
assert.ok(staticProblems(escaped, '9.9.9').some((p) => p.includes('backslash')));
|
|
});
|
|
|
|
it('reads the names in a template literal like any other', () => {
|
|
assert.deepEqual(tokenProblems(tokenize('const a = 1;\nconst b = `x${a}y`;\n')), []);
|
|
assert.ok(tokenProblems(tokenize('const b = `x${Date}y`;\n')).some((p) => p.includes('names Date')));
|
|
});
|
|
|
|
it('refuses a file without its header or without a dialect id', () => {
|
|
assert.ok(staticProblems(source.slice(source.indexOf('import')), '9.9.9').some((p) => p.includes('header')));
|
|
const noId = replaced("const DIALECT = 'HandWritten1';", "const DIALECT = 'short';");
|
|
assert.ok(staticProblems(noId, '9.9.9').some((p) => p.includes('dialect id')));
|
|
});
|
|
|
|
it('refuses a dialect that does not compute what its SELF_CHECK says', async () => {
|
|
const file = variant(replaced("seal.toString('hex')", "seal.toString('base64url')"));
|
|
const problems = await checkDialect(file, '9.9.9');
|
|
assert.deepEqual(problems, ['its request is not the one the issuer computed']);
|
|
});
|
|
|
|
it('refuses a dialect whose request TREK would not send', async () => {
|
|
const file = variant(replaced("'X-TREK-Test-Seal'", "'X-TREK-Install'"));
|
|
const problems = await checkDialect(file, '9.9.9');
|
|
assert.ok(problems.some((p) => p.includes('X-TREK-Install')));
|
|
});
|
|
});
|