@openverb/runtime 2.0.0-alpha.1 → 2.0.0-alpha.4
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/dist/index.d.mts +157 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +245 -0
- package/dist/index.mjs +200 -0
- package/package.json +4 -1
- package/src/engine.ts +0 -166
- package/src/errors.ts +0 -31
- package/src/index.ts +0 -18
- package/src/registry.ts +0 -34
- package/src/types.ts +0 -128
- package/src/validator.ts +0 -26
- package/tsconfig.json +0 -18
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
type Actor = {
|
|
2
|
+
type: 'user' | 'service' | 'ai' | 'system';
|
|
3
|
+
id: string;
|
|
4
|
+
roles?: string[];
|
|
5
|
+
};
|
|
6
|
+
type Context = {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
planId: string;
|
|
9
|
+
env: 'dev' | 'staging' | 'prod';
|
|
10
|
+
requestId: string;
|
|
11
|
+
};
|
|
12
|
+
type VerbDefinition = {
|
|
13
|
+
id: string;
|
|
14
|
+
version: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
inputSchema: any;
|
|
18
|
+
outputSchema: any;
|
|
19
|
+
effects: string[];
|
|
20
|
+
visibility: 'public' | 'internal' | 'admin';
|
|
21
|
+
handler: string;
|
|
22
|
+
policy?: {
|
|
23
|
+
rolesAllowed?: string[];
|
|
24
|
+
plansAllowed?: string[];
|
|
25
|
+
};
|
|
26
|
+
billing?: {
|
|
27
|
+
meterKey?: string;
|
|
28
|
+
defaultDelta?: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
type PolicyDecision = {
|
|
32
|
+
decision: 'allow';
|
|
33
|
+
reasons: string[];
|
|
34
|
+
limits?: Record<string, any>;
|
|
35
|
+
meter?: {
|
|
36
|
+
key: string;
|
|
37
|
+
delta: number;
|
|
38
|
+
};
|
|
39
|
+
} | {
|
|
40
|
+
decision: 'deny';
|
|
41
|
+
reasons: string[];
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
upsell?: {
|
|
45
|
+
suggestedPlanId: string;
|
|
46
|
+
cta: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
type Event = {
|
|
50
|
+
type: string;
|
|
51
|
+
timestamp: string;
|
|
52
|
+
executionId: string;
|
|
53
|
+
data: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
type Receipt = {
|
|
56
|
+
executionId: string;
|
|
57
|
+
verbId: string;
|
|
58
|
+
verbVersion: string;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
actorId: string;
|
|
61
|
+
tenantId: string;
|
|
62
|
+
status: 'ok' | 'denied' | 'error';
|
|
63
|
+
durationMs: number;
|
|
64
|
+
};
|
|
65
|
+
type HandlerContext = {
|
|
66
|
+
actor: Actor;
|
|
67
|
+
context: Context;
|
|
68
|
+
adapters: Record<string, any>;
|
|
69
|
+
logger: {
|
|
70
|
+
info: (message: string, meta?: any) => void;
|
|
71
|
+
error: (message: string, meta?: any) => void;
|
|
72
|
+
};
|
|
73
|
+
emitEvent: (event: Omit<Event, 'timestamp' | 'executionId'>) => void;
|
|
74
|
+
};
|
|
75
|
+
type Handler<TArgs = any, TResult = any> = (ctx: HandlerContext, args: TArgs) => Promise<TResult>;
|
|
76
|
+
type ExecuteRequest = {
|
|
77
|
+
verbId: string;
|
|
78
|
+
args: unknown;
|
|
79
|
+
actor: Actor;
|
|
80
|
+
context: Context;
|
|
81
|
+
};
|
|
82
|
+
type ExecuteResponse = {
|
|
83
|
+
ok: true;
|
|
84
|
+
result: unknown;
|
|
85
|
+
receipt: Receipt;
|
|
86
|
+
events: Event[];
|
|
87
|
+
uiHints?: Array<{
|
|
88
|
+
type: string;
|
|
89
|
+
level: string;
|
|
90
|
+
message: string;
|
|
91
|
+
}>;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
denied: true;
|
|
95
|
+
reason: {
|
|
96
|
+
code: string;
|
|
97
|
+
message: string;
|
|
98
|
+
};
|
|
99
|
+
upsell?: {
|
|
100
|
+
suggestedPlanId: string;
|
|
101
|
+
cta: string;
|
|
102
|
+
};
|
|
103
|
+
receipt: Receipt;
|
|
104
|
+
events: Event[];
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
error: string;
|
|
108
|
+
receipt: Receipt;
|
|
109
|
+
events: Event[];
|
|
110
|
+
};
|
|
111
|
+
type PolicyEngine = (verb: VerbDefinition, actor: Actor, context: Context) => Promise<PolicyDecision>;
|
|
112
|
+
type Runtime = {
|
|
113
|
+
execute: (req: ExecuteRequest) => Promise<ExecuteResponse>;
|
|
114
|
+
introspect: () => VerbDefinition[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type RuntimeConfig = {
|
|
118
|
+
verbs?: VerbDefinition[];
|
|
119
|
+
verbsDir?: string;
|
|
120
|
+
policy: PolicyEngine;
|
|
121
|
+
handlers: Record<string, Handler>;
|
|
122
|
+
adapters?: Record<string, any>;
|
|
123
|
+
onEvent?: (event: Event) => void | Promise<void>;
|
|
124
|
+
onReceipt?: (receipt: Receipt) => void | Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
declare function createRuntime(config: RuntimeConfig): Runtime;
|
|
127
|
+
|
|
128
|
+
declare function loadVerbs(verbsDir?: string): VerbDefinition[];
|
|
129
|
+
declare function getVerbById(verbId: string, verbs?: VerbDefinition[]): VerbDefinition | null;
|
|
130
|
+
|
|
131
|
+
declare function validateInput(schema: any, data: unknown): void;
|
|
132
|
+
declare function validateOutput(schema: any, data: unknown): void;
|
|
133
|
+
|
|
134
|
+
declare class VerbNotFoundError extends Error {
|
|
135
|
+
constructor(verbId: string);
|
|
136
|
+
}
|
|
137
|
+
declare class ValidationError extends Error {
|
|
138
|
+
errors: any[];
|
|
139
|
+
constructor(message: string, errors: any[]);
|
|
140
|
+
}
|
|
141
|
+
declare class PolicyDeniedError extends Error {
|
|
142
|
+
code: string;
|
|
143
|
+
upsell?: {
|
|
144
|
+
suggestedPlanId: string;
|
|
145
|
+
cta: string;
|
|
146
|
+
} | undefined;
|
|
147
|
+
constructor(message: string, code: string, upsell?: {
|
|
148
|
+
suggestedPlanId: string;
|
|
149
|
+
cta: string;
|
|
150
|
+
} | undefined);
|
|
151
|
+
}
|
|
152
|
+
declare class HandlerError extends Error {
|
|
153
|
+
cause?: Error | undefined;
|
|
154
|
+
constructor(message: string, cause?: Error | undefined);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { type Actor, type Context, type Event, type ExecuteRequest, type ExecuteResponse, type Handler, type HandlerContext, HandlerError, type PolicyDecision, PolicyDeniedError, type Receipt, type Runtime, ValidationError, type VerbDefinition, VerbNotFoundError, createRuntime, getVerbById, loadVerbs, validateInput, validateOutput };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
type Actor = {
|
|
2
|
+
type: 'user' | 'service' | 'ai' | 'system';
|
|
3
|
+
id: string;
|
|
4
|
+
roles?: string[];
|
|
5
|
+
};
|
|
6
|
+
type Context = {
|
|
7
|
+
tenantId: string;
|
|
8
|
+
planId: string;
|
|
9
|
+
env: 'dev' | 'staging' | 'prod';
|
|
10
|
+
requestId: string;
|
|
11
|
+
};
|
|
12
|
+
type VerbDefinition = {
|
|
13
|
+
id: string;
|
|
14
|
+
version: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
inputSchema: any;
|
|
18
|
+
outputSchema: any;
|
|
19
|
+
effects: string[];
|
|
20
|
+
visibility: 'public' | 'internal' | 'admin';
|
|
21
|
+
handler: string;
|
|
22
|
+
policy?: {
|
|
23
|
+
rolesAllowed?: string[];
|
|
24
|
+
plansAllowed?: string[];
|
|
25
|
+
};
|
|
26
|
+
billing?: {
|
|
27
|
+
meterKey?: string;
|
|
28
|
+
defaultDelta?: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
type PolicyDecision = {
|
|
32
|
+
decision: 'allow';
|
|
33
|
+
reasons: string[];
|
|
34
|
+
limits?: Record<string, any>;
|
|
35
|
+
meter?: {
|
|
36
|
+
key: string;
|
|
37
|
+
delta: number;
|
|
38
|
+
};
|
|
39
|
+
} | {
|
|
40
|
+
decision: 'deny';
|
|
41
|
+
reasons: string[];
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
upsell?: {
|
|
45
|
+
suggestedPlanId: string;
|
|
46
|
+
cta: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
type Event = {
|
|
50
|
+
type: string;
|
|
51
|
+
timestamp: string;
|
|
52
|
+
executionId: string;
|
|
53
|
+
data: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
type Receipt = {
|
|
56
|
+
executionId: string;
|
|
57
|
+
verbId: string;
|
|
58
|
+
verbVersion: string;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
actorId: string;
|
|
61
|
+
tenantId: string;
|
|
62
|
+
status: 'ok' | 'denied' | 'error';
|
|
63
|
+
durationMs: number;
|
|
64
|
+
};
|
|
65
|
+
type HandlerContext = {
|
|
66
|
+
actor: Actor;
|
|
67
|
+
context: Context;
|
|
68
|
+
adapters: Record<string, any>;
|
|
69
|
+
logger: {
|
|
70
|
+
info: (message: string, meta?: any) => void;
|
|
71
|
+
error: (message: string, meta?: any) => void;
|
|
72
|
+
};
|
|
73
|
+
emitEvent: (event: Omit<Event, 'timestamp' | 'executionId'>) => void;
|
|
74
|
+
};
|
|
75
|
+
type Handler<TArgs = any, TResult = any> = (ctx: HandlerContext, args: TArgs) => Promise<TResult>;
|
|
76
|
+
type ExecuteRequest = {
|
|
77
|
+
verbId: string;
|
|
78
|
+
args: unknown;
|
|
79
|
+
actor: Actor;
|
|
80
|
+
context: Context;
|
|
81
|
+
};
|
|
82
|
+
type ExecuteResponse = {
|
|
83
|
+
ok: true;
|
|
84
|
+
result: unknown;
|
|
85
|
+
receipt: Receipt;
|
|
86
|
+
events: Event[];
|
|
87
|
+
uiHints?: Array<{
|
|
88
|
+
type: string;
|
|
89
|
+
level: string;
|
|
90
|
+
message: string;
|
|
91
|
+
}>;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
denied: true;
|
|
95
|
+
reason: {
|
|
96
|
+
code: string;
|
|
97
|
+
message: string;
|
|
98
|
+
};
|
|
99
|
+
upsell?: {
|
|
100
|
+
suggestedPlanId: string;
|
|
101
|
+
cta: string;
|
|
102
|
+
};
|
|
103
|
+
receipt: Receipt;
|
|
104
|
+
events: Event[];
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
error: string;
|
|
108
|
+
receipt: Receipt;
|
|
109
|
+
events: Event[];
|
|
110
|
+
};
|
|
111
|
+
type PolicyEngine = (verb: VerbDefinition, actor: Actor, context: Context) => Promise<PolicyDecision>;
|
|
112
|
+
type Runtime = {
|
|
113
|
+
execute: (req: ExecuteRequest) => Promise<ExecuteResponse>;
|
|
114
|
+
introspect: () => VerbDefinition[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type RuntimeConfig = {
|
|
118
|
+
verbs?: VerbDefinition[];
|
|
119
|
+
verbsDir?: string;
|
|
120
|
+
policy: PolicyEngine;
|
|
121
|
+
handlers: Record<string, Handler>;
|
|
122
|
+
adapters?: Record<string, any>;
|
|
123
|
+
onEvent?: (event: Event) => void | Promise<void>;
|
|
124
|
+
onReceipt?: (receipt: Receipt) => void | Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
declare function createRuntime(config: RuntimeConfig): Runtime;
|
|
127
|
+
|
|
128
|
+
declare function loadVerbs(verbsDir?: string): VerbDefinition[];
|
|
129
|
+
declare function getVerbById(verbId: string, verbs?: VerbDefinition[]): VerbDefinition | null;
|
|
130
|
+
|
|
131
|
+
declare function validateInput(schema: any, data: unknown): void;
|
|
132
|
+
declare function validateOutput(schema: any, data: unknown): void;
|
|
133
|
+
|
|
134
|
+
declare class VerbNotFoundError extends Error {
|
|
135
|
+
constructor(verbId: string);
|
|
136
|
+
}
|
|
137
|
+
declare class ValidationError extends Error {
|
|
138
|
+
errors: any[];
|
|
139
|
+
constructor(message: string, errors: any[]);
|
|
140
|
+
}
|
|
141
|
+
declare class PolicyDeniedError extends Error {
|
|
142
|
+
code: string;
|
|
143
|
+
upsell?: {
|
|
144
|
+
suggestedPlanId: string;
|
|
145
|
+
cta: string;
|
|
146
|
+
} | undefined;
|
|
147
|
+
constructor(message: string, code: string, upsell?: {
|
|
148
|
+
suggestedPlanId: string;
|
|
149
|
+
cta: string;
|
|
150
|
+
} | undefined);
|
|
151
|
+
}
|
|
152
|
+
declare class HandlerError extends Error {
|
|
153
|
+
cause?: Error | undefined;
|
|
154
|
+
constructor(message: string, cause?: Error | undefined);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { type Actor, type Context, type Event, type ExecuteRequest, type ExecuteResponse, type Handler, type HandlerContext, HandlerError, type PolicyDecision, PolicyDeniedError, type Receipt, type Runtime, ValidationError, type VerbDefinition, VerbNotFoundError, createRuntime, getVerbById, loadVerbs, validateInput, validateOutput };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
HandlerError: () => HandlerError,
|
|
34
|
+
PolicyDeniedError: () => PolicyDeniedError,
|
|
35
|
+
ValidationError: () => ValidationError,
|
|
36
|
+
VerbNotFoundError: () => VerbNotFoundError,
|
|
37
|
+
createRuntime: () => createRuntime,
|
|
38
|
+
getVerbById: () => getVerbById,
|
|
39
|
+
loadVerbs: () => loadVerbs,
|
|
40
|
+
validateInput: () => validateInput,
|
|
41
|
+
validateOutput: () => validateOutput
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/engine.ts
|
|
46
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
47
|
+
|
|
48
|
+
// src/registry.ts
|
|
49
|
+
var fs = __toESM(require("fs"));
|
|
50
|
+
var path = __toESM(require("path"));
|
|
51
|
+
var cachedVerbs = null;
|
|
52
|
+
function loadVerbs(verbsDir = "./openverb/verbs") {
|
|
53
|
+
if (cachedVerbs) return cachedVerbs;
|
|
54
|
+
const resolvedDir = path.resolve(process.cwd(), verbsDir);
|
|
55
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
56
|
+
console.warn(`Verbs directory not found: ${resolvedDir}`);
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const files = fs.readdirSync(resolvedDir).filter((f) => f.endsWith(".json"));
|
|
60
|
+
cachedVerbs = files.map((file) => {
|
|
61
|
+
const content = fs.readFileSync(path.join(resolvedDir, file), "utf-8");
|
|
62
|
+
return JSON.parse(content);
|
|
63
|
+
});
|
|
64
|
+
return cachedVerbs;
|
|
65
|
+
}
|
|
66
|
+
function getVerbById(verbId, verbs) {
|
|
67
|
+
const registry = verbs || loadVerbs();
|
|
68
|
+
return registry.find((v) => v.id === verbId) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/validator.ts
|
|
72
|
+
var import_ajv = __toESM(require("ajv"));
|
|
73
|
+
|
|
74
|
+
// src/errors.ts
|
|
75
|
+
var VerbNotFoundError = class extends Error {
|
|
76
|
+
constructor(verbId) {
|
|
77
|
+
super(`Verb not found: ${verbId}`);
|
|
78
|
+
this.name = "VerbNotFoundError";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var ValidationError = class extends Error {
|
|
82
|
+
constructor(message, errors) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.errors = errors;
|
|
85
|
+
this.name = "ValidationError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var PolicyDeniedError = class extends Error {
|
|
89
|
+
constructor(message, code, upsell) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.upsell = upsell;
|
|
93
|
+
this.name = "PolicyDeniedError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var HandlerError = class extends Error {
|
|
97
|
+
constructor(message, cause) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.cause = cause;
|
|
100
|
+
this.name = "HandlerError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/validator.ts
|
|
105
|
+
var ajv = new import_ajv.default({ allErrors: true });
|
|
106
|
+
function validateInput(schema, data) {
|
|
107
|
+
const validate = ajv.compile(schema);
|
|
108
|
+
const valid = validate(data);
|
|
109
|
+
if (!valid) {
|
|
110
|
+
throw new ValidationError(
|
|
111
|
+
"Input validation failed",
|
|
112
|
+
validate.errors || []
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function validateOutput(schema, data) {
|
|
117
|
+
const validate = ajv.compile(schema);
|
|
118
|
+
const valid = validate(data);
|
|
119
|
+
if (!valid) {
|
|
120
|
+
console.error("Output validation failed:", validate.errors);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/engine.ts
|
|
125
|
+
function createRuntime(config) {
|
|
126
|
+
const verbs = config.verbs || loadVerbs(config.verbsDir);
|
|
127
|
+
const events = [];
|
|
128
|
+
const executionId = import_node_crypto.default.randomUUID();
|
|
129
|
+
async function execute(req) {
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
const verb = getVerbById(req.verbId, verbs);
|
|
132
|
+
if (!verb) {
|
|
133
|
+
const receipt = createReceipt(req, "error", startTime);
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `Verb not found: ${req.verbId}`,
|
|
137
|
+
receipt,
|
|
138
|
+
events: []
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
validateInput(verb.inputSchema, req.args);
|
|
143
|
+
const decision = await config.policy(verb, req.actor, req.context);
|
|
144
|
+
emitEvent({
|
|
145
|
+
type: decision.decision === "allow" ? "policy.allowed" : "policy.denied",
|
|
146
|
+
data: { verbId: verb.id, decision }
|
|
147
|
+
});
|
|
148
|
+
if (decision.decision === "deny") {
|
|
149
|
+
const receipt2 = createReceipt(req, "denied", startTime);
|
|
150
|
+
await config.onReceipt?.(receipt2);
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
denied: true,
|
|
154
|
+
reason: { code: decision.code, message: decision.message },
|
|
155
|
+
upsell: decision.upsell,
|
|
156
|
+
receipt: receipt2,
|
|
157
|
+
events
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const handler = config.handlers[verb.handler];
|
|
161
|
+
if (!handler) {
|
|
162
|
+
throw new HandlerError(`Handler not found: ${verb.handler}`);
|
|
163
|
+
}
|
|
164
|
+
const ctx = {
|
|
165
|
+
actor: req.actor,
|
|
166
|
+
context: req.context,
|
|
167
|
+
adapters: config.adapters || {},
|
|
168
|
+
logger: {
|
|
169
|
+
info: (msg, meta) => console.log(`[INFO] ${msg}`, meta),
|
|
170
|
+
error: (msg, meta) => console.error(`[ERROR] ${msg}`, meta)
|
|
171
|
+
},
|
|
172
|
+
emitEvent
|
|
173
|
+
};
|
|
174
|
+
const result = await handler(ctx, req.args);
|
|
175
|
+
validateOutput(verb.outputSchema, result);
|
|
176
|
+
const receipt = createReceipt(req, "ok", startTime);
|
|
177
|
+
await config.onReceipt?.(receipt);
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
result,
|
|
181
|
+
receipt,
|
|
182
|
+
events
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
emitEvent({
|
|
186
|
+
type: "handler.error",
|
|
187
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
188
|
+
});
|
|
189
|
+
const receipt = createReceipt(req, "error", startTime);
|
|
190
|
+
await config.onReceipt?.(receipt);
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
194
|
+
receipt,
|
|
195
|
+
events
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function emitEvent(event) {
|
|
200
|
+
const fullEvent = {
|
|
201
|
+
...event,
|
|
202
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
203
|
+
executionId
|
|
204
|
+
};
|
|
205
|
+
events.push(fullEvent);
|
|
206
|
+
config.onEvent?.(fullEvent);
|
|
207
|
+
}
|
|
208
|
+
function createReceipt(req, status, startTime) {
|
|
209
|
+
return {
|
|
210
|
+
executionId,
|
|
211
|
+
verbId: req.verbId,
|
|
212
|
+
verbVersion: getVerbById(req.verbId, verbs)?.version || "0.0.0",
|
|
213
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
214
|
+
actorId: req.actor.id,
|
|
215
|
+
tenantId: req.context.tenantId,
|
|
216
|
+
status,
|
|
217
|
+
durationMs: Date.now() - startTime
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function introspect() {
|
|
221
|
+
return verbs.map((v) => ({
|
|
222
|
+
id: v.id,
|
|
223
|
+
version: v.version,
|
|
224
|
+
summary: v.summary,
|
|
225
|
+
inputSchema: v.inputSchema,
|
|
226
|
+
outputSchema: v.outputSchema,
|
|
227
|
+
effects: v.effects,
|
|
228
|
+
visibility: v.visibility,
|
|
229
|
+
handler: v.handler
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
return { execute, introspect };
|
|
233
|
+
}
|
|
234
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
235
|
+
0 && (module.exports = {
|
|
236
|
+
HandlerError,
|
|
237
|
+
PolicyDeniedError,
|
|
238
|
+
ValidationError,
|
|
239
|
+
VerbNotFoundError,
|
|
240
|
+
createRuntime,
|
|
241
|
+
getVerbById,
|
|
242
|
+
loadVerbs,
|
|
243
|
+
validateInput,
|
|
244
|
+
validateOutput
|
|
245
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/engine.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/registry.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
var cachedVerbs = null;
|
|
8
|
+
function loadVerbs(verbsDir = "./openverb/verbs") {
|
|
9
|
+
if (cachedVerbs) return cachedVerbs;
|
|
10
|
+
const resolvedDir = path.resolve(process.cwd(), verbsDir);
|
|
11
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
12
|
+
console.warn(`Verbs directory not found: ${resolvedDir}`);
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const files = fs.readdirSync(resolvedDir).filter((f) => f.endsWith(".json"));
|
|
16
|
+
cachedVerbs = files.map((file) => {
|
|
17
|
+
const content = fs.readFileSync(path.join(resolvedDir, file), "utf-8");
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
});
|
|
20
|
+
return cachedVerbs;
|
|
21
|
+
}
|
|
22
|
+
function getVerbById(verbId, verbs) {
|
|
23
|
+
const registry = verbs || loadVerbs();
|
|
24
|
+
return registry.find((v) => v.id === verbId) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/validator.ts
|
|
28
|
+
import Ajv from "ajv";
|
|
29
|
+
|
|
30
|
+
// src/errors.ts
|
|
31
|
+
var VerbNotFoundError = class extends Error {
|
|
32
|
+
constructor(verbId) {
|
|
33
|
+
super(`Verb not found: ${verbId}`);
|
|
34
|
+
this.name = "VerbNotFoundError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ValidationError = class extends Error {
|
|
38
|
+
constructor(message, errors) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.errors = errors;
|
|
41
|
+
this.name = "ValidationError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var PolicyDeniedError = class extends Error {
|
|
45
|
+
constructor(message, code, upsell) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.code = code;
|
|
48
|
+
this.upsell = upsell;
|
|
49
|
+
this.name = "PolicyDeniedError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var HandlerError = class extends Error {
|
|
53
|
+
constructor(message, cause) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.cause = cause;
|
|
56
|
+
this.name = "HandlerError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/validator.ts
|
|
61
|
+
var ajv = new Ajv({ allErrors: true });
|
|
62
|
+
function validateInput(schema, data) {
|
|
63
|
+
const validate = ajv.compile(schema);
|
|
64
|
+
const valid = validate(data);
|
|
65
|
+
if (!valid) {
|
|
66
|
+
throw new ValidationError(
|
|
67
|
+
"Input validation failed",
|
|
68
|
+
validate.errors || []
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function validateOutput(schema, data) {
|
|
73
|
+
const validate = ajv.compile(schema);
|
|
74
|
+
const valid = validate(data);
|
|
75
|
+
if (!valid) {
|
|
76
|
+
console.error("Output validation failed:", validate.errors);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/engine.ts
|
|
81
|
+
function createRuntime(config) {
|
|
82
|
+
const verbs = config.verbs || loadVerbs(config.verbsDir);
|
|
83
|
+
const events = [];
|
|
84
|
+
const executionId = crypto.randomUUID();
|
|
85
|
+
async function execute(req) {
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
const verb = getVerbById(req.verbId, verbs);
|
|
88
|
+
if (!verb) {
|
|
89
|
+
const receipt = createReceipt(req, "error", startTime);
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: `Verb not found: ${req.verbId}`,
|
|
93
|
+
receipt,
|
|
94
|
+
events: []
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
validateInput(verb.inputSchema, req.args);
|
|
99
|
+
const decision = await config.policy(verb, req.actor, req.context);
|
|
100
|
+
emitEvent({
|
|
101
|
+
type: decision.decision === "allow" ? "policy.allowed" : "policy.denied",
|
|
102
|
+
data: { verbId: verb.id, decision }
|
|
103
|
+
});
|
|
104
|
+
if (decision.decision === "deny") {
|
|
105
|
+
const receipt2 = createReceipt(req, "denied", startTime);
|
|
106
|
+
await config.onReceipt?.(receipt2);
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
denied: true,
|
|
110
|
+
reason: { code: decision.code, message: decision.message },
|
|
111
|
+
upsell: decision.upsell,
|
|
112
|
+
receipt: receipt2,
|
|
113
|
+
events
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const handler = config.handlers[verb.handler];
|
|
117
|
+
if (!handler) {
|
|
118
|
+
throw new HandlerError(`Handler not found: ${verb.handler}`);
|
|
119
|
+
}
|
|
120
|
+
const ctx = {
|
|
121
|
+
actor: req.actor,
|
|
122
|
+
context: req.context,
|
|
123
|
+
adapters: config.adapters || {},
|
|
124
|
+
logger: {
|
|
125
|
+
info: (msg, meta) => console.log(`[INFO] ${msg}`, meta),
|
|
126
|
+
error: (msg, meta) => console.error(`[ERROR] ${msg}`, meta)
|
|
127
|
+
},
|
|
128
|
+
emitEvent
|
|
129
|
+
};
|
|
130
|
+
const result = await handler(ctx, req.args);
|
|
131
|
+
validateOutput(verb.outputSchema, result);
|
|
132
|
+
const receipt = createReceipt(req, "ok", startTime);
|
|
133
|
+
await config.onReceipt?.(receipt);
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
result,
|
|
137
|
+
receipt,
|
|
138
|
+
events
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
emitEvent({
|
|
142
|
+
type: "handler.error",
|
|
143
|
+
data: { error: error instanceof Error ? error.message : "Unknown error" }
|
|
144
|
+
});
|
|
145
|
+
const receipt = createReceipt(req, "error", startTime);
|
|
146
|
+
await config.onReceipt?.(receipt);
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
150
|
+
receipt,
|
|
151
|
+
events
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function emitEvent(event) {
|
|
156
|
+
const fullEvent = {
|
|
157
|
+
...event,
|
|
158
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
+
executionId
|
|
160
|
+
};
|
|
161
|
+
events.push(fullEvent);
|
|
162
|
+
config.onEvent?.(fullEvent);
|
|
163
|
+
}
|
|
164
|
+
function createReceipt(req, status, startTime) {
|
|
165
|
+
return {
|
|
166
|
+
executionId,
|
|
167
|
+
verbId: req.verbId,
|
|
168
|
+
verbVersion: getVerbById(req.verbId, verbs)?.version || "0.0.0",
|
|
169
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
170
|
+
actorId: req.actor.id,
|
|
171
|
+
tenantId: req.context.tenantId,
|
|
172
|
+
status,
|
|
173
|
+
durationMs: Date.now() - startTime
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function introspect() {
|
|
177
|
+
return verbs.map((v) => ({
|
|
178
|
+
id: v.id,
|
|
179
|
+
version: v.version,
|
|
180
|
+
summary: v.summary,
|
|
181
|
+
inputSchema: v.inputSchema,
|
|
182
|
+
outputSchema: v.outputSchema,
|
|
183
|
+
effects: v.effects,
|
|
184
|
+
visibility: v.visibility,
|
|
185
|
+
handler: v.handler
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
return { execute, introspect };
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
HandlerError,
|
|
192
|
+
PolicyDeniedError,
|
|
193
|
+
ValidationError,
|
|
194
|
+
VerbNotFoundError,
|
|
195
|
+
createRuntime,
|
|
196
|
+
getVerbById,
|
|
197
|
+
loadVerbs,
|
|
198
|
+
validateInput,
|
|
199
|
+
validateOutput
|
|
200
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openverb/runtime",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.4",
|
|
4
4
|
"description": "OpenVerb execution runtime",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"import": "./dist/index.mjs"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
15
18
|
"scripts": {
|
|
16
19
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
17
20
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
package/src/engine.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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'
|
package/src/registry.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
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
|
-
}
|
package/src/validator.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|