feat(api): advance capability and secure operations slices

This commit is contained in:
chick
2026-07-17 11:57:20 +08:00
parent 8018b36adf
commit 1cc4a995ee
19 changed files with 2102 additions and 22 deletions
@@ -0,0 +1,258 @@
import Database from 'better-sqlite3';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
import { UpstreamError } from '../../infrastructure/transport/upstream-error.js';
import {
CapabilityObservationError,
CapabilityObservationService,
parseHealthVersion,
type CapabilityOperationDescriptor,
} from './capability-observation-service.js';
const databases: Database.Database[] = [];
afterEach(() => {
for (const database of databases.splice(0)) database.close();
});
const operation = (
overrides: Partial<CapabilityOperationDescriptor> = {},
): CapabilityOperationDescriptor => ({
operationId: 'getHealth',
method: 'GET',
pathTemplate: '/api/health',
riskLevel: 'R0',
...overrides,
});
function fixture(
options: {
descriptor?: CapabilityOperationDescriptor;
response?: {
status: number;
headers: Readonly<Record<string, string | undefined>>;
body: string;
};
failure?: unknown;
now?: string;
} = {},
) {
const db = new Database(':memory:');
db.pragma('foreign_keys=ON');
migrateDatabase(db);
databases.push(db);
const created = '2026-07-17T10:00:00.000Z';
db.prepare(
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)',
).run('instance-1', 'LAN', 'http://192.168.1.20:3000', 'none', 1, 1, created, created);
const events: string[] = [];
const instances = {
get: vi.fn(async (id: string) => {
events.push('instance');
return id === 'instance-1' ? { id, origin: 'http://192.168.1.20:3000' } : undefined;
}),
};
const registry = {
requireOperation: vi.fn((id: string) => {
events.push('operation');
if (id !== 'getHealth') throw new Error('unknown operation');
return options.descriptor ?? operation();
}),
};
const transport = {
get: vi.fn(async (url: string) => {
events.push('network');
expect(url).toBe('http://192.168.1.20:3000/api/health');
if (options.failure) throw options.failure;
return options.response ?? { status: 200, headers: {}, body: '{"version":"1.2.3"}' };
}),
};
const service = new CapabilityObservationService({
db,
instances,
registry,
transport,
now: () => new Date(options.now ?? '2026-07-17T12:00:00.000Z'),
});
return { db, events, instances, registry, transport, service };
}
const row = (db: Database.Database) =>
db
.prepare(
'SELECT instance_id,operation_id,state,upstream_version,detail_code,observed_at,created_at,updated_at FROM capabilities',
)
.get();
describe('parseHealthVersion', () => {
it.each([
['{"version":"1.2.3"}', '1.2.3'],
['{"data":{"version":"v2026.07.17-beta+5"}}', 'v2026.07.17-beta+5'],
['{"version":" 2.0.0 "}', '2.0.0'],
])('extracts a bounded version from supported health envelopes', (body, expected) => {
expect(parseHealthVersion(body)).toBe(expected);
});
it.each([
['not-json'],
['null'],
['{"version":7}'],
['{"version":""}'],
[`{"version":"${'a'.repeat(129)}"}`],
['{"version":"1.2.3\\nsecret"}'],
['{"data":{"version":{"raw":"secret"}}}'],
])('rejects malformed, unbounded, or unsafe values without leaking them', (body) => {
expect(parseHealthVersion(body)).toBeUndefined();
});
});
describe('CapabilityObservationService', () => {
it('resolves instance then operation before probing an R0 GET and persists one redacted supported row', async () => {
const { db, events, service } = fixture();
await expect(service.observe('instance-1', 'getHealth')).resolves.toEqual({
instanceId: 'instance-1',
operationId: 'getHealth',
state: 'supported',
upstreamVersion: '1.2.3',
detailCode: 'ACTIVE_PROBE_SUPPORTED',
observedAt: '2026-07-17T12:00:00.000Z',
});
expect(events).toEqual(['instance', 'operation', 'network']);
expect(row(db)).toEqual({
instance_id: 'instance-1',
operation_id: 'getHealth',
state: 'supported',
upstream_version: '1.2.3',
detail_code: 'ACTIVE_PROBE_SUPPORTED',
observed_at: '2026-07-17T12:00:00.000Z',
created_at: '2026-07-17T12:00:00.000Z',
updated_at: '2026-07-17T12:00:00.000Z',
});
});
it.each([
[401, 'auth-required', 'ACTIVE_PROBE_AUTH_REQUIRED'],
[403, 'auth-required', 'ACTIVE_PROBE_AUTH_REQUIRED'],
[404, 'unsupported', 'ACTIVE_PROBE_UNSUPPORTED'],
[500, 'degraded', 'ACTIVE_PROBE_UPSTREAM_DEGRADED'],
[503, 'degraded', 'ACTIVE_PROBE_UPSTREAM_DEGRADED'],
] as const)(
'classifies HTTP %i without persisting response content',
async (status, state, detailCode) => {
const body = '{"password":"do-not-store","version":"9.9.9"}';
const { db, service } = fixture({
response: { status, headers: { 'set-cookie': 'secret=1' }, body },
});
await expect(service.observe('instance-1', 'getHealth')).resolves.toMatchObject({
state,
detailCode,
upstreamVersion: undefined,
});
expect(JSON.stringify(row(db))).not.toContain('do-not-store');
expect(JSON.stringify(row(db))).not.toContain('secret=1');
},
);
it.each([
[new UpstreamError('UPSTREAM_TIMEOUT'), 'ACTIVE_PROBE_TIMEOUT'],
[new UpstreamError('UPSTREAM_UNAVAILABLE'), 'ACTIVE_PROBE_NETWORK_ERROR'],
[new Error('password=never-persist'), 'ACTIVE_PROBE_NETWORK_ERROR'],
])('maps transport failures to unknown stable codes only', async (failure, detailCode) => {
const { db, service } = fixture({ failure });
await expect(service.observe('instance-1', 'getHealth')).resolves.toMatchObject({
state: 'unknown',
detailCode,
});
expect(JSON.stringify(row(db))).not.toContain(failure.message);
});
it.each([
[operation({ operationId: 'getHealth', method: 'POST', riskLevel: 'R1' })],
[operation({ operationId: 'getHealth', method: 'DELETE', riskLevel: 'R3' })],
[operation({ operationId: 'getHealth', method: 'GET', riskLevel: 'R1' })],
])('never actively probes non-R0-GET operations and records unknown', async (descriptor) => {
const { db, service, transport } = fixture({ descriptor });
await expect(service.observe('instance-1', 'getHealth')).resolves.toMatchObject({
state: 'unknown',
detailCode: 'ACTIVE_PROBE_NOT_SAFE',
upstreamVersion: undefined,
});
expect(transport.get).not.toHaveBeenCalled();
expect(row(db)).toBeTruthy();
});
it('does not let an older in-flight observation overwrite a newer completed observation', async () => {
const { db, service, transport } = fixture();
let resolveOlder!: (response: {
status: number;
headers: Record<string, string>;
body: string;
}) => void;
const olderResponse = new Promise<{
status: number;
headers: Record<string, string>;
body: string;
}>((resolve) => {
resolveOlder = resolve;
});
transport.get
.mockImplementationOnce(async () => olderResponse)
.mockResolvedValueOnce({ status: 404, headers: {}, body: 'newer-sensitive-body' });
const older = service.observe('instance-1', 'getHealth');
await vi.waitFor(() => expect(transport.get).toHaveBeenCalledTimes(1));
const newer = service.observe('instance-1', 'getHealth');
await expect(newer).resolves.toMatchObject({
state: 'unsupported',
detailCode: 'ACTIVE_PROBE_UNSUPPORTED',
});
resolveOlder({ status: 200, headers: {}, body: '{"version":"older-secret-version"}' });
await expect(older).resolves.toMatchObject({ state: 'supported' });
expect(row(db)).toMatchObject({
state: 'unsupported',
upstream_version: null,
detail_code: 'ACTIVE_PROBE_UNSUPPORTED',
});
expect(JSON.stringify(row(db))).not.toContain('older-secret-version');
expect(JSON.stringify(row(db))).not.toContain('newer-sensitive-body');
});
it('upserts the single instance-operation row while preserving created_at', async () => {
const first = fixture();
await first.service.observe('instance-1', 'getHealth');
const second = new CapabilityObservationService({
db: first.db,
instances: first.instances,
registry: first.registry,
transport: { get: async () => ({ status: 404, headers: {}, body: 'sensitive' }) },
now: () => new Date('2026-07-17T13:00:00.000Z'),
});
await second.observe('instance-1', 'getHealth');
expect(first.db.prepare('SELECT COUNT(*) count FROM capabilities').get()).toEqual({ count: 1 });
expect(row(first.db)).toMatchObject({
state: 'unsupported',
upstream_version: null,
created_at: '2026-07-17T12:00:00.000Z',
updated_at: '2026-07-17T13:00:00.000Z',
});
});
it('does no operation resolution, write, or network for an unknown instance', async () => {
const { db, service, registry, transport } = fixture();
await expect(service.observe('missing', 'getHealth')).rejects.toEqual(
expect.objectContaining<Partial<CapabilityObservationError>>({ code: 'INSTANCE_NOT_FOUND' }),
);
expect(registry.requireOperation).not.toHaveBeenCalled();
expect(transport.get).not.toHaveBeenCalled();
expect(row(db)).toBeUndefined();
});
it('does no write or network for an unknown operation', async () => {
const { db, service, transport } = fixture();
await expect(service.observe('instance-1', 'missing')).rejects.toEqual(
expect.objectContaining<Partial<CapabilityObservationError>>({ code: 'OPERATION_NOT_FOUND' }),
);
expect(transport.get).not.toHaveBeenCalled();
expect(row(db)).toBeUndefined();
});
});
@@ -0,0 +1,216 @@
import type Database from 'better-sqlite3';
import { UpstreamError } from '../../infrastructure/transport/upstream-error.js';
import type { TransportResponse } from '../../infrastructure/transport/safe-instance-transport.js';
export type CapabilityState =
| 'supported'
| 'unsupported'
| 'auth-required'
| 'degraded'
| 'unknown';
export type CapabilityDetailCode =
| 'ACTIVE_PROBE_SUPPORTED'
| 'ACTIVE_PROBE_AUTH_REQUIRED'
| 'ACTIVE_PROBE_UNSUPPORTED'
| 'ACTIVE_PROBE_UPSTREAM_DEGRADED'
| 'ACTIVE_PROBE_TIMEOUT'
| 'ACTIVE_PROBE_NETWORK_ERROR'
| 'ACTIVE_PROBE_UNEXPECTED_STATUS'
| 'ACTIVE_PROBE_NOT_SAFE';
export interface CapabilityOperationDescriptor {
readonly operationId: string;
readonly method: string;
readonly pathTemplate: string;
readonly riskLevel: 'R0' | 'R1' | 'R2' | 'R3';
}
export interface CapabilityInstance {
readonly id: string;
readonly origin: string;
}
export interface CapabilityObservation {
readonly instanceId: string;
readonly operationId: string;
readonly state: CapabilityState;
readonly upstreamVersion: string | undefined;
readonly detailCode: CapabilityDetailCode;
readonly observedAt: string;
}
export interface CapabilityObservationOptions {
readonly db: Database.Database;
readonly instances: { get(instanceId: string): Promise<CapabilityInstance | undefined> };
readonly registry: { requireOperation(operationId: string): CapabilityOperationDescriptor };
readonly transport: { get(url: string): Promise<TransportResponse> };
readonly now?: () => Date;
}
export type CapabilityObservationErrorCode = 'INSTANCE_NOT_FOUND' | 'OPERATION_NOT_FOUND';
export class CapabilityObservationError extends Error {
constructor(readonly code: CapabilityObservationErrorCode) {
super(code);
this.name = 'CapabilityObservationError';
}
}
const MAX_VERSION_LENGTH = 128;
const VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
/** Maps only documented health envelope locations into bounded, inert version metadata. */
export function parseHealthVersion(body: string): string | undefined {
let value: unknown;
try {
value = JSON.parse(body);
} catch {
return undefined;
}
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
const root = value as Record<string, unknown>;
const data = root.data;
const candidate =
root.version ??
(data !== null && typeof data === 'object' && !Array.isArray(data)
? (data as Record<string, unknown>).version
: undefined);
if (typeof candidate !== 'string') return undefined;
const normalized = candidate.trim();
return normalized.length > 0 &&
normalized.length <= MAX_VERSION_LENGTH &&
VERSION_PATTERN.test(normalized)
? normalized
: undefined;
}
const joinUrl = (origin: string, path: string): string =>
`${origin.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
function classifyStatus(status: number): {
state: CapabilityState;
detailCode: CapabilityDetailCode;
} {
if (status >= 200 && status < 300)
return { state: 'supported', detailCode: 'ACTIVE_PROBE_SUPPORTED' };
if (status === 401 || status === 403)
return { state: 'auth-required', detailCode: 'ACTIVE_PROBE_AUTH_REQUIRED' };
if (status === 404) return { state: 'unsupported', detailCode: 'ACTIVE_PROBE_UNSUPPORTED' };
if (status >= 500 && status < 600)
return { state: 'degraded', detailCode: 'ACTIVE_PROBE_UPSTREAM_DEGRADED' };
return { state: 'unknown', detailCode: 'ACTIVE_PROBE_UNEXPECTED_STATUS' };
}
export class CapabilityObservationService {
readonly #db: Database.Database;
readonly #instances: CapabilityObservationOptions['instances'];
readonly #registry: CapabilityObservationOptions['registry'];
readonly #transport: CapabilityObservationOptions['transport'];
readonly #now: () => Date;
readonly #latestGeneration = new Map<string, number>();
#nextGeneration = 0;
constructor(options: CapabilityObservationOptions) {
this.#db = options.db;
this.#instances = options.instances;
this.#registry = options.registry;
this.#transport = options.transport;
this.#now = options.now ?? (() => new Date());
}
async observe(instanceId: string, operationId: string): Promise<CapabilityObservation> {
// Resolution order is intentional: neither unknown identity crosses the network or persistence boundary.
const instance = await this.#instances.get(instanceId);
if (!instance) throw new CapabilityObservationError('INSTANCE_NOT_FOUND');
let operation: CapabilityOperationDescriptor;
try {
operation = this.#registry.requireOperation(operationId);
} catch {
throw new CapabilityObservationError('OPERATION_NOT_FOUND');
}
const observationKey = JSON.stringify([instanceId, operation.operationId]);
const generation = ++this.#nextGeneration;
this.#latestGeneration.set(observationKey, generation);
const observedAt = this.#now().toISOString();
if (operation.method !== 'GET' || operation.riskLevel !== 'R0') {
return this.#persist(
{
instanceId,
operationId: operation.operationId,
state: 'unknown',
upstreamVersion: undefined,
detailCode: 'ACTIVE_PROBE_NOT_SAFE',
observedAt,
},
observationKey,
generation,
);
}
let state: CapabilityState;
let detailCode: CapabilityDetailCode;
let upstreamVersion: string | undefined;
try {
const response = await this.#transport.get(joinUrl(instance.origin, operation.pathTemplate));
({ state, detailCode } = classifyStatus(response.status));
if (state === 'supported' && operation.operationId === 'getHealth') {
upstreamVersion = parseHealthVersion(response.body);
}
} catch (error) {
state = 'unknown';
detailCode =
error instanceof UpstreamError && error.code === 'UPSTREAM_TIMEOUT'
? 'ACTIVE_PROBE_TIMEOUT'
: 'ACTIVE_PROBE_NETWORK_ERROR';
}
return this.#persist(
{
instanceId,
operationId: operation.operationId,
state,
upstreamVersion,
detailCode,
observedAt,
},
observationKey,
generation,
);
}
#persist(
observation: CapabilityObservation,
observationKey: string,
generation: number,
): CapabilityObservation {
if (this.#latestGeneration.get(observationKey) !== generation) {
return Object.freeze({ ...observation });
}
this.#db
.prepare(
`INSERT INTO capabilities
(instance_id,operation_id,state,upstream_version,detail_code,observed_at,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?)
ON CONFLICT(instance_id,operation_id) DO UPDATE SET
state=excluded.state,
upstream_version=excluded.upstream_version,
detail_code=excluded.detail_code,
observed_at=excluded.observed_at,
updated_at=excluded.updated_at`,
)
.run(
observation.instanceId,
observation.operationId,
observation.state,
observation.upstreamVersion ?? null,
observation.detailCode,
observation.observedAt,
observation.observedAt,
observation.observedAt,
);
this.#latestGeneration.delete(observationKey);
return Object.freeze({ ...observation });
}
}
@@ -192,6 +192,78 @@ describe('DeleteInstanceOperation', () => {
expect(db.prepare('SELECT COUNT(*) count FROM jobs').get()).toEqual({ count: 0 });
});
it.each([
['risk_level', 'R2'],
['target_origin', 'http://192.168.1.99'],
['method', 'POST'],
['path', '/api/v1/instances/someone-else'],
['canonical_query', 'force=true'],
['body_digest', 'a'.repeat(64)],
['content_type', 'application/json'],
['nonce', 'tampered-nonce'],
])(
'fails closed and invalidates a confirmation with tampered durable %s binding',
async (column, value) => {
const { db, instances, operation } = fixture();
const instance = await instances.create({ name: 'A', origin: 'http://192.168.1.10' });
const prepared = await operation.prepare({
operationId: 'deleteInstance',
targets: [{ instanceId: instance.id, revision: 1 }],
parameters: { parameterSchemaId: DELETE_INSTANCE_PARAMETER_SCHEMA_ID, fields: [] },
});
db.prepare(`UPDATE operation_preparations SET ${column}=? WHERE id=?`).run(
value,
prepared.id,
);
await expect(
operation.executeDelete({
instanceId: instance.id,
revision: 1,
preparationId: prepared.id,
confirmationToken: prepared.confirmationToken,
actor: 'loopback-control-plane',
requestId: 'tampered-binding',
}),
).rejects.toMatchObject({ code: 'CONFIRMATION_INVALID' });
expect(await instances.get(instance.id)).toBeDefined();
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(prepared.id),
).toEqual({ status: 'invalidated' });
expect(db.prepare('SELECT COUNT(*) count FROM jobs').get()).toEqual({ count: 0 });
},
);
it('fails closed when the current target origin no longer matches the durable binding', async () => {
const { db, instances, operation } = fixture();
const instance = await instances.create({ name: 'A', origin: 'http://192.168.1.10' });
const prepared = await operation.prepare({
operationId: 'deleteInstance',
targets: [{ instanceId: instance.id, revision: 1 }],
parameters: { parameterSchemaId: DELETE_INSTANCE_PARAMETER_SCHEMA_ID, fields: [] },
});
db.prepare('UPDATE instances SET base_url=? WHERE id=?').run(
'http://192.168.1.11',
instance.id,
);
await expect(
operation.executeDelete({
instanceId: instance.id,
revision: 1,
preparationId: prepared.id,
confirmationToken: prepared.confirmationToken,
actor: 'loopback-control-plane',
requestId: 'changed-origin',
}),
).rejects.toMatchObject({ code: 'CONFIRMATION_INVALID' });
expect(await instances.get(instance.id)).toBeDefined();
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(prepared.id),
).toEqual({ status: 'invalidated' });
expect(db.prepare('SELECT COUNT(*) count FROM jobs').get()).toEqual({ count: 0 });
});
it('returns typed target errors after consuming a correctly bound confirmation', async () => {
const { db, instances, operation } = fixture();
const instance = await instances.create({ name: 'A', origin: 'http://192.168.1.10' });
@@ -29,11 +29,19 @@ export class DeleteInstanceOperationError extends Error {
interface PreparationRow {
operation_id: string;
risk_level: string;
status: string;
target_instance_id: string;
target_revision: number;
target_origin: string;
method: string;
path: string;
canonical_query: string;
body_digest: string;
content_type: string;
parameter_schema_id: string;
parameters_digest: string;
nonce: string;
token_digest: string;
expires_at: string;
}
@@ -128,8 +136,10 @@ export class DeleteInstanceOperation {
this.db
.prepare(
`INSERT INTO operation_preparations
(id,operation_id,risk_level,status,target_instance_id,target_revision,parameter_schema_id,parameters_digest,token_digest,requested_by,request_id,expires_at,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
(id,operation_id,risk_level,status,target_instance_id,target_revision,target_origin,method,path,
canonical_query,body_digest,content_type,parameter_schema_id,parameters_digest,nonce,token_digest,
requested_by,request_id,expires_at,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
)
.run(
id,
@@ -138,8 +148,15 @@ export class DeleteInstanceOperation {
'prepared',
target.instanceId,
target.revision,
current.origin,
'DELETE',
`/api/v1/instances/${target.instanceId}`,
'',
digest(''),
'',
DELETE_INSTANCE_PARAMETER_SCHEMA_ID,
PARAMETERS_DIGEST,
`delete-${id}`,
digest(token),
ACTOR,
requestId,
@@ -169,8 +186,9 @@ export class DeleteInstanceOperation {
this.db.transaction(() => {
const row = this.db
.prepare(
`SELECT operation_id,status,target_instance_id,target_revision,parameter_schema_id,
parameters_digest,token_digest,expires_at
`SELECT operation_id,risk_level,status,target_instance_id,target_revision,target_origin,
method,path,canonical_query,body_digest,content_type,parameter_schema_id,
parameters_digest,nonce,token_digest,expires_at
FROM operation_preparations WHERE id=?`,
)
.get(input.preparationId) as PreparationRow | undefined;
@@ -180,15 +198,44 @@ export class DeleteInstanceOperation {
'Confirmation could not be accepted',
);
const secretValid =
row.expires_at > now &&
equalDigest(row.token_digest, digest(input.confirmationToken)) &&
const confirmationValid =
row.expires_at > now && equalDigest(row.token_digest, digest(input.confirmationToken));
if (!confirmationValid) return;
const expectedPath = `/api/v1/instances/${input.instanceId}`;
const bindingValid =
row.operation_id === OPERATION_ID &&
row.risk_level === 'R3' &&
row.target_instance_id === input.instanceId &&
row.target_revision === input.revision &&
row.method === 'DELETE' &&
row.path === expectedPath &&
row.canonical_query === '' &&
row.body_digest === digest('') &&
row.content_type === '' &&
row.parameter_schema_id === DELETE_INSTANCE_PARAMETER_SCHEMA_ID &&
row.parameters_digest === PARAMETERS_DIGEST;
if (!secretValid) return;
row.parameters_digest === PARAMETERS_DIGEST &&
row.nonce === `delete-${input.preparationId}`;
if (!bindingValid) {
this.db
.prepare(
"UPDATE operation_preparations SET status='invalidated',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
return;
}
const current = this.db
.prepare('SELECT base_url,config_revision FROM instances WHERE id=?')
.get(input.instanceId) as { base_url: string; config_revision: number } | undefined;
if (current && current.base_url !== row.target_origin) {
this.db
.prepare(
"UPDATE operation_preparations SET status='invalidated',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
return;
}
// A correctly authenticated confirmation is one-shot, including stale/missing target results.
this.db
@@ -196,9 +243,6 @@ export class DeleteInstanceOperation {
"UPDATE operation_preparations SET status='consumed',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
const current = this.db
.prepare('SELECT config_revision FROM instances WHERE id=?')
.get(input.instanceId) as { config_revision: number } | undefined;
if (!current) {
targetError = 'NOT_FOUND';
return;
@@ -0,0 +1,215 @@
import Database from 'better-sqlite3';
import { createHash } from 'node:crypto';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
import {
OperationNotDispatchedError,
SecureOperationExecution,
secureOperationRegistry,
type SafeOperationTransport,
} from './secure-operation-execution.js';
const SCHEMA = 'simadmin.58e2204.postNetworkRegisterAuto.parameters.v1';
const databases: Database.Database[] = [];
afterEach(() => databases.splice(0).forEach((db) => db.close()));
function fixture(transport?: SafeOperationTransport) {
const db = new Database(':memory:');
db.pragma('foreign_keys=ON');
migrateDatabase(db);
databases.push(db);
db.prepare(
`INSERT INTO instances
(id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at)
VALUES ('i-1','one','http://192.168.1.10','none',1,3,'2026-07-17T00:00:00.000Z','2026-07-17T00:00:00.000Z')`,
).run();
let sequence = 0;
const request = vi.fn(async () => ({ status: 204 }));
const execution = new SecureOperationExecution({
db,
registry: secureOperationRegistry,
transport: transport ?? { request },
now: () => new Date('2026-07-17T12:00:00.000Z'),
idFactory: () => `id-${++sequence}`,
tokenFactory: () => Buffer.alloc(32, 9).toString('base64url'),
nonceFactory: () => `nonce-${++sequence}`,
});
const input = {
operationId: 'postNetworkRegisterAuto',
targets: [{ instanceId: 'i-1', revision: 3 }],
parameters: { parameterSchemaId: SCHEMA, fields: [] },
} as const;
return { db, execution, input, request };
}
const expectCode = async (promise: Promise<unknown>, code: string) =>
expect(promise).rejects.toMatchObject({ code });
describe('SecureOperationExecution generic R2 slice', () => {
it('prepares the sole audited zero-parameter operation without upstream or a job and persists full binding plus digest only', async () => {
const { db, execution, input, request } = fixture();
const prepared = await execution.prepare(input, 'request-1');
expect(prepared).toMatchObject({
operationId: input.operationId,
risk: 'R2',
status: 'prepared',
targetCount: 1,
});
expect(prepared.expiresAt).toBe('2026-07-17T12:05:00.000Z');
expect(request).not.toHaveBeenCalled();
expect(db.prepare('SELECT count(*) count FROM jobs').get()).toEqual({ count: 0 });
const row = db
.prepare('SELECT * FROM operation_preparations WHERE id=?')
.get(prepared.id) as Record<string, unknown>;
expect(row).toMatchObject({
target_instance_id: 'i-1',
target_revision: 3,
target_origin: 'http://192.168.1.10',
operation_id: input.operationId,
risk_level: 'R2',
method: 'POST',
path: '/api/network/register-auto',
canonical_query: '',
body_digest: createHash('sha256').update('').digest('hex'),
content_type: '',
parameter_schema_id: SCHEMA,
status: 'prepared',
});
expect(row.token_digest).toBe(
createHash('sha256').update(prepared.confirmationToken).digest('hex'),
);
expect(JSON.stringify(row)).not.toContain(prepared.confirmationToken);
expect(typeof row.nonce).toBe('string');
});
it('strictly rejects unknown shapes, operations, parameters, targets, and stale revisions', async () => {
const { execution, input } = fixture();
await expectCode(execution.prepare({ ...input, extra: true } as never), 'VALIDATION_FAILED');
await expectCode(
execution.prepare({ ...input, operationId: 'getDevice' }),
'OPERATION_NOT_ALLOWED',
);
await expectCode(execution.prepare({ ...input, targets: [] }), 'VALIDATION_FAILED');
await expectCode(
execution.prepare({ ...input, targets: [{ instanceId: 'i-1', revision: 2 }] }),
'REVISION_CONFLICT',
);
await expectCode(
execution.prepare({
...input,
parameters: { ...input.parameters, fields: [{ fieldId: 'x', kind: 'string', value: 'x' }] },
} as never),
'VALIDATION_FAILED',
);
});
it('does not consume on wrong token, expires atomically, and consumes exactly once after re-resolving bindings', async () => {
const { db, execution, input, request } = fixture();
const prepared = await execution.prepare(input);
await expectCode(
execution.execute({ preparationId: prepared.id, confirmationToken: 'wrong' }, 'actor', 'r2'),
'CONFIRMATION_INVALID',
);
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(prepared.id),
).toEqual({ status: 'prepared' });
const job = await execution.execute(
{ preparationId: prepared.id, confirmationToken: prepared.confirmationToken },
'actor',
'r3',
);
expect(job).toMatchObject({
operationId: input.operationId,
status: 'succeeded',
items: [{ targetId: 'i-1', state: 'succeeded' }],
});
expect(request).toHaveBeenCalledWith({
origin: 'http://192.168.1.10',
method: 'POST',
path: '/api/network/register-auto',
query: '',
contentType: '',
body: undefined,
});
await expectCode(
execution.execute(
{ preparationId: prepared.id, confirmationToken: prepared.confirmationToken },
'actor',
'r4',
),
'CONFIRMATION_INVALID',
);
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(prepared.id),
).toEqual({ status: 'consumed' });
});
it('marks an expired preparation expired without transport', async () => {
const { db, execution, input, request } = fixture();
const prepared = await execution.prepare(input);
db.prepare(
"UPDATE operation_preparations SET expires_at='2020-01-01T00:00:00.000Z' WHERE id=?",
).run(prepared.id);
await expectCode(
execution.execute(
{ preparationId: prepared.id, confirmationToken: prepared.confirmationToken },
'a',
'r',
),
'CONFIRMATION_INVALID',
);
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(prepared.id),
).toEqual({ status: 'expired' });
expect(request).not.toHaveBeenCalled();
});
it('durably creates running lineage before transport and maps ambiguity/interruption to unknown-result', async () => {
let observed = false;
const { db, execution, input } = fixture({
request: async () => {
observed =
(
db.prepare("SELECT count(*) count FROM jobs WHERE status='running'").get() as {
count: number;
}
).count === 1;
throw new Error('socket reset');
},
});
const prepared = await execution.prepare(input);
const job = await execution.execute(
{ preparationId: prepared.id, confirmationToken: prepared.confirmationToken },
'a',
'r',
);
expect(observed).toBe(true);
expect(job.status).toBe('unknown-result');
expect(job.attempts[0]?.state).toBe('unknown-result');
expect(execution.reconcileInterruptedJobs()).toBe(0);
db.prepare("UPDATE jobs SET status='running',finished_at=NULL").run();
db.prepare("UPDATE job_items SET status='running',finished_at=NULL").run();
db.prepare("UPDATE job_attempts SET status='running',finished_at=NULL").run();
expect(execution.reconcileInterruptedJobs()).toBe(1);
expect(db.prepare('SELECT status FROM jobs').get()).toEqual({ status: 'unknown-result' });
});
it('maps a deterministic pre-dispatch rejection to failed rather than unknown-result', async () => {
const { execution, input, db } = fixture({
request: async () => {
throw new OperationNotDispatchedError('UPSTREAM_REQUEST_INVALID');
},
});
const prepared = await execution.prepare(input);
const job = await execution.execute(
{ preparationId: prepared.id, confirmationToken: prepared.confirmationToken },
'a',
'r',
);
expect(job.status).toBe('failed');
expect(job.items[0]?.state).toBe('failed');
expect(db.prepare('SELECT result_code FROM job_items').get()).toEqual({
result_code: 'UPSTREAM_NOT_DISPATCHED',
});
});
});
@@ -0,0 +1,466 @@
import type Database from 'better-sqlite3';
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
import type {
ExecuteOperationRequest,
Job,
Preparation,
PrepareOperationRequest,
} from '@multi-simadmin/contracts';
export interface SecureOperationDescriptor {
readonly operationId: string;
readonly title: string;
readonly riskLevel: string;
readonly method: string;
readonly pathTemplate: string;
readonly requestContentType: string;
readonly parameterSchemaId: string;
}
export interface SecureOperationRegistry {
requireExecutableOperation(operationId: string): SecureOperationDescriptor;
}
export const secureOperationRegistry: SecureOperationRegistry = {
requireExecutableOperation(operationId) {
if (operationId !== 'postNetworkRegisterAuto') throw new Error('UNKNOWN_OPERATION');
return {
operationId: 'postNetworkRegisterAuto',
title: 'Register Network Automatically',
riskLevel: 'R2',
method: 'POST',
pathTemplate: '/api/network/register-auto',
requestContentType: 'none',
parameterSchemaId: 'simadmin.58e2204.postNetworkRegisterAuto.parameters.v1',
};
},
};
const ALLOWED_OPERATION = 'postNetworkRegisterAuto';
const ACTOR = 'loopback-control-plane';
const TTL_MS = 5 * 60 * 1000;
const EMPTY_DIGEST = createHash('sha256').update('').digest('hex');
export interface SafeOperationTransportRequest {
readonly origin: string;
readonly method: 'POST';
readonly path: string;
readonly query: string;
readonly contentType: string;
readonly body: undefined;
}
export interface SafeOperationTransport {
request(request: SafeOperationTransportRequest): Promise<{ readonly status: number }>;
}
/** The request was rejected before any bytes could have reached the upstream. */
export class OperationNotDispatchedError extends Error {
readonly dispatched = false;
constructor(readonly code: string) {
super(code);
this.name = 'OperationNotDispatchedError';
}
}
export type SecureOperationExecutionErrorCode =
| 'VALIDATION_FAILED'
| 'OPERATION_NOT_ALLOWED'
| 'NOT_FOUND'
| 'REVISION_CONFLICT'
| 'CONFIRMATION_INVALID';
export class SecureOperationExecutionError extends Error {
constructor(
readonly code: SecureOperationExecutionErrorCode,
message: string,
) {
super(message);
this.name = 'SecureOperationExecutionError';
}
}
interface Options {
readonly db: Database.Database;
readonly registry: SecureOperationRegistry;
readonly transport: SafeOperationTransport;
readonly now?: () => Date;
readonly idFactory?: () => string;
readonly tokenFactory?: () => string;
readonly nonceFactory?: () => string;
}
interface PreparationRow {
operation_id: string;
risk_level: string;
status: string;
target_instance_id: string;
target_revision: number;
target_origin: string;
method: string;
path: string;
canonical_query: string;
body_digest: string;
content_type: string;
parameter_schema_id: string;
parameters_digest: string;
nonce: string;
token_digest: string;
expires_at: string;
}
const digest = (value: string): string => createHash('sha256').update(value, 'utf8').digest('hex');
const equalDigest = (left: string, right: string): boolean => {
const a = Buffer.from(left, 'hex');
const b = Buffer.from(right, 'hex');
return a.length === 32 && b.length === 32 && timingSafeEqual(a, b);
};
const validRevision = (value: unknown): value is number =>
typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
const parameterDigest = (schema: string): string =>
digest(JSON.stringify({ parameterSchemaId: schema, fields: [] }));
export class SecureOperationExecution {
private readonly clock: () => Date;
private readonly id: () => string;
private readonly token: () => string;
private readonly nonce: () => string;
constructor(private readonly options: Options) {
this.clock = options.now ?? (() => new Date());
this.id = options.idFactory ?? randomUUID;
this.token = options.tokenFactory ?? (() => randomBytes(32).toString('base64url'));
this.nonce = options.nonceFactory ?? randomUUID;
}
async prepare(input: PrepareOperationRequest, requestId = this.id()): Promise<Preparation> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).some((key) => !['operationId', 'targets', 'parameters'].includes(key)) ||
typeof input.operationId !== 'string'
)
this.validation();
if (input.operationId !== ALLOWED_OPERATION)
throw new SecureOperationExecutionError(
'OPERATION_NOT_ALLOWED',
'Operation is not enabled for generic execution',
);
let descriptor: SecureOperationDescriptor;
try {
descriptor = this.options.registry.requireExecutableOperation(input.operationId);
} catch {
throw new SecureOperationExecutionError(
'OPERATION_NOT_ALLOWED',
'Operation is not enabled for generic execution',
);
}
if (
descriptor.riskLevel !== 'R2' ||
descriptor.method !== 'POST' ||
descriptor.pathTemplate !== '/api/network/register-auto' ||
descriptor.requestContentType !== 'none' ||
!Array.isArray(input.targets) ||
input.targets.length !== 1
)
this.validation();
const target = input.targets[0];
if (
!target ||
typeof target !== 'object' ||
Array.isArray(target) ||
Object.keys(target).some((key) => !['instanceId', 'revision'].includes(key)) ||
typeof target.instanceId !== 'string' ||
!target.instanceId ||
!validRevision(target.revision)
)
this.validation();
const parameters = input.parameters;
if (
!parameters ||
typeof parameters !== 'object' ||
Array.isArray(parameters) ||
Object.keys(parameters).some((key) => !['parameterSchemaId', 'fields'].includes(key)) ||
parameters.parameterSchemaId !== descriptor.parameterSchemaId ||
!Array.isArray(parameters.fields) ||
parameters.fields.length !== 0
)
this.validation();
const instance = this.options.db
.prepare('SELECT base_url,config_revision FROM instances WHERE id=? AND enabled=1')
.get(target.instanceId) as { base_url: string; config_revision: number } | undefined;
if (!instance) throw new SecureOperationExecutionError('NOT_FOUND', 'Instance was not found');
if (instance.config_revision !== target.revision)
throw new SecureOperationExecutionError(
'REVISION_CONFLICT',
'Instance revision does not match',
);
const token = this.token();
let tokenBytes: Buffer;
try {
tokenBytes = Buffer.from(token, 'base64url');
} catch {
tokenBytes = Buffer.alloc(0);
}
if (tokenBytes.length < 32 || token.length < 20 || !/^[A-Za-z0-9_-]+$/.test(token))
throw new Error('Confirmation token factory returned an unsafe token');
const id = this.id();
const created = this.clock();
const now = created.toISOString();
const expiresAt = new Date(created.getTime() + TTL_MS).toISOString();
this.options.db
.prepare(
`INSERT INTO operation_preparations
(id,operation_id,risk_level,status,target_instance_id,target_revision,target_origin,method,path,
canonical_query,body_digest,content_type,parameter_schema_id,parameters_digest,nonce,token_digest,
requested_by,request_id,expires_at,created_at,updated_at)
VALUES (?,?,?,'prepared',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
)
.run(
id,
descriptor.operationId,
descriptor.riskLevel,
target.instanceId,
target.revision,
instance.base_url,
descriptor.method,
descriptor.pathTemplate,
'',
EMPTY_DIGEST,
'',
descriptor.parameterSchemaId,
parameterDigest(descriptor.parameterSchemaId),
this.nonce(),
digest(token),
ACTOR,
requestId,
expiresAt,
now,
now,
);
return {
id,
status: 'prepared',
operationId: descriptor.operationId,
risk: 'R2',
expiresAt,
confirmationToken: token,
confirmationPrompt: `Execute ${descriptor.title} on ${target.instanceId}?`,
targetCount: 1,
};
}
async execute(input: ExecuteOperationRequest, actor: string, requestId: string): Promise<Job> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).some((key) => !['preparationId', 'confirmationToken'].includes(key)) ||
typeof input.preparationId !== 'string' ||
!input.preparationId ||
typeof input.confirmationToken !== 'string' ||
!input.confirmationToken
)
this.validation();
const now = this.clock().toISOString();
const ids = { job: this.id(), item: this.id(), attempt: this.id() };
let bound: PreparationRow | undefined;
const outcome = this.options.db.transaction((): 'accepted' | 'invalid' => {
const row = this.options.db
.prepare('SELECT * FROM operation_preparations WHERE id=?')
.get(input.preparationId) as PreparationRow | undefined;
if (!row || row.status !== 'prepared') return 'invalid';
if (row.expires_at <= now) {
this.options.db
.prepare(
"UPDATE operation_preparations SET status='expired',updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, input.preparationId);
return 'invalid';
}
if (!equalDigest(row.token_digest, digest(input.confirmationToken))) return 'invalid';
let descriptor: SecureOperationDescriptor;
try {
descriptor = this.options.registry.requireExecutableOperation(row.operation_id);
} catch {
return 'invalid';
}
const instance = this.options.db
.prepare('SELECT base_url,config_revision FROM instances WHERE id=? AND enabled=1')
.get(row.target_instance_id) as { base_url: string; config_revision: number } | undefined;
const bindingValid =
descriptor.operationId === ALLOWED_OPERATION &&
descriptor.riskLevel === 'R2' &&
descriptor.method === row.method &&
descriptor.pathTemplate === row.path &&
descriptor.parameterSchemaId === row.parameter_schema_id &&
row.canonical_query === '' &&
row.content_type === '' &&
row.body_digest === EMPTY_DIGEST &&
row.parameters_digest === parameterDigest(row.parameter_schema_id) &&
!!row.nonce &&
!!instance &&
instance.base_url === row.target_origin &&
instance.config_revision === row.target_revision;
if (!bindingValid) {
this.options.db
.prepare(
"UPDATE operation_preparations SET status='invalidated',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
return 'invalid';
}
const consumed = this.options.db
.prepare(
"UPDATE operation_preparations SET status='consumed',consumed_at=?,updated_at=? WHERE id=? AND status='prepared'",
)
.run(now, now, input.preparationId);
if (consumed.changes !== 1) return 'invalid';
this.options.db
.prepare(
`INSERT INTO jobs
(id,parent_job_id,root_job_id,retry_of_job_id,operation_id,risk_level,status,requested_by,request_id,parameters_digest,created_at,started_at,updated_at)
VALUES (?,NULL,?,NULL,?,'R2','running',?,?,?,?,?,?)`,
)
.run(
ids.job,
ids.job,
row.operation_id,
actor,
requestId,
row.parameters_digest,
now,
now,
now,
);
this.options.db
.prepare(
"INSERT INTO job_attempts (id,job_id,status,started_at,created_at) VALUES (?,?,'running',?,?)",
)
.run(ids.attempt, ids.job, now, now);
this.options.db
.prepare(
`INSERT INTO job_items (id,job_id,instance_id,attempt_number,status,created_at,started_at,updated_at)
VALUES (?,?,?,1,'running',?,?,?)`,
)
.run(ids.item, ids.job, row.target_instance_id, now, now, now);
bound = row;
return 'accepted';
})();
if (outcome !== 'accepted' || !bound)
throw new SecureOperationExecutionError(
'CONFIRMATION_INVALID',
'Confirmation could not be accepted',
);
let state: 'succeeded' | 'failed' | 'unknown-result';
let code: string;
try {
const response = await this.options.transport.request({
origin: bound.target_origin,
method: 'POST',
path: bound.path,
query: '',
contentType: '',
body: undefined,
});
state = response.status >= 200 && response.status < 300 ? 'succeeded' : 'failed';
code = state === 'succeeded' ? 'UPSTREAM_SUCCEEDED' : 'UPSTREAM_REJECTED';
} catch (error) {
if (error instanceof OperationNotDispatchedError) {
state = 'failed';
code = 'UPSTREAM_NOT_DISPATCHED';
} else {
state = 'unknown-result';
code = 'UPSTREAM_RESULT_UNKNOWN';
}
}
this.finish(ids, state, code);
return this.job(ids.job);
}
reconcileInterruptedJobs(): number {
const now = this.clock().toISOString();
return this.options.db.transaction(() => {
const jobs = this.options.db
.prepare(
"SELECT id FROM jobs WHERE operation_id=? AND risk_level='R2' AND status='running'",
)
.all(ALLOWED_OPERATION) as Array<{ id: string }>;
for (const row of jobs) this.finishByJob(row.id, now, 'unknown-result', 'INTERRUPTED');
return jobs.length;
})();
}
private finish(
ids: { job: string; item: string; attempt: string },
state: string,
code: string,
): void {
const now = this.clock().toISOString();
this.options.db.transaction(() => {
this.options.db
.prepare(
"UPDATE job_items SET status=?,result_code=?,finished_at=?,updated_at=? WHERE id=? AND status='running'",
)
.run(state, code, now, now, ids.item);
this.options.db
.prepare("UPDATE job_attempts SET status=?,finished_at=? WHERE id=? AND status='running'")
.run(state, now, ids.attempt);
this.options.db
.prepare(
"UPDATE jobs SET status=?,finished_at=?,updated_at=? WHERE id=? AND status='running'",
)
.run(state, now, now, ids.job);
})();
}
private finishByJob(jobId: string, now: string, state: string, code: string): void {
this.options.db
.prepare(
"UPDATE job_items SET status=?,result_code=?,finished_at=?,updated_at=? WHERE job_id=? AND status='running'",
)
.run(state, code, now, now, jobId);
this.options.db
.prepare("UPDATE job_attempts SET status=?,finished_at=? WHERE job_id=? AND status='running'")
.run(state, now, jobId);
this.options.db
.prepare(
"UPDATE jobs SET status=?,finished_at=?,updated_at=? WHERE id=? AND status='running'",
)
.run(state, now, now, jobId);
}
private job(id: string): Job {
const row = this.options.db
.prepare(
'SELECT operation_id,status,root_job_id,retry_of_job_id,created_at FROM jobs WHERE id=?',
)
.get(id) as {
operation_id: string;
status: Job['status'];
root_job_id: string;
retry_of_job_id: string | null;
created_at: string;
};
const items = this.options.db
.prepare('SELECT id,instance_id,status FROM job_items WHERE job_id=?')
.all(id) as Array<{
id: string;
instance_id: string;
status: 'succeeded' | 'failed' | 'unknown-result';
}>;
const attempts = this.options.db
.prepare('SELECT id,status,started_at,finished_at FROM job_attempts WHERE job_id=?')
.all(id) as Array<{
id: string;
status: 'succeeded' | 'failed' | 'unknown-result';
started_at: string;
finished_at: string | null;
}>;
return {
id,
operationId: row.operation_id,
status: row.status,
rootJobId: row.root_job_id,
...(row.retry_of_job_id ? { retryOfJobId: row.retry_of_job_id } : {}),
items: items.map((item) => ({ id: item.id, targetId: item.instance_id, state: item.status })),
attempts: attempts.map((attempt) => ({
id: attempt.id,
state: attempt.status,
startedAt: attempt.started_at,
...(attempt.finished_at ? { finishedAt: attempt.finished_at } : {}),
})),
createdAt: row.created_at,
};
}
private validation(): never {
throw new SecureOperationExecutionError('VALIDATION_FAILED', 'Invalid operation request');
}
}
@@ -0,0 +1,249 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { StatusPollScheduler } from './status-poll-scheduler.js';
const deferred = () => {
let resolve!: () => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<void>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
const flush = async () => {
await Promise.resolve();
await Promise.resolve();
};
afterEach(() => {
vi.useRealTimers();
});
describe('StatusPollScheduler', () => {
it('starts registered keys in FIFO order while respecting the global concurrency bound', async () => {
const runs: Array<{ id: string; done: ReturnType<typeof deferred> }> = [];
const scheduler = new StatusPollScheduler({
intervalMs: 1_000,
concurrency: 2,
task: (id) => {
const done = deferred();
runs.push({ id, done });
return done.promise;
},
});
scheduler.add('a');
scheduler.add('b');
scheduler.add('c');
scheduler.start();
expect(runs.map(({ id }) => id)).toEqual(['a', 'b']);
runs[0]!.done.resolve();
await flush();
expect(runs.map(({ id }) => id)).toEqual(['a', 'b', 'c']);
runs[1]!.done.resolve();
runs[2]!.done.resolve();
await scheduler.stop();
});
it('maintains exactly one recurring timer chain when tasks resolve before the cadence', async () => {
vi.useFakeTimers();
const task = vi.fn(async () => undefined);
const scheduler = new StatusPollScheduler({ intervalMs: 10, concurrency: 1, task });
scheduler.add('a');
scheduler.start();
await flush();
expect(task).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(1);
for (let expectedCalls = 2; expectedCalls <= 5; expectedCalls++) {
await vi.advanceTimersByTimeAsync(10);
expect(task).toHaveBeenCalledTimes(expectedCalls);
expect(vi.getTimerCount()).toBe(1);
}
await scheduler.stop();
expect(vi.getTimerCount()).toBe(0);
});
it('is single-flight per key and coalesces arbitrarily many interval ticks into one rerun', async () => {
vi.useFakeTimers();
const runs: ReturnType<typeof deferred>[] = [];
const scheduler = new StatusPollScheduler({
intervalMs: 10,
concurrency: 3,
task: async () => {
const done = deferred();
runs.push(done);
await done.promise;
},
});
scheduler.add('a');
scheduler.start();
await vi.advanceTimersByTimeAsync(100);
expect(runs).toHaveLength(1);
runs[0]!.resolve();
await flush();
expect(runs).toHaveLength(2);
runs[1]!.resolve();
await scheduler.stop();
});
it('keeps equal-due recurring work FIFO instead of letting a busy key starve queued keys', async () => {
vi.useFakeTimers();
const runs: Array<{ id: string; done: ReturnType<typeof deferred> }> = [];
const scheduler = new StatusPollScheduler({
intervalMs: 10,
concurrency: 1,
task: async (id) => {
const done = deferred();
runs.push({ id, done });
await done.promise;
},
});
scheduler.add('a');
scheduler.add('b');
scheduler.start();
await vi.advanceTimersByTimeAsync(50);
runs[0]!.done.resolve();
await flush();
expect(runs.map(({ id }) => id)).toEqual(['a', 'b']);
runs[1]!.done.resolve();
await flush();
expect(runs.map(({ id }) => id)).toEqual(['a', 'b', 'a']);
runs[2]!.done.resolve();
await scheduler.stop();
});
it('supports per-key interval updates and fences the obsolete generation', async () => {
vi.useFakeTimers();
const calls: Array<{ signal: AbortSignal; done: ReturnType<typeof deferred> }> = [];
const scheduler = new StatusPollScheduler({
intervalMs: 100,
concurrency: 1,
task: async (_id, signal) => {
const done = deferred();
calls.push({ signal, done });
await done.promise;
},
});
scheduler.add('a');
scheduler.start();
scheduler.update('a', 10);
expect(calls[0]!.signal.aborted).toBe(true);
await vi.advanceTimersByTimeAsync(50);
expect(calls).toHaveLength(1);
calls[0]!.done.resolve();
await flush();
expect(calls).toHaveLength(2);
calls[1]!.done.resolve();
await flush();
await vi.advanceTimersByTimeAsync(9);
expect(calls).toHaveLength(2);
await vi.advanceTimersByTimeAsync(1);
expect(calls).toHaveLength(3);
calls[2]!.done.resolve();
await scheduler.stop();
});
it('remove cancels queued work and aborts active work; re-add is a fresh fenced generation', async () => {
const calls: Array<{ id: string; signal: AbortSignal; done: ReturnType<typeof deferred> }> = [];
const scheduler = new StatusPollScheduler({
intervalMs: 100,
concurrency: 1,
task: async (id, signal) => {
const done = deferred();
calls.push({ id, signal, done });
await done.promise;
},
});
scheduler.add('a');
scheduler.add('b');
scheduler.start();
scheduler.remove('b');
scheduler.remove('a');
expect(calls[0]!.signal.aborted).toBe(true);
scheduler.add('a');
calls[0]!.done.resolve();
await flush();
expect(calls.map(({ id }) => id)).toEqual(['a', 'a']);
calls[1]!.done.resolve();
await scheduler.stop();
});
it('releases slots after rejection without an unhandled rejection', async () => {
const calls: string[] = [];
const unhandled = vi.fn();
process.on('unhandledRejection', unhandled);
const scheduler = new StatusPollScheduler({
intervalMs: 1_000,
concurrency: 1,
task: async (id) => {
calls.push(id);
if (id === 'a') throw new Error('probe failed');
},
});
scheduler.add('a');
scheduler.add('b');
scheduler.start();
await flush();
expect(calls).toEqual(['a', 'b']);
expect(unhandled).not.toHaveBeenCalled();
process.off('unhandledRejection', unhandled);
await scheduler.stop();
});
it('start is idempotent and add/update reject invalid registration values', async () => {
const task = vi.fn(async () => undefined);
const scheduler = new StatusPollScheduler({ intervalMs: 20, concurrency: 1, task });
expect(() => scheduler.add('')).toThrow();
expect(() => scheduler.add('a', 0)).toThrow();
expect(() => scheduler.update('missing', 10)).toThrow();
scheduler.add('a');
scheduler.start();
scheduler.start();
await flush();
expect(task).toHaveBeenCalledTimes(1);
await scheduler.stop();
});
it('stop is idempotent, clears timers, aborts active tasks, waits for settlement, and starts no queued work', async () => {
vi.useFakeTimers();
const active = deferred();
const calls: Array<{ id: string; signal: AbortSignal }> = [];
const scheduler = new StatusPollScheduler({
intervalMs: 10,
concurrency: 1,
task: async (id, signal) => {
calls.push({ id, signal });
await active.promise;
},
});
scheduler.add('a');
scheduler.add('b');
scheduler.start();
const stopped = scheduler.stop();
let settled = false;
void stopped.then(() => {
settled = true;
});
await flush();
expect(calls).toHaveLength(1);
expect(calls[0]!.signal.aborted).toBe(true);
expect(settled).toBe(false);
active.resolve();
await stopped;
await scheduler.stop();
await vi.advanceTimersByTimeAsync(100);
expect(calls).toHaveLength(1);
});
});
@@ -0,0 +1,220 @@
export type StatusPollTask = (instanceId: string, signal: AbortSignal) => Promise<void>;
export interface StatusPollSchedulerOptions {
task: StatusPollTask;
intervalMs: number;
concurrency: number;
}
interface Registration {
generation: number;
intervalMs: number;
timer: ReturnType<typeof setTimeout> | undefined;
}
interface QueueItem {
instanceId: string;
generation: number;
}
interface ActiveRun {
generation: number;
controller: AbortController;
promise: Promise<void>;
rerunGeneration?: number;
}
/**
* A bounded, keyed polling scheduler. Registrations are the queue: repeated
* ticks only set one rerun bit, so backlog cannot grow with time.
*/
export class StatusPollScheduler {
readonly #task: StatusPollTask;
readonly #defaultIntervalMs: number;
readonly #concurrency: number;
readonly #registrations = new Map<string, Registration>();
readonly #queue: QueueItem[] = [];
readonly #queuedGeneration = new Map<string, number>();
readonly #active = new Map<string, ActiveRun>();
#nextGeneration = 1;
#started = false;
#stopping = false;
#stopPromise?: Promise<void>;
constructor(options: StatusPollSchedulerOptions) {
this.#defaultIntervalMs = this.#validInterval(options.intervalMs);
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {
throw new RangeError('concurrency must be a positive integer');
}
this.#concurrency = options.concurrency;
this.#task = options.task;
}
add(instanceId: string, intervalMs = this.#defaultIntervalMs): void {
this.#validId(instanceId);
const interval = this.#validInterval(intervalMs);
if (this.#registrations.has(instanceId)) {
throw new Error(`instance is already registered: ${instanceId}`);
}
if (this.#stopping) throw new Error('scheduler is stopping');
const registration: Registration = {
generation: this.#nextGeneration++,
intervalMs: interval,
timer: undefined,
};
this.#registrations.set(instanceId, registration);
if (this.#started) this.#enqueue(instanceId, registration.generation);
}
update(instanceId: string, intervalMs: number): void {
const interval = this.#validInterval(intervalMs);
const previous = this.#registrations.get(instanceId);
if (!previous) throw new Error(`instance is not registered: ${instanceId}`);
if (this.#stopping) throw new Error('scheduler is stopping');
if (previous.timer !== undefined) clearTimeout(previous.timer);
this.#cancelQueued(instanceId);
const active = this.#active.get(instanceId);
active?.controller.abort();
const registration: Registration = {
generation: this.#nextGeneration++,
intervalMs: interval,
timer: undefined,
};
this.#registrations.set(instanceId, registration);
if (this.#started) this.#enqueue(instanceId, registration.generation);
}
remove(instanceId: string): boolean {
const registration = this.#registrations.get(instanceId);
if (!registration) return false;
this.#registrations.delete(instanceId);
if (registration.timer !== undefined) clearTimeout(registration.timer);
this.#cancelQueued(instanceId);
this.#active.get(instanceId)?.controller.abort();
return true;
}
start(): void {
if (this.#started || this.#stopping) return;
this.#started = true;
for (const [instanceId, registration] of this.#registrations) {
this.#enqueue(instanceId, registration.generation);
}
}
async stop(): Promise<void> {
if (this.#stopPromise) return this.#stopPromise;
this.#stopping = true;
this.#started = false;
for (const registration of this.#registrations.values()) {
if (registration.timer !== undefined) clearTimeout(registration.timer);
registration.timer = undefined;
}
this.#queue.length = 0;
this.#queuedGeneration.clear();
for (const run of this.#active.values()) run.controller.abort();
const settling = [...this.#active.values()].map(({ promise }) => promise);
this.#stopPromise = Promise.allSettled(settling).then(() => undefined);
return this.#stopPromise;
}
#enqueue(instanceId: string, generation: number): void {
if (!this.#started || this.#stopping) return;
const registration = this.#registrations.get(instanceId);
if (!registration || registration.generation !== generation) return;
const active = this.#active.get(instanceId);
if (active) {
active.rerunGeneration = generation;
return;
}
if (this.#queuedGeneration.get(instanceId) === generation) return;
this.#cancelQueued(instanceId);
this.#queue.push({ instanceId, generation });
this.#queuedGeneration.set(instanceId, generation);
this.#drain();
}
#drain(): void {
while (!this.#stopping && this.#active.size < this.#concurrency && this.#queue.length > 0) {
const item = this.#queue.shift()!;
if (this.#queuedGeneration.get(item.instanceId) === item.generation) {
this.#queuedGeneration.delete(item.instanceId);
}
const registration = this.#registrations.get(item.instanceId);
if (!registration || registration.generation !== item.generation) continue;
if (this.#active.has(item.instanceId)) {
this.#active.get(item.instanceId)!.rerunGeneration = item.generation;
continue;
}
this.#run(item, registration);
}
}
#run(item: QueueItem, registration: Registration): void {
const controller = new AbortController();
const active: ActiveRun = {
generation: item.generation,
controller,
promise: undefined as unknown as Promise<void>,
};
this.#active.set(item.instanceId, active);
this.#scheduleTick(item.instanceId, registration);
let result: Promise<void>;
try {
result = this.#task(item.instanceId, controller.signal);
} catch (error) {
result = Promise.reject(error);
}
const promise = Promise.resolve(result).then(
() => this.#finish(item.instanceId, active),
() => this.#finish(item.instanceId, active),
);
active.promise = promise;
}
#scheduleTick(instanceId: string, registration: Registration): void {
if (registration.timer !== undefined) return;
registration.timer = setTimeout(() => {
registration.timer = undefined;
if (this.#registrations.get(instanceId) !== registration || this.#stopping) return;
this.#scheduleTick(instanceId, registration);
this.#enqueue(instanceId, registration.generation);
}, registration.intervalMs);
}
#finish(instanceId: string, completed: ActiveRun): void {
if (this.#active.get(instanceId) !== completed) return;
this.#active.delete(instanceId);
if (!this.#stopping) {
const current = this.#registrations.get(instanceId);
const rerun = completed.rerunGeneration;
if (current && rerun === current.generation) this.#enqueue(instanceId, rerun);
this.#drain();
}
}
#cancelQueued(instanceId: string): void {
this.#queuedGeneration.delete(instanceId);
for (let index = this.#queue.length - 1; index >= 0; index--) {
if (this.#queue[index]!.instanceId === instanceId) this.#queue.splice(index, 1);
}
}
#validId(instanceId: string): void {
if (instanceId.length === 0) throw new TypeError('instanceId must not be empty');
}
#validInterval(intervalMs: number): number {
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new RangeError('intervalMs must be greater than zero');
}
return intervalMs;
}
}
@@ -19,6 +19,10 @@ it('registers the read-only operations catalog without touching upstream', async
upstreamCalls += 1;
throw new Error('unexpected');
},
postNetworkRegisterAuto: async () => {
upstreamCalls += 1;
throw new Error('unexpected');
},
},
});
const response = await app.inject('/api/v1/operations?pageSize=1');
+1
View File
@@ -86,6 +86,7 @@ describe('buildControlPlaneApp', () => {
});
},
request: async () => ({ status: 200, headers: {}, body: '' }),
postNetworkRegisterAuto: async () => ({ status: 200 }),
},
});
const created = await app.inject({
+25 -2
View File
@@ -1,6 +1,10 @@
import type Database from 'better-sqlite3';
import { registerOperationRoutes } from './interface/http/operation-routes.js';
import { operationCatalogRegistry } from './application/operations/operation-catalog-data.js';
import {
SecureOperationExecution,
secureOperationRegistry,
} from './application/operations/secure-operation-execution.js';
import type { FastifyInstance } from 'fastify';
import { buildApp, type BuildAppOptions } from './app.js';
import {
@@ -21,6 +25,7 @@ import { registerInstanceRoutes } from './interface/http/instance-routes.js';
export interface SafeControlPlaneUpstream extends ConnectionTransport {
request: UpstreamSessionClientOptions['request'];
postNetworkRegisterAuto(origin: string): Promise<{ readonly status: number }>;
}
export interface ControlPlaneOptions {
readonly db: Database.Database;
@@ -54,11 +59,29 @@ export function buildControlPlaneApp(options: ControlPlaneOptions): ControlPlane
? new DeleteInstanceOperation({ db: options.db, instances, now: options.now })
: new DeleteInstanceOperation({ db: options.db, instances });
deletion.reconcileInterruptedJobs();
const secureExecution = new SecureOperationExecution({
db: options.db,
registry: secureOperationRegistry,
transport: {
request: async ({ origin }) => {
const response = await options.upstream.postNetworkRegisterAuto(origin);
return { status: response.status };
},
},
...(options.now ? { now: options.now } : {}),
});
secureExecution.reconcileInterruptedJobs();
const app = buildApp({
...options.app,
registerRoutes: (app) => {
registerInstanceRoutes(app, { instances, connections, login, deletion });
registerOperationRoutes(app, operationCatalogRegistry);
registerInstanceRoutes(app, {
instances,
connections,
login,
deletion,
registerDeletionPreparationRoute: false,
});
registerOperationRoutes(app, operationCatalogRegistry, secureExecution, deletion);
},
});
Object.assign(app, {
@@ -228,6 +228,48 @@ export const MIGRATIONS: readonly Migration[] = [
'CREATE INDEX idx_operation_preparations_status_expires_at ON operation_preparations(status, expires_at)',
],
},
{
id: 5,
name: 'generic-secure-operation-preparation-binding',
statements: [
`CREATE TABLE operation_preparations_v5 (
id TEXT PRIMARY KEY,
operation_id TEXT NOT NULL,
risk_level TEXT NOT NULL CHECK (risk_level IN ('R2','R3')),
status TEXT NOT NULL CHECK (status IN ('prepared','consumed','expired','invalidated')),
target_instance_id TEXT NOT NULL,
target_revision INTEGER NOT NULL CHECK (target_revision > 0),
target_origin TEXT NOT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
canonical_query TEXT NOT NULL,
body_digest TEXT NOT NULL CHECK (length(body_digest) = 64),
content_type TEXT NOT NULL,
parameter_schema_id TEXT NOT NULL,
parameters_digest TEXT NOT NULL CHECK (length(parameters_digest) = 64),
nonce TEXT NOT NULL UNIQUE,
token_digest TEXT NOT NULL CHECK (length(token_digest) = 64),
requested_by TEXT NOT NULL,
request_id TEXT NOT NULL,
expires_at TEXT NOT NULL,
consumed_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
`INSERT INTO operation_preparations_v5
(id,operation_id,risk_level,status,target_instance_id,target_revision,target_origin,method,path,
canonical_query,body_digest,content_type,parameter_schema_id,parameters_digest,nonce,token_digest,
requested_by,request_id,expires_at,consumed_at,created_at,updated_at)
SELECT p.id,p.operation_id,p.risk_level,p.status,p.target_instance_id,p.target_revision,
COALESCE(i.base_url,''),'DELETE','/api/v1/instances/' || p.target_instance_id,'',
'${createHash('sha256').update('').digest('hex')}','',p.parameter_schema_id,p.parameters_digest,
'legacy-' || p.id,p.token_digest,p.requested_by,p.request_id,p.expires_at,p.consumed_at,p.created_at,p.updated_at
FROM operation_preparations p LEFT JOIN instances i ON i.id=p.target_instance_id`,
'DROP TABLE operation_preparations',
'ALTER TABLE operation_preparations_v5 RENAME TO operation_preparations',
'CREATE INDEX idx_operation_preparations_status_expires_at ON operation_preparations(status, expires_at)',
],
},
];
const createMigrationsTable = `CREATE TABLE schema_migrations (
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { createSafeControlPlaneUpstream } from './safe-control-plane-upstream.js';
describe('createSafeControlPlaneUpstream', () => {
it('routes both health GET and login POST through the same safe transport', async () => {
it('routes health, login, and the audited R2 operation through the same safe transport', async () => {
const calls: unknown[] = [];
const upstream = createSafeControlPlaneUpstream({
get: async (url) => {
@@ -22,6 +22,7 @@ describe('createSafeControlPlaneUpstream', () => {
secret: '[REDACTED]',
body: '[REDACTED]',
});
await upstream.postNetworkRegisterAuto('http://192.168.1.20');
expect(calls).toEqual([
{ method: 'GET', url: 'http://192.168.1.20/api/health' },
{
@@ -30,6 +31,12 @@ describe('createSafeControlPlaneUpstream', () => {
headers: { 'content-type': 'application/json' },
body: '{"password":"[REDACTED]"}',
},
{
method: 'POST',
url: 'http://192.168.1.20/api/network/register-auto',
headers: {},
body: '',
},
]);
});
});
@@ -4,6 +4,7 @@ import { SafeUpstreamGateway, type SafeUpstreamTransport } from './safe-upstream
export interface SafeControlPlaneUpstream extends ConnectionTransport {
request: UpstreamSessionClientOptions['request'];
postNetworkRegisterAuto(origin: string): Promise<{ readonly status: number }>;
}
export function createSafeControlPlaneUpstream(
transport: SafeUpstreamTransport,
@@ -12,5 +13,6 @@ export function createSafeControlPlaneUpstream(
return {
get: (url) => transport.get(url),
request: (request) => gateway.request(request),
postNetworkRegisterAuto: (origin) => gateway.postNetworkRegisterAuto(origin),
};
}
@@ -1,7 +1,35 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { SafeUpstreamGateway } from './safe-upstream-gateway.js';
describe('SafeUpstreamGateway', () => {
it('dispatches only the audited zero-body network registration operation through pinned POST', async () => {
const calls: unknown[] = [];
const gateway = new SafeUpstreamGateway({
transport: {
get: async () => ({ status: 200, headers: {}, body: '' }),
post: async (url, headers, body) => {
calls.push({ url, headers, body });
return { status: 204, headers: {}, body: '' };
},
},
});
const response = await gateway.postNetworkRegisterAuto('http://192.168.1.20:8080');
expect(response.status).toBe(204);
expect(calls).toEqual([
{ url: 'http://192.168.1.20:8080/api/network/register-auto', headers: {}, body: '' },
]);
});
it('rejects malformed operation origins before calling transport', async () => {
const post = vi.fn(async () => ({ status: 204, headers: {}, body: '' }));
const gateway = new SafeUpstreamGateway({
transport: { get: async () => ({ status: 200, headers: {}, body: '' }), post },
});
await expect(
gateway.postNetworkRegisterAuto('http://192.168.1.20/base?next=x'),
).rejects.toMatchObject({ code: 'UPSTREAM_REQUEST_INVALID', dispatched: false });
expect(post).not.toHaveBeenCalled();
});
it('sends login password only as JSON through the pinned POST transport', async () => {
const calls: unknown[] = [];
const gateway = new SafeUpstreamGateway({
@@ -4,6 +4,7 @@ import type {
} from '../../application/connections/upstream-session-client.js';
import type { TransportResponse } from './safe-instance-transport.js';
import { UpstreamError } from './upstream-error.js';
import { OperationNotDispatchedError } from '../../application/operations/secure-operation-execution.js';
export interface SafeUpstreamTransport {
get(url: string): Promise<TransportResponse>;
@@ -15,6 +16,34 @@ export interface SafeUpstreamTransport {
}
export class SafeUpstreamGateway {
constructor(private readonly options: { readonly transport: SafeUpstreamTransport }) {}
async postNetworkRegisterAuto(origin: string): Promise<UpstreamResponse> {
let parsed: URL;
try {
parsed = new URL(origin);
} catch {
throw this.notDispatched();
}
if (
(parsed.protocol !== 'http:' && parsed.protocol !== 'https:') ||
parsed.username ||
parsed.password ||
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash
)
throw this.notDispatched();
const url = `${parsed.origin}/api/network/register-auto`;
try {
return await this.options.transport.post(url, {}, '');
} catch (error) {
if (
error instanceof UpstreamError &&
(error.code === 'UNSAFE_ORIGIN' || error.code === 'UNSAFE_RESOLUTION')
)
throw new OperationNotDispatchedError(error.code);
throw error;
}
}
async request(request: UpstreamRequest): Promise<UpstreamResponse> {
const url = new URL(request.url);
if (url.protocol !== 'https:') throw new UpstreamError('UPSTREAM_INSECURE_AUTH');
@@ -35,4 +64,7 @@ export class SafeUpstreamGateway {
}
throw new UpstreamError('UPSTREAM_REQUEST_INVALID');
}
private notDispatched(): OperationNotDispatchedError {
return new OperationNotDispatchedError('UPSTREAM_REQUEST_INVALID');
}
}
@@ -25,6 +25,7 @@ export interface InstanceRoutesOptions {
readonly connections?: ConnectionProbe;
readonly login?: InstanceLoginService;
readonly deletion?: DeleteInstanceOperation;
readonly registerDeletionPreparationRoute?: boolean;
}
const problem = (
request: FastifyRequest,
@@ -379,10 +380,12 @@ export function registerInstanceRoutes(app: FastifyInstance, options: InstanceRo
}
if (options.deletion) {
app.post(
'/api/v1/operations/prepare',
wrap(async (request) => options.deletion!.prepare(request.body as never, request.id)),
);
if (options.registerDeletionPreparationRoute !== false) {
app.post(
'/api/v1/operations/prepare',
wrap(async (request) => options.deletion!.prepare(request.body as never, request.id)),
);
}
app.delete('/api/v1/instances/:instanceId', async (request, reply) => {
const handled = await wrap(async (wrappedRequest) => {
const preparationId = wrappedRequest.headers['x-preparation-id'];
@@ -1,7 +1,19 @@
import type { OperationPageQuery } from '@multi-simadmin/contracts';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import type {
ExecuteOperationRequest,
OperationPageQuery,
PrepareOperationRequest,
} from '@multi-simadmin/contracts';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { OperationCatalogService } from '../../application/operations/operation-catalog-service.js';
import type { OperationCatalogRegistry } from '../../application/operations/operation-catalog-service.js';
import {
DeleteInstanceOperation,
DeleteInstanceOperationError,
} from '../../application/operations/delete-instance-operation.js';
import {
SecureOperationExecution,
SecureOperationExecutionError,
} from '../../application/operations/secure-operation-execution.js';
const capabilities = new Set(['query', 'command', 'job']);
const keys = new Set([
@@ -89,6 +101,8 @@ function parseQuery(request: FastifyRequest): OperationPageQuery {
export function registerOperationRoutes(
app: FastifyInstance,
registry: OperationCatalogRegistry,
execution?: SecureOperationExecution,
deletion?: DeleteInstanceOperation,
): void {
const service = new OperationCatalogService(registry);
app.get('/api/v1/operations', async (request, reply) => {
@@ -106,4 +120,67 @@ export function registerOperationRoutes(
});
}
});
if (!execution) return;
const problem = (
request: FastifyRequest,
reply: FastifyReply,
error: SecureOperationExecutionError,
) => {
const status =
error.code === 'NOT_FOUND' ? 404 : error.code === 'REVISION_CONFLICT' ? 409 : 400;
const title = status === 404 ? 'Not Found' : status === 409 ? 'Conflict' : 'Bad Request';
return reply.code(status).type('application/problem+json').send({
type: 'about:blank',
title,
status,
code: error.code,
detail: error.message,
requestId: request.id,
});
};
app.post('/api/v1/operations/prepare', async (request, reply) => {
try {
const body = request.body as { operationId?: unknown };
if (body && typeof body === 'object' && body.operationId === 'deleteInstance' && deletion)
return reply.code(200).send(await deletion.prepare(request.body as never, request.id));
return reply
.code(200)
.send(await execution.prepare(request.body as PrepareOperationRequest, request.id));
} catch (error) {
if (error instanceof DeleteInstanceOperationError) {
const status =
error.code === 'NOT_FOUND' ? 404 : error.code === 'REVISION_CONFLICT' ? 412 : 400;
return reply
.code(status)
.type('application/problem+json')
.send({
type: 'about:blank',
title:
status === 404 ? 'Not Found' : status === 412 ? 'Precondition Failed' : 'Bad Request',
status,
code: error.code,
detail: 'The requested operation could not be prepared.',
requestId: request.id,
});
}
if (error instanceof SecureOperationExecutionError) return problem(request, reply, error);
throw error;
}
});
app.post('/api/v1/operations/execute', async (request, reply) => {
try {
return reply
.code(202)
.send(
await execution.execute(
request.body as ExecuteOperationRequest,
'loopback-control-plane',
request.id,
),
);
} catch (error) {
if (error instanceof SecureOperationExecutionError) return problem(request, reply, error);
throw error;
}
});
}
@@ -0,0 +1,121 @@
import Database from 'better-sqlite3';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { buildApp } from '../../app.js';
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
import {
SecureOperationExecution,
secureOperationRegistry,
} from '../../application/operations/secure-operation-execution.js';
import { operationCatalogRegistry } from '../../application/operations/operation-catalog-data.js';
import { registerOperationRoutes } from './operation-routes.js';
const resources: Array<{ close(): unknown }> = [];
afterEach(async () => {
for (const resource of resources.splice(0)) await resource.close();
});
function fixture() {
const db = new Database(':memory:');
resources.push(db);
db.pragma('foreign_keys=ON');
migrateDatabase(db);
db.prepare(
`INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at)
VALUES ('i','one','http://192.168.1.10','none',1,1,'2026-07-17T00:00:00.000Z','2026-07-17T00:00:00.000Z')`,
).run();
const request = vi.fn(async () => ({ status: 200 }));
const execution = new SecureOperationExecution({
db,
registry: secureOperationRegistry,
transport: { request },
idFactory: (() => {
let n = 0;
return () => `id-${++n}`;
})(),
tokenFactory: () => Buffer.alloc(32, 4).toString('base64url'),
});
const app = buildApp({
registerRoutes: (server) =>
registerOperationRoutes(server, operationCatalogRegistry, execution),
});
resources.push(app);
return { app, db, request };
}
const prepareBody = {
operationId: 'postNetworkRegisterAuto',
targets: [{ instanceId: 'i', revision: 1 }],
parameters: {
parameterSchemaId: 'simadmin.58e2204.postNetworkRegisterAuto.parameters.v1',
fields: [],
},
};
describe('secure operation routes', () => {
it('returns 200 Preparation then 202 terminal Job with request IDs', async () => {
const { app, db, request } = fixture();
const prepared = await app.inject({
method: 'POST',
url: '/api/v1/operations/prepare',
payload: prepareBody,
});
expect(prepared.statusCode).toBe(200);
expect(prepared.headers['x-request-id']).toBeTruthy();
const preparation = prepared.json();
expect(db.prepare('SELECT count(*) count FROM jobs').get()).toEqual({ count: 0 });
const executed = await app.inject({
method: 'POST',
url: '/api/v1/operations/execute',
payload: {
preparationId: preparation.id,
confirmationToken: preparation.confirmationToken,
},
});
expect(executed.statusCode).toBe(202);
expect(executed.headers['x-request-id']).toBeTruthy();
expect(executed.json()).toMatchObject({
operationId: 'postNetworkRegisterAuto',
status: 'succeeded',
});
expect(request).toHaveBeenCalledOnce();
});
it.each([
['/api/v1/operations/prepare', { ...prepareBody, extra: true }],
[
'/api/v1/operations/prepare',
{ ...prepareBody, targets: [{ instanceId: 'i', revision: 1, extra: true }] },
],
[
'/api/v1/operations/prepare',
{ ...prepareBody, parameters: { ...prepareBody.parameters, extra: true } },
],
['/api/v1/operations/execute', { preparationId: 'x', confirmationToken: 'x', extra: true }],
])('strictly rejects additional properties at %s', async (url, payload) => {
const { app } = fixture();
const response = await app.inject({ method: 'POST', url, payload });
expect(response.statusCode).toBe(400);
expect(response.headers['content-type']).toContain('application/problem+json');
expect(response.headers['x-request-id']).toBeTruthy();
expect(response.json()).toMatchObject({
status: 400,
code: 'VALIDATION_FAILED',
requestId: response.headers['x-request-id'],
});
});
it('returns Problem Details and does not consume a wrong confirmation', async () => {
const { app, db } = fixture();
const preparation = (
await app.inject({ method: 'POST', url: '/api/v1/operations/prepare', payload: prepareBody })
).json();
const response = await app.inject({
method: 'POST',
url: '/api/v1/operations/execute',
payload: { preparationId: preparation.id, confirmationToken: 'wrong' },
});
expect(response.statusCode).toBe(400);
expect(response.json()).toMatchObject({ status: 400, code: 'CONFIRMATION_INVALID' });
expect(
db.prepare('SELECT status FROM operation_preparations WHERE id=?').get(preparation.id),
).toEqual({ status: 'prepared' });
});
});