test(stalker): add fail-closed fixture validation

This commit is contained in:
4gray committed 2026-07-27 09:55:53 +02:00
1 parent 375169a69b
commit a2fd389e74
20 files changed
+1731 -1

No files matched your search

@@ -0,0 +1,129 @@
{
"description": "Synthetic resolver root landing redirect.",
"entry": {
"origin": "portal",
"path": "/c/"
},
"expectedEndpoint": {
"origin": "portal",
"path": "/portal.php"
},
"failOnUnexpectedRequest": true,
"initialState": "start",
"origins": {
"portal": {}
},
"phases": [
{
"expectations": [
{
"cardinality": {
"max": 1,
"min": 1
},
"id": "landing",
"method": "GET",
"operation": "landing",
"origin": "portal",
"path": "/c/",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [],
"attributes": {},
"exact": {},
"present": []
},
"headers": {
"absent": [],
"exact": {},
"present": []
},
"query": {
"absent": [],
"exact": {},
"present": []
}
},
"response": {
"body": {
"kind": "empty"
},
"headers": {
"location": [
"/portal.php"
]
},
"status": 302
}
}
],
"mode": "ordered",
"name": "resolve",
"nextState": "landing-resolved",
"state": "start"
},
{
"expectations": [
{
"cardinality": {
"max": 1,
"min": 1
},
"id": "portal-entry",
"method": "GET",
"operation": "portal-entry",
"origin": "portal",
"path": "/portal.php",
"request": {
"body": {
"kind": "absent"
},
"cookies": {
"absent": [],
"attributes": {},
"exact": {},
"present": []
},
"headers": {
"absent": [],
"exact": {},
"present": []
},
"query": {
"absent": [],
"exact": {},
"present": []
}
},
"response": {
"body": {
"kind": "json",
"value": {
"js": {
"status": "ok"
}
}
},
"headers": {
"content-type": [
"application/json"
]
},
"status": 200
}
}
],
"mode": "ordered",
"name": "portal-entry",
"nextState": "complete",
"state": "landing-resolved"
}
],
"scenarioId": "resolver-root-landing",
"schemaVersion": 1,
"symbols": [],
"terminalState": "complete"
}
+6
View File
@@ -36,6 +36,12 @@
},
"test": {
"executor": "@nx/jest:jest",
"inputs": [
"default",
"^production",
"{workspaceRoot}/jest.preset.js",
"stalkerReplayFixtures"
],
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/stalker-mock-server/jest.config.ts"
@@ -0,0 +1,88 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { createReplayRun, type ReplayRun } from './replay-run.js';
import { parseReplayFixtureText } from './replay-schema.js';
import type { ReplayObservedRequest } from './replay.types.js';
const FIXTURE_ROOT = resolve(
process.cwd(),
'apps/stalker-mock-server/fixtures/replay'
);
function collectFixturePaths(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true })
.sort((left, right) => compareCodeUnits(left.name, right.name))
.flatMap((entry) => {
const entryPath = join(directory, entry.name);
if (entry.isDirectory()) {
return collectFixturePaths(entryPath);
}
if (entry.isFile() && entry.name.endsWith('.json')) {
return [entryPath];
}
throw new Error(`Unsafe replay fixture entry: ${entry.name}.`);
});
}
function emptyGet(path: string): ReplayObservedRequest {
return {
origin: 'portal',
method: 'GET',
path,
query: {},
headers: {},
cookies: {},
body: { kind: 'absent' },
};
}
const FIXTURE_DRIVERS: Readonly<
Record<string, (run: ReplayRun) => Promise<void>>
> = {
'resolver-root-landing': async (run) => {
await run.request(emptyGet('/c/'));
await run.request(emptyGet('/portal.php'));
},
};
describe('committed replay fixture corpus', () => {
it('drives every fixture through exact terminal/cardinality evidence', async () => {
const fixturePaths = collectFixturePaths(FIXTURE_ROOT);
expect(fixturePaths.length).toBeGreaterThan(0);
for (const fixturePath of fixturePaths) {
const fixture = parseReplayFixtureText(
readFileSync(fixturePath, 'utf8')
);
const driver = FIXTURE_DRIVERS[fixture.scenarioId];
if (driver === undefined) {
throw new Error(
`Replay fixture driver missing: ${fixture.scenarioId}.`
);
}
const run = createReplayRun(fixture, {
runId: `fixture-${fixture.scenarioId}`,
});
try {
await driver(run);
expect({
scenarioId: fixture.scenarioId,
result: run.finalize(),
}).toMatchObject({
scenarioId: fixture.scenarioId,
result: { ok: true },
});
} finally {
run.dispose();
}
}
});
});
function compareCodeUnits(left: string, right: string): number {
if (left === right) {
return 0;
}
return left < right ? -1 : 1;
}
+4 -1
View File
@@ -12,7 +12,10 @@
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/test-setup.[jt]s"
],
"sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"]
"sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"],
"stalkerReplayFixtures": [
"{workspaceRoot}/apps/stalker-mock-server/fixtures/**/*.json"
]
},
"targetDefaults": {
"@angular/build:application": {
+2
View File
@@ -66,6 +66,7 @@
"release:notes:changelog": "node tools/release/build-release-notes.mjs --format changelog",
"release:notes:blog": "node tools/release/build-release-notes.mjs --format blog",
"release:screenshots": "tsx tools/release/capture-release-screenshots.ts",
"stalker:fixtures:validate": "nx run stalker-fixture-tools:validate",
"lint": "nx run-many --target=lint --all",
"build": "nx build electron-backend"
},
@@ -110,6 +111,7 @@
"rxjs": "7.8.2",
"saxes": "6.0.0",
"shaka-player": "5.2.1",
"tough-cookie": "5.1.2",
"uuid": "9.0.0",
"video.js": "8.23.4",
"videojs-contrib-quality-levels": "4.1.0",
+3
View File
@@ -154,6 +154,9 @@ importers:
shaka-player:
specifier: 5.2.1
version: 5.2.1
tough-cookie:
specifier: 5.1.2
version: 5.1.2
uuid:
specifier: 9.0.0
version: 9.0.0
+12
View File
@@ -252,6 +252,18 @@
"validationCommand": "pnpm nx test stalker-mock-server",
"reason": "Stateful Stalker replay behavior is validated by focused contract tests; percentage coverage is not a shipped-source quality signal."
},
{
"name": "portal-stalker-replay-fixtures",
"root": "libs/portal/stalker/replay-fixtures",
"validationCommand": "pnpm nx test portal-stalker-replay-fixtures",
"reason": "The Node-only replay grammar is covered by focused fail-closed schema contract tests."
},
{
"name": "stalker-fixture-tools",
"root": "tools/stalker-fixtures",
"validationCommand": "pnpm nx test stalker-fixture-tools",
"reason": "Fixture validation tooling is covered by focused scanner, schema, formatting, and filesystem contract tests."
},
{
"name": "remote-control-web",
"root": "apps/remote-control-web",
+16
View File
@@ -0,0 +1,16 @@
export default {
displayName: 'stalker-fixture-tools',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
moduleFileExtensions: ['ts', 'js'],
coverageDirectory: '../../coverage/tools/stalker-fixtures',
};
+40
View File
@@ -0,0 +1,40 @@
{
"name": "stalker-fixture-tools",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "tools/stalker-fixtures/src",
"projectType": "library",
"tags": ["scope:tools", "domain:stalker", "type:tool"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"cache": true,
"inputs": [
"default",
"^production",
"{workspaceRoot}/jest.preset.js",
"stalkerReplayFixtures"
],
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "tools/stalker-fixtures/jest.config.ts",
"tsConfig": "tools/stalker-fixtures/tsconfig.spec.json"
}
},
"validate": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"production",
"^production",
"stalkerReplayFixtures"
],
"options": {
"command": "pnpm tsx --tsconfig tools/stalker-fixtures/tsconfig.json tools/stalker-fixtures/src/cli.ts validate",
"cwd": "{workspaceRoot}"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
+38
View File
@@ -0,0 +1,38 @@
import { resolve } from 'node:path';
import {
FIXTURE_VALIDATION_CLI_ERROR_CODES,
FixtureValidationCliError,
validateReplayFixtureDirectory,
} from './lib/fixture-validation-cli';
import { FixtureValidationError } from './lib/fixture-validator';
const FIXTURE_ROOT = resolve(
process.cwd(),
'apps/stalker-mock-server/fixtures/replay'
);
async function main(): Promise<void> {
const arguments_ = process.argv.slice(2);
if (arguments_.length !== 1 || arguments_[0] !== 'validate') {
throw new FixtureValidationCliError(
FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsupportedCommand
);
}
const fixtureCount = await validateReplayFixtureDirectory(FIXTURE_ROOT);
process.stdout.write(
`Validated ${fixtureCount} Stalker replay fixture(s).\n`
);
}
void main().catch((error: unknown) => {
const safeError =
error instanceof FixtureValidationCliError ||
error instanceof FixtureValidationError
? error
: new FixtureValidationCliError(
FIXTURE_VALIDATION_CLI_ERROR_CODES.InternalError
);
process.stderr.write(`${safeError.message}\n`);
process.exitCode = 1;
});
+8
View File
@@ -0,0 +1,8 @@
export * from './lib/fixture-secret-scanner';
export {
FIXTURE_VALIDATION_CLI_ERROR_CODES,
FixtureValidationCliError,
validateReplayFixtureDirectory,
} from './lib/fixture-validation-cli';
export type { FixtureValidationCliErrorCode } from './lib/fixture-validation-cli';
export * from './lib/fixture-validator';
@@ -0,0 +1,274 @@
import type {
ReplayFixtureV1,
ReplayTemplateValue,
} from '@iptvnator/portal/stalker/replay-fixtures';
import {
assertReplayFixtureContainsNoSecrets,
FixtureSecretScanError,
FIXTURE_SECRET_SCAN_ERROR_CODES,
type FixtureSecretScanErrorCode,
} from './fixture-secret-scanner';
function replayFixture(value: ReplayTemplateValue = { js: true }): ReplayFixtureV1 {
return {
schemaVersion: 1,
scenarioId: 'fixture-scanner-contract',
description:
'Synthetic Authorization failed response at https://portal.test/c/.',
origins: { portal: {} },
entry: { origin: 'portal', path: '/c/' },
expectedEndpoint: { origin: 'portal', path: '/portal.php' },
initialState: 'start',
terminalState: 'complete',
failOnUnexpectedRequest: true,
symbols: [
{
kind: 'generate',
symbol: 'safe-token',
valueKind: 'token',
},
],
phases: [
{
name: 'resolve',
state: 'start',
nextState: 'complete',
mode: 'ordered',
expectations: [
{
id: 'landing',
operation: 'landing',
origin: 'portal',
method: 'GET',
path: '/c/',
request: {
query: {
exact: {},
present: ['password'],
absent: ['token'],
},
headers: {
exact: {},
present: [],
absent: ['authorization'],
},
cookies: {
exact: {},
present: [],
absent: ['session'],
attributes: {},
},
body: { kind: 'absent' },
},
response: {
status: 200,
headers: {
'content-type': ['application/json'],
},
body: { kind: 'json', value },
},
cardinality: { min: 1, max: 1 },
},
],
},
],
};
}
function fixtureWithDescription(description: string): ReplayFixtureV1 {
return { ...replayFixture(), description };
}
function expectScanCode(
fixture: ReplayFixtureV1,
code: FixtureSecretScanErrorCode
): void {
try {
assertReplayFixtureContainsNoSecrets(fixture);
throw new Error('fixture unexpectedly passed');
} catch (error) {
expect(error).toBeInstanceOf(FixtureSecretScanError);
expect((error as FixtureSecretScanError).code).toBe(code);
expect((error as Error).message).toBe(
`Stalker fixture rejected: ${code}.`
);
}
}
describe('fixture secret scanner', () => {
it('allows typed symbols, protocol literals, matcher field names, and reserved test hosts', () => {
const fixture = replayFixture({
js: {
token: { kind: 'ref', symbol: 'safe-token' },
'set-cookie': {
kind: 'parts',
parts: [
{ kind: 'literal', value: 'session=' },
{ kind: 'ref', symbol: 'safe-token' },
{ kind: 'literal', value: '; Path=/; HttpOnly' },
],
},
redirect: 'https://auth.portal.test/login',
result: 'Authorization failed',
},
});
expect(() =>
assertReplayFixtureContainsNoSecrets(fixture)
).not.toThrow();
});
it.each([
'password=private-value',
'password%3Dprivate-value',
'password%253Dprivate-value',
'pa\\u0073sword=private-value',
'{\\"password\\":\\"private-value\\"}',
'Set-Cookie: session=private-value',
])('rejects raw and encoded secret evidence without echoing it: %s', (value) => {
expectScanCode(
fixtureWithDescription(value),
FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral
);
});
it('scans JSON object keys for external-origin evidence', () => {
expectScanCode(
replayFixture({
'https://untrusted.example.tv/account': true,
}),
FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl
);
});
it('rejects literal fragments smuggled into credential parts', () => {
expectScanCode(
replayFixture({
password: {
kind: 'parts',
parts: [
{ kind: 'literal', value: 'private-value' },
{ kind: 'ref', symbol: 'safe-token' },
],
},
}),
FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral
);
});
it('rejects JWT-shaped literals', () => {
expectScanCode(
fixtureWithDescription(
'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhY2NvdW50In0.c2lnbmF0dXJlMTIzNDU2'
),
FIXTURE_SECRET_SCAN_ERROR_CODES.JwtLiteral
);
});
it('rejects literal MAC addresses', () => {
expectScanCode(
fixtureWithDescription('00:1A:79:12:34:56'),
FIXTURE_SECRET_SCAN_ERROR_CODES.MacLiteral
);
});
it('scans typed placeholder labels for secret evidence', () => {
const fixture = replayFixture();
fixture.symbols = [
{
kind: 'generate',
symbol: '00:1A:79:12:34:56',
valueKind: 'token',
},
];
expectScanCode(
fixture,
FIXTURE_SECRET_SCAN_ERROR_CODES.MacLiteral
);
});
it.each([
'authorization',
'cookie',
'set-cookie',
'password',
'credential',
'username',
'account_id',
'device_id',
'serial',
'signature',
'prehash',
])('rejects a raw value under sensitive key %s', (key) => {
expectScanCode(
replayFixture({ [key]: 'private-value' }),
FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral
);
});
it.each([
'https://provider.example.tv/api',
'https://stream.vendor.example.tv/live/42',
'https://images.vendor.example.tv/poster.jpg',
'https://untrusted.example.com/portal.php',
])('rejects unknown external provider evidence %s', (value) => {
expectScanCode(
fixtureWithDescription(value),
FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl
);
});
it.each(['stream_url', 'artwork_url', 'provider_url'])(
'rejects a URL under sensitive fixture key %s',
(key) => {
expectScanCode(
replayFixture({ [key]: 'https://media.portal.test/resource' }),
FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl
);
}
);
it('rejects unsupported stream URL schemes', () => {
expectScanCode(
replayFixture({
stream_url: 'rtsp://media.portal.test/live',
}),
FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl
);
});
it('rejects suspicious high-entropy literals', () => {
expectScanCode(
fixtureWithDescription(
'N7vQ2mK9xP4sR8tW3yB6cD1fG5hJ0lZ2uV7nQ9pS'
),
FIXTURE_SECRET_SCAN_ERROR_CODES.HighEntropyLiteral
);
});
it.each([
'2026-07-27T03:14:15.926Z',
1_785_123_456,
1_785_123_456_000,
])('rejects nondeterministic timestamp evidence %s', (value) => {
expectScanCode(
replayFixture({ observedAt: value }),
FIXTURE_SECRET_SCAN_ERROR_CODES.NondeterministicTimestamp
);
});
it.each([
0,
4_102_444_800,
4_102_444_800_000,
'1970-01-01T00:00:00.000Z',
'2100-01-01T00:00:00.000Z',
])('allows canonical deterministic timestamp sentinel %s', (value) => {
expect(() =>
assertReplayFixtureContainsNoSecrets(
replayFixture({ observedAt: value })
)
).not.toThrow();
});
});
@@ -0,0 +1,329 @@
import type { ReplayFixtureV1 } from '@iptvnator/portal/stalker/replay-fixtures';
export const FIXTURE_SECRET_SCAN_ERROR_CODES = {
SensitiveLiteral: 'sensitive-literal',
JwtLiteral: 'jwt-literal',
MacLiteral: 'mac-literal',
ExternalUrl: 'external-url',
HighEntropyLiteral: 'high-entropy-literal',
NondeterministicTimestamp: 'nondeterministic-timestamp',
} as const;
export type FixtureSecretScanErrorCode =
(typeof FIXTURE_SECRET_SCAN_ERROR_CODES)[keyof typeof FIXTURE_SECRET_SCAN_ERROR_CODES];
export class FixtureSecretScanError extends Error {
constructor(readonly code: FixtureSecretScanErrorCode) {
super(`Stalker fixture rejected: ${code}.`);
this.name = 'FixtureSecretScanError';
}
}
const SENSITIVE_KEY =
/^(?:authorization|cookie|set[-_]?cookie|credential|login|mac|password|passwd|prehash|random|serial|signature\d*|sn|token|username|account_?id|device_?id\d*)$/i;
const SENSITIVE_URL_KEY =
/^(?:artwork|artwork_url|image|image_url|logo|logo_url|poster|poster_url|provider|provider_url|stream|stream_url|thumbnail|thumbnail_url)$/i;
const SENSITIVE_PARTS_KEY =
/^(?:authorization|cookie|set[-_]?cookie)$/i;
const SENSITIVE_ASSIGNMENT =
/(?:^|[?&;\s"'{}:,])(?:authorization|cookie|set[-_]?cookie|credential|login|mac|password|passwd|prehash|random|serial|signature\d*|sn|token|username|account_?id|device_?id\d*)["']?\s*(?:=|:)\s*[^\s&;,}]+/i;
const AUTHORIZATION_VALUE = /\b(?:basic|bearer)\s+[A-Za-z0-9+/_=.-]{8,}/i;
const JWT_LITERAL =
/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{8,}\b/;
const MAC_LITERAL = /\b(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b/i;
const ISO_TIMESTAMP =
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z\b/i;
const URL_LITERAL =
/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s<>"'\\]+/g;
const HIGH_ENTROPY_TOKEN = /[A-Za-z0-9+/_=-]{32,}/g;
const JSON_UNICODE_ESCAPE = /\\u([0-9A-F]{4})/gi;
const JSON_HEX_ESCAPE = /\\x([0-9A-F]{2})/gi;
const JSON_QUOTE_ESCAPE = /\\"/g;
const UNIX_SECONDS_MIN = 946_684_800;
const UNIX_SECONDS_MAX = 4_102_444_800;
const UNIX_MILLISECONDS_MIN = UNIX_SECONDS_MIN * 1000;
const UNIX_MILLISECONDS_MAX = UNIX_SECONDS_MAX * 1000;
const DETERMINISTIC_TIMESTAMP_NUMBERS = new Set([
0,
UNIX_SECONDS_MAX,
UNIX_MILLISECONDS_MAX,
]);
const DETERMINISTIC_ISO_TIMESTAMPS = new Set([
'1970-01-01T00:00:00.000Z',
'2100-01-01T00:00:00.000Z',
]);
export function assertReplayFixtureContainsNoSecrets(
fixture: ReplayFixtureV1
): void {
inspectValue(fixture);
}
function inspectValue(value: unknown, parentKey?: string): void {
if (typeof value === 'string') {
if (
parentKey !== undefined &&
SENSITIVE_URL_KEY.test(parentKey) &&
containsUrl(value)
) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl);
}
inspectString(value);
return;
}
if (typeof value === 'number') {
if (isTimestampNumber(value)) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.NondeterministicTimestamp);
}
return;
}
if (value === null || typeof value !== 'object') {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
inspectValue(item, parentKey);
}
return;
}
const record = value as Record<string, unknown>;
if (isOpaqueTypedNode(record)) {
inspectString(record['symbol'] as string);
return;
}
if (record['kind'] === 'parts' && Array.isArray(record['parts'])) {
if (
parentKey !== undefined &&
SENSITIVE_KEY.test(parentKey) &&
(!SENSITIVE_PARTS_KEY.test(parentKey) ||
!containsReference(record['parts']))
) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral);
}
for (const part of record['parts']) {
inspectValue(part);
}
return;
}
if (record['kind'] === 'literal' && typeof record['value'] === 'string') {
inspectString(record['value']);
return;
}
for (const [key, child] of Object.entries(record)) {
inspectString(key);
if (SENSITIVE_KEY.test(key) && !isProtectedValue(key, child)) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral);
}
if (
SENSITIVE_URL_KEY.test(key) &&
typeof child === 'string' &&
containsUrl(child)
) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl);
}
inspectValue(child, key);
}
}
function isOpaqueTypedNode(value: Record<string, unknown>): boolean {
return (
(value['kind'] === 'generate' &&
typeof value['symbol'] === 'string' &&
typeof value['valueKind'] === 'string') ||
(value['kind'] === 'ref' && typeof value['symbol'] === 'string')
);
}
function isProtectedValue(key: string, value: unknown): boolean {
if (value === null || typeof value !== 'object') {
return false;
}
if (Array.isArray(value)) {
return (
value.length > 0 &&
value.every((item) => isProtectedValue(key, item))
);
}
const record = value as Record<string, unknown>;
if (isOpaqueTypedNode(record)) {
return true;
}
return (
SENSITIVE_PARTS_KEY.test(key) &&
record['kind'] === 'parts' &&
Array.isArray(record['parts']) &&
containsReference(record['parts'])
);
}
function containsReference(parts: readonly unknown[]): boolean {
return parts.some((part) => {
if (part === null || typeof part !== 'object' || Array.isArray(part)) {
return false;
}
return (part as Record<string, unknown>)['kind'] === 'ref';
});
}
function inspectString(value: string): void {
for (const variant of decodedVariants(value)) {
if (
SENSITIVE_ASSIGNMENT.test(variant) ||
AUTHORIZATION_VALUE.test(variant)
) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral);
}
if (JWT_LITERAL.test(variant)) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.JwtLiteral);
}
if (MAC_LITERAL.test(variant)) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.MacLiteral);
}
inspectUrls(variant);
if (
(ISO_TIMESTAMP.test(variant) &&
!DETERMINISTIC_ISO_TIMESTAMPS.has(variant)) ||
(isIntegerString(variant) &&
isTimestampNumber(Number.parseInt(variant, 10)))
) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.NondeterministicTimestamp);
}
if (containsHighEntropyLiteral(variant)) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.HighEntropyLiteral);
}
}
}
function decodedVariants(value: string): readonly string[] {
const variants = new Set<string>([value]);
let candidate = decodeJsonEscapes(value);
variants.add(candidate);
for (let depth = 0; depth < 2; depth += 1) {
if (!/%[0-9A-F]{2}/i.test(candidate)) {
break;
}
try {
const decoded = decodeURIComponent(candidate);
variants.add(decoded);
if (decoded === candidate) {
break;
}
candidate = decodeJsonEscapes(decoded);
variants.add(candidate);
} catch {
break;
}
}
return [...variants];
}
function decodeJsonEscapes(value: string): string {
return value
.replace(JSON_UNICODE_ESCAPE, (_match, digits: string) =>
String.fromCharCode(Number.parseInt(digits, 16))
)
.replace(JSON_HEX_ESCAPE, (_match, digits: string) =>
String.fromCharCode(Number.parseInt(digits, 16))
)
.replace(JSON_QUOTE_ESCAPE, '"');
}
function inspectUrls(value: string): void {
URL_LITERAL.lastIndex = 0;
for (const match of value.matchAll(URL_LITERAL)) {
const candidate = trimUrlPunctuation(match[0]);
let parsed: URL;
try {
parsed = new URL(candidate);
} catch {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl);
}
if (
!['http:', 'https:'].includes(parsed.protocol) ||
!isReservedTestHost(parsed.hostname)
) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.ExternalUrl);
}
for (const key of parsed.searchParams.keys()) {
if (SENSITIVE_KEY.test(key)) {
reject(FIXTURE_SECRET_SCAN_ERROR_CODES.SensitiveLiteral);
}
}
}
}
function containsUrl(value: string): boolean {
URL_LITERAL.lastIndex = 0;
return URL_LITERAL.test(value);
}
function trimUrlPunctuation(value: string): string {
return value.replace(/[),.;\]}]+$/g, '');
}
function isReservedTestHost(hostname: string): boolean {
const normalized = hostname.toLowerCase();
return (
normalized === 'localhost' ||
normalized === '127.0.0.1' ||
normalized === '::1' ||
normalized === 'example.com' ||
normalized === 'example.net' ||
normalized === 'example.org' ||
normalized.endsWith('.localhost') ||
normalized.endsWith('.test') ||
normalized.endsWith('.invalid') ||
normalized.endsWith('.example')
);
}
function isIntegerString(value: string): boolean {
return /^\d{10,13}$/.test(value);
}
function isTimestampNumber(value: number): boolean {
if (DETERMINISTIC_TIMESTAMP_NUMBERS.has(value)) {
return false;
}
return (
(Number.isInteger(value) &&
value >= UNIX_SECONDS_MIN &&
value <= UNIX_SECONDS_MAX) ||
(Number.isInteger(value) &&
value >= UNIX_MILLISECONDS_MIN &&
value <= UNIX_MILLISECONDS_MAX)
);
}
function containsHighEntropyLiteral(value: string): boolean {
for (const match of value.matchAll(HIGH_ENTROPY_TOKEN)) {
const token = match[0].replace(/=+$/g, '');
if (shannonEntropy(token) >= 4) {
return true;
}
}
return false;
}
function shannonEntropy(value: string): number {
const counts = new Map<string, number>();
for (const character of value) {
counts.set(character, (counts.get(character) ?? 0) + 1);
}
let entropy = 0;
for (const count of counts.values()) {
const probability = count / value.length;
entropy -= probability * Math.log2(probability);
}
return entropy;
}
function reject(code: FixtureSecretScanErrorCode): never {
throw new FixtureSecretScanError(code);
}
@@ -0,0 +1,267 @@
import {
link,
mkdtemp,
mkdir,
rename,
rm,
symlink,
utimes,
writeFile,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import {
REPLAY_MAX_FIXTURE_BYTES,
type ReplayFixtureV1,
} from '@iptvnator/portal/stalker/replay-fixtures';
import {
FIXTURE_VALIDATION_CLI_ERROR_CODES,
FixtureValidationCliError,
readFixtureFileBoundedForValidation,
validateReplayFixtureDirectory,
} from './fixture-validation-cli';
import {
FIXTURE_VALIDATION_ERROR_CODES,
FixtureValidationError,
validateReplayFixtureText,
} from './fixture-validator';
function replayFixture(): ReplayFixtureV1 {
return {
schemaVersion: 1,
scenarioId: 'fixture-cli-contract',
description: 'Synthetic resolver fixture.',
origins: { portal: {} },
entry: { origin: 'portal', path: '/c/' },
expectedEndpoint: { origin: 'portal', path: '/portal.php' },
initialState: 'start',
terminalState: 'complete',
failOnUnexpectedRequest: true,
symbols: [],
phases: [
{
name: 'resolve',
state: 'start',
nextState: 'complete',
mode: 'ordered',
expectations: [
{
id: 'landing',
operation: 'landing',
origin: 'portal',
method: 'GET',
path: '/c/',
request: {
query: { exact: {}, present: [], absent: [] },
headers: { exact: {}, present: [], absent: [] },
cookies: {
exact: {},
present: [],
absent: [],
attributes: {},
},
body: { kind: 'absent' },
},
response: {
status: 200,
headers: {
'content-type': ['application/json'],
},
body: { kind: 'json', value: { js: true } },
},
cardinality: { min: 1, max: 1 },
},
],
},
],
};
}
async function expectCliCode(
operation: Promise<unknown>,
code: string
): Promise<void> {
try {
await operation;
throw new Error('fixture directory unexpectedly passed');
} catch (error) {
expect(error).toBeInstanceOf(FixtureValidationCliError);
expect((error as FixtureValidationCliError).code).toBe(code);
expect((error as Error).message).toBe(
`Stalker fixture validation failed: ${code}.`
);
}
}
async function expectValidationCode(
operation: Promise<unknown>,
code: string
): Promise<void> {
try {
await operation;
throw new Error('fixture directory unexpectedly passed');
} catch (error) {
expect(error).toBeInstanceOf(FixtureValidationError);
expect((error as FixtureValidationError).code).toBe(code);
expect((error as Error).message).toBe(
`Stalker fixture validation failed: ${code}.`
);
}
}
describe('fixture validation CLI', () => {
let fixtureRoot: string;
beforeEach(async () => {
fixtureRoot = await mkdtemp(join(tmpdir(), 'stalker-fixtures-'));
});
afterEach(async () => {
await rm(fixtureRoot, { force: true, recursive: true });
});
it('validates canonical JSON fixtures recursively', async () => {
const resolverDirectory = join(fixtureRoot, 'resolver');
await mkdir(resolverDirectory);
const formatted = validateReplayFixtureText(
JSON.stringify(replayFixture())
).formatted;
await writeFile(join(resolverDirectory, 'root-landing.json'), formatted);
await expect(validateReplayFixtureDirectory(fixtureRoot)).resolves.toBe(
1
);
});
it('rejects valid but noncanonical JSON', async () => {
await writeFile(
join(fixtureRoot, 'root-landing.json'),
JSON.stringify(replayFixture())
);
await expectCliCode(
validateReplayFixtureDirectory(fixtureRoot),
FIXTURE_VALIDATION_CLI_ERROR_CODES.NoncanonicalFormat
);
});
it('rejects symlinks instead of following them', async () => {
const formatted = validateReplayFixtureText(
JSON.stringify(replayFixture())
).formatted;
const sourcePath = join(fixtureRoot, 'source.json');
await writeFile(sourcePath, formatted);
await symlink(sourcePath, join(fixtureRoot, 'linked.json'));
await expectCliCode(
validateReplayFixtureDirectory(fixtureRoot),
FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath
);
});
it('rejects an oversized file before materializing its contents', async () => {
await writeFile(
join(fixtureRoot, 'oversized.json'),
'x'.repeat(REPLAY_MAX_FIXTURE_BYTES + 1)
);
await expectValidationCode(
validateReplayFixtureDirectory(fixtureRoot),
FIXTURE_VALIDATION_ERROR_CODES.FixtureTooLarge
);
});
it('rejects multiply linked fixture files', async () => {
const formatted = validateReplayFixtureText(
JSON.stringify(replayFixture())
).formatted;
const sourcePath = join(fixtureRoot, 'source.json');
await writeFile(sourcePath, formatted);
await link(sourcePath, join(fixtureRoot, 'linked.json'));
await expectCliCode(
validateReplayFixtureDirectory(fixtureRoot),
FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath
);
});
it('rejects a file swapped between preflight and open', async () => {
const formatted = validateReplayFixtureText(
JSON.stringify(replayFixture())
).formatted;
const fixturePath = join(fixtureRoot, 'fixture.json');
const replacementPath = join(fixtureRoot, 'replacement.json');
await writeFile(fixturePath, formatted);
await writeFile(replacementPath, formatted);
await expectCliCode(
readFixtureFileBoundedForValidation(fixturePath, async () => {
await rename(fixturePath, join(fixtureRoot, 'original.json'));
await rename(replacementPath, fixturePath);
}),
FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath
);
});
it('rejects same-inode same-size mutation after preflight', async () => {
const formatted = validateReplayFixtureText(
JSON.stringify(replayFixture())
).formatted;
const fixturePath = join(fixtureRoot, 'fixture.json');
await writeFile(fixturePath, formatted);
await expectCliCode(
readFixtureFileBoundedForValidation(fixturePath, async () => {
await writeFile(
fixturePath,
formatted.replace('Synthetic', 'Fynthetic')
);
await utimes(fixturePath, new Date(0), new Date(0));
}),
FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath
);
});
it('rejects malformed UTF-8 without replacement decoding', async () => {
await writeFile(
join(fixtureRoot, 'invalid-utf8.json'),
Buffer.from([0xc3, 0x28])
);
await expectCliCode(
validateReplayFixtureDirectory(fixtureRoot),
FIXTURE_VALIDATION_CLI_ERROR_CODES.InvalidUtf8
);
});
it('rejects a UTF-8 BOM instead of silently normalizing it', async () => {
const formatted = validateReplayFixtureText(
JSON.stringify(replayFixture())
).formatted;
await writeFile(
join(fixtureRoot, 'bom.json'),
Buffer.concat([
Buffer.from([0xef, 0xbb, 0xbf]),
Buffer.from(formatted),
])
);
await expectValidationCode(
validateReplayFixtureDirectory(fixtureRoot),
FIXTURE_VALIDATION_ERROR_CODES.InvalidSchema
);
});
});
describe('committed fixture corpus validation', () => {
it('runs the fail-closed validator over every committed fixture', async () => {
const fixtureRoot = resolve(
process.cwd(),
'apps/stalker-mock-server/fixtures/replay'
);
await expect(
validateReplayFixtureDirectory(fixtureRoot)
).resolves.toBeGreaterThan(0);
});
});
@@ -0,0 +1,244 @@
import {
constants,
type BigIntStats,
type Stats,
} from 'node:fs';
import {
lstat,
open,
readdir,
type FileHandle,
} from 'node:fs/promises';
import { join } from 'node:path';
import { TextDecoder } from 'node:util';
import { REPLAY_MAX_FIXTURE_BYTES } from '@iptvnator/portal/stalker/replay-fixtures';
import {
FIXTURE_VALIDATION_ERROR_CODES,
FixtureValidationError,
validateReplayFixtureText,
} from './fixture-validator';
export const FIXTURE_VALIDATION_CLI_ERROR_CODES = {
FixtureRootUnavailable: 'fixture-root-unavailable',
NoFixtures: 'no-fixtures',
TooManyFixtures: 'too-many-fixtures',
UnsafeFixturePath: 'unsafe-fixture-path',
FixtureReadFailed: 'fixture-read-failed',
InvalidUtf8: 'invalid-utf8',
NoncanonicalFormat: 'noncanonical-format',
UnsupportedCommand: 'unsupported-command',
InternalError: 'internal-error',
} as const;
export type FixtureValidationCliErrorCode =
(typeof FIXTURE_VALIDATION_CLI_ERROR_CODES)[keyof typeof FIXTURE_VALIDATION_CLI_ERROR_CODES];
export class FixtureValidationCliError extends Error {
constructor(readonly code: FixtureValidationCliErrorCode) {
super(`Stalker fixture validation failed: ${code}.`);
this.name = 'FixtureValidationCliError';
}
}
const MAX_FIXTURE_COUNT = 256;
const MAX_DIRECTORY_DEPTH = 8;
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
export async function validateReplayFixtureDirectory(
fixtureRoot: string
): Promise<number> {
let rootStat: Stats;
try {
rootStat = await lstat(fixtureRoot);
} catch {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.FixtureRootUnavailable);
}
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
const fixturePaths: string[] = [];
await collectFixturePaths(fixtureRoot, fixturePaths, 0);
if (fixturePaths.length === 0) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.NoFixtures);
}
for (const fixturePath of fixturePaths) {
const text = await readFixtureFileBoundedForValidation(fixturePath);
const validated = validateReplayFixtureText(text);
if (validated.formatted !== text) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.NoncanonicalFormat);
}
}
return fixturePaths.length;
}
/**
* Internal file-boundary helper exported for deterministic race regression
* coverage. It is deliberately omitted from the package public index.
*/
export async function readFixtureFileBoundedForValidation(
fixturePath: string,
beforeOpen: () => Promise<void> = async () => undefined
): Promise<string> {
let handle: FileHandle | undefined;
try {
const preflight = await lstat(fixturePath, { bigint: true });
assertSafeRegularFile(preflight);
assertFixtureSize(preflight.size);
await beforeOpen();
handle = await open(
fixturePath,
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
);
const opened = await handle.stat({ bigint: true });
assertSafeRegularFile(opened);
assertSameFile(preflight, opened);
assertFixtureSize(opened.size);
const content = await readBounded(handle);
const afterRead = await handle.stat({ bigint: true });
const afterPath = await lstat(fixturePath, { bigint: true });
assertSafeRegularFile(afterRead);
assertSafeRegularFile(afterPath);
assertSameFile(opened, afterRead);
assertSameFile(opened, afterPath);
assertFixtureSize(afterRead.size);
assertFixtureSize(afterPath.size);
if (BigInt(content.byteLength) !== afterRead.size) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
await handle.close();
handle = undefined;
return content.text;
} catch (error) {
if (
error instanceof FixtureValidationCliError ||
error instanceof FixtureValidationError
) {
throw error;
}
throw new FixtureValidationCliError(
FIXTURE_VALIDATION_CLI_ERROR_CODES.FixtureReadFailed
);
} finally {
await handle?.close().catch(() => undefined);
}
}
async function collectFixturePaths(
directory: string,
fixturePaths: string[],
depth: number
): Promise<void> {
if (depth > MAX_DIRECTORY_DEPTH) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.FixtureReadFailed);
}
entries.sort((left, right) => compareCodeUnits(left.name, right.name));
for (const entry of entries) {
if (
!SAFE_PATH_SEGMENT.test(entry.name) ||
entry.isSymbolicLink() ||
(!entry.isDirectory() && !entry.isFile())
) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
const entryPath = join(directory, entry.name);
if (entry.isDirectory()) {
await collectFixturePaths(entryPath, fixturePaths, depth + 1);
continue;
}
if (!entry.name.endsWith('.json')) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
fixturePaths.push(entryPath);
if (fixturePaths.length > MAX_FIXTURE_COUNT) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.TooManyFixtures);
}
}
}
async function readBounded(
handle: FileHandle
): Promise<{ byteLength: number; text: string }> {
const buffer = Buffer.allocUnsafe(REPLAY_MAX_FIXTURE_BYTES + 1);
let byteLength = 0;
while (byteLength < buffer.length) {
const result = await handle.read(
buffer,
byteLength,
buffer.length - byteLength,
byteLength
);
if (result.bytesRead === 0) {
break;
}
byteLength += result.bytesRead;
}
assertFixtureSize(byteLength);
return {
byteLength,
text: decodeUtf8(buffer, byteLength),
};
}
function decodeUtf8(buffer: Buffer, byteLength: number): string {
try {
return new TextDecoder('utf-8', {
fatal: true,
ignoreBOM: true,
}).decode(buffer.subarray(0, byteLength));
} catch {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.InvalidUtf8);
}
}
function assertSafeRegularFile(stat: BigIntStats): void {
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
}
function assertSameFile(expected: BigIntStats, actual: BigIntStats): void {
if (
expected.dev !== actual.dev ||
expected.ino !== actual.ino ||
expected.mode !== actual.mode ||
expected.size !== actual.size ||
expected.mtimeNs !== actual.mtimeNs ||
expected.ctimeNs !== actual.ctimeNs
) {
reject(FIXTURE_VALIDATION_CLI_ERROR_CODES.UnsafeFixturePath);
}
}
function assertFixtureSize(size: number | bigint): void {
if (BigInt(size) > BigInt(REPLAY_MAX_FIXTURE_BYTES)) {
throw new FixtureValidationError(
FIXTURE_VALIDATION_ERROR_CODES.FixtureTooLarge
);
}
}
function reject(code: FixtureValidationCliErrorCode): never {
throw new FixtureValidationCliError(code);
}
function compareCodeUnits(left: string, right: string): number {
if (left === right) {
return 0;
}
return left < right ? -1 : 1;
}
@@ -0,0 +1,132 @@
import {
REPLAY_MAX_FIXTURE_BYTES,
type ReplayFixtureV1,
} from '@iptvnator/portal/stalker/replay-fixtures';
import {
FIXTURE_VALIDATION_ERROR_CODES,
FixtureValidationError,
validateReplayFixtureText,
} from './fixture-validator';
function replayFixture(): ReplayFixtureV1 {
return {
schemaVersion: 1,
scenarioId: 'fixture-validator-contract',
description: 'Synthetic resolver fixture.',
origins: { portal: {} },
entry: { origin: 'portal', path: '/c/' },
expectedEndpoint: { origin: 'portal', path: '/portal.php' },
initialState: 'start',
terminalState: 'complete',
failOnUnexpectedRequest: true,
symbols: [],
phases: [
{
name: 'resolve',
state: 'start',
nextState: 'complete',
mode: 'ordered',
expectations: [
{
id: 'landing',
operation: 'landing',
origin: 'portal',
method: 'GET',
path: '/c/',
request: {
query: { exact: {}, present: [], absent: [] },
headers: { exact: {}, present: [], absent: [] },
cookies: {
exact: {},
present: [],
absent: [],
attributes: {},
},
body: { kind: 'absent' },
},
response: {
status: 200,
headers: {
'content-type': ['application/json'],
},
body: {
kind: 'json',
value: {
alpha: true,
Zebra: true,
js: true,
},
},
},
cardinality: { min: 1, max: 1 },
},
],
},
],
};
}
function expectValidationCode(text: string, code: string): void {
try {
validateReplayFixtureText(text);
throw new Error('fixture unexpectedly passed');
} catch (error) {
expect(error).toBeInstanceOf(FixtureValidationError);
expect((error as FixtureValidationError).code).toBe(code);
expect((error as Error).message).toBe(
`Stalker fixture validation failed: ${code}.`
);
}
}
describe('fixture validator', () => {
it('uses the shared runtime parser and emits deterministic formatting', () => {
const fixture = replayFixture();
const reverseRootOrder = Object.fromEntries(
Object.entries(fixture).reverse()
);
const first = validateReplayFixtureText(JSON.stringify(fixture));
const second = validateReplayFixtureText(
JSON.stringify(reverseRootOrder)
);
expect(first.fixture.scenarioId).toBe('fixture-validator-contract');
expect(first.formatted).toBe(second.formatted);
expect(first.formatted.endsWith('\n')).toBe(true);
expect(first.formatted).toContain('"schemaVersion": 1');
expect(first.formatted.indexOf('"Zebra"')).toBeLessThan(
first.formatted.indexOf('"alpha"')
);
});
it('rejects oversized input before parsing', () => {
expectValidationCode(
'x'.repeat(REPLAY_MAX_FIXTURE_BYTES + 1),
FIXTURE_VALIDATION_ERROR_CODES.FixtureTooLarge
);
});
it('maps every shared-parser failure to one sanitized schema code', () => {
expectValidationCode(
JSON.stringify({ schemaVersion: 1 }),
FIXTURE_VALIDATION_ERROR_CODES.InvalidSchema
);
});
it('preserves scanner codes without exposing the offending literal', () => {
const secret = '00:1A:79:12:34:56';
const fixture = { ...replayFixture(), description: secret };
try {
validateReplayFixtureText(JSON.stringify(fixture));
throw new Error('fixture unexpectedly passed');
} catch (error) {
expect(error).toBeInstanceOf(FixtureValidationError);
expect((error as FixtureValidationError).code).toBe(
FIXTURE_VALIDATION_ERROR_CODES.MacLiteral
);
expect((error as Error).message).not.toContain(secret);
}
});
});
@@ -0,0 +1,87 @@
import {
parseReplayFixtureText,
REPLAY_MAX_FIXTURE_BYTES,
type ReplayFixtureV1,
} from '@iptvnator/portal/stalker/replay-fixtures';
import {
assertReplayFixtureContainsNoSecrets,
FixtureSecretScanError,
FIXTURE_SECRET_SCAN_ERROR_CODES,
} from './fixture-secret-scanner';
export const FIXTURE_VALIDATION_ERROR_CODES = {
FixtureTooLarge: 'fixture-too-large',
InvalidSchema: 'invalid-schema',
...FIXTURE_SECRET_SCAN_ERROR_CODES,
} as const;
export type FixtureValidationErrorCode =
(typeof FIXTURE_VALIDATION_ERROR_CODES)[keyof typeof FIXTURE_VALIDATION_ERROR_CODES];
export class FixtureValidationError extends Error {
constructor(readonly code: FixtureValidationErrorCode) {
super(`Stalker fixture validation failed: ${code}.`);
this.name = 'FixtureValidationError';
}
}
export interface ValidatedReplayFixture {
fixture: ReplayFixtureV1;
formatted: string;
}
export function validateReplayFixtureText(
text: string
): ValidatedReplayFixture {
if (Buffer.byteLength(text) > REPLAY_MAX_FIXTURE_BYTES) {
throw new FixtureValidationError(
FIXTURE_VALIDATION_ERROR_CODES.FixtureTooLarge
);
}
let fixture: ReplayFixtureV1;
try {
fixture = parseReplayFixtureText(text);
} catch {
throw new FixtureValidationError(
FIXTURE_VALIDATION_ERROR_CODES.InvalidSchema
);
}
try {
assertReplayFixtureContainsNoSecrets(fixture);
} catch (error) {
if (error instanceof FixtureSecretScanError) {
throw new FixtureValidationError(error.code);
}
throw new FixtureValidationError(
FIXTURE_VALIDATION_ERROR_CODES.SensitiveLiteral
);
}
return {
fixture,
formatted: `${JSON.stringify(sortJsonValue(fixture), null, 4)}\n`,
};
}
function sortJsonValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(sortJsonValue);
}
if (value === null || typeof value !== 'object') {
return value;
}
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => compareCodeUnits(left, right))
.map(([key, child]) => [key, sortJsonValue(child)])
);
}
function compareCodeUnits(left: string, right: string): number {
if (left === right) {
return 0;
}
return left < right ? -1 : 1;
}
+25
View File
@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"isolatedModules": true,
"target": "es2022",
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve",
"types": ["node"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}