feat(contracts): share notification, organization and device capability models

Move the Hub channel table, organization tags and device capability definitions
into the workspace packages so the control plane and the console validate the
same contract, and refresh the frozen upstream evidence for the new paths.
This commit is contained in:
chick
2026-09-05 18:52:54 +08:00
parent 03fa2e6c7b
commit f11877f13e
19 changed files with 15535 additions and 11768 deletions
@@ -1,90 +1,311 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts';
import { collectOne, selectReadonlyOperations, validateReadonlyOperation, validateInstanceOrigin } from '../scripts/collector.ts';
import {
collectOne,
selectReadonlyOperations,
validateReadonlyOperation,
validateInstanceOrigin,
} from '../scripts/collector.ts';
import { loadInstances } from '../scripts/collect-readonly-fixtures.ts';
import { redact, redactOperationPayload, parseResponseSensitivePath } from '../src/redactor.ts';
const health=upstream58e2204Operations.find((x:any)=>x.operationId==='getHealth')!;
const fakeResponse=async()=>new Response('{"ok":true}',{headers:{'content-type':'application/json'}});
const health = upstream58e2204Operations.find((x: any) => x.operationId === 'getHealth')!;
const fakeResponse = async () =>
new Response('{"ok":true}', { headers: { 'content-type': 'application/json' } });
test('operation validator rejects path and registry identity bypasses', async()=>{
const attacks=['https://example.invalid/api/health','//example.invalid/api/health','/api/health?scan=1','/api/health#x','/api/%73can','/api/%252e%252e/health','/api/a/../health','/api\\health','/api//health'];
for(const pathTemplate of attacks) assert.throws(()=>validateReadonlyOperation({...health,pathTemplate}));
assert.throws(()=>validateReadonlyOperation({...health}));
assert.throws(()=>validateReadonlyOperation({operationId:health.operationId,method:'GET',riskLevel:'R0',pathTemplate:health.pathTemplate}));
await assert.rejects(()=>collectOne({origin:'http://192.168.1.2',alias:'instance-1'},{...health},fakeResponse));
test('operation validator rejects path and registry identity bypasses', async () => {
const attacks = [
'https://example.invalid/api/health',
'//example.invalid/api/health',
'/api/health?scan=1',
'/api/health#x',
'/api/%73can',
'/api/%252e%252e/health',
'/api/a/../health',
'/api\\health',
'/api//health',
];
for (const pathTemplate of attacks)
assert.throws(() => validateReadonlyOperation({ ...health, pathTemplate }));
assert.throws(() => validateReadonlyOperation({ ...health }));
assert.throws(() =>
validateReadonlyOperation({
operationId: health.operationId,
method: 'GET',
riskLevel: 'R0',
pathTemplate: health.pathTemplate,
}),
);
await assert.rejects(() =>
collectOne({ origin: 'http://192.168.1.2', alias: 'instance-1' }, { ...health }, fakeResponse),
);
});
test('all path bypass classes fail before transport',async()=>{
let calls=0;const transport=async()=>{calls++;return new Response('{}',{headers:{'content-type':'application/json'}})};
const attacks=['https://evil.invalid/api/health','//evil.invalid/api/health','/api/health?q=1','/api/health#x','/api/%68ealth','/api/%252e%252e/x','/api/a/../x','/api\\health','/api//health'];
for(const pathTemplate of attacks) await assert.rejects(()=>collectOne({origin:'http://192.168.1.2',alias:'instance-1'},{...health,pathTemplate},transport));
assert.equal(calls,0);
assert.throws(()=>selectReadonlyOperations([{...health}]));
test('all path bypass classes fail before transport', async () => {
let calls = 0;
const transport = async () => {
calls++;
return new Response('{}', { headers: { 'content-type': 'application/json' } });
};
const attacks = [
'https://evil.invalid/api/health',
'//evil.invalid/api/health',
'/api/health?q=1',
'/api/health#x',
'/api/%68ealth',
'/api/%252e%252e/x',
'/api/a/../x',
'/api\\health',
'/api//health',
];
for (const pathTemplate of attacks)
await assert.rejects(() =>
collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
{ ...health, pathTemplate },
transport,
),
);
assert.equal(calls, 0);
assert.throws(() => selectReadonlyOperations([{ ...health }]));
});
test('collectOne validates RFC1918 origin before transport',()=>{
for(const origin of ['http://127.0.0.1','http://169.254.1.1','http://100.100.100.200','http://8.8.8.8','http://router.local','http://u:p@192.168.1.2','http://192.168.1.2/x','http://192.168.1.2?q=1','ftp://192.168.1.2']) assert.throws(()=>validateInstanceOrigin(origin));
assert.equal(validateInstanceOrigin('https://172.16.2.3:8443'),'https://172.16.2.3:8443');
test('collectOne validates RFC1918 origin before transport', () => {
for (const origin of [
'http://127.0.0.1',
'http://169.254.1.1',
'http://100.100.100.200',
'http://8.8.8.8',
'http://router.local',
'http://u:p@192.168.1.2',
'http://192.168.1.2/x',
'http://192.168.1.2?q=1',
'ftp://192.168.1.2',
])
assert.throws(() => validateInstanceOrigin(origin));
assert.equal(validateInstanceOrigin('https://172.16.2.3:8443'), 'https://172.16.2.3:8443');
});
test('passive selection explicitly denies connectivity and active scan',()=>{
const {selected,denied}=selectReadonlyOperations(upstream58e2204Operations);
assert.ok(denied.some((x:any)=>x.pathTemplate==='/api/connectivity'));
assert.ok(denied.some((x:any)=>x.pathTemplate==='/api/network/operators/scan'));
assert.ok(!selected.some((x:any)=>x.pathTemplate==='/api/connectivity'));
test('passive selection explicitly denies connectivity and active scan', () => {
const { selected, denied } = selectReadonlyOperations(upstream58e2204Operations);
assert.ok(denied.some((x: any) => x.pathTemplate === '/api/connectivity'));
assert.ok(denied.some((x: any) => x.pathTemplate === '/api/network/operators/scan'));
assert.ok(!selected.some((x: any) => x.pathTemplate === '/api/connectivity'));
});
test('config uses formal validation plus collector LAN/auth/origin constraints',()=>{
const good={instances:[{id:'a',url:'http://192.168.1.2',auth:{mode:'none'}},{id:'b',url:'https://10.0.0.2',auth:{mode:'none'}}]};
assert.deepEqual(loadInstances(good).map((x:any)=>x.alias),['instance-1','instance-2']);
const bad=['http://127.0.0.1','http://169.254.1.2','http://100.100.100.200','http://8.8.8.8','http://router.local','http://u:***@192.168.1.2','http://192.168.1.2/path','http://192.168.1.2?q=1','http://192.168.1.2#x','http://[::1]'];
for(const url of bad) assert.throws(()=>loadInstances({instances:[{id:'a',url},{id:'b',url:'http://10.0.0.2'}]}));
assert.throws(()=>loadInstances({instances:[{id:'a',url:'http://192.168.1.2',auth:{password:'x'}},{id:'b',url:'http://10.0.0.2'}]}));
assert.throws(()=>loadInstances({instances:[{id:'a',url:'http://192.168.1.2'}]}));
assert.throws(()=>loadInstances({instances:[{id:'a',url:'http://192.168.1.2'},{id:'b',url:'http://10.0.0.2'},{id:'c',url:'http://10.0.0.3'}]}));
test('config uses formal validation plus collector LAN/auth/origin constraints', () => {
const good = {
instances: [
{ id: 'a', url: 'http://192.168.1.2', auth: { mode: 'none' } },
{ id: 'b', url: 'https://10.0.0.2', auth: { mode: 'none' } },
],
};
assert.deepEqual(
loadInstances(good).map((x: any) => x.alias),
['instance-1', 'instance-2'],
);
const bad = [
'http://127.0.0.1',
'http://169.254.1.2',
'http://100.100.100.200',
'http://8.8.8.8',
'http://router.local',
'http://u:***@192.168.1.2',
'http://192.168.1.2/path',
'http://192.168.1.2?q=1',
'http://192.168.1.2#x',
'http://[::1]',
];
for (const url of bad)
assert.throws(() =>
loadInstances({
instances: [
{ id: 'a', url },
{ id: 'b', url: 'http://10.0.0.2' },
],
}),
);
assert.throws(() =>
loadInstances({
instances: [
{ id: 'a', url: 'http://192.168.1.2', auth: { password: 'x' } },
{ id: 'b', url: 'http://10.0.0.2' },
],
}),
);
assert.throws(() => loadInstances({ instances: [{ id: 'a', url: 'http://192.168.1.2' }] }));
assert.throws(() =>
loadInstances({
instances: [
{ id: 'a', url: 'http://192.168.1.2' },
{ id: 'b', url: 'http://10.0.0.2' },
{ id: 'c', url: 'http://10.0.0.3' },
],
}),
);
});
test('directional response JSONPaths redact every declared path and preserve shape/types',()=>{
const ops=upstream58e2204Operations.filter((op:any)=>op.sensitiveFields.some((f:any)=>f.direction==='response'));
assert.ok(ops.length>0);
for(const op of ops) for(const field of op.sensitiveFields.filter((f:any)=>f.direction==='response')){
const parts=parseResponseSensitivePath(field.path); assert.ok(parts.length>0,`${op.operationId} unsupported response path`);
let leaf:any={s:'private',n:42,b:true,z:null,a:['x',{q:9}]};
let payload:any=leaf;
for(let i=parts.length-1;i>=0;i--){const p=parts[i]; payload=p==='*'?[payload]:{[p]:payload};}
const out:any=redactOperationPayload(payload,op);
let hit=out; for(const p of parts) hit=p==='*'?hit[0]:hit[p];
assert.deepEqual(hit,{s:'[REDACTED]',n:0,b:false,z:null,a:['[REDACTED]',{q:0}]});
}
test('directional response JSONPaths redact every declared path and preserve shape/types', () => {
const ops = upstream58e2204Operations.filter((op: any) =>
op.sensitiveFields.some((f: any) => f.direction === 'response'),
);
assert.ok(ops.length > 0);
for (const op of ops)
for (const field of op.sensitiveFields.filter((f: any) => f.direction === 'response')) {
const parts = parseResponseSensitivePath(field.path);
assert.ok(parts.length > 0, `${op.operationId} unsupported response path`);
let leaf: any = { s: 'private', n: 42, b: true, z: null, a: ['x', { q: 9 }] };
let payload: any = leaf;
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
payload = p === '*' ? [payload] : { [p]: payload };
}
const out: any = redactOperationPayload(payload, op);
let hit = out;
for (const p of parts) hit = p === '*' ? hit[0] : hit[p];
assert.deepEqual(hit, {
s: '[REDACTED]',
n: 0,
b: false,
z: null,
a: ['[REDACTED]', { q: 0 }],
});
}
});
test('deep sanitizer handles reviewed key/value bypasses without schema destruction',()=>{
const input:any={credential:'x',api_key:'x',access_key:'x',device_key:'x',private_key:'x',pin:12,puk:34,username:'x',user:'x',account:'x',serial:'x',revision:'x',path:'/private/a',template:'user=x token=abc123 path=/private/a',config:{access_token:'tiny'},phone_number_is_manual:true,sms_center_is_manual:false,message:'API is healthy',nested:'{"username":"x","enabled":true}',host:'private.internal:8080',short_number:'12345678',split_id:'12-34-56-78-90-12-34',ipv6:'::1%lo0',latitude:1.234567,lon:2.345678,cell_id:99,lac:2,tac:3,pci:4,enb:5,gnb:6,uptime:777,traffic:888,timestamp:'2026-07-16T12:34:56Z',unknown:'AKIAIOSFODNN7EXAMPLE'};
const x:any=redact(input);
for(const k of ['credential','api_key','access_key','device_key','private_key','username','user','account','serial','revision','path','template','host','short_number','split_id','ipv6','timestamp','unknown']) assert.equal(x[k],'[REDACTED]');
for(const k of ['pin','puk','latitude','lon','cell_id','lac','tac','pci','enb','gnb','uptime','traffic']) assert.equal(x[k],0);
assert.equal(typeof x.nested,'string'); assert.deepEqual(JSON.parse(x.nested),{username:'[REDACTED]',enabled:true});
assert.deepEqual(x.config,{access_token:'[REDACTED]'}); assert.equal(x.phone_number_is_manual,true); assert.equal(x.sms_center_is_manual,false); assert.equal(x.message,'API is healthy');
test('deep sanitizer handles reviewed key/value bypasses without schema destruction', () => {
const input: any = {
credential: 'x',
api_key: 'x',
access_key: 'x',
device_key: 'x',
private_key: 'x',
pin: 12,
puk: 34,
username: 'x',
user: 'x',
account: 'x',
serial: 'x',
revision: 'x',
path: '/private/a',
template: 'user=x token=abc123 path=/private/a',
config: { access_token: 'tiny' },
phone_number_is_manual: true,
sms_center_is_manual: false,
message: 'API is healthy',
nested: '{"username":"x","enabled":true}',
host: 'private.internal:8080',
short_number: '12345678',
split_id: '12-34-56-78-90-12-34',
ipv6: '::1%lo0',
latitude: 1.234567,
lon: 2.345678,
cell_id: 99,
lac: 2,
tac: 3,
pci: 4,
enb: 5,
gnb: 6,
uptime: 777,
traffic: 888,
timestamp: '2026-07-16T12:34:56Z',
unknown: 'AKIAIOSFODNN7EXAMPLE',
};
const x: any = redact(input);
for (const k of [
'credential',
'api_key',
'access_key',
'device_key',
'private_key',
'username',
'user',
'account',
'serial',
'revision',
'path',
'template',
'host',
'short_number',
'split_id',
'ipv6',
'timestamp',
'unknown',
])
assert.equal(x[k], '[REDACTED]');
for (const k of [
'pin',
'puk',
'latitude',
'lon',
'cell_id',
'lac',
'tac',
'pci',
'enb',
'gnb',
'uptime',
'traffic',
])
assert.equal(x[k], 0);
assert.equal(typeof x.nested, 'string');
assert.deepEqual(JSON.parse(x.nested), { username: '[REDACTED]', enabled: true });
assert.deepEqual(x.config, { access_token: '[REDACTED]' });
assert.equal(x.phone_number_is_manual, true);
assert.equal(x.sms_center_is_manual, false);
assert.equal(x.message, 'API is healthy');
});
test('schema shapes and primitive types survive conservative redaction',()=>{
const x:any=redact({phone_numbers:['12345678','87654321'],ip_addresses:[{v4:'192.168.1.9',active:true}],urls:['private.example/x'],webhook:{url:'//private.example/h',enabled:true},config:{s:'secret',n:42,b:true,z:null,a:['x',{n:9}]}});
assert.ok(Array.isArray(x.phone_numbers));assert.equal(x.phone_numbers.length,2);
assert.ok(Array.isArray(x.ip_addresses));assert.equal(typeof x.ip_addresses[0],'object');
assert.ok(Array.isArray(x.urls));assert.equal(typeof x.webhook,'object');
assert.deepEqual(x.config,{s:'[REDACTED]',n:0,b:false,z:null,a:['[REDACTED]',{n:0}]});
test('schema shapes and primitive types survive conservative redaction', () => {
const x: any = redact({
phone_numbers: ['12345678', '87654321'],
ip_addresses: [{ v4: '192.168.1.9', active: true }],
urls: ['private.example/x'],
webhook: { url: '//private.example/h', enabled: true },
config: { s: 'secret', n: 42, b: true, z: null, a: ['x', { n: 9 }] },
});
assert.ok(Array.isArray(x.phone_numbers));
assert.equal(x.phone_numbers.length, 2);
assert.ok(Array.isArray(x.ip_addresses));
assert.equal(typeof x.ip_addresses[0], 'object');
assert.ok(Array.isArray(x.urls));
assert.equal(typeof x.webhook, 'object');
assert.deepEqual(x.config, {
s: '[REDACTED]',
n: 0,
b: false,
z: null,
a: ['[REDACTED]', { n: 0 }],
});
});
test('invalid declared JSON and non-JSON bodies are non-json and discarded',async()=>{
const invalid=await collectOne({origin:'http://192.168.1.2',alias:'instance-1'},health,async()=>new Response('{bad',{status:200,headers:{'content-type':'application/json'}}));
assert.equal(invalid.statusCategory,'non-json');assert.deepEqual(invalid.payload,{error:'invalid-json'});
const text=await collectOne({origin:'http://192.168.1.2',alias:'instance-1'},health,async()=>new Response('private body',{status:200,headers:{'content-type':'text/plain'}}));
assert.equal(text.statusCategory,'non-json');assert.deepEqual(text.payload,{text:'[REDACTED]'});
test('invalid declared JSON and non-JSON bodies are non-json and discarded', async () => {
const invalid = await collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
health,
async () =>
new Response('{bad', { status: 200, headers: { 'content-type': 'application/json' } }),
);
assert.equal(invalid.statusCategory, 'non-json');
assert.deepEqual(invalid.payload, { error: 'invalid-json' });
const text = await collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
health,
async () =>
new Response('private body', { status: 200, headers: { 'content-type': 'text/plain' } }),
);
assert.equal(text.statusCategory, 'non-json');
assert.deepEqual(text.payload, { text: '[REDACTED]' });
});
test('collector errors never echo rejected origin values',async()=>{
const secret='http://user:password@evil.invalid/private?token=x';
try{await collectOne({origin:secret,alias:'instance-1'},health,fakeResponse);assert.fail('expected rejection');}catch(error:any){assert.ok(!String(error.message).includes(secret));assert.ok(!String(error.message).includes('password'));}
test('collector errors never echo rejected origin values', async () => {
const secret = 'http://user:password@evil.invalid/private?token=x';
try {
await collectOne({ origin: secret, alias: 'instance-1' }, health, fakeResponse);
assert.fail('expected rejection');
} catch (error: any) {
assert.ok(!String(error.message).includes(secret));
assert.ok(!String(error.message).includes('password'));
}
});