feat(service): PlatformGates seam + fail-closed decideOrThrow (P1.3)

LocalDevPorts = permissive in-service default (no testkit at runtime);
decideOrThrow turns OPA deny/throw/timeout into PolicyDeniedError. Tests drive
deny/timeout via testkit fakes. 12 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 21:18:23 +05:30
parent 9cfd1ab1ab
commit 8cd5d9b248
5 changed files with 112 additions and 1 deletions
+2 -1
View File
@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { PlatformModule } from './platform/platform.module';
import { HealthController } from './health.controller';
@Module({
imports: [PrismaModule],
imports: [PrismaModule, PlatformModule],
controllers: [HealthController],
})
export class AppModule {}
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { makeFakePorts } from '@insignia/iios-testkit';
import { PolicyDeniedError } from '@insignia/iios-contracts';
import { decideOrThrow } from './fail-closed';
describe('decideOrThrow (fail-closed gate)', () => {
it('returns the decision when OPA allows', async () => {
const ports = makeFakePorts();
const decision = await decideOrThrow(ports, {});
expect(decision.allow).toBe(true);
});
it('throws PolicyDeniedError when OPA denies', async () => {
const ports = makeFakePorts();
ports.opa.deny('blocked');
await expect(decideOrThrow(ports, {})).rejects.toBeInstanceOf(PolicyDeniedError);
});
it('throws PolicyDeniedError when OPA times out (never implicit-allow)', async () => {
const ports = makeFakePorts();
ports.opa.timeout();
await expect(decideOrThrow(ports, {})).rejects.toBeInstanceOf(PolicyDeniedError);
});
});
@@ -0,0 +1,21 @@
import { IiosPlatformPorts, PolicyDecision, PolicyDeniedError } from '@insignia/iios-contracts';
/**
* Fail-closed authorization (Bottom-Up hard gate; Critics KG-03).
*
* Calls the OPA port. The kernel proceeds ONLY on an explicit allow. A denial,
* a thrown error, or a timeout all become a PolicyDeniedError — never an
* implicit allow. Callers must let this propagate so nothing is written.
*/
export async function decideOrThrow(ports: IiosPlatformPorts, input: unknown): Promise<PolicyDecision> {
let decision: PolicyDecision;
try {
decision = await ports.opa.decide(input);
} catch (err) {
throw new PolicyDeniedError(`policy port unavailable: ${(err as Error).message}`);
}
if (!decision.allow) {
throw new PolicyDeniedError(decision.obligations[0]?.reason ?? 'policy denied');
}
return decision;
}
@@ -0,0 +1,55 @@
import type {
IiosPlatformPorts,
PlatformPrincipal,
PolicyDecision,
ContextDecisionBundle,
SourceHandleResolution,
} from '@insignia/iios-contracts';
/** DI token for the platform ports (Session/OPA/CMP/MDM/CRRE/SAS/Capability). */
export const PLATFORM_PORTS = Symbol('PLATFORM_PORTS');
/**
* P1 default binding: a permissive, in-service implementation so `nest start`
* runs without the real platform fabric (which other teams own) and without a
* runtime dependency on the testkit. Tests inject @insignia/iios-testkit fakes
* to exercise deny / timeout / ambiguous paths. Real adapters replace this from
* P5 onward.
*/
export class LocalDevPorts implements IiosPlatformPorts {
session = {
async verify(_token: string): Promise<PlatformPrincipal> {
return { principalRef: 'local-dev', orgId: 'org_demo', appId: 'portal-demo', assurance: 'service' };
},
};
opa = {
async decide(_input: unknown): Promise<PolicyDecision> {
return { decisionRef: 'local-allow', allow: true, obligations: [], ttlSeconds: 60 };
},
};
cmp = {
async checkPurpose(_input: unknown): Promise<{ receiptRef?: string; status: 'ALLOW' | 'DENY' | 'NOT_REQUIRED' }> {
return { status: 'NOT_REQUIRED' };
},
};
mdm = {
async resolveSourceHandle(_input: unknown): Promise<SourceHandleResolution> {
return { confidence: 0, status: 'UNRESOLVED' };
},
};
crre = {
async resolve(_input: unknown): Promise<ContextDecisionBundle> {
return { bundleRef: 'local', scopeHash: 'local', resourceSet: [], constraints: [] };
},
};
sas = {
async tokenizeParts(parts: unknown[]): Promise<unknown[]> {
return parts;
},
};
capability = {
async invoke(_input: unknown): Promise<{ providerRef: string; outcome: string }> {
return { providerRef: 'local', outcome: 'SENT' };
},
};
}
@@ -0,0 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { PLATFORM_PORTS, LocalDevPorts } from './platform-ports';
/** Binds the platform ports (P1: the permissive in-service default). */
@Global()
@Module({
providers: [{ provide: PLATFORM_PORTS, useClass: LocalDevPorts }],
exports: [PLATFORM_PORTS],
})
export class PlatformModule {}