@newhomestar/sdk 0.8.3 → 0.8.5

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.
@@ -12,6 +12,55 @@ import { z } from "zod";
12
12
  /** Symbol used to tag lean schema/event wrappers (non-enumerable) */
13
13
  const NOVA_SCHEMA = Symbol.for('nova.schema');
14
14
  const NOVA_EVENT = Symbol.for('nova.event');
15
+ /*─────────────────────────* Config Field Definitions *──────────────────────────*/
16
+ // ─── Lifecycle Phase ───────────────────────────────────────────────────────
17
+ // Zod schema for runtime validation — no magic strings.
18
+ /** Zod schema for config field lifecycle phase validation */
19
+ export const ConfigPhaseSchema = z.enum(["before_auth", "after_auth"]);
20
+ /**
21
+ * Named constants for config field lifecycle phases.
22
+ * Use these instead of raw strings for IDE autocomplete and compile-time safety.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { ConfigPhase } from "@newhomestar/sdk";
27
+ *
28
+ * { when: ConfigPhase.BeforeAuth } // ✅ autocomplete + type-safe
29
+ * { when: "before_auth" } // ✅ also works (same type)
30
+ * { when: "beforeAuth" } // ❌ TypeScript error
31
+ * ```
32
+ */
33
+ export const ConfigPhase = {
34
+ /** Collected before OAuth redirect (e.g., company domain needed to build auth URL) */
35
+ BeforeAuth: "before_auth",
36
+ /** Collected after OAuth completes (e.g., preferences that need API access) */
37
+ AfterAuth: "after_auth",
38
+ };
39
+ // ─── Widget Type ───────────────────────────────────────────────────────────
40
+ /** Zod schema for config field widget type validation */
41
+ export const ConfigWidgetSchema = z.enum([
42
+ "text", "select", "boolean", "number", "textarea", "password", "date",
43
+ ]);
44
+ /**
45
+ * Named constants for config field widget types.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { ConfigWidget } from "@newhomestar/sdk";
50
+ *
51
+ * { widget: ConfigWidget.Select } // ✅
52
+ * { widget: "select" } // ✅ also works
53
+ * ```
54
+ */
55
+ export const ConfigWidget = {
56
+ Text: "text",
57
+ Select: "select",
58
+ Boolean: "boolean",
59
+ Number: "number",
60
+ Textarea: "textarea",
61
+ Password: "password",
62
+ Date: "date",
63
+ };
15
64
  /**
16
65
  * **Verbose** helper — define a schema with all metadata explicitly.
17
66
  *
@@ -228,7 +277,153 @@ export const IntegrationDefSchema = z.object({
228
277
  })).optional(),
229
278
  })),
230
279
  }).optional(),
280
+ configFields: z.array(z.object({
281
+ key: z.string().regex(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/, 'Config field key must be a valid JS identifier'),
282
+ label: z.string().min(1),
283
+ schema: z.any(), // Zod schema instance — validated structurally by normalizeConfigFields
284
+ when: ConfigPhaseSchema,
285
+ required: z.boolean().optional(),
286
+ defaultValue: z.unknown().optional(),
287
+ helpText: z.string().optional(),
288
+ placeholder: z.string().optional(),
289
+ widget: ConfigWidgetSchema.optional(),
290
+ options: z.array(z.object({
291
+ label: z.string(),
292
+ value: z.string(),
293
+ })).optional(),
294
+ optionsFetcher: z.any().optional(), // { schema, handler } — validated structurally by normalizeConfigFields
295
+ group: z.string().optional(),
296
+ visibleWhen: z.object({
297
+ field: z.string(),
298
+ equals: z.unknown(),
299
+ }).optional(),
300
+ })).optional(),
231
301
  });
302
+ /*─────────────────────────* Config Field Normalization *──────────────────────────*/
303
+ /**
304
+ * Infer a widget type from a Zod schema when the developer doesn't explicitly set one.
305
+ * Falls back to "text" if the schema type can't be determined.
306
+ */
307
+ function inferWidget(zodSchema) {
308
+ // Walk through wrappers (ZodOptional, ZodDefault, ZodNullable)
309
+ let inner = zodSchema;
310
+ while (inner?._def) {
311
+ const typeName = inner._def.typeName;
312
+ if (typeName === 'ZodOptional' || typeName === 'ZodDefault' || typeName === 'ZodNullable') {
313
+ inner = inner._def.innerType;
314
+ continue;
315
+ }
316
+ break;
317
+ }
318
+ const typeName = inner?._def?.typeName ?? '';
319
+ switch (typeName) {
320
+ case 'ZodString':
321
+ return 'text';
322
+ case 'ZodNumber':
323
+ case 'ZodBigInt':
324
+ return 'number';
325
+ case 'ZodBoolean':
326
+ return 'boolean';
327
+ case 'ZodEnum':
328
+ case 'ZodNativeEnum':
329
+ return 'select';
330
+ case 'ZodDate':
331
+ return 'date';
332
+ default:
333
+ return 'text';
334
+ }
335
+ }
336
+ /**
337
+ * Normalize and validate `configFields` array.
338
+ * Runs as Phase 0 of defineIntegration(), before schema/event normalization.
339
+ *
340
+ * Validates:
341
+ * - Keys are valid JS identifiers (no duplicates)
342
+ * - `when` is a valid ConfigPhase
343
+ * - `widget` is a valid ConfigWidget (if provided)
344
+ * - `optionsFetcher` requires `when: AfterAuth` (needs credentials)
345
+ * - `optionsFetcher.handler` must be a function
346
+ * - `visibleWhen.field` references another config field key
347
+ * - Infers widget from Zod schema when not explicitly set
348
+ * - Defaults `required` to `true`
349
+ *
350
+ * Stores validated config fields (with `optionsFetcher` handlers) as a
351
+ * non-enumerable `__configFields` property for runtime route registration.
352
+ *
353
+ * @throws {Error} on validation failures (descriptive messages with field index/key)
354
+ */
355
+ function normalizeConfigFields(fields) {
356
+ if (!fields || fields.length === 0)
357
+ return;
358
+ const allKeys = fields.map(f => f.key);
359
+ const seen = new Set();
360
+ for (let i = 0; i < fields.length; i++) {
361
+ const field = fields[i];
362
+ const ctx = `configFields[${i}] ("${field.key}")`;
363
+ // ── Validate: key must be a valid JS identifier ──
364
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(field.key)) {
365
+ throw new Error(`${ctx}: invalid key "${field.key}" — must be a valid JS identifier ` +
366
+ `(letters, digits, underscore, dollar sign; cannot start with a digit)`);
367
+ }
368
+ // ── Validate: no duplicate keys ──
369
+ if (seen.has(field.key)) {
370
+ throw new Error(`configFields: duplicate key "${field.key}" — each config field must have a unique key`);
371
+ }
372
+ seen.add(field.key);
373
+ // ── Validate: phase is a valid enum value (Zod runtime check) ──
374
+ const phaseResult = ConfigPhaseSchema.safeParse(field.when);
375
+ if (!phaseResult.success) {
376
+ throw new Error(`${ctx}: invalid "when" phase "${field.when}" — ` +
377
+ `must be "${ConfigPhase.BeforeAuth}" or "${ConfigPhase.AfterAuth}". ` +
378
+ `Use ConfigPhase.BeforeAuth or ConfigPhase.AfterAuth for type safety.`);
379
+ }
380
+ // ── Validate: widget is a valid enum value if provided ──
381
+ if (field.widget) {
382
+ const widgetResult = ConfigWidgetSchema.safeParse(field.widget);
383
+ if (!widgetResult.success) {
384
+ throw new Error(`${ctx}: invalid widget "${field.widget}" — ` +
385
+ `must be one of: ${ConfigWidgetSchema.options.join(', ')}. ` +
386
+ `Use ConfigWidget.* constants for type safety.`);
387
+ }
388
+ }
389
+ // ── Validate: optionsFetcher constraints ──
390
+ if (field.optionsFetcher) {
391
+ // optionsFetcher requires after_auth (needs credentials for API calls)
392
+ if (field.when === ConfigPhase.BeforeAuth) {
393
+ throw new Error(`${ctx}: has optionsFetcher but when: "${ConfigPhase.BeforeAuth}" — ` +
394
+ `dynamic options require credentials from OAuth, so they can only be used with ` +
395
+ `when: ConfigPhase.AfterAuth ("${ConfigPhase.AfterAuth}").`);
396
+ }
397
+ // handler must be a function
398
+ if (typeof field.optionsFetcher.handler !== 'function') {
399
+ throw new Error(`${ctx}: optionsFetcher.handler must be a function, got ${typeof field.optionsFetcher.handler}`);
400
+ }
401
+ // schema must be present
402
+ if (!field.optionsFetcher.schema) {
403
+ throw new Error(`${ctx}: optionsFetcher.schema is required — provide a Zod schema for the option items ` +
404
+ `(e.g., z.object({ label: z.string(), value: z.string() }))`);
405
+ }
406
+ }
407
+ // ── Validate: visibleWhen.field references another config field key ──
408
+ if (field.visibleWhen) {
409
+ if (!allKeys.includes(field.visibleWhen.field)) {
410
+ throw new Error(`${ctx}: visibleWhen.field references unknown config field "${field.visibleWhen.field}" — ` +
411
+ `available keys: ${allKeys.join(', ')}`);
412
+ }
413
+ // Cannot reference itself
414
+ if (field.visibleWhen.field === field.key) {
415
+ throw new Error(`${ctx}: visibleWhen.field cannot reference itself`);
416
+ }
417
+ }
418
+ // ── Apply defaults: required → true, widget → inferred ──
419
+ if (field.required === undefined) {
420
+ field.required = true;
421
+ }
422
+ if (!field.widget) {
423
+ field.widget = inferWidget(field.schema);
424
+ }
425
+ }
426
+ }
232
427
  /**
233
428
  * Validate an integration definition before build or push.
234
429
  *
@@ -350,6 +545,16 @@ function camelToSnake(str) {
350
545
  return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
351
546
  }
352
547
  export function defineIntegration(def) {
548
+ // ── Phase 0: Normalize and validate config fields ──
549
+ // Must run before Zod validation (Phase 4) because it applies defaults
550
+ // (required=true, widget inference) that make the fields pass Zod checks.
551
+ // Also validates semantic constraints that Zod can't express (unique keys,
552
+ // optionsFetcher requires AfterAuth, visibleWhen references, etc.)
553
+ if (def.configFields && def.configFields.length > 0) {
554
+ normalizeConfigFields(def.configFields);
555
+ console.log(`[nova] ✅ Validated ${def.configFields.length} config fields: ` +
556
+ `${def.configFields.map(f => f.key).join(', ')}`);
557
+ }
353
558
  // ── Phase 1: Normalize lean schemas ──
354
559
  // Fill slug/name from the key when using schema() helper
355
560
  for (const [key, schemaDef] of Object.entries(def.schemas)) {
package/package.json CHANGED
@@ -1,58 +1,58 @@
1
- {
2
- "name": "@newhomestar/sdk",
3
- "version": "0.8.3",
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": "module",
16
- "main": "dist/index.js",
17
- "types": "dist/index.d.ts",
18
- "exports": {
19
- ".": {
20
- "import": "./dist/index.js",
21
- "types": "./dist/index.d.ts"
22
- },
23
- "./next": {
24
- "import": "./dist/next.js",
25
- "types": "./dist/next.d.ts"
26
- },
27
- "./events": {
28
- "import": "./dist/events.js",
29
- "types": "./dist/events.d.ts"
30
- }
31
- },
32
- "files": [
33
- "dist"
34
- ],
35
- "scripts": {
36
- "build": "tsc"
37
- },
38
- "dependencies": {
39
- "@openfga/sdk": "^0.9.0",
40
- "@orpc/openapi": "1.7.4",
41
- "@orpc/server": "1.7.4",
42
- "@supabase/supabase-js": "^2.39.0",
43
- "body-parser": "^1.20.2",
44
- "dotenv": "^16.4.3",
45
- "express": "^4.18.2",
46
- "express-oauth2-jwt-bearer": "^1.7.4",
47
- "undici": "^7.24.4",
48
- "yaml": "^2.7.1"
49
- },
50
- "peerDependencies": {
51
- "zod": ">=4.0.0"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^20.11.17",
55
- "typescript": "^5.4.4",
56
- "zod": "^4.3.0"
57
- }
58
- }
1
+ {
2
+ "name": "@newhomestar/sdk",
3
+ "version": "0.8.5",
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": "module",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "types": "./dist/index.d.ts"
22
+ },
23
+ "./next": {
24
+ "import": "./dist/next.js",
25
+ "types": "./dist/next.d.ts"
26
+ },
27
+ "./events": {
28
+ "import": "./dist/events.js",
29
+ "types": "./dist/events.d.ts"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc"
37
+ },
38
+ "dependencies": {
39
+ "@openfga/sdk": "^0.9.0",
40
+ "@orpc/openapi": "1.7.4",
41
+ "@orpc/server": "1.7.4",
42
+ "@supabase/supabase-js": "^2.39.0",
43
+ "body-parser": "^1.20.2",
44
+ "dotenv": "^16.4.3",
45
+ "express": "^4.18.2",
46
+ "express-oauth2-jwt-bearer": "^1.7.4",
47
+ "undici": "^7.24.4",
48
+ "yaml": "^2.7.1"
49
+ },
50
+ "peerDependencies": {
51
+ "zod": ">=4.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.11.17",
55
+ "typescript": "^5.4.4",
56
+ "zod": "^4.3.0"
57
+ }
58
+ }