feat(registry): add fail-closed runtime lookup
This commit is contained in:
@@ -2,5 +2,12 @@
|
||||
"name": "@multi-simadmin/operation-registry",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
OperationRegistry,
|
||||
RuntimeRegistryError,
|
||||
listOperations,
|
||||
operationRegistry,
|
||||
requireExecutableOperation,
|
||||
requireOperation,
|
||||
} from './runtime-registry.ts';
|
||||
export type { RuntimeOperationDescriptor, RuntimeRegistryErrorCode } from './runtime-registry.ts';
|
||||
@@ -0,0 +1,122 @@
|
||||
import { operationAcceptanceOverrides58e2204 } from './acceptance-58e2204.ts';
|
||||
import { upstream58e2204Operations } from './upstream-58e2204.ts';
|
||||
|
||||
export type RuntimeRegistryErrorCode = 'UNKNOWN_OPERATION' | 'DEDICATED_FLOW_REQUIRED';
|
||||
|
||||
export class RuntimeRegistryError extends Error {
|
||||
readonly code: RuntimeRegistryErrorCode;
|
||||
|
||||
constructor(code: RuntimeRegistryErrorCode, message: string) {
|
||||
super(message);
|
||||
this.name = 'RuntimeRegistryError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
type UpstreamOperation = (typeof upstream58e2204Operations)[number];
|
||||
type AcceptanceOverride = Record<string, unknown> & { uiStrategy?: unknown };
|
||||
|
||||
export type RuntimeOperationDescriptor = Readonly<
|
||||
UpstreamOperation & {
|
||||
module: UpstreamOperation['upstreamDomain'];
|
||||
uiStrategy: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const OPERATION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/;
|
||||
const EXPECTED_OPERATION_COUNT = 117;
|
||||
|
||||
function deepFreeze<T>(value: T): T {
|
||||
if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
||||
for (const child of Object.values(value)) deepFreeze(child);
|
||||
return Object.freeze(value);
|
||||
}
|
||||
|
||||
function unknownOperation(operationId: unknown): RuntimeRegistryError {
|
||||
const rendered = typeof operationId === 'string' && operationId ? operationId : '<invalid>';
|
||||
return new RuntimeRegistryError('UNKNOWN_OPERATION', `Unknown operation: ${rendered}`);
|
||||
}
|
||||
|
||||
/** Runtime-only, operationId-keyed projection of the frozen audited registry. */
|
||||
export class OperationRegistry {
|
||||
readonly #byOperationId: ReadonlyMap<string, RuntimeOperationDescriptor>;
|
||||
readonly #operations: readonly RuntimeOperationDescriptor[];
|
||||
|
||||
constructor(
|
||||
operations: readonly UpstreamOperation[],
|
||||
acceptanceOverrides: Readonly<Record<string, AcceptanceOverride>>,
|
||||
) {
|
||||
const sourceById = new Map<string, UpstreamOperation>();
|
||||
for (const operation of operations) {
|
||||
const id = operation.operationId;
|
||||
if (!OPERATION_ID_PATTERN.test(id)) throw new Error(`invalid operationId in registry: ${id}`);
|
||||
if (sourceById.has(id)) throw new Error(`duplicate operationId: ${id}`);
|
||||
sourceById.set(id, operation);
|
||||
}
|
||||
if (sourceById.size !== EXPECTED_OPERATION_COUNT) {
|
||||
throw new Error(
|
||||
`runtime registry must contain exactly ${EXPECTED_OPERATION_COUNT} operations`,
|
||||
);
|
||||
}
|
||||
|
||||
const descriptors: RuntimeOperationDescriptor[] = [];
|
||||
for (const id of [...sourceById.keys()].sort()) {
|
||||
if (!Object.hasOwn(acceptanceOverrides, id)) {
|
||||
throw new Error(`missing acceptance override: ${id}`);
|
||||
}
|
||||
const acceptance = acceptanceOverrides[id];
|
||||
if (!acceptance || typeof acceptance.uiStrategy !== 'string' || !acceptance.uiStrategy) {
|
||||
throw new Error(`missing acceptance override: ${id}`);
|
||||
}
|
||||
const operation = sourceById.get(id)!;
|
||||
const descriptor = structuredClone({
|
||||
...operation,
|
||||
module: operation.upstreamDomain,
|
||||
uiStrategy: acceptance.uiStrategy,
|
||||
}) as RuntimeOperationDescriptor;
|
||||
descriptors.push(deepFreeze(descriptor));
|
||||
}
|
||||
|
||||
this.#operations = Object.freeze(descriptors);
|
||||
this.#byOperationId = new Map(
|
||||
descriptors.map((descriptor) => [descriptor.operationId, descriptor]),
|
||||
);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
listOperations(): readonly RuntimeOperationDescriptor[] {
|
||||
return this.#operations;
|
||||
}
|
||||
|
||||
requireOperation(operationId: string): RuntimeOperationDescriptor {
|
||||
if (typeof operationId !== 'string' || !OPERATION_ID_PATTERN.test(operationId)) {
|
||||
throw unknownOperation(operationId);
|
||||
}
|
||||
const operation = this.#byOperationId.get(operationId);
|
||||
if (!operation) throw unknownOperation(operationId);
|
||||
return operation;
|
||||
}
|
||||
|
||||
requireExecutableOperation(operationId: string): RuntimeOperationDescriptor {
|
||||
const operation = this.requireOperation(operationId);
|
||||
if (operation.executionPolicy === 'dedicatedFlow') {
|
||||
throw new RuntimeRegistryError(
|
||||
'DEDICATED_FLOW_REQUIRED',
|
||||
`Operation requires a dedicated flow: ${operationId}`,
|
||||
);
|
||||
}
|
||||
return operation;
|
||||
}
|
||||
}
|
||||
|
||||
export const operationRegistry = new OperationRegistry(
|
||||
upstream58e2204Operations,
|
||||
operationAcceptanceOverrides58e2204,
|
||||
);
|
||||
|
||||
export const listOperations = (): readonly RuntimeOperationDescriptor[] =>
|
||||
operationRegistry.listOperations();
|
||||
export const requireOperation = (operationId: string): RuntimeOperationDescriptor =>
|
||||
operationRegistry.requireOperation(operationId);
|
||||
export const requireExecutableOperation = (operationId: string): RuntimeOperationDescriptor =>
|
||||
operationRegistry.requireExecutableOperation(operationId);
|
||||
@@ -0,0 +1,105 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
OperationRegistry,
|
||||
RuntimeRegistryError,
|
||||
listOperations,
|
||||
operationRegistry,
|
||||
requireExecutableOperation,
|
||||
requireOperation,
|
||||
} from '../src/index.ts';
|
||||
import { upstream58e2204Operations } from '../src/upstream-58e2204.ts';
|
||||
import { operationAcceptanceOverrides58e2204 } from '../src/acceptance-58e2204.ts';
|
||||
|
||||
test('runtime registry projects exactly 117 descriptors in stable operationId order', () => {
|
||||
const listed = listOperations();
|
||||
assert.equal(listed.length, 117);
|
||||
assert.equal(new Set(listed.map((row) => row.operationId)).size, 117);
|
||||
assert.deepEqual(
|
||||
listed.map((row) => row.operationId),
|
||||
listed.map((row) => row.operationId).toSorted(),
|
||||
);
|
||||
assert.ok(Object.isFrozen(listed));
|
||||
assert.ok(listed.every((row) => Object.isFrozen(row)));
|
||||
|
||||
const device = requireOperation('getDevice');
|
||||
assert.equal(device.module, device.upstreamDomain);
|
||||
assert.equal(device.uiStrategy, operationAcceptanceOverrides58e2204.getDevice.uiStrategy);
|
||||
assert.equal(device.method, 'GET');
|
||||
assert.ok(Object.isFrozen(device.sourceEvidence));
|
||||
assert.strictEqual(requireOperation('getDevice'), device);
|
||||
assert.strictEqual(operationRegistry.requireOperation('getDevice'), device);
|
||||
});
|
||||
|
||||
test('runtime construction rejects duplicate rows and missing acceptance overrides', () => {
|
||||
const rows = upstream58e2204Operations as readonly (typeof upstream58e2204Operations)[number][];
|
||||
assert.throws(
|
||||
() => new OperationRegistry([...rows, rows[0]], operationAcceptanceOverrides58e2204),
|
||||
/duplicate operationId: getDevice/,
|
||||
);
|
||||
const missing = { ...operationAcceptanceOverrides58e2204 };
|
||||
delete (missing as Record<string, unknown>).getDevice;
|
||||
assert.throws(
|
||||
() => new OperationRegistry(rows, missing),
|
||||
/missing acceptance override: getDevice/,
|
||||
);
|
||||
});
|
||||
|
||||
test('requireOperation validates only operationId and fails closed with a stable typed error', () => {
|
||||
for (const id of [
|
||||
'',
|
||||
' ',
|
||||
'GET /api/device',
|
||||
'/api/device',
|
||||
'__proto__',
|
||||
'constructor',
|
||||
'get-device',
|
||||
undefined,
|
||||
null,
|
||||
]) {
|
||||
assert.throws(
|
||||
() => requireOperation(id as string),
|
||||
(error: unknown) =>
|
||||
error instanceof RuntimeRegistryError && error.code === 'UNKNOWN_OPERATION',
|
||||
String(id),
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => requireOperation('getDefinitelyUnknown'),
|
||||
(error: unknown) => error instanceof RuntimeRegistryError && error.code === 'UNKNOWN_OPERATION',
|
||||
);
|
||||
});
|
||||
|
||||
test('dedicated auth flows are catalogued but cannot cross executable boundary', () => {
|
||||
for (const id of [
|
||||
'postAuthSetup',
|
||||
'postAuthPassword',
|
||||
'postAuthSettings',
|
||||
'postAuthLogin',
|
||||
'postAuthLogout',
|
||||
]) {
|
||||
assert.equal(requireOperation(id).executionPolicy, 'dedicatedFlow');
|
||||
assert.throws(
|
||||
() => requireExecutableOperation(id),
|
||||
(error: unknown) =>
|
||||
error instanceof RuntimeRegistryError && error.code === 'DEDICATED_FLOW_REQUIRED',
|
||||
id,
|
||||
);
|
||||
}
|
||||
assert.strictEqual(requireExecutableOperation('getDevice'), requireOperation('getDevice'));
|
||||
});
|
||||
|
||||
test('descriptors are deeply immutable and mutation cannot affect later lookups or frozen source', () => {
|
||||
const source = upstream58e2204Operations.find((row) => row.operationId === 'getDevice')!;
|
||||
const originalSourceEvidence = structuredClone(source.sourceEvidence);
|
||||
const descriptor = requireOperation('getDevice');
|
||||
|
||||
assert.ok(Object.isFrozen(descriptor));
|
||||
assert.ok(Object.isFrozen(descriptor.sourceEvidence));
|
||||
assert.ok(Object.isFrozen(descriptor.sensitiveFields));
|
||||
assert.throws(() => ((descriptor as { method: string }).method = 'DELETE'), TypeError);
|
||||
assert.throws(() => (descriptor.sourceEvidence as string[]).push('mutation'), TypeError);
|
||||
|
||||
assert.equal(requireOperation('getDevice').method, 'GET');
|
||||
assert.deepEqual(source.sourceEvidence, originalSourceEvidence);
|
||||
});
|
||||
Reference in New Issue
Block a user