@openverb/runtime 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@openverb/runtime",
3
+ "version": "2.0.0-alpha.1",
4
+ "description": "OpenVerb execution runtime",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format cjs,esm --dts",
17
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
18
+ "lint": "tsc --noEmit",
19
+ "test": "vitest run"
20
+ },
21
+ "dependencies": {
22
+ "ajv": "^8.12.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.10.0",
26
+ "tsup": "^8.0.1",
27
+ "typescript": "^5.3.2",
28
+ "vitest": "^1.0.4"
29
+ },
30
+ "keywords": ["openverb", "runtime", "execution"],
31
+ "license": "MIT"
32
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,166 @@
1
+ import crypto from 'node:crypto'
2
+ import { loadVerbs, getVerbById } from './registry'
3
+ import { validateInput, validateOutput } from './validator'
4
+ import { VerbNotFoundError, PolicyDeniedError, HandlerError } from './errors'
5
+ import type {
6
+ Runtime,
7
+ ExecuteRequest,
8
+ ExecuteResponse,
9
+ VerbDefinition,
10
+ PolicyEngine,
11
+ Handler,
12
+ Event,
13
+ Receipt,
14
+ HandlerContext
15
+ } from './types'
16
+
17
+ type RuntimeConfig = {
18
+ verbs?: VerbDefinition[]
19
+ verbsDir?: string
20
+ policy: PolicyEngine
21
+ handlers: Record<string, Handler>
22
+ adapters?: Record<string, any>
23
+ onEvent?: (event: Event) => void | Promise<void>
24
+ onReceipt?: (receipt: Receipt) => void | Promise<void>
25
+ }
26
+
27
+ export function createRuntime(config: RuntimeConfig): Runtime {
28
+ const verbs = config.verbs || loadVerbs(config.verbsDir)
29
+ const events: Event[] = []
30
+ const executionId = crypto.randomUUID()
31
+
32
+ async function execute(req: ExecuteRequest): Promise<ExecuteResponse> {
33
+ const startTime = Date.now()
34
+
35
+ // Stage 1: Resolve verb
36
+ const verb = getVerbById(req.verbId, verbs)
37
+ if (!verb) {
38
+ const receipt = createReceipt(req, 'error', startTime)
39
+ return {
40
+ ok: false,
41
+ error: `Verb not found: ${req.verbId}`,
42
+ receipt,
43
+ events: []
44
+ }
45
+ }
46
+
47
+ try {
48
+ // Stage 2: Validate input
49
+ validateInput(verb.inputSchema, req.args)
50
+
51
+ // Stage 3: Policy check
52
+ const decision = await config.policy(verb, req.actor, req.context)
53
+
54
+ emitEvent({
55
+ type: decision.decision === 'allow' ? 'policy.allowed' : 'policy.denied',
56
+ data: { verbId: verb.id, decision }
57
+ })
58
+
59
+ if (decision.decision === 'deny') {
60
+ const receipt = createReceipt(req, 'denied', startTime)
61
+ await config.onReceipt?.(receipt)
62
+
63
+ return {
64
+ ok: false,
65
+ denied: true,
66
+ reason: { code: decision.code, message: decision.message },
67
+ upsell: decision.upsell,
68
+ receipt,
69
+ events
70
+ }
71
+ }
72
+
73
+ // Stage 4: Execute handler
74
+ const handler = config.handlers[verb.handler]
75
+ if (!handler) {
76
+ throw new HandlerError(`Handler not found: ${verb.handler}`)
77
+ }
78
+
79
+ const ctx: HandlerContext = {
80
+ actor: req.actor,
81
+ context: req.context,
82
+ adapters: config.adapters || {},
83
+ logger: {
84
+ info: (msg, meta) => console.log(`[INFO] ${msg}`, meta),
85
+ error: (msg, meta) => console.error(`[ERROR] ${msg}`, meta)
86
+ },
87
+ emitEvent
88
+ }
89
+
90
+ const result = await handler(ctx, req.args)
91
+
92
+ // Stage 5: Validate output
93
+ validateOutput(verb.outputSchema, result)
94
+
95
+ // Stage 6: Create receipt
96
+ const receipt = createReceipt(req, 'ok', startTime)
97
+ await config.onReceipt?.(receipt)
98
+
99
+ // Stage 7: Return response
100
+ return {
101
+ ok: true,
102
+ result,
103
+ receipt,
104
+ events
105
+ }
106
+
107
+ } catch (error) {
108
+ emitEvent({
109
+ type: 'handler.error',
110
+ data: { error: error instanceof Error ? error.message : 'Unknown error' }
111
+ })
112
+
113
+ const receipt = createReceipt(req, 'error', startTime)
114
+ await config.onReceipt?.(receipt)
115
+
116
+ return {
117
+ ok: false,
118
+ error: error instanceof Error ? error.message : 'Unknown error',
119
+ receipt,
120
+ events
121
+ }
122
+ }
123
+ }
124
+
125
+ function emitEvent(event: Omit<Event, 'timestamp' | 'executionId'>) {
126
+ const fullEvent: Event = {
127
+ ...event,
128
+ timestamp: new Date().toISOString(),
129
+ executionId
130
+ }
131
+ events.push(fullEvent)
132
+ config.onEvent?.(fullEvent)
133
+ }
134
+
135
+ function createReceipt(
136
+ req: ExecuteRequest,
137
+ status: 'ok' | 'denied' | 'error',
138
+ startTime: number
139
+ ): Receipt {
140
+ return {
141
+ executionId,
142
+ verbId: req.verbId,
143
+ verbVersion: getVerbById(req.verbId, verbs)?.version || '0.0.0',
144
+ timestamp: new Date().toISOString(),
145
+ actorId: req.actor.id,
146
+ tenantId: req.context.tenantId,
147
+ status,
148
+ durationMs: Date.now() - startTime
149
+ }
150
+ }
151
+
152
+ function introspect(): VerbDefinition[] {
153
+ return verbs.map(v => ({
154
+ id: v.id,
155
+ version: v.version,
156
+ summary: v.summary,
157
+ inputSchema: v.inputSchema,
158
+ outputSchema: v.outputSchema,
159
+ effects: v.effects,
160
+ visibility: v.visibility,
161
+ handler: v.handler
162
+ }))
163
+ }
164
+
165
+ return { execute, introspect }
166
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,31 @@
1
+ export class VerbNotFoundError extends Error {
2
+ constructor(verbId: string) {
3
+ super(`Verb not found: ${verbId}`)
4
+ this.name = 'VerbNotFoundError'
5
+ }
6
+ }
7
+
8
+ export class ValidationError extends Error {
9
+ constructor(message: string, public errors: any[]) {
10
+ super(message)
11
+ this.name = 'ValidationError'
12
+ }
13
+ }
14
+
15
+ export class PolicyDeniedError extends Error {
16
+ constructor(
17
+ message: string,
18
+ public code: string,
19
+ public upsell?: { suggestedPlanId: string; cta: string }
20
+ ) {
21
+ super(message)
22
+ this.name = 'PolicyDeniedError'
23
+ }
24
+ }
25
+
26
+ export class HandlerError extends Error {
27
+ constructor(message: string, public cause?: Error) {
28
+ super(message)
29
+ this.name = 'HandlerError'
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ // @openverb/runtime - Main entry point
2
+ export { createRuntime } from './engine'
3
+ export { loadVerbs, getVerbById } from './registry'
4
+ export { validateInput, validateOutput } from './validator'
5
+ export type {
6
+ Runtime,
7
+ ExecuteRequest,
8
+ ExecuteResponse,
9
+ VerbDefinition,
10
+ HandlerContext,
11
+ Handler,
12
+ Actor,
13
+ Context,
14
+ Receipt,
15
+ Event,
16
+ PolicyDecision
17
+ } from './types'
18
+ export { VerbNotFoundError, ValidationError, PolicyDeniedError, HandlerError } from './errors'
@@ -0,0 +1,34 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
3
+ import type { VerbDefinition } from './types'
4
+
5
+ let cachedVerbs: VerbDefinition[] | null = null
6
+
7
+ export function loadVerbs(verbsDir: string = './openverb/verbs'): VerbDefinition[] {
8
+ if (cachedVerbs) return cachedVerbs
9
+
10
+ const resolvedDir = path.resolve(process.cwd(), verbsDir)
11
+
12
+ if (!fs.existsSync(resolvedDir)) {
13
+ console.warn(`Verbs directory not found: ${resolvedDir}`)
14
+ return []
15
+ }
16
+
17
+ const files = fs.readdirSync(resolvedDir).filter(f => f.endsWith('.json'))
18
+
19
+ cachedVerbs = files.map(file => {
20
+ const content = fs.readFileSync(path.join(resolvedDir, file), 'utf-8')
21
+ return JSON.parse(content) as VerbDefinition
22
+ })
23
+
24
+ return cachedVerbs
25
+ }
26
+
27
+ export function getVerbById(verbId: string, verbs?: VerbDefinition[]): VerbDefinition | null {
28
+ const registry = verbs || loadVerbs()
29
+ return registry.find(v => v.id === verbId) || null
30
+ }
31
+
32
+ export function clearCache() {
33
+ cachedVerbs = null
34
+ }
package/src/types.ts ADDED
@@ -0,0 +1,128 @@
1
+ import type { JSONSchemaType } from 'ajv'
2
+
3
+ // Core types
4
+ export type Actor = {
5
+ type: 'user' | 'service' | 'ai' | 'system'
6
+ id: string
7
+ roles?: string[]
8
+ }
9
+
10
+ export type Context = {
11
+ tenantId: string
12
+ planId: string
13
+ env: 'dev' | 'staging' | 'prod'
14
+ requestId: string
15
+ }
16
+
17
+ export type VerbDefinition = {
18
+ id: string
19
+ version: string
20
+ summary: string
21
+ description?: string
22
+ inputSchema: any // JSON Schema
23
+ outputSchema: any // JSON Schema
24
+ effects: string[]
25
+ visibility: 'public' | 'internal' | 'admin'
26
+ handler: string
27
+ policy?: {
28
+ rolesAllowed?: string[]
29
+ plansAllowed?: string[]
30
+ }
31
+ billing?: {
32
+ meterKey?: string
33
+ defaultDelta?: number
34
+ }
35
+ }
36
+
37
+ export type PolicyDecision =
38
+ | {
39
+ decision: 'allow'
40
+ reasons: string[]
41
+ limits?: Record<string, any>
42
+ meter?: { key: string; delta: number }
43
+ }
44
+ | {
45
+ decision: 'deny'
46
+ reasons: string[]
47
+ code: string
48
+ message: string
49
+ upsell?: {
50
+ suggestedPlanId: string
51
+ cta: string
52
+ }
53
+ }
54
+
55
+ export type Event = {
56
+ type: string
57
+ timestamp: string
58
+ executionId: string
59
+ data: Record<string, unknown>
60
+ }
61
+
62
+ export type Receipt = {
63
+ executionId: string
64
+ verbId: string
65
+ verbVersion: string
66
+ timestamp: string
67
+ actorId: string
68
+ tenantId: string
69
+ status: 'ok' | 'denied' | 'error'
70
+ durationMs: number
71
+ }
72
+
73
+ export type HandlerContext = {
74
+ actor: Actor
75
+ context: Context
76
+ adapters: Record<string, any>
77
+ logger: {
78
+ info: (message: string, meta?: any) => void
79
+ error: (message: string, meta?: any) => void
80
+ }
81
+ emitEvent: (event: Omit<Event, 'timestamp' | 'executionId'>) => void
82
+ }
83
+
84
+ export type Handler<TArgs = any, TResult = any> = (
85
+ ctx: HandlerContext,
86
+ args: TArgs
87
+ ) => Promise<TResult>
88
+
89
+ export type ExecuteRequest = {
90
+ verbId: string
91
+ args: unknown
92
+ actor: Actor
93
+ context: Context
94
+ }
95
+
96
+ export type ExecuteResponse =
97
+ | {
98
+ ok: true
99
+ result: unknown
100
+ receipt: Receipt
101
+ events: Event[]
102
+ uiHints?: Array<{ type: string; level: string; message: string }>
103
+ }
104
+ | {
105
+ ok: false
106
+ denied: true
107
+ reason: { code: string; message: string }
108
+ upsell?: { suggestedPlanId: string; cta: string }
109
+ receipt: Receipt
110
+ events: Event[]
111
+ }
112
+ | {
113
+ ok: false
114
+ error: string
115
+ receipt: Receipt
116
+ events: Event[]
117
+ }
118
+
119
+ export type PolicyEngine = (
120
+ verb: VerbDefinition,
121
+ actor: Actor,
122
+ context: Context
123
+ ) => Promise<PolicyDecision>
124
+
125
+ export type Runtime = {
126
+ execute: (req: ExecuteRequest) => Promise<ExecuteResponse>
127
+ introspect: () => VerbDefinition[]
128
+ }
@@ -0,0 +1,26 @@
1
+ import Ajv from 'ajv'
2
+ import { ValidationError } from './errors'
3
+
4
+ const ajv = new Ajv({ allErrors: true })
5
+
6
+ export function validateInput(schema: any, data: unknown): void {
7
+ const validate = ajv.compile(schema)
8
+ const valid = validate(data)
9
+
10
+ if (!valid) {
11
+ throw new ValidationError(
12
+ 'Input validation failed',
13
+ validate.errors || []
14
+ )
15
+ }
16
+ }
17
+
18
+ export function validateOutput(schema: any, data: unknown): void {
19
+ const validate = ajv.compile(schema)
20
+ const valid = validate(data)
21
+
22
+ if (!valid) {
23
+ console.error('Output validation failed:', validate.errors)
24
+ // Don't throw - log warning instead
25
+ }
26
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020"],
6
+ "moduleResolution": "node",
7
+ "resolveJsonModule": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "outDir": "dist",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }