@newhomestar/sdk 0.2.6 → 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 CHANGED
@@ -1,9 +1,18 @@
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;
5
6
  output: O;
6
7
  handler: (input: z.infer<I>, ctx: ActionCtx) => Promise<z.infer<O>>;
8
+ fga?: {
9
+ resourceType: string;
10
+ relation: string;
11
+ resourceIdKey?: string;
12
+ policy?: string;
13
+ };
14
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
15
+ path?: string;
7
16
  }
8
17
  export interface ActionCtx {
9
18
  jobId: string;
@@ -13,28 +22,58 @@ export declare function action<I extends ZodTypeAny, O extends ZodTypeAny>(cfg:
13
22
  name?: string;
14
23
  input: I;
15
24
  output: O;
25
+ fga?: {
26
+ resourceType: string;
27
+ relation: string;
28
+ resourceIdKey?: string;
29
+ policy?: string;
30
+ };
31
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
32
+ path?: string;
16
33
  handler: (input: z.infer<I>, ctx: ActionCtx) => Promise<z.infer<O>>;
17
34
  }): ActionDef<I, O>;
18
- export interface WorkerDef {
19
- name: string;
20
- queue: string;
21
- actions: Record<string, ActionDef<any, any>>;
22
- envSpec?: {
23
- name: string;
24
- secret: boolean;
25
- default?: string;
26
- }[];
27
- }
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
+ */
28
44
  export declare function defineWorker<T extends WorkerDef>(def: T): T;
29
45
  /** Enqueue an async action and receive `{ job_id }` */
30
46
  export declare function enqueue<P extends object>(actionPath: `${string}.${string}`, payload: P): Promise<{
31
47
  job_id: string;
32
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>;
33
61
  export declare function runWorker(def: WorkerDef): Promise<void>;
34
- export type { ZodTypeAny as SchemaAny, ZodTypeAny };
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
+ }>;
35
71
  /**
36
- * Spin up an HTTP server exposing each action under POST /<worker>/<action>
72
+ * Enhanced HTTP server exposing each action under configurable routes
37
73
  */
38
- export declare function runHttpServer(def: WorkerDef, opts?: {
74
+ export declare function runHttpServer<T extends WorkerDef>(def: T, opts?: {
39
75
  port?: number;
40
76
  }): void;
77
+ export type { ZodTypeAny as SchemaAny, ZodTypeAny };
78
+ export { parseNovaSpec } from "./parseSpec";
79
+ export type { NovaSpec } from "./parseSpec";
package/dist/index.js CHANGED
@@ -3,25 +3,61 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseNovaSpec = void 0;
6
7
  exports.action = action;
7
8
  exports.defineWorker = defineWorker;
8
9
  exports.enqueue = enqueue;
10
+ exports.createORPCRouter = createORPCRouter;
11
+ exports.runORPCServer = runORPCServer;
9
12
  exports.runWorker = runWorker;
13
+ exports.generateOpenAPISpec = generateOpenAPISpec;
10
14
  exports.runHttpServer = runHttpServer;
11
15
  const dotenv_1 = __importDefault(require("dotenv"));
12
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");
13
38
  if (!process.env.RUNTIME_SUPABASE_URL) {
14
39
  // local dev – read .env.local
15
40
  dotenv_1.default.config({ path: ".env.local", override: true });
16
41
  }
17
42
  function action(cfg) {
18
- return { name: cfg.name ?? "unnamed", ...cfg };
43
+ return {
44
+ name: cfg.name ?? "unnamed",
45
+ method: cfg.method ?? 'POST',
46
+ path: cfg.path,
47
+ ...cfg
48
+ };
19
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
+ */
20
55
  function defineWorker(def) {
21
- return def; // identity – type info retained for CLI
56
+ // Runtime validation of the worker definition
57
+ workerSchema_1.WorkerDefSchema.parse(def);
58
+ return def;
22
59
  }
23
- /*──────────────────────* Client‑side enqueue() *──────────────────*/
24
- // CLIENT_SUPABASE_… vars exist in gateways / other services
60
+ /*──────────────────────* Client‑side enqueue() (UNCHANGED) *──────────────────*/
25
61
  const CLIENT_SUPABASE_URL = process.env.CLIENT_SUPABASE_PUBLIC_URL;
26
62
  const CLIENT_SUPABASE_KEY = process.env.CLIENT_SUPABASE_SERVICE_ROLE_KEY;
27
63
  let clientSupabase;
@@ -42,7 +78,77 @@ async function enqueue(actionPath, payload) {
42
78
  throw error;
43
79
  return data;
44
80
  }
45
- /*──────────────── Runtime harness (Supabase RPC) ───────────────*/
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 ───────────────*/
46
152
  const RUNTIME_SUPABASE_URL = process.env.RUNTIME_SUPABASE_URL;
47
153
  const RUNTIME_SUPABASE_KEY = process.env.RUNTIME_SUPABASE_SERVICE_ROLE_KEY;
48
154
  const runtime = RUNTIME_SUPABASE_URL && RUNTIME_SUPABASE_KEY
@@ -77,6 +183,47 @@ async function runWorker(def) {
77
183
  await nack(msg.msg_id, def.queue);
78
184
  continue;
79
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
+ }
80
227
  try {
81
228
  const parsedInput = act.input.parse(payload);
82
229
  const ctx = {
@@ -103,20 +250,64 @@ async function nack(id, q) {
103
250
  await runtime.schema("pgmq_public").rpc("nack", { queue_name: q, message_id: id });
104
251
  }
105
252
  function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
106
- //──────────────────── HTTP Server Harness ─────────────────────
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) ───────────────*/
107
295
  const express_1 = __importDefault(require("express"));
108
296
  const body_parser_1 = __importDefault(require("body-parser"));
109
297
  /**
110
- * Spin up an HTTP server exposing each action under POST /<worker>/<action>
298
+ * Enhanced HTTP server exposing each action under configurable routes
111
299
  */
112
300
  function runHttpServer(def, opts = {}) {
113
301
  const app = (0, express_1.default)();
114
302
  app.use(body_parser_1.default.json());
115
303
  for (const [actionName, act] of Object.entries(def.actions)) {
116
- const route = `/${def.name}/${actionName}`;
117
- app.post(route, async (req, res) => {
304
+ const method = (act.method || 'POST').toLowerCase();
305
+ const route = act.path || `/${def.name}/${actionName}`;
306
+ // unified handler: parse JSON body or default to empty object
307
+ const handler = async (req, res) => {
118
308
  try {
119
- const input = act.input.parse(req.body);
309
+ const payload = req.body && typeof req.body === 'object' ? req.body : {};
310
+ const input = act.input.parse(payload);
120
311
  const ctx = { jobId: `http-${Date.now()}`, progress: () => { } };
121
312
  const out = await act.handler(input, ctx);
122
313
  act.output.parse(out);
@@ -125,10 +316,24 @@ function runHttpServer(def, opts = {}) {
125
316
  catch (err) {
126
317
  res.status(400).json({ error: err.message });
127
318
  }
128
- });
319
+ };
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);
325
+ }
129
326
  }
130
327
  const port = opts.port ?? (process.env.PORT ? parseInt(process.env.PORT) : 3000);
131
328
  app.listen(port, () => {
132
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
+ });
133
335
  });
134
336
  }
337
+ // YAML spec parsing utility
338
+ var parseSpec_1 = require("./parseSpec");
339
+ Object.defineProperty(exports, "parseNovaSpec", { enumerable: true, get: function () { return parseSpec_1.parseNovaSpec; } });
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ export declare const NovaSpecSchema: z.ZodObject<{
3
+ apiVersion: z.ZodString;
4
+ kind: z.ZodString;
5
+ metadata: z.ZodObject<{
6
+ name: z.ZodString;
7
+ displayName: z.ZodOptional<z.ZodString>;
8
+ description: z.ZodOptional<z.ZodString>;
9
+ icon: z.ZodOptional<z.ZodString>;
10
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ }, z.core.$strip>;
12
+ spec: z.ZodObject<{
13
+ runtime: z.ZodObject<{
14
+ type: z.ZodString;
15
+ image: z.ZodString;
16
+ resources: z.ZodObject<{
17
+ cpu: z.ZodString;
18
+ memory: z.ZodString;
19
+ }, z.core.$strip>;
20
+ command: z.ZodArray<z.ZodString>;
21
+ queue: z.ZodString;
22
+ port: z.ZodNumber;
23
+ envSpec: z.ZodOptional<z.ZodArray<z.ZodObject<{
24
+ name: z.ZodString;
25
+ value: z.ZodOptional<z.ZodString>;
26
+ secret: z.ZodOptional<z.ZodBoolean>;
27
+ default: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$strip>>>;
29
+ }, z.core.$strip>;
30
+ actions: z.ZodArray<z.ZodObject<{
31
+ name: z.ZodString;
32
+ displayName: z.ZodOptional<z.ZodString>;
33
+ description: z.ZodOptional<z.ZodString>;
34
+ icon: z.ZodOptional<z.ZodString>;
35
+ async: z.ZodOptional<z.ZodBoolean>;
36
+ input: z.ZodOptional<z.ZodUnknown>;
37
+ output: z.ZodOptional<z.ZodUnknown>;
38
+ schema: z.ZodOptional<z.ZodObject<{
39
+ input: z.ZodString;
40
+ output: z.ZodString;
41
+ }, z.core.$strip>>;
42
+ fga: z.ZodOptional<z.ZodObject<{
43
+ resourceType: z.ZodString;
44
+ relation: z.ZodString;
45
+ }, z.core.$strip>>;
46
+ }, z.core.$strip>>;
47
+ }, z.core.$strip>;
48
+ build: z.ZodOptional<z.ZodObject<{
49
+ dockerfile: z.ZodString;
50
+ context: z.ZodString;
51
+ }, z.core.$strip>>;
52
+ ui: z.ZodOptional<z.ZodObject<{
53
+ category: z.ZodOptional<z.ZodString>;
54
+ color: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strip>>;
56
+ fga: z.ZodOptional<z.ZodObject<{
57
+ types: z.ZodArray<z.ZodObject<{
58
+ name: z.ZodString;
59
+ relations: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
60
+ computedUserset: z.ZodObject<{
61
+ object: z.ZodString;
62
+ relation: z.ZodString;
63
+ }, z.core.$strip>;
64
+ }, z.core.$strip>]>>;
65
+ }, z.core.$strip>>;
66
+ }, z.core.$strip>>;
67
+ }, z.core.$strict>;
68
+ export type NovaSpec = z.infer<typeof NovaSpecSchema>;
69
+ /**
70
+ * Parse nova.yaml content and validate against the NovaSpec schema.
71
+ * Unknown fields are stripped; missing required fields throw an error.
72
+ * @param yamlContent String contents of a nova.yaml specification
73
+ * @returns Parsed NovaSpec object
74
+ * @throws Error with validation details if parsing or validation fail
75
+ */
76
+ export declare function parseNovaSpec(yamlContent: string): NovaSpec;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NovaSpecSchema = void 0;
4
+ exports.parseNovaSpec = parseNovaSpec;
5
+ const yaml_1 = require("yaml");
6
+ const zod_1 = require("zod");
7
+ // Zod schema for nova.yaml
8
+ exports.NovaSpecSchema = zod_1.z.object({
9
+ apiVersion: zod_1.z.string(),
10
+ kind: zod_1.z.string(),
11
+ metadata: zod_1.z.object({
12
+ name: zod_1.z.string(),
13
+ displayName: zod_1.z.string().optional(),
14
+ description: zod_1.z.string().optional(),
15
+ icon: zod_1.z.string().optional(),
16
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
17
+ }),
18
+ spec: zod_1.z.object({
19
+ runtime: zod_1.z.object({
20
+ type: zod_1.z.string(),
21
+ image: zod_1.z.string(),
22
+ resources: zod_1.z.object({
23
+ cpu: zod_1.z.string(),
24
+ memory: zod_1.z.string(),
25
+ }),
26
+ command: zod_1.z.array(zod_1.z.string()),
27
+ queue: zod_1.z.string(),
28
+ port: zod_1.z.number(),
29
+ envSpec: zod_1.z.array(zod_1.z.object({
30
+ name: zod_1.z.string(),
31
+ value: zod_1.z.string().optional(),
32
+ secret: zod_1.z.boolean().optional(),
33
+ default: zod_1.z.string().optional(),
34
+ })).optional(),
35
+ }),
36
+ actions: zod_1.z.array(zod_1.z.object({
37
+ name: zod_1.z.string(),
38
+ displayName: zod_1.z.string().optional(),
39
+ description: zod_1.z.string().optional(),
40
+ icon: zod_1.z.string().optional(),
41
+ async: zod_1.z.boolean().optional(),
42
+ input: zod_1.z.unknown().optional(), // JSON schema object
43
+ output: zod_1.z.unknown().optional(), // JSON schema object
44
+ schema: zod_1.z.object({
45
+ input: zod_1.z.string(),
46
+ output: zod_1.z.string(),
47
+ }).optional(),
48
+ fga: zod_1.z.object({
49
+ resourceType: zod_1.z.string(),
50
+ relation: zod_1.z.string(),
51
+ }).optional(),
52
+ })),
53
+ }),
54
+ build: zod_1.z.object({
55
+ dockerfile: zod_1.z.string(),
56
+ context: zod_1.z.string(),
57
+ }).optional(),
58
+ ui: zod_1.z.object({
59
+ category: zod_1.z.string().optional(),
60
+ color: zod_1.z.string().optional(),
61
+ }).optional(),
62
+ // OpenFGA policy hints embedded in nova.yaml
63
+ fga: zod_1.z.object({
64
+ types: zod_1.z.array(zod_1.z.object({
65
+ name: zod_1.z.string(),
66
+ relations: zod_1.z.record(zod_1.z.string(), zod_1.z.union([
67
+ zod_1.z.array(zod_1.z.string()),
68
+ zod_1.z.object({
69
+ computedUserset: zod_1.z.object({
70
+ object: zod_1.z.string(),
71
+ relation: zod_1.z.string(),
72
+ }),
73
+ }),
74
+ ])),
75
+ })),
76
+ }).optional(),
77
+ }).strict();
78
+ /**
79
+ * Parse nova.yaml content and validate against the NovaSpec schema.
80
+ * Unknown fields are stripped; missing required fields throw an error.
81
+ * @param yamlContent String contents of a nova.yaml specification
82
+ * @returns Parsed NovaSpec object
83
+ * @throws Error with validation details if parsing or validation fail
84
+ */
85
+ function parseNovaSpec(yamlContent) {
86
+ let parsed;
87
+ try {
88
+ parsed = (0, yaml_1.parse)(yamlContent);
89
+ }
90
+ catch (e) {
91
+ throw new Error(`Failed to parse YAML content: ${e.message}`);
92
+ }
93
+ try {
94
+ return exports.NovaSpecSchema.parse(parsed);
95
+ }
96
+ catch (e) {
97
+ if (e instanceof zod_1.z.ZodError) {
98
+ const details = e.issues
99
+ .map((issue) => `Path '${issue.path.join(".")}': ${issue.message}`)
100
+ .join("; ");
101
+ throw new Error(`nova.yaml validation error(s): ${details}`);
102
+ }
103
+ throw e;
104
+ }
105
+ }
@@ -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,35 +1,40 @@
1
- {
2
- "name": "@newhomestar/sdk",
3
- "version": "0.2.6",
4
- "description": "Type-safe SDK for building Nova pipelines (workers & functions)",
5
- "homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
6
- "bugs": {
7
- "url": "https://github.com/newhomestar/nova-node-sdk/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/newhomestar/nova-node-sdk.git"
12
- },
13
- "license": "ISC",
14
- "author": "Christian Gomez",
15
- "type": "commonjs",
16
- "main": "dist/index.js",
17
- "types": "dist/index.d.ts",
18
- "files": [
19
- "dist"
20
- ],
21
- "scripts": {
22
- "build": "tsc"
23
- },
24
- "dependencies": {
25
- "@supabase/supabase-js": "^2.39.0",
26
- "dotenv": "^16.4.3",
27
- "zod": "^3.23.8",
28
- "express": "^4.18.2",
29
- "body-parser": "^1.20.2"
30
- },
31
- "devDependencies": {
32
- "@types/node": "^20.11.17",
33
- "typescript": "^5.4.4"
34
- }
35
- }
1
+ {
2
+ "name": "@newhomestar/sdk",
3
+ "version": "0.4.0",
4
+ "description": "Type-safe SDK for building Nova pipelines (workers & functions)",
5
+ "homepage": "https://github.com/newhomestar/nova-node-sdk#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/newhomestar/nova-node-sdk/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/newhomestar/nova-node-sdk.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "Christian Gomez",
15
+ "type": "commonjs",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc"
23
+ },
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",
29
+ "@supabase/supabase-js": "^2.39.0",
30
+ "body-parser": "^1.20.2",
31
+ "dotenv": "^16.4.3",
32
+ "express": "^4.18.2",
33
+ "yaml": "^2.7.1",
34
+ "zod": "^4.0.5"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.11.17",
38
+ "typescript": "^5.4.4"
39
+ }
40
+ }