@omni-api/core 0.0.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.
@@ -0,0 +1,263 @@
1
+ import { ZodTypeAny, z } from 'zod';
2
+
3
+ /**
4
+ * 用户主体(Principal)
5
+ *
6
+ * 由各 Adapter 的鉴权中间件填充到 Context.user。
7
+ * 业务层只需要相信这个字段为 true,无需关心鉴权方式。
8
+ */
9
+ interface UserPrincipal {
10
+ /** 全局唯一用户 ID */
11
+ id: string;
12
+ /** 角色,自由字符串数组(如 ['admin','op']) */
13
+ roles?: string[];
14
+ /** 其他自定义字段 */
15
+ [key: string]: unknown;
16
+ }
17
+ /** 极简 Logger 接口,避免强绑定具体库;插件可注入 pino/winston 适配 */
18
+ interface Logger {
19
+ debug(msg: string, meta?: Record<string, unknown>): void;
20
+ info(msg: string, meta?: Record<string, unknown>): void;
21
+ warn(msg: string, meta?: Record<string, unknown>): void;
22
+ error(msg: string, meta?: Record<string, unknown>): void;
23
+ child(bindings: Record<string, unknown>): Logger;
24
+ }
25
+ /**
26
+ * 调用来源标识
27
+ *
28
+ * - http: 来自 HTTP Adapter
29
+ * - mcp: 来自 MCP Adapter
30
+ * - test: 单元测试中的直接调用
31
+ */
32
+ type ContextSource = 'http' | 'mcp' | 'test' | (string & {});
33
+ /**
34
+ * 请求级上下文。
35
+ *
36
+ * 中间件链 / handler 都拿到同一个 Context 实例。
37
+ * raw 字段是 Adapter 的私有数据(如 Fastify request/reply),业务原则上不应直接使用。
38
+ */
39
+ interface Context {
40
+ /** 调用来源 */
41
+ source: ContextSource;
42
+ /** 请求级唯一 ID */
43
+ requestId: string;
44
+ /** 用户身份(鉴权后填充) */
45
+ user?: UserPrincipal;
46
+ /** 请求级 logger */
47
+ logger: Logger;
48
+ /** 中间件之间传值的临时槽位 */
49
+ state: Record<string, unknown>;
50
+ /** 取消信号 */
51
+ signal: AbortSignal;
52
+ /** Adapter 私有上下文,类型由各 Adapter 自行声明合并 */
53
+ raw?: unknown;
54
+ /**
55
+ * 当前正在执行的 procedure 引用(由 runProcedure 在执行前注入)。
56
+ * 中间件可据此区分场景,例如限流按 procedure name 隔离。
57
+ * 调用方一般不需要主动传,runProcedure 会负责设置。
58
+ */
59
+ procedure?: {
60
+ readonly name: string;
61
+ };
62
+ }
63
+ /** 控制台 Logger,作为默认 / 测试用 fallback */
64
+ declare const consoleLogger: Logger;
65
+ /**
66
+ * 创建一个测试 / Adapter 默认用的 Context。
67
+ *
68
+ * 各 Adapter 一般会基于此函数再补充自己的 raw 字段。
69
+ */
70
+ declare function createContext(partial: Partial<Context> & {
71
+ source: ContextSource;
72
+ }): Context;
73
+
74
+ /**
75
+ * 中间件函数签名(类 Koa onion 模型)
76
+ *
77
+ * - 必须 await next() 才能让后续中间件 / handler 执行
78
+ * - 可在 next() 前后做任何事(鉴权、计时、错误转换…)
79
+ * - 抛出的错误会被 runProcedure 捕获并规范化
80
+ */
81
+ type Middleware<C extends Context = Context> = (ctx: C, next: () => Promise<unknown>) => Promise<unknown>;
82
+ /**
83
+ * 把多个中间件合并为单个执行函数。
84
+ *
85
+ * 调用 `compose(mws)(ctx, finalHandler)` 会按 mws 顺序进入、反向退出,
86
+ * 最终调用 finalHandler 并返回其结果。
87
+ *
88
+ * 实现要点:
89
+ * 1. 每个中间件 next 只能调用一次(防止重复 await)
90
+ * 2. finalHandler 的返回值会原样向上传递
91
+ */
92
+ declare function compose<C extends Context>(middlewares: ReadonlyArray<Middleware<C>>): (ctx: C, finalHandler: () => Promise<unknown>) => Promise<unknown>;
93
+
94
+ /**
95
+ * Procedure 元信息 —— 仅声明真实出口(HTTP / MCP)。
96
+ *
97
+ * 各 Adapter 只读自己关心的那块;插件可通过自定义 key 扩展。
98
+ */
99
+ interface ProcedureMeta {
100
+ /** HTTP 出口配置;不写则用默认约定(按 name 派生路径,按动词推断 method) */
101
+ http?: {
102
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
103
+ /** 默认 /<namespace>/<action>,如 order.create → /order/create */
104
+ path?: string;
105
+ /** 成功状态码,默认 200 */
106
+ status?: number;
107
+ /** 是否暴露到 HTTP,默认 true */
108
+ expose?: boolean;
109
+ };
110
+ /** MCP 出口配置 */
111
+ mcp?: {
112
+ /** 是否暴露到 MCP,默认 true */
113
+ expose?: boolean;
114
+ /** 在 MCP Client 中的分组(如 commerce / admin) */
115
+ category?: string;
116
+ /** 是否在执行前要求 MCP Host 用户确认(destructive 操作建议 true) */
117
+ requireConfirmation?: boolean;
118
+ /** 危险等级:拼接进 description 头部,帮助 LLM 谨慎使用 */
119
+ dangerLevel?: 'read' | 'write' | 'destructive';
120
+ /** 给 LLM 的额外示例,会拼接进 description */
121
+ examples?: ReadonlyArray<{
122
+ input: unknown;
123
+ when: string;
124
+ output?: unknown;
125
+ }>;
126
+ /** 别名:拼接进 description,提升 LLM 召回 */
127
+ aliases?: ReadonlyArray<string>;
128
+ };
129
+ /** 插件自由扩展用的自定义元信息 */
130
+ [key: string]: unknown;
131
+ }
132
+ /** Procedure 定义(用户传给 defineProcedure 的形态) */
133
+ interface ProcedureDef<Input extends ZodTypeAny, Output extends ZodTypeAny, C extends Context = Context> {
134
+ /** 全局唯一名,建议 namespace.action 格式:order.create */
135
+ name: string;
136
+ /** 单行简述(用于列表 / table) */
137
+ summary?: string;
138
+ /** 详细描述:作为 OpenAPI / MCP description 的主体 */
139
+ description: string;
140
+ /** 输入 schema */
141
+ input: Input;
142
+ /** 输出 schema */
143
+ output: Output;
144
+ /** 多端元信息 */
145
+ meta?: ProcedureMeta;
146
+ /** 中间件链(按声明顺序进入、反向退出) */
147
+ middleware?: ReadonlyArray<Middleware<C>>;
148
+ /** 业务逻辑 */
149
+ handler: (params: {
150
+ input: z.infer<Input>;
151
+ ctx: C;
152
+ }) => Promise<z.infer<Output>> | z.infer<Output>;
153
+ /** 标签,用于分组、按需暴露 */
154
+ tags?: ReadonlyArray<string>;
155
+ }
156
+ /** 冻结后的 Procedure 实例 */
157
+ interface Procedure<Input extends ZodTypeAny = ZodTypeAny, Output extends ZodTypeAny = ZodTypeAny, C extends Context = Context> extends Required<Pick<ProcedureDef<Input, Output, C>, 'name' | 'description' | 'input' | 'output' | 'handler'>> {
158
+ readonly summary?: string;
159
+ readonly meta: Readonly<ProcedureMeta>;
160
+ readonly middleware: ReadonlyArray<Middleware<C>>;
161
+ readonly tags: ReadonlyArray<string>;
162
+ }
163
+ /**
164
+ * 定义一个 Procedure。
165
+ *
166
+ * 工厂的价值:
167
+ * 1. 类型推导起点:I/O 由 input/output 自动推出,业务无需手写泛型
168
+ * 2. 声明期校验:name 合法性、必要字段
169
+ * 3. 不可变保证:返回 frozen 对象,防止运行时被改写
170
+ */
171
+ declare function defineProcedure<Input extends ZodTypeAny, Output extends ZodTypeAny, C extends Context = Context>(def: ProcedureDef<Input, Output, C>): Procedure<Input, Output, C>;
172
+ /** 取 Procedure 的 namespace("order.create" → "order";无点则返回整个 name) */
173
+ declare function getNamespace(p: Pick<Procedure, 'name'>): string;
174
+
175
+ /**
176
+ * 任意 Procedure(输入输出类型不限)。
177
+ *
178
+ * 用 `Procedure<any, any>` 而非 `Procedure<ZodTypeAny, ZodTypeAny>` 来获得协变行为,
179
+ * 让具体 procedure(带窄 schema 类型)能被赋值到 router.procedures 数组里。
180
+ */
181
+ type AnyProcedure = Procedure<any, any>;
182
+ /**
183
+ * Router:Procedure 的批量组合 + 共享 middleware 的语法糖。
184
+ *
185
+ * 不引入新的运行时概念 —— Registry 注册时会把 Router 拍平为 Procedure 列表,
186
+ * 并把 router.middleware 前置到每个 procedure.middleware 之前。
187
+ */
188
+ interface Router<C extends Context = Context> {
189
+ /** 命名空间(可选) */
190
+ readonly namespace?: string;
191
+ /** 该组下所有 procedure 共享的中间件 */
192
+ readonly middleware: ReadonlyArray<Middleware<C>>;
193
+ /** 组内的 procedure 列表 */
194
+ readonly procedures: ReadonlyArray<AnyProcedure>;
195
+ }
196
+ interface RouterDef<C extends Context = Context> {
197
+ namespace?: string;
198
+ middleware?: ReadonlyArray<Middleware<C>>;
199
+ procedures: ReadonlyArray<AnyProcedure>;
200
+ }
201
+ declare function createRouter<C extends Context = Context>(def: RouterDef<C>): Router<C>;
202
+
203
+ /** Adapter 名称的常量集合(也允许字符串扩展) */
204
+ type AdapterName = 'http' | 'mcp' | (string & {});
205
+ interface ListFilter {
206
+ /** 按 namespace 精确过滤 */
207
+ namespace?: string;
208
+ /** 必须包含所有给定 tag */
209
+ tags?: ReadonlyArray<string>;
210
+ }
211
+ interface RegistryOptions {
212
+ /**
213
+ * 全局前置中间件 —— 在每个 procedure 的 router.middleware + procedure.middleware 之前。
214
+ *
215
+ * 典型用途:attachDataSource / observability.middleware / 全局 audit。
216
+ *
217
+ * 顺序:全局 → router → procedure
218
+ */
219
+ globalMiddleware?: ReadonlyArray<Middleware<Context>>;
220
+ }
221
+ /**
222
+ * 全局注册中心:所有 Adapter 共享的单一事实源。
223
+ *
224
+ * 职责:
225
+ * - 收集 Procedure / Router
226
+ * - 校验 name 唯一性
227
+ * - 按 Adapter 维度(meta.<adapter>.expose)过滤可暴露集合
228
+ * - 提供基于 namespace / tag 的查询
229
+ * - 维护"全局 → router → procedure"的中间件叠加
230
+ */
231
+ declare class Registry {
232
+ private readonly entries;
233
+ private readonly globalMiddleware;
234
+ constructor(options?: RegistryOptions);
235
+ /** 注册单个 Procedure */
236
+ register(procedure: AnyProcedure): void;
237
+ /** 注册一个 Router,会把 router.middleware 前置合并到每个 procedure */
238
+ registerRouter(router: Router): void;
239
+ /** 按 name 查找 */
240
+ get(name: string): AnyProcedure | undefined;
241
+ /** 取某个 procedure 的最终中间件链(router 共享 + procedure 自有) */
242
+ getMiddleware(name: string): ReadonlyArray<Middleware<Context>>;
243
+ /** 是否存在 */
244
+ has(name: string): boolean;
245
+ /** 总数 */
246
+ get size(): number;
247
+ /** 列出全部(可选过滤) */
248
+ list(filter?: ListFilter): AnyProcedure[];
249
+ /**
250
+ * 列出某个 Adapter 应该暴露的 Procedure。
251
+ *
252
+ * 规则:
253
+ * - meta[adapter].expose === false → 排除
254
+ * - 否则 → 包含(默认暴露)
255
+ *
256
+ * 注:CLI 等"白名单式"出口可由该 adapter 自己再过滤一遍。
257
+ */
258
+ listFor(adapter: AdapterName): AnyProcedure[];
259
+ /** 清空(测试用) */
260
+ clear(): void;
261
+ }
262
+
263
+ export { type AdapterName as A, type Context as C, type ListFilter as L, type Middleware as M, type Procedure as P, Registry as R, type UserPrincipal as U, type AnyProcedure as a, type ContextSource as b, type Logger as c, type ProcedureDef as d, type ProcedureMeta as e, type RegistryOptions as f, type Router as g, type RouterDef as h, compose as i, consoleLogger as j, createContext as k, createRouter as l, defineProcedure as m, getNamespace as n };
@@ -0,0 +1,171 @@
1
+ import { ZodTypeAny, ZodType } from 'zod';
2
+ import { P as Procedure, R as Registry, C as Context } from '../registry-ZWfxJhSP.js';
3
+
4
+ /**
5
+ * 把 Zod schema 转 JSON Schema(draft-07 风格,OpenAPI/MCP/LLM 通用)。
6
+ *
7
+ * 这里做一层薄封装,统一选项,并去掉 zod-to-json-schema 默认加上的 `$schema` 字段
8
+ * (MCP / OpenAI tool 格式都不需要也不允许该字段)。
9
+ */
10
+ declare function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown>;
11
+
12
+ /** OpenAI Function Calling 工具格式 */
13
+ interface OpenAITool {
14
+ type: 'function';
15
+ function: {
16
+ name: string;
17
+ description: string;
18
+ parameters: Record<string, unknown>;
19
+ };
20
+ }
21
+ /** Anthropic(Claude)Tool Use 格式 */
22
+ interface AnthropicTool {
23
+ name: string;
24
+ description: string;
25
+ input_schema: Record<string, unknown>;
26
+ }
27
+ /** 通用格式(直接是 input schema + 名称描述) */
28
+ interface GenericTool {
29
+ name: string;
30
+ description: string;
31
+ parameters: Record<string, unknown>;
32
+ }
33
+ type ToolFormat = 'openai' | 'anthropic' | 'generic';
34
+ interface ExportToolsOptions {
35
+ /** 输出格式,默认 'openai' */
36
+ format?: ToolFormat;
37
+ /**
38
+ * 名字转换函数。
39
+ *
40
+ * - openai: 默认把 "." 替换为 "_"(OpenAI tool name 不允许 ".")
41
+ * - anthropic / generic: 默认保留原名
42
+ */
43
+ renameTool?: (name: string) => string;
44
+ /** 自定义过滤;默认导出所有 procedure */
45
+ filter?: (p: Procedure) => boolean;
46
+ }
47
+ /**
48
+ * 把 Registry 中的 Procedure 导出为给 LLM 的工具数组。
49
+ *
50
+ * 这是为了"老平台兼容 / 调试"而存在的逃生出口;推荐场景仍是直接走 MCP。
51
+ */
52
+ declare function exportToolsJSON(registry: Registry, options: ExportToolsOptions & {
53
+ format: 'openai';
54
+ }): OpenAITool[];
55
+ declare function exportToolsJSON(registry: Registry, options: ExportToolsOptions & {
56
+ format: 'anthropic';
57
+ }): AnthropicTool[];
58
+ declare function exportToolsJSON(registry: Registry, options?: ExportToolsOptions & {
59
+ format?: 'generic';
60
+ }): GenericTool[];
61
+
62
+ /**
63
+ * Graceful shutdown helper —— 跨 adapter / plugin 的优雅停机协调器。
64
+ *
65
+ * 设计原则:
66
+ * 1. 顺序停机:先停接收(adapters),再停依赖(dataSource / cache / observability)
67
+ * 2. 超时强杀:每个 hook 有独立超时,整体也有超时;防止挂死
68
+ * 3. 幂等:重复信号只触发一次(防止 double-shutdown 撞死)
69
+ * 4. 信号统一:SIGTERM / SIGINT / 可选 uncaughtException
70
+ *
71
+ * 用法:
72
+ * ```ts
73
+ * const g = createGracefulShutdown({ logger });
74
+ * g.add('http', () => httpAdapter.close(), 10_000);
75
+ * g.add('db', () => dataSource.disconnect(), 5_000);
76
+ * g.add('logs', () => logger.flush?.(), 2_000);
77
+ * g.listen(); // 注册 SIGTERM/SIGINT
78
+ * ```
79
+ */
80
+ interface ShutdownHook {
81
+ /** 名字,仅用于日志 */
82
+ name: string;
83
+ /** 关闭函数 */
84
+ fn: () => Promise<void> | void;
85
+ /** 该 hook 的超时(毫秒),默认 5000 */
86
+ timeoutMs?: number;
87
+ }
88
+ interface GracefulShutdownOptions {
89
+ /** 整体超时(毫秒),超过则强制 process.exit。默认 30s */
90
+ totalTimeoutMs?: number;
91
+ /** 日志记录器(任意 console-like 接口) */
92
+ logger?: {
93
+ info(msg: string, meta?: Record<string, unknown>): void;
94
+ error(msg: string, meta?: Record<string, unknown>): void;
95
+ warn?(msg: string, meta?: Record<string, unknown>): void;
96
+ };
97
+ /** 是否处理 uncaughtException / unhandledRejection(默认 true) */
98
+ handleFatalErrors?: boolean;
99
+ /** 退出函数(默认 process.exit);测试时可注入 */
100
+ exit?: (code: number) => void;
101
+ /** 信号注入点(默认监听 process);测试时可传自定义 emitter */
102
+ on?: (signal: 'SIGTERM' | 'SIGINT', handler: () => void) => void;
103
+ }
104
+ interface GracefulShutdown {
105
+ /** 注册 shutdown hook,按注册顺序倒序执行(后注册的先关) */
106
+ add(name: string, fn: () => Promise<void> | void, timeoutMs?: number): void;
107
+ /** 开始监听信号 */
108
+ listen(): void;
109
+ /** 主动触发 shutdown(用于测试 / 业务自管) */
110
+ trigger(reason: string): Promise<void>;
111
+ /** 是否已经在 shutdown 中 */
112
+ readonly isShuttingDown: boolean;
113
+ }
114
+ declare function createGracefulShutdown(options?: GracefulShutdownOptions): GracefulShutdown;
115
+
116
+ /**
117
+ * 启动期环境变量校验。
118
+ *
119
+ * 设计原则:
120
+ * - 缺关键变量直接挂(fail-fast),比启动后第一个请求才报错好
121
+ * - 错误信息友好(指出具体哪个变量错了)
122
+ * - 类型化输出,业务用 `env.JWT_SECRET` 直接拿到 string
123
+ *
124
+ * 用法:
125
+ * ```ts
126
+ * import { z } from 'zod';
127
+ * import { loadEnv } from '@omni-api/core/utils';
128
+ *
129
+ * const Env = z.object({
130
+ * PORT: z.coerce.number().default(3000),
131
+ * JWT_SECRET: z.string().min(32, 'must be ≥ 32 chars'),
132
+ * DATABASE_URL: z.string().url(),
133
+ * });
134
+ * export const env = loadEnv(Env);
135
+ * ```
136
+ */
137
+ declare function loadEnv<T>(schema: ZodType<T>, source?: Record<string, string | undefined>): T;
138
+
139
+ /**
140
+ * createTracedFetch —— 带请求上下文的 fetch 包装。
141
+ *
142
+ * 自动注入:
143
+ * - x-request-id:当前请求 ID(贯穿调用链)
144
+ * - traceparent:W3C Trace Context(如果当前 ctx 有 / 或装了 OTel 能取到 active span)
145
+ * - signal:当前请求的 AbortSignal(客户端断开 → 下游也取消)
146
+ *
147
+ * 业务用法:
148
+ * const fetchx = createTracedFetch(ctx);
149
+ * const res = await fetchx('https://internal-api/users/1');
150
+ *
151
+ * 设计:返回 fetch-compatible 函数,业务无需改动调用语义。
152
+ */
153
+
154
+ interface TracedFetchOptions {
155
+ /** 自定义 fetch 实现(默认 globalThis.fetch) */
156
+ fetch?: typeof fetch;
157
+ /**
158
+ * 额外要透传的请求头键名,从 ctx.state 中按 key 取(小写)。
159
+ * 例如 ['x-tenant-id'] 会让租户 ID 自动透传到下游。
160
+ */
161
+ forwardStateKeys?: string[];
162
+ }
163
+ /**
164
+ * 创建一个绑定到当前 ctx 的 fetch。
165
+ *
166
+ * 注意:返回值是普通 fetch 函数,可以直接传给任何接受 fetch 的 SDK
167
+ * (比如 OpenAI SDK 的 `fetch` 选项)。
168
+ */
169
+ declare function createTracedFetch(ctx: Context, options?: TracedFetchOptions): typeof fetch;
170
+
171
+ export { type AnthropicTool, type ExportToolsOptions, type GenericTool, type GracefulShutdown, type GracefulShutdownOptions, type OpenAITool, type ShutdownHook, type ToolFormat, type TracedFetchOptions, createGracefulShutdown, createTracedFetch, exportToolsJSON, loadEnv, zodToJsonSchema };
@@ -0,0 +1,221 @@
1
+ import { zodToJsonSchema as zodToJsonSchema$1 } from 'zod-to-json-schema';
2
+
3
+ // src/utils/zod-to-json-schema.ts
4
+ function zodToJsonSchema(schema) {
5
+ const raw = zodToJsonSchema$1(schema, {
6
+ target: "jsonSchema7",
7
+ $refStrategy: "none"
8
+ // 内联,避免下游处理 $ref
9
+ });
10
+ const { $schema, ...rest } = raw;
11
+ return rest;
12
+ }
13
+
14
+ // src/utils/export-tools.ts
15
+ function buildAgentDescription(p) {
16
+ const mcpMeta = p.meta.mcp;
17
+ const lines = [];
18
+ if (mcpMeta?.dangerLevel === "destructive") {
19
+ lines.push("[DESTRUCTIVE] \u6B64\u64CD\u4F5C\u4F1A\u9020\u6210\u4E0D\u53EF\u9006\u540E\u679C\uFF0C\u8C03\u7528\u524D\u52A1\u5FC5\u4E0E\u7528\u6237\u786E\u8BA4\u3002");
20
+ } else if (mcpMeta?.dangerLevel === "write") {
21
+ lines.push("[WRITE] \u6B64\u64CD\u4F5C\u4F1A\u4FEE\u6539\u6570\u636E\u3002");
22
+ }
23
+ lines.push(p.description.trim());
24
+ if (mcpMeta?.aliases && mcpMeta.aliases.length > 0) {
25
+ lines.push(`\u522B\u540D: ${mcpMeta.aliases.join(", ")}`);
26
+ }
27
+ if (mcpMeta?.examples && mcpMeta.examples.length > 0) {
28
+ lines.push("\u793A\u4F8B:");
29
+ for (const ex of mcpMeta.examples) {
30
+ const inputStr = JSON.stringify(ex.input);
31
+ lines.push(` - \u5F53 ${ex.when}: ${inputStr}`);
32
+ }
33
+ }
34
+ return lines.join("\n");
35
+ }
36
+ function exportToolsJSON(registry, options = {}) {
37
+ const format = options.format ?? "generic";
38
+ const procedures = registry.list().filter((p) => options.filter ? options.filter(p) : true);
39
+ const defaultRename = (name) => format === "openai" ? name.replace(/\./g, "_") : name;
40
+ const rename = options.renameTool ?? defaultRename;
41
+ return procedures.map((p) => {
42
+ const description = buildAgentDescription(p);
43
+ const parameters = zodToJsonSchema(p.input);
44
+ const name = rename(p.name);
45
+ if (format === "openai") {
46
+ const tool2 = {
47
+ type: "function",
48
+ function: { name, description, parameters }
49
+ };
50
+ return tool2;
51
+ }
52
+ if (format === "anthropic") {
53
+ const tool2 = { name, description, input_schema: parameters };
54
+ return tool2;
55
+ }
56
+ const tool = { name, description, parameters };
57
+ return tool;
58
+ });
59
+ }
60
+
61
+ // src/utils/graceful-shutdown.ts
62
+ function withTimeout(p, timeoutMs, label) {
63
+ return new Promise((resolve, reject) => {
64
+ let settled = false;
65
+ const timer = setTimeout(() => {
66
+ if (settled) return;
67
+ settled = true;
68
+ reject(new Error(`Shutdown hook "${label}" timed out after ${timeoutMs}ms`));
69
+ }, timeoutMs);
70
+ Promise.resolve(p).then(
71
+ (v) => {
72
+ if (settled) return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ resolve(v);
76
+ },
77
+ (e) => {
78
+ if (settled) return;
79
+ settled = true;
80
+ clearTimeout(timer);
81
+ reject(e);
82
+ }
83
+ );
84
+ });
85
+ }
86
+ function createGracefulShutdown(options = {}) {
87
+ const totalTimeoutMs = options.totalTimeoutMs ?? 3e4;
88
+ const logger = options.logger ?? console;
89
+ const exit = options.exit ?? ((code) => process.exit(code));
90
+ const on = options.on ?? ((signal, handler) => {
91
+ process.on(signal, handler);
92
+ });
93
+ const hooks = [];
94
+ let shuttingDown = false;
95
+ let shutdownPromise;
96
+ async function runShutdown(reason, exitCode = 0) {
97
+ if (shutdownPromise) return shutdownPromise;
98
+ shuttingDown = true;
99
+ logger.info("shutdown.start", { reason, hooks: hooks.length });
100
+ shutdownPromise = (async () => {
101
+ const startedAt = Date.now();
102
+ const killTimer = setTimeout(() => {
103
+ logger.error("shutdown.timeout_force_exit", { totalTimeoutMs });
104
+ exit(exitCode || 1);
105
+ }, totalTimeoutMs);
106
+ const ordered = [...hooks].reverse();
107
+ for (const h of ordered) {
108
+ const t = h.timeoutMs ?? 5e3;
109
+ try {
110
+ await withTimeout(h.fn(), t, h.name);
111
+ logger.info("shutdown.hook.ok", { name: h.name });
112
+ } catch (err) {
113
+ logger.error("shutdown.hook.fail", {
114
+ name: h.name,
115
+ error: err instanceof Error ? err.message : String(err)
116
+ });
117
+ }
118
+ }
119
+ clearTimeout(killTimer);
120
+ logger.info("shutdown.complete", { durationMs: Date.now() - startedAt });
121
+ exit(exitCode);
122
+ })();
123
+ return shutdownPromise;
124
+ }
125
+ if (options.handleFatalErrors !== false) {
126
+ process.on("uncaughtException", (err) => {
127
+ logger.error("uncaughtException", { error: err.message, stack: err.stack });
128
+ void runShutdown("uncaughtException", 1);
129
+ });
130
+ process.on("unhandledRejection", (reason) => {
131
+ logger.error("unhandledRejection", {
132
+ reason: reason instanceof Error ? reason.message : String(reason)
133
+ });
134
+ void runShutdown("unhandledRejection", 1);
135
+ });
136
+ }
137
+ return {
138
+ add(name, fn, timeoutMs) {
139
+ if (shuttingDown) {
140
+ logger.warn?.("shutdown.add_after_start_ignored", { name });
141
+ return;
142
+ }
143
+ hooks.push({ name, fn, timeoutMs });
144
+ },
145
+ listen() {
146
+ on("SIGTERM", () => void runShutdown("SIGTERM"));
147
+ on("SIGINT", () => void runShutdown("SIGINT"));
148
+ },
149
+ trigger(reason) {
150
+ return runShutdown(reason);
151
+ },
152
+ get isShuttingDown() {
153
+ return shuttingDown;
154
+ }
155
+ };
156
+ }
157
+
158
+ // src/utils/load-env.ts
159
+ function loadEnv(schema, source = process.env) {
160
+ const parsed = schema.safeParse(source);
161
+ if (parsed.success) return parsed.data;
162
+ const issues = parsed.error?.issues ?? [];
163
+ const lines = issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`);
164
+ const msg = `Invalid environment variables:
165
+ ${lines.join("\n")}`;
166
+ console.error(msg);
167
+ throw new Error(msg);
168
+ }
169
+
170
+ // src/utils/traced-fetch.ts
171
+ function createTracedFetch(ctx, options = {}) {
172
+ const baseFetch = options.fetch ?? globalThis.fetch;
173
+ if (typeof baseFetch !== "function") {
174
+ throw new Error("createTracedFetch: globalThis.fetch is not available; provide options.fetch");
175
+ }
176
+ const forwardStateKeys = options.forwardStateKeys ?? [];
177
+ const tracedFetch = async (input, init) => {
178
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
179
+ if (!headers.has("x-request-id")) {
180
+ headers.set("x-request-id", ctx.requestId);
181
+ }
182
+ const tp = pickTraceparent(ctx);
183
+ if (tp && !headers.has("traceparent")) {
184
+ headers.set("traceparent", tp);
185
+ }
186
+ for (const key of forwardStateKeys) {
187
+ const v = ctx.state[key];
188
+ if (typeof v === "string" && !headers.has(key)) {
189
+ headers.set(key, v);
190
+ }
191
+ }
192
+ const signal = init?.signal ?? ctx.signal;
193
+ return baseFetch(input, {
194
+ ...init,
195
+ headers,
196
+ signal
197
+ });
198
+ };
199
+ return tracedFetch;
200
+ }
201
+ function pickTraceparent(ctx) {
202
+ const fromState = ctx.state["traceparent"];
203
+ if (typeof fromState === "string") return fromState;
204
+ const otel = globalThis.__OMNI_OTEL_API__;
205
+ if (otel?.trace?.getActiveSpan) {
206
+ try {
207
+ const span = otel.trace.getActiveSpan();
208
+ const sc = span?.spanContext?.();
209
+ if (sc?.traceId && sc?.spanId) {
210
+ const flags = (sc.traceFlags ?? 0).toString(16).padStart(2, "0");
211
+ return `00-${sc.traceId}-${sc.spanId}-${flags}`;
212
+ }
213
+ } catch {
214
+ }
215
+ }
216
+ return void 0;
217
+ }
218
+
219
+ export { createGracefulShutdown, createTracedFetch, exportToolsJSON, loadEnv, zodToJsonSchema };
220
+ //# sourceMappingURL=index.js.map
221
+ //# sourceMappingURL=index.js.map