@newhomestar/sdk 0.2.7 → 0.4.0
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.ts +42 -13
- package/dist/index.js +206 -13
- package/dist/parseSpec.d.ts +18 -386
- package/dist/parseSpec.js +2 -2
- package/dist/workerSchema.d.ts +46 -0
- package/dist/workerSchema.js +41 -0
- package/package.json +8 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z, ZodTypeAny } from "zod";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
3
|
export interface ActionDef<I extends ZodTypeAny, O extends ZodTypeAny> {
|
|
3
4
|
name: string;
|
|
4
5
|
input: I;
|
|
@@ -7,7 +8,11 @@ export interface ActionDef<I extends ZodTypeAny, O extends ZodTypeAny> {
|
|
|
7
8
|
fga?: {
|
|
8
9
|
resourceType: string;
|
|
9
10
|
relation: string;
|
|
11
|
+
resourceIdKey?: string;
|
|
12
|
+
policy?: string;
|
|
10
13
|
};
|
|
14
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
15
|
+
path?: string;
|
|
11
16
|
}
|
|
12
17
|
export interface ActionCtx {
|
|
13
18
|
jobId: string;
|
|
@@ -20,31 +25,55 @@ export declare function action<I extends ZodTypeAny, O extends ZodTypeAny>(cfg:
|
|
|
20
25
|
fga?: {
|
|
21
26
|
resourceType: string;
|
|
22
27
|
relation: string;
|
|
28
|
+
resourceIdKey?: string;
|
|
29
|
+
policy?: string;
|
|
23
30
|
};
|
|
31
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
32
|
+
path?: string;
|
|
24
33
|
handler: (input: z.infer<I>, ctx: ActionCtx) => Promise<z.infer<O>>;
|
|
25
34
|
}): ActionDef<I, O>;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
35
|
+
import type { NovaSpec } from './parseSpec';
|
|
36
|
+
/**
|
|
37
|
+
* FGA policy hints embedded in nova.yaml
|
|
38
|
+
*/
|
|
39
|
+
export type FgaSpec = NovaSpec['fga'];
|
|
40
|
+
import { WorkerDef } from './workerSchema';
|
|
41
|
+
/**
|
|
42
|
+
* Register a worker definition - SAME API as before
|
|
43
|
+
*/
|
|
36
44
|
export declare function defineWorker<T extends WorkerDef>(def: T): T;
|
|
37
45
|
/** Enqueue an async action and receive `{ job_id }` */
|
|
38
46
|
export declare function enqueue<P extends object>(actionPath: `${string}.${string}`, payload: P): Promise<{
|
|
39
47
|
job_id: string;
|
|
40
48
|
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Create an oRPC router from a WorkerDef, mapping each action to an oRPC procedure
|
|
51
|
+
*/
|
|
52
|
+
export declare function createORPCRouter<T extends WorkerDef>(def: T): Record<string, any>;
|
|
53
|
+
export interface ORPCServerOptions {
|
|
54
|
+
port?: number;
|
|
55
|
+
plugins?: any[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run an oRPC server with OpenAPI support - replaces runTRPCServer
|
|
59
|
+
*/
|
|
60
|
+
export declare function runORPCServer<T extends WorkerDef>(def: T, options?: ORPCServerOptions): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
41
61
|
export declare function runWorker(def: WorkerDef): Promise<void>;
|
|
42
|
-
export
|
|
62
|
+
export declare function generateOpenAPISpec<T extends WorkerDef>(def: T): Promise<{
|
|
63
|
+
openapi: string;
|
|
64
|
+
info: {
|
|
65
|
+
title: string;
|
|
66
|
+
version: string;
|
|
67
|
+
description: string;
|
|
68
|
+
};
|
|
69
|
+
paths: any;
|
|
70
|
+
}>;
|
|
43
71
|
/**
|
|
44
|
-
*
|
|
72
|
+
* Enhanced HTTP server exposing each action under configurable routes
|
|
45
73
|
*/
|
|
46
|
-
export declare function runHttpServer(def:
|
|
74
|
+
export declare function runHttpServer<T extends WorkerDef>(def: T, opts?: {
|
|
47
75
|
port?: number;
|
|
48
76
|
}): void;
|
|
77
|
+
export type { ZodTypeAny as SchemaAny, ZodTypeAny };
|
|
49
78
|
export { parseNovaSpec } from "./parseSpec";
|
|
50
79
|
export type { NovaSpec } from "./parseSpec";
|
package/dist/index.js
CHANGED
|
@@ -7,22 +7,57 @@ exports.parseNovaSpec = void 0;
|
|
|
7
7
|
exports.action = action;
|
|
8
8
|
exports.defineWorker = defineWorker;
|
|
9
9
|
exports.enqueue = enqueue;
|
|
10
|
+
exports.createORPCRouter = createORPCRouter;
|
|
11
|
+
exports.runORPCServer = runORPCServer;
|
|
10
12
|
exports.runWorker = runWorker;
|
|
13
|
+
exports.generateOpenAPISpec = generateOpenAPISpec;
|
|
11
14
|
exports.runHttpServer = runHttpServer;
|
|
12
15
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
13
16
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
17
|
+
const sdk_1 = require("@openfga/sdk");
|
|
18
|
+
// TODO: Install oRPC dependencies
|
|
19
|
+
// import { os } from "@orpc/server";
|
|
20
|
+
// import { OpenAPIHandler } from "@orpc/openapi/node";
|
|
21
|
+
// Temporary stubs for oRPC until dependencies are installed
|
|
22
|
+
const os = {
|
|
23
|
+
route: (config) => ({
|
|
24
|
+
input: (schema) => ({
|
|
25
|
+
output: (schema) => ({
|
|
26
|
+
handler: (handler) => ({ _route: config, _input: schema, _output: schema, _handler: handler })
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
const OpenAPIHandler = class {
|
|
32
|
+
constructor(router, options) { }
|
|
33
|
+
async handle(req, res, context) {
|
|
34
|
+
return { matched: false };
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const node_http_1 = require("node:http");
|
|
14
38
|
if (!process.env.RUNTIME_SUPABASE_URL) {
|
|
15
39
|
// local dev – read .env.local
|
|
16
40
|
dotenv_1.default.config({ path: ".env.local", override: true });
|
|
17
41
|
}
|
|
18
42
|
function action(cfg) {
|
|
19
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
name: cfg.name ?? "unnamed",
|
|
45
|
+
method: cfg.method ?? 'POST',
|
|
46
|
+
path: cfg.path,
|
|
47
|
+
...cfg
|
|
48
|
+
};
|
|
20
49
|
}
|
|
50
|
+
// WorkerDef represents the code-level definition passed into defineWorker()
|
|
51
|
+
const workerSchema_1 = require("./workerSchema");
|
|
52
|
+
/**
|
|
53
|
+
* Register a worker definition - SAME API as before
|
|
54
|
+
*/
|
|
21
55
|
function defineWorker(def) {
|
|
22
|
-
|
|
56
|
+
// Runtime validation of the worker definition
|
|
57
|
+
workerSchema_1.WorkerDefSchema.parse(def);
|
|
58
|
+
return def;
|
|
23
59
|
}
|
|
24
|
-
/*──────────────────────* Client‑side enqueue() *──────────────────*/
|
|
25
|
-
// CLIENT_SUPABASE_… vars exist in gateways / other services
|
|
60
|
+
/*──────────────────────* Client‑side enqueue() (UNCHANGED) *──────────────────*/
|
|
26
61
|
const CLIENT_SUPABASE_URL = process.env.CLIENT_SUPABASE_PUBLIC_URL;
|
|
27
62
|
const CLIENT_SUPABASE_KEY = process.env.CLIENT_SUPABASE_SERVICE_ROLE_KEY;
|
|
28
63
|
let clientSupabase;
|
|
@@ -43,7 +78,77 @@ async function enqueue(actionPath, payload) {
|
|
|
43
78
|
throw error;
|
|
44
79
|
return data;
|
|
45
80
|
}
|
|
46
|
-
/*────────────────
|
|
81
|
+
/*──────────────── NEW: oRPC Router Creation ───────────────*/
|
|
82
|
+
/**
|
|
83
|
+
* Create an oRPC router from a WorkerDef, mapping each action to an oRPC procedure
|
|
84
|
+
*/
|
|
85
|
+
function createORPCRouter(def) {
|
|
86
|
+
const procedures = {};
|
|
87
|
+
for (const [actionName, actionDef] of Object.entries(def.actions)) {
|
|
88
|
+
const method = actionDef.method || 'POST';
|
|
89
|
+
const path = actionDef.path || `/${actionName}`;
|
|
90
|
+
// Create oRPC procedure with route information
|
|
91
|
+
const procedure = os
|
|
92
|
+
.route({ method, path })
|
|
93
|
+
.input(actionDef.input)
|
|
94
|
+
.output(actionDef.output)
|
|
95
|
+
.handler(async ({ input, context }) => {
|
|
96
|
+
const ctx = {
|
|
97
|
+
jobId: context?.jobId || `orpc-${Date.now()}`,
|
|
98
|
+
progress: (percent, meta) => {
|
|
99
|
+
console.log(`[${actionName}] Progress: ${percent}%`, meta);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return await actionDef.handler(input, ctx);
|
|
103
|
+
});
|
|
104
|
+
procedures[actionName] = procedure;
|
|
105
|
+
}
|
|
106
|
+
return procedures;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Run an oRPC server with OpenAPI support - replaces runTRPCServer
|
|
110
|
+
*/
|
|
111
|
+
function runORPCServer(def, options = {}) {
|
|
112
|
+
const router = createORPCRouter(def);
|
|
113
|
+
const handler = new OpenAPIHandler(router, {
|
|
114
|
+
plugins: options.plugins || []
|
|
115
|
+
});
|
|
116
|
+
const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await handler.handle(req, res, {
|
|
119
|
+
context: {
|
|
120
|
+
worker: def,
|
|
121
|
+
queue: def.queue,
|
|
122
|
+
jobId: `http-${Date.now()}`
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (!result.matched) {
|
|
126
|
+
res.statusCode = 404;
|
|
127
|
+
res.setHeader('Content-Type', 'application/json');
|
|
128
|
+
res.end(JSON.stringify({ error: 'No procedure matched' }));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error('[oRPC Server Error]:', error);
|
|
133
|
+
res.statusCode = 500;
|
|
134
|
+
res.setHeader('Content-Type', 'application/json');
|
|
135
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT) : 3000);
|
|
139
|
+
server.listen(port, () => {
|
|
140
|
+
console.log(`[nova] oRPC server "${def.name}" listening on http://localhost:${port}`);
|
|
141
|
+
console.log(`[nova] OpenAPI spec available at http://localhost:${port}/openapi.json`);
|
|
142
|
+
// Log available endpoints
|
|
143
|
+
Object.entries(def.actions).forEach(([actionName, actionDef]) => {
|
|
144
|
+
const method = actionDef.method || 'POST';
|
|
145
|
+
const path = actionDef.path || `/${actionName}`;
|
|
146
|
+
console.log(`[nova] ${method} ${path} -> ${actionName}`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return server;
|
|
150
|
+
}
|
|
151
|
+
/*──────────────── Runtime harness (Supabase RPC) - UNCHANGED ───────────────*/
|
|
47
152
|
const RUNTIME_SUPABASE_URL = process.env.RUNTIME_SUPABASE_URL;
|
|
48
153
|
const RUNTIME_SUPABASE_KEY = process.env.RUNTIME_SUPABASE_SERVICE_ROLE_KEY;
|
|
49
154
|
const runtime = RUNTIME_SUPABASE_URL && RUNTIME_SUPABASE_KEY
|
|
@@ -78,6 +183,47 @@ async function runWorker(def) {
|
|
|
78
183
|
await nack(msg.msg_id, def.queue);
|
|
79
184
|
continue;
|
|
80
185
|
}
|
|
186
|
+
// FGA enforcement (unchanged from original)
|
|
187
|
+
const hints = act.fga ? (Array.isArray(act.fga) ? act.fga : [act.fga]) : [];
|
|
188
|
+
if (hints.length) {
|
|
189
|
+
const apiEndpoint = process.env.OPENFGA_API_ENDPOINT;
|
|
190
|
+
const storeId = process.env.OPENFGA_STORE_ID;
|
|
191
|
+
const authToken = process.env.OPENFGA_AUTH_TOKEN;
|
|
192
|
+
if (!apiEndpoint || !storeId || !authToken) {
|
|
193
|
+
throw new Error('Missing OPENFGA_API_ENDPOINT, OPENFGA_STORE_ID, or OPENFGA_AUTH_TOKEN for FGA enforcement');
|
|
194
|
+
}
|
|
195
|
+
const fgaClient = new sdk_1.OpenFgaClient({ apiUrl: apiEndpoint, storeId });
|
|
196
|
+
let authorized = true;
|
|
197
|
+
for (const hint of hints) {
|
|
198
|
+
const key = hint.resourceIdKey;
|
|
199
|
+
if (!key) {
|
|
200
|
+
throw new Error(`Missing resourceIdKey for FGA hint on action '${actName}'`);
|
|
201
|
+
}
|
|
202
|
+
const id = payload[key];
|
|
203
|
+
if (!id) {
|
|
204
|
+
throw new Error(`Payload field '${key}' required for FGA hint on action '${actName}'`);
|
|
205
|
+
}
|
|
206
|
+
const tupleKey = {
|
|
207
|
+
user: `user:${user_id}`,
|
|
208
|
+
relation: hint.relation,
|
|
209
|
+
object: `${hint.resourceType}:${id}`
|
|
210
|
+
};
|
|
211
|
+
const result = await fgaClient.check({
|
|
212
|
+
user: tupleKey.user,
|
|
213
|
+
relation: tupleKey.relation,
|
|
214
|
+
object: tupleKey.object
|
|
215
|
+
});
|
|
216
|
+
if (!result.allowed) {
|
|
217
|
+
authorized = false;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!authorized) {
|
|
222
|
+
await runtime.from("jobs").update({ status: "failed", error: "Unauthorized" }).eq("id", jobId);
|
|
223
|
+
await ack(msg.msg_id, def.queue);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
81
227
|
try {
|
|
82
228
|
const parsedInput = act.input.parse(payload);
|
|
83
229
|
const ctx = {
|
|
@@ -104,17 +250,59 @@ async function nack(id, q) {
|
|
|
104
250
|
await runtime.schema("pgmq_public").rpc("nack", { queue_name: q, message_id: id });
|
|
105
251
|
}
|
|
106
252
|
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
107
|
-
|
|
253
|
+
/*──────────────── NEW: OpenAPI Spec Generation ───────────────*/
|
|
254
|
+
async function generateOpenAPISpec(def) {
|
|
255
|
+
// This would use oRPC's built-in OpenAPI generation
|
|
256
|
+
// For now, return a basic spec structure
|
|
257
|
+
return {
|
|
258
|
+
openapi: "3.0.0",
|
|
259
|
+
info: {
|
|
260
|
+
title: `${def.name} API`,
|
|
261
|
+
version: "1.0.0",
|
|
262
|
+
description: `OpenAPI specification for Nova worker: ${def.name}`
|
|
263
|
+
},
|
|
264
|
+
paths: Object.entries(def.actions).reduce((paths, [actionName, actionDef]) => {
|
|
265
|
+
const method = (actionDef.method || 'POST').toLowerCase();
|
|
266
|
+
const path = actionDef.path || `/${actionName}`;
|
|
267
|
+
paths[path] = {
|
|
268
|
+
[method]: {
|
|
269
|
+
operationId: actionName,
|
|
270
|
+
summary: `Execute ${actionName} action`,
|
|
271
|
+
requestBody: {
|
|
272
|
+
content: {
|
|
273
|
+
'application/json': {
|
|
274
|
+
schema: { type: 'object' } // Would be generated from Zod schema
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
responses: {
|
|
279
|
+
'200': {
|
|
280
|
+
description: 'Success',
|
|
281
|
+
content: {
|
|
282
|
+
'application/json': {
|
|
283
|
+
schema: { type: 'object' } // Would be generated from Zod schema
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
return paths;
|
|
291
|
+
}, {})
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/*──────────────── HTTP Server Harness (Enhanced) ───────────────*/
|
|
108
295
|
const express_1 = __importDefault(require("express"));
|
|
109
296
|
const body_parser_1 = __importDefault(require("body-parser"));
|
|
110
297
|
/**
|
|
111
|
-
*
|
|
298
|
+
* Enhanced HTTP server exposing each action under configurable routes
|
|
112
299
|
*/
|
|
113
300
|
function runHttpServer(def, opts = {}) {
|
|
114
301
|
const app = (0, express_1.default)();
|
|
115
302
|
app.use(body_parser_1.default.json());
|
|
116
303
|
for (const [actionName, act] of Object.entries(def.actions)) {
|
|
117
|
-
const
|
|
304
|
+
const method = (act.method || 'POST').toLowerCase();
|
|
305
|
+
const route = act.path || `/${def.name}/${actionName}`;
|
|
118
306
|
// unified handler: parse JSON body or default to empty object
|
|
119
307
|
const handler = async (req, res) => {
|
|
120
308
|
try {
|
|
@@ -129,16 +317,21 @@ function runHttpServer(def, opts = {}) {
|
|
|
129
317
|
res.status(400).json({ error: err.message });
|
|
130
318
|
}
|
|
131
319
|
};
|
|
132
|
-
//
|
|
133
|
-
app
|
|
134
|
-
// expose health under GET /health for liveness checks
|
|
135
|
-
if (actionName === 'health') {
|
|
136
|
-
app.get(
|
|
320
|
+
// Register route with specified HTTP method
|
|
321
|
+
app[method](route, handler);
|
|
322
|
+
// Special case: expose health checks under GET /health for liveness checks
|
|
323
|
+
if (actionName === 'health' && method !== 'get') {
|
|
324
|
+
app.get('/health', handler);
|
|
137
325
|
}
|
|
138
326
|
}
|
|
139
327
|
const port = opts.port ?? (process.env.PORT ? parseInt(process.env.PORT) : 3000);
|
|
140
328
|
app.listen(port, () => {
|
|
141
329
|
console.log(`[nova] HTTP server listening on http://localhost:${port}`);
|
|
330
|
+
Object.entries(def.actions).forEach(([actionName, actionDef]) => {
|
|
331
|
+
const method = (actionDef.method || 'POST').toUpperCase();
|
|
332
|
+
const path = actionDef.path || `/${def.name}/${actionName}`;
|
|
333
|
+
console.log(`[nova] ${method} ${path} -> ${actionName}`);
|
|
334
|
+
});
|
|
142
335
|
});
|
|
143
336
|
}
|
|
144
337
|
// YAML spec parsing utility
|
package/dist/parseSpec.d.ts
CHANGED
|
@@ -7,20 +7,8 @@ export declare const NovaSpecSchema: z.ZodObject<{
|
|
|
7
7
|
displayName: z.ZodOptional<z.ZodString>;
|
|
8
8
|
description: z.ZodOptional<z.ZodString>;
|
|
9
9
|
icon: z.ZodOptional<z.ZodString>;
|
|
10
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
11
|
-
},
|
|
12
|
-
name: string;
|
|
13
|
-
displayName?: string | undefined;
|
|
14
|
-
description?: string | undefined;
|
|
15
|
-
icon?: string | undefined;
|
|
16
|
-
tags?: string[] | undefined;
|
|
17
|
-
}, {
|
|
18
|
-
name: string;
|
|
19
|
-
displayName?: string | undefined;
|
|
20
|
-
description?: string | undefined;
|
|
21
|
-
icon?: string | undefined;
|
|
22
|
-
tags?: string[] | undefined;
|
|
23
|
-
}>;
|
|
10
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
|
+
}, z.core.$strip>;
|
|
24
12
|
spec: z.ZodObject<{
|
|
25
13
|
runtime: z.ZodObject<{
|
|
26
14
|
type: z.ZodString;
|
|
@@ -28,14 +16,8 @@ export declare const NovaSpecSchema: z.ZodObject<{
|
|
|
28
16
|
resources: z.ZodObject<{
|
|
29
17
|
cpu: z.ZodString;
|
|
30
18
|
memory: z.ZodString;
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
memory: string;
|
|
34
|
-
}, {
|
|
35
|
-
cpu: string;
|
|
36
|
-
memory: string;
|
|
37
|
-
}>;
|
|
38
|
-
command: z.ZodArray<z.ZodString, "many">;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
command: z.ZodArray<z.ZodString>;
|
|
39
21
|
queue: z.ZodString;
|
|
40
22
|
port: z.ZodNumber;
|
|
41
23
|
envSpec: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -43,50 +25,8 @@ export declare const NovaSpecSchema: z.ZodObject<{
|
|
|
43
25
|
value: z.ZodOptional<z.ZodString>;
|
|
44
26
|
secret: z.ZodOptional<z.ZodBoolean>;
|
|
45
27
|
default: z.ZodOptional<z.ZodString>;
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
value?: string | undefined;
|
|
49
|
-
secret?: boolean | undefined;
|
|
50
|
-
default?: string | undefined;
|
|
51
|
-
}, {
|
|
52
|
-
name: string;
|
|
53
|
-
value?: string | undefined;
|
|
54
|
-
secret?: boolean | undefined;
|
|
55
|
-
default?: string | undefined;
|
|
56
|
-
}>, "many">>;
|
|
57
|
-
}, "strip", z.ZodTypeAny, {
|
|
58
|
-
type: string;
|
|
59
|
-
image: string;
|
|
60
|
-
resources: {
|
|
61
|
-
cpu: string;
|
|
62
|
-
memory: string;
|
|
63
|
-
};
|
|
64
|
-
command: string[];
|
|
65
|
-
queue: string;
|
|
66
|
-
port: number;
|
|
67
|
-
envSpec?: {
|
|
68
|
-
name: string;
|
|
69
|
-
value?: string | undefined;
|
|
70
|
-
secret?: boolean | undefined;
|
|
71
|
-
default?: string | undefined;
|
|
72
|
-
}[] | undefined;
|
|
73
|
-
}, {
|
|
74
|
-
type: string;
|
|
75
|
-
image: string;
|
|
76
|
-
resources: {
|
|
77
|
-
cpu: string;
|
|
78
|
-
memory: string;
|
|
79
|
-
};
|
|
80
|
-
command: string[];
|
|
81
|
-
queue: string;
|
|
82
|
-
port: number;
|
|
83
|
-
envSpec?: {
|
|
84
|
-
name: string;
|
|
85
|
-
value?: string | undefined;
|
|
86
|
-
secret?: boolean | undefined;
|
|
87
|
-
default?: string | undefined;
|
|
88
|
-
}[] | undefined;
|
|
89
|
-
}>;
|
|
28
|
+
}, z.core.$strip>>>;
|
|
29
|
+
}, z.core.$strip>;
|
|
90
30
|
actions: z.ZodArray<z.ZodObject<{
|
|
91
31
|
name: z.ZodString;
|
|
92
32
|
displayName: z.ZodOptional<z.ZodString>;
|
|
@@ -98,341 +38,33 @@ export declare const NovaSpecSchema: z.ZodObject<{
|
|
|
98
38
|
schema: z.ZodOptional<z.ZodObject<{
|
|
99
39
|
input: z.ZodString;
|
|
100
40
|
output: z.ZodString;
|
|
101
|
-
},
|
|
102
|
-
input: string;
|
|
103
|
-
output: string;
|
|
104
|
-
}, {
|
|
105
|
-
input: string;
|
|
106
|
-
output: string;
|
|
107
|
-
}>>;
|
|
41
|
+
}, z.core.$strip>>;
|
|
108
42
|
fga: z.ZodOptional<z.ZodObject<{
|
|
109
43
|
resourceType: z.ZodString;
|
|
110
44
|
relation: z.ZodString;
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}, {
|
|
115
|
-
resourceType: string;
|
|
116
|
-
relation: string;
|
|
117
|
-
}>>;
|
|
118
|
-
}, "strip", z.ZodTypeAny, {
|
|
119
|
-
name: string;
|
|
120
|
-
displayName?: string | undefined;
|
|
121
|
-
description?: string | undefined;
|
|
122
|
-
icon?: string | undefined;
|
|
123
|
-
async?: boolean | undefined;
|
|
124
|
-
input?: unknown;
|
|
125
|
-
output?: unknown;
|
|
126
|
-
schema?: {
|
|
127
|
-
input: string;
|
|
128
|
-
output: string;
|
|
129
|
-
} | undefined;
|
|
130
|
-
fga?: {
|
|
131
|
-
resourceType: string;
|
|
132
|
-
relation: string;
|
|
133
|
-
} | undefined;
|
|
134
|
-
}, {
|
|
135
|
-
name: string;
|
|
136
|
-
displayName?: string | undefined;
|
|
137
|
-
description?: string | undefined;
|
|
138
|
-
icon?: string | undefined;
|
|
139
|
-
async?: boolean | undefined;
|
|
140
|
-
input?: unknown;
|
|
141
|
-
output?: unknown;
|
|
142
|
-
schema?: {
|
|
143
|
-
input: string;
|
|
144
|
-
output: string;
|
|
145
|
-
} | undefined;
|
|
146
|
-
fga?: {
|
|
147
|
-
resourceType: string;
|
|
148
|
-
relation: string;
|
|
149
|
-
} | undefined;
|
|
150
|
-
}>, "many">;
|
|
151
|
-
}, "strip", z.ZodTypeAny, {
|
|
152
|
-
runtime: {
|
|
153
|
-
type: string;
|
|
154
|
-
image: string;
|
|
155
|
-
resources: {
|
|
156
|
-
cpu: string;
|
|
157
|
-
memory: string;
|
|
158
|
-
};
|
|
159
|
-
command: string[];
|
|
160
|
-
queue: string;
|
|
161
|
-
port: number;
|
|
162
|
-
envSpec?: {
|
|
163
|
-
name: string;
|
|
164
|
-
value?: string | undefined;
|
|
165
|
-
secret?: boolean | undefined;
|
|
166
|
-
default?: string | undefined;
|
|
167
|
-
}[] | undefined;
|
|
168
|
-
};
|
|
169
|
-
actions: {
|
|
170
|
-
name: string;
|
|
171
|
-
displayName?: string | undefined;
|
|
172
|
-
description?: string | undefined;
|
|
173
|
-
icon?: string | undefined;
|
|
174
|
-
async?: boolean | undefined;
|
|
175
|
-
input?: unknown;
|
|
176
|
-
output?: unknown;
|
|
177
|
-
schema?: {
|
|
178
|
-
input: string;
|
|
179
|
-
output: string;
|
|
180
|
-
} | undefined;
|
|
181
|
-
fga?: {
|
|
182
|
-
resourceType: string;
|
|
183
|
-
relation: string;
|
|
184
|
-
} | undefined;
|
|
185
|
-
}[];
|
|
186
|
-
}, {
|
|
187
|
-
runtime: {
|
|
188
|
-
type: string;
|
|
189
|
-
image: string;
|
|
190
|
-
resources: {
|
|
191
|
-
cpu: string;
|
|
192
|
-
memory: string;
|
|
193
|
-
};
|
|
194
|
-
command: string[];
|
|
195
|
-
queue: string;
|
|
196
|
-
port: number;
|
|
197
|
-
envSpec?: {
|
|
198
|
-
name: string;
|
|
199
|
-
value?: string | undefined;
|
|
200
|
-
secret?: boolean | undefined;
|
|
201
|
-
default?: string | undefined;
|
|
202
|
-
}[] | undefined;
|
|
203
|
-
};
|
|
204
|
-
actions: {
|
|
205
|
-
name: string;
|
|
206
|
-
displayName?: string | undefined;
|
|
207
|
-
description?: string | undefined;
|
|
208
|
-
icon?: string | undefined;
|
|
209
|
-
async?: boolean | undefined;
|
|
210
|
-
input?: unknown;
|
|
211
|
-
output?: unknown;
|
|
212
|
-
schema?: {
|
|
213
|
-
input: string;
|
|
214
|
-
output: string;
|
|
215
|
-
} | undefined;
|
|
216
|
-
fga?: {
|
|
217
|
-
resourceType: string;
|
|
218
|
-
relation: string;
|
|
219
|
-
} | undefined;
|
|
220
|
-
}[];
|
|
221
|
-
}>;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
}, z.core.$strip>>;
|
|
47
|
+
}, z.core.$strip>;
|
|
222
48
|
build: z.ZodOptional<z.ZodObject<{
|
|
223
49
|
dockerfile: z.ZodString;
|
|
224
50
|
context: z.ZodString;
|
|
225
|
-
},
|
|
226
|
-
dockerfile: string;
|
|
227
|
-
context: string;
|
|
228
|
-
}, {
|
|
229
|
-
dockerfile: string;
|
|
230
|
-
context: string;
|
|
231
|
-
}>>;
|
|
51
|
+
}, z.core.$strip>>;
|
|
232
52
|
ui: z.ZodOptional<z.ZodObject<{
|
|
233
53
|
category: z.ZodOptional<z.ZodString>;
|
|
234
54
|
color: z.ZodOptional<z.ZodString>;
|
|
235
|
-
},
|
|
236
|
-
category?: string | undefined;
|
|
237
|
-
color?: string | undefined;
|
|
238
|
-
}, {
|
|
239
|
-
category?: string | undefined;
|
|
240
|
-
color?: string | undefined;
|
|
241
|
-
}>>;
|
|
55
|
+
}, z.core.$strip>>;
|
|
242
56
|
fga: z.ZodOptional<z.ZodObject<{
|
|
243
57
|
types: z.ZodArray<z.ZodObject<{
|
|
244
58
|
name: z.ZodString;
|
|
245
|
-
relations: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodArray<z.ZodString
|
|
59
|
+
relations: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
246
60
|
computedUserset: z.ZodObject<{
|
|
247
61
|
object: z.ZodString;
|
|
248
62
|
relation: z.ZodString;
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
relation: string;
|
|
255
|
-
}>;
|
|
256
|
-
}, "strip", z.ZodTypeAny, {
|
|
257
|
-
computedUserset: {
|
|
258
|
-
object: string;
|
|
259
|
-
relation: string;
|
|
260
|
-
};
|
|
261
|
-
}, {
|
|
262
|
-
computedUserset: {
|
|
263
|
-
object: string;
|
|
264
|
-
relation: string;
|
|
265
|
-
};
|
|
266
|
-
}>]>>;
|
|
267
|
-
}, "strip", z.ZodTypeAny, {
|
|
268
|
-
name: string;
|
|
269
|
-
relations: Record<string, string[] | {
|
|
270
|
-
computedUserset: {
|
|
271
|
-
object: string;
|
|
272
|
-
relation: string;
|
|
273
|
-
};
|
|
274
|
-
}>;
|
|
275
|
-
}, {
|
|
276
|
-
name: string;
|
|
277
|
-
relations: Record<string, string[] | {
|
|
278
|
-
computedUserset: {
|
|
279
|
-
object: string;
|
|
280
|
-
relation: string;
|
|
281
|
-
};
|
|
282
|
-
}>;
|
|
283
|
-
}>, "many">;
|
|
284
|
-
}, "strip", z.ZodTypeAny, {
|
|
285
|
-
types: {
|
|
286
|
-
name: string;
|
|
287
|
-
relations: Record<string, string[] | {
|
|
288
|
-
computedUserset: {
|
|
289
|
-
object: string;
|
|
290
|
-
relation: string;
|
|
291
|
-
};
|
|
292
|
-
}>;
|
|
293
|
-
}[];
|
|
294
|
-
}, {
|
|
295
|
-
types: {
|
|
296
|
-
name: string;
|
|
297
|
-
relations: Record<string, string[] | {
|
|
298
|
-
computedUserset: {
|
|
299
|
-
object: string;
|
|
300
|
-
relation: string;
|
|
301
|
-
};
|
|
302
|
-
}>;
|
|
303
|
-
}[];
|
|
304
|
-
}>>;
|
|
305
|
-
}, "strict", z.ZodTypeAny, {
|
|
306
|
-
apiVersion: string;
|
|
307
|
-
kind: string;
|
|
308
|
-
metadata: {
|
|
309
|
-
name: string;
|
|
310
|
-
displayName?: string | undefined;
|
|
311
|
-
description?: string | undefined;
|
|
312
|
-
icon?: string | undefined;
|
|
313
|
-
tags?: string[] | undefined;
|
|
314
|
-
};
|
|
315
|
-
spec: {
|
|
316
|
-
runtime: {
|
|
317
|
-
type: string;
|
|
318
|
-
image: string;
|
|
319
|
-
resources: {
|
|
320
|
-
cpu: string;
|
|
321
|
-
memory: string;
|
|
322
|
-
};
|
|
323
|
-
command: string[];
|
|
324
|
-
queue: string;
|
|
325
|
-
port: number;
|
|
326
|
-
envSpec?: {
|
|
327
|
-
name: string;
|
|
328
|
-
value?: string | undefined;
|
|
329
|
-
secret?: boolean | undefined;
|
|
330
|
-
default?: string | undefined;
|
|
331
|
-
}[] | undefined;
|
|
332
|
-
};
|
|
333
|
-
actions: {
|
|
334
|
-
name: string;
|
|
335
|
-
displayName?: string | undefined;
|
|
336
|
-
description?: string | undefined;
|
|
337
|
-
icon?: string | undefined;
|
|
338
|
-
async?: boolean | undefined;
|
|
339
|
-
input?: unknown;
|
|
340
|
-
output?: unknown;
|
|
341
|
-
schema?: {
|
|
342
|
-
input: string;
|
|
343
|
-
output: string;
|
|
344
|
-
} | undefined;
|
|
345
|
-
fga?: {
|
|
346
|
-
resourceType: string;
|
|
347
|
-
relation: string;
|
|
348
|
-
} | undefined;
|
|
349
|
-
}[];
|
|
350
|
-
};
|
|
351
|
-
fga?: {
|
|
352
|
-
types: {
|
|
353
|
-
name: string;
|
|
354
|
-
relations: Record<string, string[] | {
|
|
355
|
-
computedUserset: {
|
|
356
|
-
object: string;
|
|
357
|
-
relation: string;
|
|
358
|
-
};
|
|
359
|
-
}>;
|
|
360
|
-
}[];
|
|
361
|
-
} | undefined;
|
|
362
|
-
build?: {
|
|
363
|
-
dockerfile: string;
|
|
364
|
-
context: string;
|
|
365
|
-
} | undefined;
|
|
366
|
-
ui?: {
|
|
367
|
-
category?: string | undefined;
|
|
368
|
-
color?: string | undefined;
|
|
369
|
-
} | undefined;
|
|
370
|
-
}, {
|
|
371
|
-
apiVersion: string;
|
|
372
|
-
kind: string;
|
|
373
|
-
metadata: {
|
|
374
|
-
name: string;
|
|
375
|
-
displayName?: string | undefined;
|
|
376
|
-
description?: string | undefined;
|
|
377
|
-
icon?: string | undefined;
|
|
378
|
-
tags?: string[] | undefined;
|
|
379
|
-
};
|
|
380
|
-
spec: {
|
|
381
|
-
runtime: {
|
|
382
|
-
type: string;
|
|
383
|
-
image: string;
|
|
384
|
-
resources: {
|
|
385
|
-
cpu: string;
|
|
386
|
-
memory: string;
|
|
387
|
-
};
|
|
388
|
-
command: string[];
|
|
389
|
-
queue: string;
|
|
390
|
-
port: number;
|
|
391
|
-
envSpec?: {
|
|
392
|
-
name: string;
|
|
393
|
-
value?: string | undefined;
|
|
394
|
-
secret?: boolean | undefined;
|
|
395
|
-
default?: string | undefined;
|
|
396
|
-
}[] | undefined;
|
|
397
|
-
};
|
|
398
|
-
actions: {
|
|
399
|
-
name: string;
|
|
400
|
-
displayName?: string | undefined;
|
|
401
|
-
description?: string | undefined;
|
|
402
|
-
icon?: string | undefined;
|
|
403
|
-
async?: boolean | undefined;
|
|
404
|
-
input?: unknown;
|
|
405
|
-
output?: unknown;
|
|
406
|
-
schema?: {
|
|
407
|
-
input: string;
|
|
408
|
-
output: string;
|
|
409
|
-
} | undefined;
|
|
410
|
-
fga?: {
|
|
411
|
-
resourceType: string;
|
|
412
|
-
relation: string;
|
|
413
|
-
} | undefined;
|
|
414
|
-
}[];
|
|
415
|
-
};
|
|
416
|
-
fga?: {
|
|
417
|
-
types: {
|
|
418
|
-
name: string;
|
|
419
|
-
relations: Record<string, string[] | {
|
|
420
|
-
computedUserset: {
|
|
421
|
-
object: string;
|
|
422
|
-
relation: string;
|
|
423
|
-
};
|
|
424
|
-
}>;
|
|
425
|
-
}[];
|
|
426
|
-
} | undefined;
|
|
427
|
-
build?: {
|
|
428
|
-
dockerfile: string;
|
|
429
|
-
context: string;
|
|
430
|
-
} | undefined;
|
|
431
|
-
ui?: {
|
|
432
|
-
category?: string | undefined;
|
|
433
|
-
color?: string | undefined;
|
|
434
|
-
} | undefined;
|
|
435
|
-
}>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
}, z.core.$strip>]>>;
|
|
65
|
+
}, z.core.$strip>>;
|
|
66
|
+
}, z.core.$strip>>;
|
|
67
|
+
}, z.core.$strict>;
|
|
436
68
|
export type NovaSpec = z.infer<typeof NovaSpecSchema>;
|
|
437
69
|
/**
|
|
438
70
|
* Parse nova.yaml content and validate against the NovaSpec schema.
|
package/dist/parseSpec.js
CHANGED
|
@@ -95,8 +95,8 @@ function parseNovaSpec(yamlContent) {
|
|
|
95
95
|
}
|
|
96
96
|
catch (e) {
|
|
97
97
|
if (e instanceof zod_1.z.ZodError) {
|
|
98
|
-
const details = e.
|
|
99
|
-
.map(
|
|
98
|
+
const details = e.issues
|
|
99
|
+
.map((issue) => `Path '${issue.path.join(".")}': ${issue.message}`)
|
|
100
100
|
.join("; ");
|
|
101
101
|
throw new Error(`nova.yaml validation error(s): ${details}`);
|
|
102
102
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for code-level defineWorker() argument.
|
|
4
|
+
* This enforces worker metadata, actions, envSpec, and optional FGA hints.
|
|
5
|
+
*/
|
|
6
|
+
export declare const WorkerDefSchema: z.ZodObject<{
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
queue: z.ZodString;
|
|
9
|
+
envSpec: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
10
|
+
name: z.ZodString;
|
|
11
|
+
secret: z.ZodBoolean;
|
|
12
|
+
default: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>>>;
|
|
14
|
+
fga: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
types: z.ZodArray<z.ZodObject<{
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
relations: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
18
|
+
computedUserset: z.ZodObject<{
|
|
19
|
+
object: z.ZodString;
|
|
20
|
+
relation: z.ZodString;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
}, z.core.$strip>]>>;
|
|
23
|
+
}, z.core.$strip>>;
|
|
24
|
+
}, z.core.$strip>>;
|
|
25
|
+
actions: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
26
|
+
name: z.ZodOptional<z.ZodString>;
|
|
27
|
+
input: z.ZodAny;
|
|
28
|
+
output: z.ZodAny;
|
|
29
|
+
method: z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
GET: "GET";
|
|
31
|
+
POST: "POST";
|
|
32
|
+
PUT: "PUT";
|
|
33
|
+
DELETE: "DELETE";
|
|
34
|
+
PATCH: "PATCH";
|
|
35
|
+
}>>;
|
|
36
|
+
path: z.ZodOptional<z.ZodString>;
|
|
37
|
+
fga: z.ZodOptional<z.ZodObject<{
|
|
38
|
+
resourceType: z.ZodString;
|
|
39
|
+
relation: z.ZodString;
|
|
40
|
+
resourceIdKey: z.ZodOptional<z.ZodString>;
|
|
41
|
+
policy: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>>;
|
|
43
|
+
handler: z.ZodAny;
|
|
44
|
+
}, z.core.$strip>>;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
export type WorkerDef = z.infer<typeof WorkerDefSchema>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkerDefSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/**
|
|
6
|
+
* Schema for code-level defineWorker() argument.
|
|
7
|
+
* This enforces worker metadata, actions, envSpec, and optional FGA hints.
|
|
8
|
+
*/
|
|
9
|
+
exports.WorkerDefSchema = zod_1.z.object({
|
|
10
|
+
name: zod_1.z.string(),
|
|
11
|
+
queue: zod_1.z.string(),
|
|
12
|
+
// Optional environment variable spec for nova.yaml generation
|
|
13
|
+
envSpec: zod_1.z.array(zod_1.z.object({ name: zod_1.z.string(), secret: zod_1.z.boolean(), default: zod_1.z.string().optional() })).optional(),
|
|
14
|
+
// Optional top-level FGA policy types
|
|
15
|
+
fga: zod_1.z.object({
|
|
16
|
+
types: zod_1.z.array(zod_1.z.object({
|
|
17
|
+
name: zod_1.z.string(),
|
|
18
|
+
relations: zod_1.z.record(zod_1.z.string(), zod_1.z.union([
|
|
19
|
+
zod_1.z.array(zod_1.z.string()),
|
|
20
|
+
zod_1.z.object({ computedUserset: zod_1.z.object({ object: zod_1.z.string(), relation: zod_1.z.string() }) })
|
|
21
|
+
])),
|
|
22
|
+
}))
|
|
23
|
+
}).optional(),
|
|
24
|
+
// Map of action definitions
|
|
25
|
+
actions: zod_1.z.record(zod_1.z.string(), zod_1.z.object({
|
|
26
|
+
name: zod_1.z.string().optional(),
|
|
27
|
+
input: zod_1.z.any(), // Zod schema instance expected
|
|
28
|
+
output: zod_1.z.any(), // Zod schema instance expected
|
|
29
|
+
// NEW: HTTP routing support for oRPC
|
|
30
|
+
method: zod_1.z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional(),
|
|
31
|
+
path: zod_1.z.string().optional(),
|
|
32
|
+
// Optional per-action OpenFGA hints: resource type, relation, ID key, and optional policy caveat
|
|
33
|
+
fga: zod_1.z.object({
|
|
34
|
+
resourceType: zod_1.z.string(),
|
|
35
|
+
relation: zod_1.z.string(),
|
|
36
|
+
resourceIdKey: zod_1.z.string().optional(),
|
|
37
|
+
policy: zod_1.z.string().optional(),
|
|
38
|
+
}).optional(),
|
|
39
|
+
handler: zod_1.z.any(), // function with signature (input, ctx) => Promise<…>
|
|
40
|
+
})),
|
|
41
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newhomestar/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Type-safe SDK for building Nova pipelines (workers & functions)",
|
|
5
5
|
"homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -22,12 +22,16 @@
|
|
|
22
22
|
"build": "tsc"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@openfga/sdk": "^0.9.0",
|
|
26
|
+
"@orpc/openapi": "^0.0.53",
|
|
27
|
+
"@orpc/server": "^0.0.53",
|
|
28
|
+
"@orpc/zod": "^0.0.53",
|
|
25
29
|
"@supabase/supabase-js": "^2.39.0",
|
|
30
|
+
"body-parser": "^1.20.2",
|
|
26
31
|
"dotenv": "^16.4.3",
|
|
27
|
-
"zod": "^3.23.8",
|
|
28
32
|
"express": "^4.18.2",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
33
|
+
"yaml": "^2.7.1",
|
|
34
|
+
"zod": "^4.0.5"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
33
37
|
"@types/node": "^20.11.17",
|