feat(contracts): @insignia/iios-contracts — scope, enums, platform ports, CloudEvents, errors
Verbatim IiosPlatformPorts from the bottom-up atlas; orgId/appId required scope (KG-02); PolicyDeniedError for fail-closed. Vitest as workspace test runner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Versioned command envelope (reused from support-service's command pattern).
|
||||
* Every externally-visible mutation carries a schema name + numeric version +
|
||||
* an idempotency key. Kept dependency-free here (a type); the service defines a
|
||||
* class-validator DTO that conforms to it.
|
||||
*/
|
||||
export interface CommandEnvelope<P = Record<string, unknown>> {
|
||||
schemaName: string;
|
||||
schemaVersion: number;
|
||||
idempotencyKey: string;
|
||||
payload: P;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Kernel enums, lifted verbatim from the Bottom-Up Evolution Atlas §10 DDL.
|
||||
* Exposed as const arrays + derived union types so they are usable both as
|
||||
* compile-time types and for runtime validation.
|
||||
*/
|
||||
|
||||
export const ACTOR_KINDS = ['HUMAN', 'AI', 'SERVICE', 'BOT', 'UNKNOWN'] as const;
|
||||
export type ActorKind = (typeof ACTOR_KINDS)[number];
|
||||
|
||||
export const HANDLE_KINDS = [
|
||||
'PORTAL_USER',
|
||||
'EMAIL',
|
||||
'PHONE',
|
||||
'WHATSAPP',
|
||||
'SLACK',
|
||||
'MATTERMOST',
|
||||
'TELEGRAM',
|
||||
'EXTERNAL_VISITOR',
|
||||
] as const;
|
||||
export type HandleKind = (typeof HANDLE_KINDS)[number];
|
||||
|
||||
export const INTERACTION_KINDS = [
|
||||
'MESSAGE',
|
||||
'EMAIL',
|
||||
'SYSTEM_NOTICE',
|
||||
'INBOX_WORK',
|
||||
'SUPPORT_CASE',
|
||||
'MEETING_REQUEST',
|
||||
'DIGEST',
|
||||
'SUMMARY',
|
||||
'NOTIFICATION',
|
||||
] as const;
|
||||
export type InteractionKind = (typeof INTERACTION_KINDS)[number];
|
||||
|
||||
export const MESSAGE_PART_KINDS = [
|
||||
'TEXT',
|
||||
'HTML',
|
||||
'MARKDOWN',
|
||||
'MEDIA_REF',
|
||||
'FILE_REF',
|
||||
'VOICE_REF',
|
||||
'LOCATION',
|
||||
'STRUCTURED_JSON',
|
||||
] as const;
|
||||
export type MessagePartKind = (typeof MESSAGE_PART_KINDS)[number];
|
||||
|
||||
export const DELIVERY_STATES = [
|
||||
'RECEIVED',
|
||||
'NORMALIZED',
|
||||
'POLICY_HELD',
|
||||
'STORED',
|
||||
'DELIVERED',
|
||||
'READ',
|
||||
'FAILED',
|
||||
'DELETED_LOCAL',
|
||||
'REDACTED',
|
||||
] as const;
|
||||
export type DeliveryState = (typeof DELIVERY_STATES)[number];
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Typed kernel errors carrying a stable machine code. */
|
||||
export class IiosError extends Error {
|
||||
constructor(
|
||||
public readonly code: string,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'IiosError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a platform port denies, times out, or is unavailable for a
|
||||
* privileged action. The kernel fails CLOSED: nothing is written. (Bottom-Up
|
||||
* hard gate; Critics KG-03.)
|
||||
*/
|
||||
export class PolicyDeniedError extends IiosError {
|
||||
constructor(reason = 'policy denied') {
|
||||
super('POLICY_DENIED', reason);
|
||||
this.name = 'PolicyDeniedError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* CloudEvents envelope + the IIOS event catalog (Data Model §05, Ref Arch §09).
|
||||
*
|
||||
* Every durable state change writes the business row AND an outbox event in the
|
||||
* same transaction. The `insignia` block carries correlation/causation/scope and
|
||||
* the idempotency key; payloads reference content rather than embedding raw PII.
|
||||
*/
|
||||
|
||||
export type DataClass = 'internal' | 'restricted' | 'confidential' | 'regulated';
|
||||
|
||||
export interface CloudEvent<T = unknown> {
|
||||
specversion: '1.0';
|
||||
id: string;
|
||||
type: string;
|
||||
source: string;
|
||||
subject?: string;
|
||||
time: string;
|
||||
datacontenttype?: 'application/json';
|
||||
traceparent?: string;
|
||||
insignia: {
|
||||
correlationId?: string;
|
||||
causationId?: string;
|
||||
scopeSnapshotId?: string;
|
||||
policyDecisionId?: string;
|
||||
consentReceiptRef?: string;
|
||||
contextBundleId?: string;
|
||||
idempotencyKey: string;
|
||||
dataClass?: DataClass;
|
||||
};
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** Canonical event names (the subset relevant up to P4; others added per phase). */
|
||||
export const IIOS_EVENTS = {
|
||||
rawReceived: 'com.insignia.iios.raw.received.v1',
|
||||
interactionNormalized: 'com.insignia.iios.interaction.normalized.v1',
|
||||
messagePersisted: 'com.insignia.iios.message.persisted.v1',
|
||||
inboxItemCreated: 'com.insignia.iios.inbox.item.created.v1',
|
||||
ticketCreated: 'com.insignia.iios.support.ticket.created.v1',
|
||||
notificationQueued: 'com.insignia.iios.notification.queued.v1',
|
||||
deliveryAttempted: 'com.insignia.iios.delivery.attempted.v1',
|
||||
} as const;
|
||||
|
||||
export type IiosEventType = (typeof IIOS_EVENTS)[keyof typeof IIOS_EVENTS];
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
IIOS_EVENTS,
|
||||
PolicyDeniedError,
|
||||
ACTOR_KINDS,
|
||||
type ScopeVector,
|
||||
type IiosPlatformPorts,
|
||||
} from './index';
|
||||
|
||||
describe('iios-contracts public entrypoint', () => {
|
||||
it('exports the event catalog', () => {
|
||||
expect(IIOS_EVENTS.interactionNormalized).toBe('com.insignia.iios.interaction.normalized.v1');
|
||||
});
|
||||
|
||||
it('PolicyDeniedError carries the stable POLICY_DENIED code', () => {
|
||||
const err = new PolicyDeniedError('opa denied');
|
||||
expect(err.code).toBe('POLICY_DENIED');
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('actor kinds include the four real principals + UNKNOWN', () => {
|
||||
expect(ACTOR_KINDS).toContain('HUMAN');
|
||||
expect(ACTOR_KINDS).toContain('AI');
|
||||
expect(ACTOR_KINDS).toContain('SERVICE');
|
||||
});
|
||||
|
||||
it('types are usable (compile-time check)', () => {
|
||||
const scope: ScopeVector = { orgId: 'org_1', appId: 'portal-demo' };
|
||||
const ports: Pick<IiosPlatformPorts, 'opa'> = {
|
||||
opa: { decide: async () => ({ decisionRef: 'd', allow: true, obligations: [], ttlSeconds: 60 }) },
|
||||
};
|
||||
expect(scope.appId).toBe('portal-demo');
|
||||
expect(typeof ports.opa.decide).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './scope';
|
||||
export * from './enums';
|
||||
export * from './ports';
|
||||
export * from './events';
|
||||
export * from './commands';
|
||||
export * from './errors';
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Platform ports contract — copied verbatim from the Bottom-Up Evolution Atlas
|
||||
* "Platform ports contract" TS block (the single source of truth).
|
||||
*
|
||||
* These describe the platform-governance fabric (Session/OPA/CMP/MDM/CRRE/SAS/
|
||||
* Capability) that OTHER teams own and that does not exist yet. IIOS consumes
|
||||
* them through this interface; in dev/test it binds to deterministic fakes
|
||||
* (@insignia/iios-testkit). In prod it binds to the real services. The kernel
|
||||
* never builds these — it only depends on the contract.
|
||||
*/
|
||||
|
||||
export interface PlatformPrincipal {
|
||||
principalRef: string;
|
||||
accountSubject?: string;
|
||||
canonicalEntityId?: string;
|
||||
orgId: string;
|
||||
appId: string;
|
||||
tenantId?: string;
|
||||
workspaceId?: string;
|
||||
assurance: 'anonymous' | 'authenticated' | 'mfa' | 'service';
|
||||
delegationChain?: string[];
|
||||
}
|
||||
|
||||
export interface PolicyDecision {
|
||||
decisionRef: string;
|
||||
allow: boolean;
|
||||
obligations: Array<{
|
||||
kind: 'MASK' | 'REVIEW' | 'AUDIT' | 'DENY_OUTBOUND' | 'REDACT';
|
||||
target?: string;
|
||||
reason: string;
|
||||
}>;
|
||||
ttlSeconds: number;
|
||||
}
|
||||
|
||||
export interface ContextDecisionBundle {
|
||||
bundleRef: string;
|
||||
scopeHash: string;
|
||||
resourceSet: Array<{ type: string; id: string; relation?: string }>;
|
||||
constraints: Array<{ kind: string; value: unknown; sourceRule: string }>;
|
||||
routeHints?: Array<{ routeBindingId: string; mode: 'SIMULATION_ONLY' | 'MANUAL' | 'AUTOMATIC' }>;
|
||||
}
|
||||
|
||||
export interface SourceHandleResolution {
|
||||
canonicalEntityId?: string;
|
||||
confidence: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface IiosPlatformPorts {
|
||||
session: { verify(token: string): Promise<PlatformPrincipal> };
|
||||
opa: { decide(input: unknown): Promise<PolicyDecision> };
|
||||
cmp: { checkPurpose(input: unknown): Promise<{ receiptRef?: string; status: 'ALLOW' | 'DENY' | 'NOT_REQUIRED' }> };
|
||||
mdm: { resolveSourceHandle(input: unknown): Promise<SourceHandleResolution> };
|
||||
crre: { resolve(input: unknown): Promise<ContextDecisionBundle> };
|
||||
sas: { tokenizeParts(parts: unknown[]): Promise<unknown[]> };
|
||||
capability: { invoke(input: unknown): Promise<{ providerRef: string; outcome: string }> };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* The six scope vectors (Reference Arch §08, Data Model §02).
|
||||
*
|
||||
* `orgId` + `appId` are REQUIRED. Nullable scope is treated as global access —
|
||||
* a security hole the critics flag as a kill gate (Critics KG-02). The remaining
|
||||
* dimensions are optional and carry no kernel logic yet; later phases use them.
|
||||
*/
|
||||
export interface ScopeVector {
|
||||
orgId: string;
|
||||
appId: string;
|
||||
buId?: string;
|
||||
tenantId?: string;
|
||||
workspaceId?: string;
|
||||
projectId?: string;
|
||||
userScopeId?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user