diff --git a/.gitignore b/.gitignore index b352bba..7f96da8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ packages/* !packages/eve-browser-lowering/ !packages/eve-browser-lowering/** packages/eve-browser-lowering/node_modules/ +!packages/eve-contracts/ +!packages/eve-contracts/** +packages/eve-contracts/node_modules/ +packages/eve-contracts/*.tgz !packages/org.gamecult.eve.surface/ !packages/org.gamecult.eve.surface/** !packages/org.gamecult.eve.unity-uitoolkit/ diff --git a/packages/eve-contracts/README.md b/packages/eve-contracts/README.md new file mode 100644 index 0000000..31cc69d --- /dev/null +++ b/packages/eve-contracts/README.md @@ -0,0 +1,21 @@ +# @gamecult/eve-contracts + +Canonical TypeScript types and runtime validators for Eve provider boundaries. +The generated sources come from the JSON Schemas in `../../schemas`; run +`npm run generate` after changing an authoritative schema. `npm test` rejects +schema drift. + +```ts +import { + parseEveProviderAdvertisement, + parseEveSurfaceDocument, + parseEveCommandInvocation, +} from "@gamecult/eve-contracts"; + +const provider = parseEveProviderAdvertisement(untrustedAdvertisement); +const surface = parseEveSurfaceDocument(untrustedSurface); +const command = parseEveCommandInvocation(untrustedCommand); +``` + +Providers own their state, surfaces, command acceptance, and receipts. This +package owns only the portable contract and validation boundary. diff --git a/packages/eve-contracts/dist/generated/command-descriptor.d.ts b/packages/eve-contracts/dist/generated/command-descriptor.d.ts new file mode 100644 index 0000000..9e8341f --- /dev/null +++ b/packages/eve-contracts/dist/generated/command-descriptor.d.ts @@ -0,0 +1,11 @@ +export interface EveCommandDescriptor { + schema: "gamecult.eve.command.v1"; + command: string; + label?: string; + surfaceId?: string; + transport?: string; + authority?: string; + result?: string; + payloadSchema?: string; + [k: string]: any; +} diff --git a/packages/eve-contracts/dist/generated/command-descriptor.js b/packages/eve-contracts/dist/generated/command-descriptor.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/eve-contracts/dist/generated/command-descriptor.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/eve-contracts/dist/generated/command-invocation.d.ts b/packages/eve-contracts/dist/generated/command-invocation.d.ts new file mode 100644 index 0000000..b0818b2 --- /dev/null +++ b/packages/eve-contracts/dist/generated/command-invocation.d.ts @@ -0,0 +1,21 @@ +export interface EveCommandInvocation { + schema: "gamecult.eve.command_invocation.v1"; + providerId: string; + surfaceId: string; + operation: { + operationId: string; + schemaId?: string; + idempotencyKey?: string; + routeHint?: { + [k: string]: any; + }; + [k: string]: any; + }; + payload: { + [k: string]: any; + }; + issuedAt: string; + clientId: string; + commandBoundary: string; + receiptSchema: string; +} diff --git a/packages/eve-contracts/dist/generated/command-invocation.js b/packages/eve-contracts/dist/generated/command-invocation.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/eve-contracts/dist/generated/command-invocation.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/eve-contracts/dist/generated/command-receipt.d.ts b/packages/eve-contracts/dist/generated/command-receipt.d.ts new file mode 100644 index 0000000..c0c695b --- /dev/null +++ b/packages/eve-contracts/dist/generated/command-receipt.d.ts @@ -0,0 +1,18 @@ +export interface EveCommandReceipt { + schema: "gamecult.eve.command_receipt.v1"; + receiptId: string; + commandId: string; + command: string; + state: ("accepted" | "denied" | "pending" | "reconciled"); + ownerRepo: string; + authority: string; + providerId: string; + surfaceId: string; + message?: string; + diagnostics?: { + [k: string]: any; + }[]; + issuedAtUtc?: string; + sourceVersion: number; + [k: string]: any; +} diff --git a/packages/eve-contracts/dist/generated/command-receipt.js b/packages/eve-contracts/dist/generated/command-receipt.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/eve-contracts/dist/generated/command-receipt.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/eve-contracts/dist/generated/provider-advertisement.d.ts b/packages/eve-contracts/dist/generated/provider-advertisement.d.ts new file mode 100644 index 0000000..66bf6e5 --- /dev/null +++ b/packages/eve-contracts/dist/generated/provider-advertisement.d.ts @@ -0,0 +1,72 @@ +export interface EveProviderAdvertisement { + schema: "gamecult.eve.provider_advertisement.v1"; + providerId: string; + serviceId: string; + verseId: string; + rootVerse?: string; + canonicalService?: string; + locatedService?: string; + cultMeshAddress?: string; + title: string; + kind: string; + updatedAt?: string; + updatedAtUtc?: string; + freshness: { + state: string; + lastSeenAt?: string; + lastSeenAtUtc?: string; + maxAgeMs?: number; + [k: string]: any; + }; + schemas: (string | { + [k: string]: any; + })[]; + witnesses: { + kind: string; + ref?: string; + path?: string; + summary?: string; + [k: string]: any; + }[]; + surfaces: { + surfaceId: string; + schema: string; + key?: string; + recordRef?: string; + url?: string; + transport: string; + status?: string; + surfaceKind?: string; + interactionModel?: string; + worldInteraction?: { + projectionKind: string; + stateSchemas: string[]; + commandBoundary: string; + commandRecordRef?: string; + receiptSchema: string; + receiptRecordRef?: string; + assetManifestRecordRef?: string; + loweringTargets: string[]; + ownership: string; + [k: string]: any; + }; + requiresPlugins?: { + [k: string]: any; + }[]; + [k: string]: any; + }[]; + commands: { + command: string; + transport: string; + summary?: string; + [k: string]: any; + }[]; + conformanceScenarios?: { + scenarioId: string; + schema: string; + path: string; + summary?: string; + [k: string]: any; + }[]; + [k: string]: any; +} diff --git a/packages/eve-contracts/dist/generated/provider-advertisement.js b/packages/eve-contracts/dist/generated/provider-advertisement.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/eve-contracts/dist/generated/provider-advertisement.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/eve-contracts/dist/generated/schemas.d.ts b/packages/eve-contracts/dist/generated/schemas.d.ts new file mode 100644 index 0000000..4a35c30 --- /dev/null +++ b/packages/eve-contracts/dist/generated/schemas.d.ts @@ -0,0 +1,629 @@ +export declare const eveContractSchemas: { + readonly providerAdvertisement: { + readonly $schema: "https://json-schema.org/draft/2020-12/schema"; + readonly $id: "gamecult.eve.provider_advertisement.v1"; + readonly title: "Eve Provider Advertisement"; + readonly type: "object"; + readonly required: readonly ["schema", "providerId", "serviceId", "verseId", "title", "kind", "freshness", "schemas", "witnesses", "surfaces", "commands"]; + readonly properties: { + readonly schema: { + readonly const: "gamecult.eve.provider_advertisement.v1"; + }; + readonly providerId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly serviceId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly verseId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly rootVerse: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly canonicalService: { + readonly type: "string"; + }; + readonly locatedService: { + readonly type: "string"; + }; + readonly cultMeshAddress: { + readonly type: "string"; + }; + readonly title: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly kind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly updatedAt: { + readonly type: "string"; + }; + readonly updatedAtUtc: { + readonly type: "string"; + }; + readonly freshness: { + readonly type: "object"; + readonly required: readonly ["state"]; + readonly properties: { + readonly state: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly lastSeenAt: { + readonly type: "string"; + }; + readonly lastSeenAtUtc: { + readonly type: "string"; + }; + readonly maxAgeMs: { + readonly type: "number"; + }; + }; + readonly additionalProperties: true; + }; + readonly schemas: { + readonly type: "array"; + readonly items: { + readonly type: readonly ["string", "object"]; + }; + }; + readonly witnesses: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["kind"]; + readonly properties: { + readonly kind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly ref: { + readonly type: "string"; + }; + readonly path: { + readonly type: "string"; + }; + readonly summary: { + readonly type: "string"; + }; + }; + readonly additionalProperties: true; + }; + }; + readonly surfaces: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["surfaceId", "schema", "transport"]; + readonly properties: { + readonly surfaceId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly schema: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly key: { + readonly type: "string"; + }; + readonly recordRef: { + readonly type: "string"; + }; + readonly url: { + readonly type: "string"; + }; + readonly transport: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly status: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly surfaceKind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly interactionModel: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly worldInteraction: { + readonly type: "object"; + readonly required: readonly ["projectionKind", "stateSchemas", "commandBoundary", "receiptSchema", "loweringTargets", "ownership"]; + readonly properties: { + readonly projectionKind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly stateSchemas: { + readonly type: "array"; + readonly items: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly commandBoundary: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly commandRecordRef: { + readonly type: "string"; + }; + readonly receiptSchema: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly receiptRecordRef: { + readonly type: "string"; + }; + readonly assetManifestRecordRef: { + readonly type: "string"; + }; + readonly loweringTargets: { + readonly type: "array"; + readonly items: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly ownership: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly additionalProperties: true; + }; + readonly requiresPlugins: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["pluginId", "requiredCapabilities"]; + readonly properties: { + readonly pluginId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly versionRange: { + readonly type: "string"; + }; + readonly availability: { + readonly type: "string"; + readonly enum: readonly ["required", "optional", "optional-nested"]; + }; + readonly requiredCapabilities: { + readonly type: "array"; + readonly items: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly optionalCapabilities: { + readonly type: "array"; + readonly items: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + }; + readonly allOf: readonly [{ + readonly if: { + readonly properties: { + readonly availability: { + readonly const: "required"; + }; + }; + }; + readonly then: { + readonly properties: { + readonly requiredCapabilities: { + readonly minItems: 1; + }; + }; + }; + }, { + readonly if: { + readonly required: readonly ["availability"]; + readonly properties: { + readonly availability: { + readonly const: "optional-nested"; + }; + }; + }; + readonly then: { + readonly required: readonly ["optionalCapabilities"]; + readonly properties: { + readonly requiredCapabilities: { + readonly maxItems: 0; + }; + readonly optionalCapabilities: { + readonly minItems: 1; + }; + }; + }; + }]; + readonly additionalProperties: true; + }; + }; + }; + readonly additionalProperties: true; + }; + }; + readonly commands: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["command", "transport"]; + readonly properties: { + readonly command: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly transport: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly summary: { + readonly type: "string"; + }; + }; + readonly additionalProperties: true; + }; + }; + readonly conformanceScenarios: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["scenarioId", "schema", "path"]; + readonly properties: { + readonly scenarioId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly schema: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly path: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly summary: { + readonly type: "string"; + }; + }; + readonly additionalProperties: true; + }; + }; + }; + readonly additionalProperties: true; + }; + readonly surface: { + readonly $schema: "https://json-schema.org/draft/2020-12/schema"; + readonly $id: "gamecult.eve.surface.v1"; + readonly title: "Eve Surface Document"; + readonly type: "object"; + readonly required: readonly ["type", "schema", "providerId", "providerKind", "title", "version", "surface", "commands"]; + readonly properties: { + readonly type: { + readonly const: "surface-state"; + }; + readonly schema: { + readonly const: "gamecult.eve.surface.v1"; + }; + readonly providerId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly providerKind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly title: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly version: { + readonly type: "integer"; + readonly minimum: 0; + }; + readonly updatedAt: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly updatedAtUtc: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly surface: { + readonly type: "object"; + readonly required: readonly ["root", "styles"]; + readonly properties: { + readonly id: { + readonly type: "string"; + }; + readonly title: { + readonly type: "string"; + }; + readonly root: { + readonly $ref: "#/$defs/component"; + }; + readonly styles: { + readonly type: readonly ["object", "array"]; + }; + }; + readonly additionalProperties: true; + }; + readonly commands: { + readonly type: "array"; + readonly items: { + readonly $ref: "gamecult.eve.command.v1"; + }; + }; + readonly nodes: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + }; + }; + readonly selectedNodeId: { + readonly type: "string"; + }; + }; + readonly $defs: { + readonly component: { + readonly type: "object"; + readonly required: readonly ["id", "kind", "props", "children"]; + readonly properties: { + readonly id: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly kind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly props: { + readonly type: "object"; + }; + readonly children: { + readonly type: "array"; + readonly items: { + readonly $ref: "#/$defs/component"; + }; + }; + readonly stateBindings: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["targetProp", "pointerId", "sourceId", "schemaId", "routeKind"]; + readonly properties: { + readonly targetProp: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly pointerId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly sourceId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly schemaId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly routeKind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly routeDescription: { + readonly type: "string"; + }; + }; + readonly additionalProperties: true; + }; + }; + readonly embeddedDocuments: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["slotId", "documentId", "schemaId", "presentationKind"]; + readonly properties: { + readonly slotId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly documentId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly schemaId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly presentationKind: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly routeHint: { + readonly type: readonly ["string", "object"]; + }; + }; + readonly additionalProperties: true; + }; + }; + }; + readonly additionalProperties: true; + }; + }; + readonly anyOf: readonly [{ + readonly required: readonly ["updatedAt"]; + }, { + readonly required: readonly ["updatedAtUtc"]; + }]; + readonly additionalProperties: true; + }; + readonly commandDescriptor: { + readonly $schema: "https://json-schema.org/draft/2020-12/schema"; + readonly $id: "gamecult.eve.command.v1"; + readonly title: "Eve Command Descriptor"; + readonly type: "object"; + readonly required: readonly ["schema", "command"]; + readonly properties: { + readonly schema: { + readonly const: "gamecult.eve.command.v1"; + }; + readonly command: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly label: { + readonly type: "string"; + }; + readonly surfaceId: { + readonly type: "string"; + }; + readonly transport: { + readonly type: "string"; + }; + readonly authority: { + readonly type: "string"; + }; + readonly result: { + readonly type: "string"; + }; + readonly payloadSchema: { + readonly type: "string"; + }; + }; + readonly additionalProperties: true; + }; + readonly commandInvocation: { + readonly $schema: "https://json-schema.org/draft/2020-12/schema"; + readonly $id: "gamecult.eve.command_invocation.v1"; + readonly title: "Eve Command Invocation"; + readonly type: "object"; + readonly required: readonly ["schema", "providerId", "surfaceId", "operation", "payload", "issuedAt", "clientId", "commandBoundary", "receiptSchema"]; + readonly properties: { + readonly schema: { + readonly const: "gamecult.eve.command_invocation.v1"; + }; + readonly providerId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly surfaceId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly operation: { + readonly type: "object"; + readonly required: readonly ["operationId"]; + readonly properties: { + readonly operationId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly schemaId: { + readonly type: "string"; + }; + readonly idempotencyKey: { + readonly type: "string"; + }; + readonly routeHint: { + readonly type: "object"; + }; + }; + readonly additionalProperties: true; + }; + readonly payload: { + readonly type: "object"; + }; + readonly issuedAt: { + readonly type: "string"; + }; + readonly clientId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly commandBoundary: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly receiptSchema: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly additionalProperties: false; + }; + readonly commandReceipt: { + readonly $schema: "https://json-schema.org/draft/2020-12/schema"; + readonly $id: "gamecult.eve.command_receipt.v1"; + readonly title: "Eve Command Receipt"; + readonly type: "object"; + readonly required: readonly ["schema", "receiptId", "commandId", "command", "state", "ownerRepo", "authority", "providerId", "surfaceId", "sourceVersion"]; + readonly properties: { + readonly schema: { + readonly const: "gamecult.eve.command_receipt.v1"; + }; + readonly receiptId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly commandId: { + readonly type: "string"; + }; + readonly command: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly state: { + readonly type: "string"; + readonly enum: readonly ["accepted", "denied", "pending", "reconciled"]; + }; + readonly ownerRepo: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly authority: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly providerId: { + readonly type: "string"; + }; + readonly surfaceId: { + readonly type: "string"; + }; + readonly message: { + readonly type: "string"; + }; + readonly diagnostics: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + }; + }; + readonly issuedAtUtc: { + readonly type: "string"; + }; + readonly sourceVersion: { + readonly type: "integer"; + readonly minimum: 0; + }; + }; + readonly additionalProperties: true; + }; +}; +export type EveContractSchemaName = keyof typeof eveContractSchemas; diff --git a/packages/eve-contracts/dist/generated/schemas.js b/packages/eve-contracts/dist/generated/schemas.js new file mode 100644 index 0000000..58e7b43 --- /dev/null +++ b/packages/eve-contracts/dist/generated/schemas.js @@ -0,0 +1,752 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +export const eveContractSchemas = { + "providerAdvertisement": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.provider_advertisement.v1", + "title": "Eve Provider Advertisement", + "type": "object", + "required": [ + "schema", + "providerId", + "serviceId", + "verseId", + "title", + "kind", + "freshness", + "schemas", + "witnesses", + "surfaces", + "commands" + ], + "properties": { + "schema": { + "const": "gamecult.eve.provider_advertisement.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "serviceId": { + "type": "string", + "minLength": 1 + }, + "verseId": { + "type": "string", + "minLength": 1 + }, + "rootVerse": { + "type": "string", + "minLength": 1 + }, + "canonicalService": { + "type": "string" + }, + "locatedService": { + "type": "string" + }, + "cultMeshAddress": { + "type": "string" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "updatedAt": { + "type": "string" + }, + "updatedAtUtc": { + "type": "string" + }, + "freshness": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "minLength": 1 + }, + "lastSeenAt": { + "type": "string" + }, + "lastSeenAtUtc": { + "type": "string" + }, + "maxAgeMs": { + "type": "number" + } + }, + "additionalProperties": true + }, + "schemas": { + "type": "array", + "items": { + "type": [ + "string", + "object" + ] + } + }, + "witnesses": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "minLength": 1 + }, + "ref": { + "type": "string" + }, + "path": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "surfaces": { + "type": "array", + "items": { + "type": "object", + "required": [ + "surfaceId", + "schema", + "transport" + ], + "properties": { + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "schema": { + "type": "string", + "minLength": 1 + }, + "key": { + "type": "string" + }, + "recordRef": { + "type": "string" + }, + "url": { + "type": "string" + }, + "transport": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "minLength": 1 + }, + "surfaceKind": { + "type": "string", + "minLength": 1 + }, + "interactionModel": { + "type": "string", + "minLength": 1 + }, + "worldInteraction": { + "type": "object", + "required": [ + "projectionKind", + "stateSchemas", + "commandBoundary", + "receiptSchema", + "loweringTargets", + "ownership" + ], + "properties": { + "projectionKind": { + "type": "string", + "minLength": 1 + }, + "stateSchemas": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "commandBoundary": { + "type": "string", + "minLength": 1 + }, + "commandRecordRef": { + "type": "string" + }, + "receiptSchema": { + "type": "string", + "minLength": 1 + }, + "receiptRecordRef": { + "type": "string" + }, + "assetManifestRecordRef": { + "type": "string" + }, + "loweringTargets": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "ownership": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + }, + "requiresPlugins": { + "type": "array", + "items": { + "type": "object", + "required": [ + "pluginId", + "requiredCapabilities" + ], + "properties": { + "pluginId": { + "type": "string", + "minLength": 1 + }, + "versionRange": { + "type": "string" + }, + "availability": { + "type": "string", + "enum": [ + "required", + "optional", + "optional-nested" + ] + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "optionalCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "const": "required" + } + } + }, + "then": { + "properties": { + "requiredCapabilities": { + "minItems": 1 + } + } + } + }, + { + "if": { + "required": [ + "availability" + ], + "properties": { + "availability": { + "const": "optional-nested" + } + } + }, + "then": { + "required": [ + "optionalCapabilities" + ], + "properties": { + "requiredCapabilities": { + "maxItems": 0 + }, + "optionalCapabilities": { + "minItems": 1 + } + } + } + } + ], + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + }, + "commands": { + "type": "array", + "items": { + "type": "object", + "required": [ + "command", + "transport" + ], + "properties": { + "command": { + "type": "string", + "minLength": 1 + }, + "transport": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "conformanceScenarios": { + "type": "array", + "items": { + "type": "object", + "required": [ + "scenarioId", + "schema", + "path" + ], + "properties": { + "scenarioId": { + "type": "string", + "minLength": 1 + }, + "schema": { + "type": "string", + "minLength": 1 + }, + "path": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "surface": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.surface.v1", + "title": "Eve Surface Document", + "type": "object", + "required": [ + "type", + "schema", + "providerId", + "providerKind", + "title", + "version", + "surface", + "commands" + ], + "properties": { + "type": { + "const": "surface-state" + }, + "schema": { + "const": "gamecult.eve.surface.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "providerKind": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "string", + "minLength": 1 + }, + "updatedAtUtc": { + "type": "string", + "minLength": 1 + }, + "surface": { + "type": "object", + "required": [ + "root", + "styles" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "root": { + "$ref": "#/$defs/component" + }, + "styles": { + "type": [ + "object", + "array" + ] + } + }, + "additionalProperties": true + }, + "commands": { + "type": "array", + "items": { + "$ref": "gamecult.eve.command.v1" + } + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + } + }, + "selectedNodeId": { + "type": "string" + } + }, + "$defs": { + "component": { + "type": "object", + "required": [ + "id", + "kind", + "props", + "children" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "props": { + "type": "object" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/component" + } + }, + "stateBindings": { + "type": "array", + "items": { + "type": "object", + "required": [ + "targetProp", + "pointerId", + "sourceId", + "schemaId", + "routeKind" + ], + "properties": { + "targetProp": { + "type": "string", + "minLength": 1 + }, + "pointerId": { + "type": "string", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "minLength": 1 + }, + "schemaId": { + "type": "string", + "minLength": 1 + }, + "routeKind": { + "type": "string", + "minLength": 1 + }, + "routeDescription": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "embeddedDocuments": { + "type": "array", + "items": { + "type": "object", + "required": [ + "slotId", + "documentId", + "schemaId", + "presentationKind" + ], + "properties": { + "slotId": { + "type": "string", + "minLength": 1 + }, + "documentId": { + "type": "string", + "minLength": 1 + }, + "schemaId": { + "type": "string", + "minLength": 1 + }, + "presentationKind": { + "type": "string", + "minLength": 1 + }, + "routeHint": { + "type": [ + "string", + "object" + ] + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + }, + "anyOf": [ + { + "required": [ + "updatedAt" + ] + }, + { + "required": [ + "updatedAtUtc" + ] + } + ], + "additionalProperties": true + }, + "commandDescriptor": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.command.v1", + "title": "Eve Command Descriptor", + "type": "object", + "required": [ + "schema", + "command" + ], + "properties": { + "schema": { + "const": "gamecult.eve.command.v1" + }, + "command": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string" + }, + "surfaceId": { + "type": "string" + }, + "transport": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "result": { + "type": "string" + }, + "payloadSchema": { + "type": "string" + } + }, + "additionalProperties": true + }, + "commandInvocation": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.command_invocation.v1", + "title": "Eve Command Invocation", + "type": "object", + "required": [ + "schema", + "providerId", + "surfaceId", + "operation", + "payload", + "issuedAt", + "clientId", + "commandBoundary", + "receiptSchema" + ], + "properties": { + "schema": { + "const": "gamecult.eve.command_invocation.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "operation": { + "type": "object", + "required": [ + "operationId" + ], + "properties": { + "operationId": { + "type": "string", + "minLength": 1 + }, + "schemaId": { + "type": "string" + }, + "idempotencyKey": { + "type": "string" + }, + "routeHint": { + "type": "object" + } + }, + "additionalProperties": true + }, + "payload": { + "type": "object" + }, + "issuedAt": { + "type": "string" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "commandBoundary": { + "type": "string", + "minLength": 1 + }, + "receiptSchema": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "commandReceipt": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.command_receipt.v1", + "title": "Eve Command Receipt", + "type": "object", + "required": [ + "schema", + "receiptId", + "commandId", + "command", + "state", + "ownerRepo", + "authority", + "providerId", + "surfaceId", + "sourceVersion" + ], + "properties": { + "schema": { + "const": "gamecult.eve.command_receipt.v1" + }, + "receiptId": { + "type": "string", + "minLength": 1 + }, + "commandId": { + "type": "string" + }, + "command": { + "type": "string", + "minLength": 1 + }, + "state": { + "type": "string", + "enum": [ + "accepted", + "denied", + "pending", + "reconciled" + ] + }, + "ownerRepo": { + "type": "string", + "minLength": 1 + }, + "authority": { + "type": "string", + "minLength": 1 + }, + "providerId": { + "type": "string" + }, + "surfaceId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "diagnostics": { + "type": "array", + "items": { + "type": "object" + } + }, + "issuedAtUtc": { + "type": "string" + }, + "sourceVersion": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": true + } +}; diff --git a/packages/eve-contracts/dist/generated/surface.d.ts b/packages/eve-contracts/dist/generated/surface.d.ts new file mode 100644 index 0000000..f458ab6 --- /dev/null +++ b/packages/eve-contracts/dist/generated/surface.d.ts @@ -0,0 +1,66 @@ +export type EveSurfaceDocument = ({ + [k: string]: any; +} & { + type: "surface-state"; + schema: "gamecult.eve.surface.v1"; + providerId: string; + providerKind: string; + title: string; + version: number; + updatedAt?: string; + updatedAtUtc?: string; + surface: { + id?: string; + title?: string; + root: Component; + styles: ({ + [k: string]: any; + } | any[]); + [k: string]: any; + }; + commands: EveCommandDescriptor[]; + nodes?: { + [k: string]: any; + }[]; + selectedNodeId?: string; + [k: string]: any; +}); +export interface Component { + id: string; + kind: string; + props: { + [k: string]: any; + }; + children: Component[]; + stateBindings?: { + targetProp: string; + pointerId: string; + sourceId: string; + schemaId: string; + routeKind: string; + routeDescription?: string; + [k: string]: any; + }[]; + embeddedDocuments?: { + slotId: string; + documentId: string; + schemaId: string; + presentationKind: string; + routeHint?: (string | { + [k: string]: any; + }); + [k: string]: any; + }[]; + [k: string]: any; +} +export interface EveCommandDescriptor { + schema: "gamecult.eve.command.v1"; + command: string; + label?: string; + surfaceId?: string; + transport?: string; + authority?: string; + result?: string; + payloadSchema?: string; + [k: string]: any; +} diff --git a/packages/eve-contracts/dist/generated/surface.js b/packages/eve-contracts/dist/generated/surface.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/eve-contracts/dist/generated/surface.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/eve-contracts/dist/index.d.ts b/packages/eve-contracts/dist/index.d.ts new file mode 100644 index 0000000..f880110 --- /dev/null +++ b/packages/eve-contracts/dist/index.d.ts @@ -0,0 +1,42 @@ +import { type ErrorObject } from "ajv/dist/2020.js"; +import type { EveCommandDescriptor } from "./generated/command-descriptor.js"; +import type { EveCommandInvocation } from "./generated/command-invocation.js"; +import type { EveCommandReceipt } from "./generated/command-receipt.js"; +import type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; +import type { EveSurfaceDocument } from "./generated/surface.js"; +import { type EveContractSchemaName } from "./generated/schemas.js"; +export type { EveCommandDescriptor } from "./generated/command-descriptor.js"; +export type { EveCommandInvocation } from "./generated/command-invocation.js"; +export type { EveCommandReceipt } from "./generated/command-receipt.js"; +export type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; +export type { EveSurfaceDocument } from "./generated/surface.js"; +export { eveContractSchemas, type EveContractSchemaName } from "./generated/schemas.js"; +export declare const EVE_PROVIDER_ADVERTISEMENT_SCHEMA: "gamecult.eve.provider_advertisement.v1"; +export declare const EVE_SURFACE_SCHEMA: "gamecult.eve.surface.v1"; +export declare const EVE_COMMAND_DESCRIPTOR_SCHEMA: "gamecult.eve.command.v1"; +export declare const EVE_COMMAND_INVOCATION_SCHEMA: "gamecult.eve.command_invocation.v1"; +export declare const EVE_COMMAND_RECEIPT_SCHEMA: "gamecult.eve.command_receipt.v1"; +type ContractTypes = { + providerAdvertisement: EveProviderAdvertisement; + surface: EveSurfaceDocument; + commandDescriptor: EveCommandDescriptor; + commandInvocation: EveCommandInvocation; + commandReceipt: EveCommandReceipt; +}; +export declare class EveContractValidationError extends TypeError { + readonly contract: EveContractSchemaName; + readonly errors: readonly ErrorObject[]; + constructor(contract: EveContractSchemaName, errors: readonly ErrorObject[]); +} +export declare function isEveContract(contract: K, value: unknown): value is ContractTypes[K]; +export declare function parseEveContract(contract: K, value: unknown): ContractTypes[K]; +export declare const isEveProviderAdvertisement: (value: unknown) => value is EveProviderAdvertisement; +export declare const isEveSurfaceDocument: (value: unknown) => value is EveSurfaceDocument; +export declare const isEveCommandDescriptor: (value: unknown) => value is EveCommandDescriptor; +export declare const isEveCommandInvocation: (value: unknown) => value is EveCommandInvocation; +export declare const isEveCommandReceipt: (value: unknown) => value is EveCommandReceipt; +export declare const parseEveProviderAdvertisement: (value: unknown) => EveProviderAdvertisement; +export declare const parseEveSurfaceDocument: (value: unknown) => EveSurfaceDocument; +export declare const parseEveCommandDescriptor: (value: unknown) => EveCommandDescriptor; +export declare const parseEveCommandInvocation: (value: unknown) => EveCommandInvocation; +export declare const parseEveCommandReceipt: (value: unknown) => EveCommandReceipt; diff --git a/packages/eve-contracts/dist/index.js b/packages/eve-contracts/dist/index.js new file mode 100644 index 0000000..c409e63 --- /dev/null +++ b/packages/eve-contracts/dist/index.js @@ -0,0 +1,53 @@ +import Ajv2020 from "ajv/dist/2020.js"; +import { eveContractSchemas } from "./generated/schemas.js"; +export { eveContractSchemas } from "./generated/schemas.js"; +export const EVE_PROVIDER_ADVERTISEMENT_SCHEMA = "gamecult.eve.provider_advertisement.v1"; +export const EVE_SURFACE_SCHEMA = "gamecult.eve.surface.v1"; +export const EVE_COMMAND_DESCRIPTOR_SCHEMA = "gamecult.eve.command.v1"; +export const EVE_COMMAND_INVOCATION_SCHEMA = "gamecult.eve.command_invocation.v1"; +export const EVE_COMMAND_RECEIPT_SCHEMA = "gamecult.eve.command_receipt.v1"; +const ajv = new Ajv2020({ + allErrors: true, + allowUnionTypes: true, + strict: true, + strictRequired: false, + strictTypes: false, +}); +for (const schema of Object.values(eveContractSchemas)) + ajv.addSchema(schema); +const validators = Object.fromEntries(Object.entries(eveContractSchemas).map(([name, schema]) => [name, requiredValidator(schema.$id)])); +export class EveContractValidationError extends TypeError { + contract; + errors; + constructor(contract, errors) { + super(`Invalid Eve ${contract}: ${ajv.errorsText(errors, { separator: "; " })}`); + this.name = "EveContractValidationError"; + this.contract = contract; + this.errors = errors; + } +} +export function isEveContract(contract, value) { + return Boolean(validators[contract](value)); +} +export function parseEveContract(contract, value) { + const validator = validators[contract]; + if (!validator(value)) + throw new EveContractValidationError(contract, validator.errors ?? []); + return value; +} +export const isEveProviderAdvertisement = (value) => isEveContract("providerAdvertisement", value); +export const isEveSurfaceDocument = (value) => isEveContract("surface", value); +export const isEveCommandDescriptor = (value) => isEveContract("commandDescriptor", value); +export const isEveCommandInvocation = (value) => isEveContract("commandInvocation", value); +export const isEveCommandReceipt = (value) => isEveContract("commandReceipt", value); +export const parseEveProviderAdvertisement = (value) => parseEveContract("providerAdvertisement", value); +export const parseEveSurfaceDocument = (value) => parseEveContract("surface", value); +export const parseEveCommandDescriptor = (value) => parseEveContract("commandDescriptor", value); +export const parseEveCommandInvocation = (value) => parseEveContract("commandInvocation", value); +export const parseEveCommandReceipt = (value) => parseEveContract("commandReceipt", value); +function requiredValidator(schemaId) { + const validator = ajv.getSchema(schemaId); + if (!validator) + throw new Error(`Eve contract schema ${schemaId} was not registered.`); + return validator; +} diff --git a/packages/eve-contracts/package-lock.json b/packages/eve-contracts/package-lock.json new file mode 100644 index 0000000..7d85b24 --- /dev/null +++ b/packages/eve-contracts/package-lock.json @@ -0,0 +1,302 @@ +{ + "name": "@gamecult/eve-contracts", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gamecult/eve-contracts", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "json-schema-to-typescript": "^15.0.4", + "typescript": "^5.9.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/packages/eve-contracts/package.json b/packages/eve-contracts/package.json new file mode 100644 index 0000000..b57c0ca --- /dev/null +++ b/packages/eve-contracts/package.json @@ -0,0 +1,37 @@ +{ + "name": "@gamecult/eve-contracts", + "version": "0.1.0", + "description": "Canonical TypeScript types and runtime validators for Eve provider contracts.", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./schemas": { + "types": "./dist/generated/schemas.d.ts", + "import": "./dist/generated/schemas.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "generate": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-contracts.mjs", + "generate:check": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-contracts.mjs --check", + "build": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-contracts.mjs --check && C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json", + "test": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-contracts.mjs --check && C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json && C:\\Progra~1\\nodejs\\node.exe --test test/*.test.mjs" + }, + "dependencies": { + "ajv": "^8.17.1" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "json-schema-to-typescript": "^15.0.4", + "typescript": "^5.9.0" + }, + "license": "MIT" +} diff --git a/packages/eve-contracts/scripts/generate-contracts.mjs b/packages/eve-contracts/scripts/generate-contracts.mjs new file mode 100644 index 0000000..a3138e0 --- /dev/null +++ b/packages/eve-contracts/scripts/generate-contracts.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { compile } from "json-schema-to-typescript"; + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repoRoot = resolve(packageRoot, "..", ".."); +const generatedRoot = resolve(packageRoot, "src", "generated"); +const contracts = [ + ["providerAdvertisement", "EveProviderAdvertisement", "provider-advertisement.ts", "gamecult.eve.provider_advertisement.v1.schema.json"], + ["surface", "EveSurfaceDocument", "surface.ts", "gamecult.eve.surface.v1.schema.json"], + ["commandDescriptor", "EveCommandDescriptor", "command-descriptor.ts", "gamecult.eve.command.v1.schema.json"], + ["commandInvocation", "EveCommandInvocation", "command-invocation.ts", "gamecult.eve.command_invocation.v1.schema.json"], + ["commandReceipt", "EveCommandReceipt", "command-receipt.ts", "gamecult.eve.command_receipt.v1.schema.json"], +]; + +const loaded = []; +for (const [key, typeName, generatedFileName, schemaFileName] of contracts) { + const path = resolve(repoRoot, "schemas", schemaFileName); + const source = await readFile(path, "utf8"); + const schema = JSON.parse(source); + loaded.push({ key, typeName, generatedFileName, schemaFileName, schema }); +} + +const schemasById = new Map(loaded.map(({ schema }) => [schema.$id, schema])); +for (const contract of loaded) { + const typeSchema = inlineKnownRefs(contract.schema); + contract.typeSource = (await compile(typeSchema, contract.typeName, { + bannerComment: "", + format: false, + unknownAny: false, + })).trim(); +} + +const schemasOutput = `${generatedHeader()}\nexport const eveContractSchemas = ${JSON.stringify( + Object.fromEntries(loaded.map(({ key, schema }) => [key, schema])), + null, + 2, +)} as const;\n\nexport type EveContractSchemaName = keyof typeof eveContractSchemas;\n`; + +await mkdir(generatedRoot, { recursive: true }); +for (const { generatedFileName, schemaFileName, typeSource } of loaded) { + await checkOrWrite( + resolve(generatedRoot, generatedFileName), + `${generatedHeader()}\n// Source: schemas/${schemaFileName}\n${typeSource}\n`, + ); +} +await checkOrWrite(resolve(generatedRoot, "schemas.ts"), schemasOutput); + +function generatedHeader() { + return "// Generated by scripts/generate-contracts.mjs. Do not edit by hand."; +} + +async function checkOrWrite(path, expected) { + if (process.argv.includes("--check")) { + const actual = await readFile(path, "utf8").catch(() => ""); + if (actual !== expected) { + throw new Error(`${path} is stale. Run npm run generate in ${packageRoot}.`); + } + return; + } + await writeFile(path, expected, "utf8"); +} + +function inlineKnownRefs(value) { + if (Array.isArray(value)) return value.map(inlineKnownRefs); + if (!value || typeof value !== "object") return value; + if (typeof value.$ref === "string" && schemasById.has(value.$ref)) { + return inlineKnownRefs(schemasById.get(value.$ref)); + } + return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, inlineKnownRefs(child)])); +} diff --git a/packages/eve-contracts/src/generated/command-descriptor.ts b/packages/eve-contracts/src/generated/command-descriptor.ts new file mode 100644 index 0000000..8ba22bd --- /dev/null +++ b/packages/eve-contracts/src/generated/command-descriptor.ts @@ -0,0 +1,13 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +// Source: schemas/gamecult.eve.command.v1.schema.json +export interface EveCommandDescriptor { +schema: "gamecult.eve.command.v1" +command: string +label?: string +surfaceId?: string +transport?: string +authority?: string +result?: string +payloadSchema?: string +[k: string]: any +} diff --git a/packages/eve-contracts/src/generated/command-invocation.ts b/packages/eve-contracts/src/generated/command-invocation.ts new file mode 100644 index 0000000..5c15093 --- /dev/null +++ b/packages/eve-contracts/src/generated/command-invocation.ts @@ -0,0 +1,23 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +// Source: schemas/gamecult.eve.command_invocation.v1.schema.json +export interface EveCommandInvocation { +schema: "gamecult.eve.command_invocation.v1" +providerId: string +surfaceId: string +operation: { +operationId: string +schemaId?: string +idempotencyKey?: string +routeHint?: { +[k: string]: any +} +[k: string]: any +} +payload: { +[k: string]: any +} +issuedAt: string +clientId: string +commandBoundary: string +receiptSchema: string +} diff --git a/packages/eve-contracts/src/generated/command-receipt.ts b/packages/eve-contracts/src/generated/command-receipt.ts new file mode 100644 index 0000000..61ec04f --- /dev/null +++ b/packages/eve-contracts/src/generated/command-receipt.ts @@ -0,0 +1,20 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +// Source: schemas/gamecult.eve.command_receipt.v1.schema.json +export interface EveCommandReceipt { +schema: "gamecult.eve.command_receipt.v1" +receiptId: string +commandId: string +command: string +state: ("accepted" | "denied" | "pending" | "reconciled") +ownerRepo: string +authority: string +providerId: string +surfaceId: string +message?: string +diagnostics?: { +[k: string]: any +}[] +issuedAtUtc?: string +sourceVersion: number +[k: string]: any +} diff --git a/packages/eve-contracts/src/generated/provider-advertisement.ts b/packages/eve-contracts/src/generated/provider-advertisement.ts new file mode 100644 index 0000000..19d895b --- /dev/null +++ b/packages/eve-contracts/src/generated/provider-advertisement.ts @@ -0,0 +1,74 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +// Source: schemas/gamecult.eve.provider_advertisement.v1.schema.json +export interface EveProviderAdvertisement { +schema: "gamecult.eve.provider_advertisement.v1" +providerId: string +serviceId: string +verseId: string +rootVerse?: string +canonicalService?: string +locatedService?: string +cultMeshAddress?: string +title: string +kind: string +updatedAt?: string +updatedAtUtc?: string +freshness: { +state: string +lastSeenAt?: string +lastSeenAtUtc?: string +maxAgeMs?: number +[k: string]: any +} +schemas: (string | { +[k: string]: any +})[] +witnesses: { +kind: string +ref?: string +path?: string +summary?: string +[k: string]: any +}[] +surfaces: { +surfaceId: string +schema: string +key?: string +recordRef?: string +url?: string +transport: string +status?: string +surfaceKind?: string +interactionModel?: string +worldInteraction?: { +projectionKind: string +stateSchemas: string[] +commandBoundary: string +commandRecordRef?: string +receiptSchema: string +receiptRecordRef?: string +assetManifestRecordRef?: string +loweringTargets: string[] +ownership: string +[k: string]: any +} +requiresPlugins?: { +[k: string]: any +}[] +[k: string]: any +}[] +commands: { +command: string +transport: string +summary?: string +[k: string]: any +}[] +conformanceScenarios?: { +scenarioId: string +schema: string +path: string +summary?: string +[k: string]: any +}[] +[k: string]: any +} diff --git a/packages/eve-contracts/src/generated/schemas.ts b/packages/eve-contracts/src/generated/schemas.ts new file mode 100644 index 0000000..1ed9031 --- /dev/null +++ b/packages/eve-contracts/src/generated/schemas.ts @@ -0,0 +1,754 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +export const eveContractSchemas = { + "providerAdvertisement": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.provider_advertisement.v1", + "title": "Eve Provider Advertisement", + "type": "object", + "required": [ + "schema", + "providerId", + "serviceId", + "verseId", + "title", + "kind", + "freshness", + "schemas", + "witnesses", + "surfaces", + "commands" + ], + "properties": { + "schema": { + "const": "gamecult.eve.provider_advertisement.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "serviceId": { + "type": "string", + "minLength": 1 + }, + "verseId": { + "type": "string", + "minLength": 1 + }, + "rootVerse": { + "type": "string", + "minLength": 1 + }, + "canonicalService": { + "type": "string" + }, + "locatedService": { + "type": "string" + }, + "cultMeshAddress": { + "type": "string" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "updatedAt": { + "type": "string" + }, + "updatedAtUtc": { + "type": "string" + }, + "freshness": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "minLength": 1 + }, + "lastSeenAt": { + "type": "string" + }, + "lastSeenAtUtc": { + "type": "string" + }, + "maxAgeMs": { + "type": "number" + } + }, + "additionalProperties": true + }, + "schemas": { + "type": "array", + "items": { + "type": [ + "string", + "object" + ] + } + }, + "witnesses": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "minLength": 1 + }, + "ref": { + "type": "string" + }, + "path": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "surfaces": { + "type": "array", + "items": { + "type": "object", + "required": [ + "surfaceId", + "schema", + "transport" + ], + "properties": { + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "schema": { + "type": "string", + "minLength": 1 + }, + "key": { + "type": "string" + }, + "recordRef": { + "type": "string" + }, + "url": { + "type": "string" + }, + "transport": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "minLength": 1 + }, + "surfaceKind": { + "type": "string", + "minLength": 1 + }, + "interactionModel": { + "type": "string", + "minLength": 1 + }, + "worldInteraction": { + "type": "object", + "required": [ + "projectionKind", + "stateSchemas", + "commandBoundary", + "receiptSchema", + "loweringTargets", + "ownership" + ], + "properties": { + "projectionKind": { + "type": "string", + "minLength": 1 + }, + "stateSchemas": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "commandBoundary": { + "type": "string", + "minLength": 1 + }, + "commandRecordRef": { + "type": "string" + }, + "receiptSchema": { + "type": "string", + "minLength": 1 + }, + "receiptRecordRef": { + "type": "string" + }, + "assetManifestRecordRef": { + "type": "string" + }, + "loweringTargets": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "ownership": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + }, + "requiresPlugins": { + "type": "array", + "items": { + "type": "object", + "required": [ + "pluginId", + "requiredCapabilities" + ], + "properties": { + "pluginId": { + "type": "string", + "minLength": 1 + }, + "versionRange": { + "type": "string" + }, + "availability": { + "type": "string", + "enum": [ + "required", + "optional", + "optional-nested" + ] + }, + "requiredCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "optionalCapabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "const": "required" + } + } + }, + "then": { + "properties": { + "requiredCapabilities": { + "minItems": 1 + } + } + } + }, + { + "if": { + "required": [ + "availability" + ], + "properties": { + "availability": { + "const": "optional-nested" + } + } + }, + "then": { + "required": [ + "optionalCapabilities" + ], + "properties": { + "requiredCapabilities": { + "maxItems": 0 + }, + "optionalCapabilities": { + "minItems": 1 + } + } + } + } + ], + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + }, + "commands": { + "type": "array", + "items": { + "type": "object", + "required": [ + "command", + "transport" + ], + "properties": { + "command": { + "type": "string", + "minLength": 1 + }, + "transport": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "conformanceScenarios": { + "type": "array", + "items": { + "type": "object", + "required": [ + "scenarioId", + "schema", + "path" + ], + "properties": { + "scenarioId": { + "type": "string", + "minLength": 1 + }, + "schema": { + "type": "string", + "minLength": 1 + }, + "path": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "surface": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.surface.v1", + "title": "Eve Surface Document", + "type": "object", + "required": [ + "type", + "schema", + "providerId", + "providerKind", + "title", + "version", + "surface", + "commands" + ], + "properties": { + "type": { + "const": "surface-state" + }, + "schema": { + "const": "gamecult.eve.surface.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "providerKind": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "integer", + "minimum": 0 + }, + "updatedAt": { + "type": "string", + "minLength": 1 + }, + "updatedAtUtc": { + "type": "string", + "minLength": 1 + }, + "surface": { + "type": "object", + "required": [ + "root", + "styles" + ], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "root": { + "$ref": "#/$defs/component" + }, + "styles": { + "type": [ + "object", + "array" + ] + } + }, + "additionalProperties": true + }, + "commands": { + "type": "array", + "items": { + "$ref": "gamecult.eve.command.v1" + } + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + } + }, + "selectedNodeId": { + "type": "string" + } + }, + "$defs": { + "component": { + "type": "object", + "required": [ + "id", + "kind", + "props", + "children" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "props": { + "type": "object" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/component" + } + }, + "stateBindings": { + "type": "array", + "items": { + "type": "object", + "required": [ + "targetProp", + "pointerId", + "sourceId", + "schemaId", + "routeKind" + ], + "properties": { + "targetProp": { + "type": "string", + "minLength": 1 + }, + "pointerId": { + "type": "string", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "minLength": 1 + }, + "schemaId": { + "type": "string", + "minLength": 1 + }, + "routeKind": { + "type": "string", + "minLength": 1 + }, + "routeDescription": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "embeddedDocuments": { + "type": "array", + "items": { + "type": "object", + "required": [ + "slotId", + "documentId", + "schemaId", + "presentationKind" + ], + "properties": { + "slotId": { + "type": "string", + "minLength": 1 + }, + "documentId": { + "type": "string", + "minLength": 1 + }, + "schemaId": { + "type": "string", + "minLength": 1 + }, + "presentationKind": { + "type": "string", + "minLength": 1 + }, + "routeHint": { + "type": [ + "string", + "object" + ] + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + }, + "anyOf": [ + { + "required": [ + "updatedAt" + ] + }, + { + "required": [ + "updatedAtUtc" + ] + } + ], + "additionalProperties": true + }, + "commandDescriptor": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.command.v1", + "title": "Eve Command Descriptor", + "type": "object", + "required": [ + "schema", + "command" + ], + "properties": { + "schema": { + "const": "gamecult.eve.command.v1" + }, + "command": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string" + }, + "surfaceId": { + "type": "string" + }, + "transport": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "result": { + "type": "string" + }, + "payloadSchema": { + "type": "string" + } + }, + "additionalProperties": true + }, + "commandInvocation": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.command_invocation.v1", + "title": "Eve Command Invocation", + "type": "object", + "required": [ + "schema", + "providerId", + "surfaceId", + "operation", + "payload", + "issuedAt", + "clientId", + "commandBoundary", + "receiptSchema" + ], + "properties": { + "schema": { + "const": "gamecult.eve.command_invocation.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "operation": { + "type": "object", + "required": [ + "operationId" + ], + "properties": { + "operationId": { + "type": "string", + "minLength": 1 + }, + "schemaId": { + "type": "string" + }, + "idempotencyKey": { + "type": "string" + }, + "routeHint": { + "type": "object" + } + }, + "additionalProperties": true + }, + "payload": { + "type": "object" + }, + "issuedAt": { + "type": "string" + }, + "clientId": { + "type": "string", + "minLength": 1 + }, + "commandBoundary": { + "type": "string", + "minLength": 1 + }, + "receiptSchema": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "commandReceipt": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.command_receipt.v1", + "title": "Eve Command Receipt", + "type": "object", + "required": [ + "schema", + "receiptId", + "commandId", + "command", + "state", + "ownerRepo", + "authority", + "providerId", + "surfaceId", + "sourceVersion" + ], + "properties": { + "schema": { + "const": "gamecult.eve.command_receipt.v1" + }, + "receiptId": { + "type": "string", + "minLength": 1 + }, + "commandId": { + "type": "string" + }, + "command": { + "type": "string", + "minLength": 1 + }, + "state": { + "type": "string", + "enum": [ + "accepted", + "denied", + "pending", + "reconciled" + ] + }, + "ownerRepo": { + "type": "string", + "minLength": 1 + }, + "authority": { + "type": "string", + "minLength": 1 + }, + "providerId": { + "type": "string" + }, + "surfaceId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "diagnostics": { + "type": "array", + "items": { + "type": "object" + } + }, + "issuedAtUtc": { + "type": "string" + }, + "sourceVersion": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": true + } +} as const; + +export type EveContractSchemaName = keyof typeof eveContractSchemas; diff --git a/packages/eve-contracts/src/generated/surface.ts b/packages/eve-contracts/src/generated/surface.ts new file mode 100644 index 0000000..8565b9b --- /dev/null +++ b/packages/eve-contracts/src/generated/surface.ts @@ -0,0 +1,69 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +// Source: schemas/gamecult.eve.surface.v1.schema.json +export type EveSurfaceDocument = ({ +[k: string]: any +} & { +type: "surface-state" +schema: "gamecult.eve.surface.v1" +providerId: string +providerKind: string +title: string +version: number +updatedAt?: string +updatedAtUtc?: string +surface: { +id?: string +title?: string +root: Component +styles: ({ +[k: string]: any +} | any[]) +[k: string]: any +} +commands: EveCommandDescriptor[] +nodes?: { +[k: string]: any +}[] +selectedNodeId?: string +[k: string]: any +}) + +export interface Component { +id: string +kind: string +props: { +[k: string]: any +} +children: Component[] +stateBindings?: { +targetProp: string +pointerId: string +sourceId: string +schemaId: string +routeKind: string +routeDescription?: string +[k: string]: any +}[] +embeddedDocuments?: { +slotId: string +documentId: string +schemaId: string +presentationKind: string +routeHint?: (string | { +[k: string]: any +}) +[k: string]: any +}[] +[k: string]: any +} +export interface EveCommandDescriptor { +schema: "gamecult.eve.command.v1" +command: string +label?: string +surfaceId?: string +transport?: string +authority?: string +result?: string +payloadSchema?: string +[k: string]: any +} diff --git a/packages/eve-contracts/src/index.ts b/packages/eve-contracts/src/index.ts new file mode 100644 index 0000000..b3b103d --- /dev/null +++ b/packages/eve-contracts/src/index.ts @@ -0,0 +1,97 @@ +import Ajv2020, { type ErrorObject, type ValidateFunction } from "ajv/dist/2020.js"; +import type { EveCommandDescriptor } from "./generated/command-descriptor.js"; +import type { EveCommandInvocation } from "./generated/command-invocation.js"; +import type { EveCommandReceipt } from "./generated/command-receipt.js"; +import type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; +import type { EveSurfaceDocument } from "./generated/surface.js"; +import { eveContractSchemas, type EveContractSchemaName } from "./generated/schemas.js"; + +export type { EveCommandDescriptor } from "./generated/command-descriptor.js"; +export type { EveCommandInvocation } from "./generated/command-invocation.js"; +export type { EveCommandReceipt } from "./generated/command-receipt.js"; +export type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; +export type { EveSurfaceDocument } from "./generated/surface.js"; +export { eveContractSchemas, type EveContractSchemaName } from "./generated/schemas.js"; + +export const EVE_PROVIDER_ADVERTISEMENT_SCHEMA = "gamecult.eve.provider_advertisement.v1" as const; +export const EVE_SURFACE_SCHEMA = "gamecult.eve.surface.v1" as const; +export const EVE_COMMAND_DESCRIPTOR_SCHEMA = "gamecult.eve.command.v1" as const; +export const EVE_COMMAND_INVOCATION_SCHEMA = "gamecult.eve.command_invocation.v1" as const; +export const EVE_COMMAND_RECEIPT_SCHEMA = "gamecult.eve.command_receipt.v1" as const; + +type ContractTypes = { + providerAdvertisement: EveProviderAdvertisement; + surface: EveSurfaceDocument; + commandDescriptor: EveCommandDescriptor; + commandInvocation: EveCommandInvocation; + commandReceipt: EveCommandReceipt; +}; + +const ajv = new Ajv2020({ + allErrors: true, + allowUnionTypes: true, + strict: true, + strictRequired: false, + strictTypes: false, +}); +for (const schema of Object.values(eveContractSchemas)) ajv.addSchema(schema); + +const validators = Object.fromEntries( + Object.entries(eveContractSchemas).map(([name, schema]) => [name, requiredValidator(schema.$id)]), +) as Record; + +export class EveContractValidationError extends TypeError { + public readonly contract: EveContractSchemaName; + public readonly errors: readonly ErrorObject[]; + + public constructor(contract: EveContractSchemaName, errors: readonly ErrorObject[]) { + super(`Invalid Eve ${contract}: ${ajv.errorsText(errors as ErrorObject[], { separator: "; " })}`); + this.name = "EveContractValidationError"; + this.contract = contract; + this.errors = errors; + } +} + +export function isEveContract( + contract: K, + value: unknown, +): value is ContractTypes[K] { + return Boolean(validators[contract](value)); +} + +export function parseEveContract( + contract: K, + value: unknown, +): ContractTypes[K] { + const validator = validators[contract]; + if (!validator(value)) throw new EveContractValidationError(contract, validator.errors ?? []); + return value as ContractTypes[K]; +} + +export const isEveProviderAdvertisement = (value: unknown): value is EveProviderAdvertisement => + isEveContract("providerAdvertisement", value); +export const isEveSurfaceDocument = (value: unknown): value is EveSurfaceDocument => + isEveContract("surface", value); +export const isEveCommandDescriptor = (value: unknown): value is EveCommandDescriptor => + isEveContract("commandDescriptor", value); +export const isEveCommandInvocation = (value: unknown): value is EveCommandInvocation => + isEveContract("commandInvocation", value); +export const isEveCommandReceipt = (value: unknown): value is EveCommandReceipt => + isEveContract("commandReceipt", value); + +export const parseEveProviderAdvertisement = (value: unknown): EveProviderAdvertisement => + parseEveContract("providerAdvertisement", value); +export const parseEveSurfaceDocument = (value: unknown): EveSurfaceDocument => + parseEveContract("surface", value); +export const parseEveCommandDescriptor = (value: unknown): EveCommandDescriptor => + parseEveContract("commandDescriptor", value); +export const parseEveCommandInvocation = (value: unknown): EveCommandInvocation => + parseEveContract("commandInvocation", value); +export const parseEveCommandReceipt = (value: unknown): EveCommandReceipt => + parseEveContract("commandReceipt", value); + +function requiredValidator(schemaId: string): ValidateFunction { + const validator = ajv.getSchema(schemaId); + if (!validator) throw new Error(`Eve contract schema ${schemaId} was not registered.`); + return validator; +} diff --git a/packages/eve-contracts/test/contracts.test.mjs b/packages/eve-contracts/test/contracts.test.mjs new file mode 100644 index 0000000..2f31bfe --- /dev/null +++ b/packages/eve-contracts/test/contracts.test.mjs @@ -0,0 +1,101 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import test from "node:test"; +import { + EveContractValidationError, + isEveCommandDescriptor, + parseEveCommandInvocation, + parseEveCommandReceipt, + parseEveProviderAdvertisement, + parseEveSurfaceDocument, +} from "../dist/index.js"; + +const command = { schema: "gamecult.eve.command.v1", command: "swarm.set_heat", transport: "cultmesh" }; +const surface = { + type: "surface-state", + schema: "gamecult.eve.surface.v1", + providerId: "voidbot.swarm", + providerKind: "agent.swarm", + title: "VoidBot Swarm", + version: 1, + updatedAt: "2026-07-13T00:00:00Z", + surface: { + root: { id: "root", kind: "surface", props: {}, children: [], stateBindings: [], embeddedDocuments: [] }, + styles: { tokens: {} }, + }, + commands: [command], +}; + +test("accepts canonical provider, surface, command, invocation, and receipt documents", () => { + assert.ok(isEveCommandDescriptor(command)); + assert.equal(parseEveSurfaceDocument(surface).providerId, "voidbot.swarm"); + assert.equal(parseEveProviderAdvertisement({ + schema: "gamecult.eve.provider_advertisement.v1", + providerId: "voidbot.swarm", + serviceId: "voidbot", + verseId: "voidbot.local", + title: "VoidBot Swarm", + kind: "agent.swarm", + freshness: { state: "fresh" }, + schemas: ["gamecult.eve.surface.v1"], + witnesses: [], + surfaces: [{ surfaceId: "voidbot.swarm", schema: "gamecult.eve.surface.v1", transport: "cultmesh" }], + commands: [{ command: "swarm.set_heat", transport: "cultmesh" }], + }).providerId, "voidbot.swarm"); + assert.equal(parseEveCommandInvocation({ + schema: "gamecult.eve.command_invocation.v1", + providerId: "voidbot.swarm", + surfaceId: "voidbot.swarm", + operation: { operationId: "swarm.set_heat" }, + payload: { value: 1.25 }, + issuedAt: "2026-07-13T00:00:00Z", + clientId: "test", + commandBoundary: "voidbot.swarm.commands", + receiptSchema: "gamecult.eve.command_receipt.v1", + }).operation.operationId, "swarm.set_heat"); + assert.equal(parseEveCommandReceipt({ + schema: "gamecult.eve.command_receipt.v1", + receiptId: "receipt-1", + commandId: "command-1", + command: "swarm.set_heat", + state: "reconciled", + ownerRepo: "VoidBot", + authority: "voidbot.swarm_control_state.v1", + providerId: "voidbot.swarm", + surfaceId: "voidbot.swarm", + sourceVersion: 1, + }).state, "reconciled"); +}); + +test("rejects missing required fields with contract diagnostics", () => { + assert.throws( + () => parseEveSurfaceDocument({ ...surface, providerId: undefined }), + (error) => error instanceof EveContractValidationError && + error.contract === "surface" && + error.errors.some((entry) => entry.keyword === "required" && entry.params.missingProperty === "providerId"), + ); + assert.throws( + () => parseEveCommandReceipt({ schema: "gamecult.eve.command_receipt.v1" }), + EveContractValidationError, + ); +}); + +test("published schema constants retain their authoritative ids", async () => { + const schemas = JSON.parse(await readFile(resolve(import.meta.dirname, "..", "..", "..", "schemas", "gamecult.eve.surface.v1.schema.json"), "utf8")); + assert.equal(schemas.$id, "gamecult.eve.surface.v1"); +}); + +test("validates an existing canonical Eve surface fixture", async () => { + const fixturePath = resolve( + import.meta.dirname, + "..", + "..", + "..", + "web", + "fixtures", + "eve-world-smoke-surface.json", + ); + const fixture = JSON.parse(await readFile(fixturePath, "utf8")); + assert.equal(parseEveSurfaceDocument(fixture).providerId, "eve.world-smoke"); +}); diff --git a/packages/eve-contracts/test/schema-drift.test.mjs b/packages/eve-contracts/test/schema-drift.test.mjs new file mode 100644 index 0000000..db7136b --- /dev/null +++ b/packages/eve-contracts/test/schema-drift.test.mjs @@ -0,0 +1,13 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import test from "node:test"; + +test("generated TypeScript contracts match authoritative Eve schemas", () => { + const packageRoot = resolve(import.meta.dirname, ".."); + const result = spawnSync(process.execPath, ["scripts/generate-contracts.mjs", "--check"], { + cwd: packageRoot, + encoding: "utf8", + }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); +}); diff --git a/packages/eve-contracts/tsconfig.json b/packages/eve-contracts/tsconfig.json new file mode 100644 index 0000000..4c38cb5 --- /dev/null +++ b/packages/eve-contracts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "ES2022", + "moduleResolution": "Bundler", + "noEmitOnError": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "target": "ES2022" + }, + "include": ["src/**/*.ts"] +} diff --git a/schemas/gamecult.eve.surface.v1.schema.json b/schemas/gamecult.eve.surface.v1.schema.json new file mode 100644 index 0000000..0d54f49 --- /dev/null +++ b/schemas/gamecult.eve.surface.v1.schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.surface.v1", + "title": "Eve Surface Document", + "type": "object", + "required": [ + "type", + "schema", + "providerId", + "providerKind", + "title", + "version", + "surface", + "commands" + ], + "properties": { + "type": { "const": "surface-state" }, + "schema": { "const": "gamecult.eve.surface.v1" }, + "providerId": { "type": "string", "minLength": 1 }, + "providerKind": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "minLength": 1 }, + "version": { "type": "integer", "minimum": 0 }, + "updatedAt": { "type": "string", "minLength": 1 }, + "updatedAtUtc": { "type": "string", "minLength": 1 }, + "surface": { + "type": "object", + "required": ["root", "styles"], + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "root": { "$ref": "#/$defs/component" }, + "styles": { "type": ["object", "array"] } + }, + "additionalProperties": true + }, + "commands": { + "type": "array", + "items": { "$ref": "gamecult.eve.command.v1" } + }, + "nodes": { "type": "array", "items": { "type": "object" } }, + "selectedNodeId": { "type": "string" } + }, + "$defs": { + "component": { + "type": "object", + "required": ["id", "kind", "props", "children"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "kind": { "type": "string", "minLength": 1 }, + "props": { "type": "object" }, + "children": { + "type": "array", + "items": { "$ref": "#/$defs/component" } + }, + "stateBindings": { + "type": "array", + "items": { + "type": "object", + "required": ["targetProp", "pointerId", "sourceId", "schemaId", "routeKind"], + "properties": { + "targetProp": { "type": "string", "minLength": 1 }, + "pointerId": { "type": "string", "minLength": 1 }, + "sourceId": { "type": "string", "minLength": 1 }, + "schemaId": { "type": "string", "minLength": 1 }, + "routeKind": { "type": "string", "minLength": 1 }, + "routeDescription": { "type": "string" } + }, + "additionalProperties": true + } + }, + "embeddedDocuments": { + "type": "array", + "items": { + "type": "object", + "required": ["slotId", "documentId", "schemaId", "presentationKind"], + "properties": { + "slotId": { "type": "string", "minLength": 1 }, + "documentId": { "type": "string", "minLength": 1 }, + "schemaId": { "type": "string", "minLength": 1 }, + "presentationKind": { "type": "string", "minLength": 1 }, + "routeHint": { "type": ["string", "object"] } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + }, + "anyOf": [ + { "required": ["updatedAt"] }, + { "required": ["updatedAtUtc"] } + ], + "additionalProperties": true +}