@newhomestar/sdk 0.5.2 → 0.6.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.
- package/dist/credentials.d.ts +99 -0
- package/dist/credentials.js +754 -0
- package/dist/index.d.ts +107 -3
- package/dist/index.js +264 -17
- package/dist/integration.d.ts +398 -0
- package/dist/integration.js +416 -0
- package/dist/integrationSpec.d.ts +204 -0
- package/dist/integrationSpec.js +186 -0
- package/dist/workerSchema.d.ts +45 -2
- package/dist/workerSchema.js +28 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,18 @@ export interface ActionDef<I extends ZodTypeAny, O extends ZodTypeAny> {
|
|
|
13
13
|
};
|
|
14
14
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
15
15
|
path?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Per-field parameter metadata — tells the platform where each input field
|
|
18
|
+
* goes (path, query, body, header) and what UI widget the admin dashboard
|
|
19
|
+
* should render (text, number, date, select, boolean, etc.).
|
|
20
|
+
*
|
|
21
|
+
* If not provided, the system falls back to convention:
|
|
22
|
+
* - Fields matching `:param` in path → path params
|
|
23
|
+
* - Remaining fields for GET → query params
|
|
24
|
+
* - Remaining fields for POST/PUT/PATCH → body params
|
|
25
|
+
* - UI type inferred from Zod type (string→text, number→number, boolean→boolean, enum→select)
|
|
26
|
+
*/
|
|
27
|
+
params?: Record<string, import("./integration.js").ParamMeta>;
|
|
16
28
|
capabilities?: Array<{
|
|
17
29
|
type: 'webhook';
|
|
18
30
|
eventTypes: string[];
|
|
@@ -40,12 +52,61 @@ export interface ActionDef<I extends ZodTypeAny, O extends ZodTypeAny> {
|
|
|
40
52
|
consumerGroup?: string;
|
|
41
53
|
}>;
|
|
42
54
|
}
|
|
55
|
+
/** Validated JWT claims from express-oauth2-jwt-bearer */
|
|
56
|
+
export interface JWTPayload {
|
|
57
|
+
/** Issuer (iss claim) */
|
|
58
|
+
iss?: string;
|
|
59
|
+
/** Subject (sub claim) — typically the user ID */
|
|
60
|
+
sub?: string;
|
|
61
|
+
/** Audience (aud claim) */
|
|
62
|
+
aud?: string | string[];
|
|
63
|
+
/** Expiration time (exp claim) */
|
|
64
|
+
exp?: number;
|
|
65
|
+
/** Issued at (iat claim) */
|
|
66
|
+
iat?: number;
|
|
67
|
+
/** JWT ID (jti claim) */
|
|
68
|
+
jti?: string;
|
|
69
|
+
/** Client ID (azp / client_id claim) */
|
|
70
|
+
azp?: string;
|
|
71
|
+
client_id?: string;
|
|
72
|
+
/** Any additional custom claims */
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
43
75
|
export interface ActionCtx {
|
|
44
76
|
jobId: string;
|
|
45
77
|
progress: (percent: number, meta?: unknown) => void;
|
|
78
|
+
/** Raw HTTP headers from the inbound request (HTTP mode only) */
|
|
79
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
80
|
+
/** Bearer token extracted from the Authorization header (HTTP mode only) */
|
|
81
|
+
authToken?: string;
|
|
82
|
+
/** Validated JWT payload from JWKS verification (HTTP mode only) */
|
|
83
|
+
auth?: JWTPayload;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve OAuth credentials for the current integration (or a named slug).
|
|
86
|
+
* Handles all auth modes (mtls, client_credentials, standard) with
|
|
87
|
+
* 3-tier caching: in-memory → DB (vault-encrypted) → fresh token exchange.
|
|
88
|
+
*
|
|
89
|
+
* @param slug - Integration slug (defaults to the current worker's name)
|
|
90
|
+
* @param userId - User ID for standard OAuth; omit for server flows
|
|
91
|
+
*/
|
|
92
|
+
resolveCredentials: (slug?: string, userId?: string) => Promise<import("./credentials.js").ResolvedCredentials>;
|
|
93
|
+
/**
|
|
94
|
+
* mTLS-aware fetch wrapper. Resolves credentials automatically (or accepts
|
|
95
|
+
* pre-resolved ones) and handles node:https for mTLS integrations.
|
|
96
|
+
*
|
|
97
|
+
* @param url - Full URL to fetch
|
|
98
|
+
* @param options - Standard fetch options (method, headers, body)
|
|
99
|
+
* @param credentials - Pre-resolved credentials; if omitted, calls resolveCredentials()
|
|
100
|
+
*/
|
|
101
|
+
fetch: (url: string, options?: {
|
|
102
|
+
method?: string;
|
|
103
|
+
headers?: Record<string, string>;
|
|
104
|
+
body?: string;
|
|
105
|
+
}, credentials?: import("./credentials.js").ResolvedCredentials) => Promise<Response>;
|
|
46
106
|
}
|
|
47
107
|
export declare function action<I extends ZodTypeAny, O extends ZodTypeAny>(cfg: {
|
|
48
108
|
name?: string;
|
|
109
|
+
description?: string;
|
|
49
110
|
input: I;
|
|
50
111
|
output: O;
|
|
51
112
|
fga?: {
|
|
@@ -56,6 +117,14 @@ export declare function action<I extends ZodTypeAny, O extends ZodTypeAny>(cfg:
|
|
|
56
117
|
};
|
|
57
118
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
58
119
|
path?: string;
|
|
120
|
+
scopes?: string[];
|
|
121
|
+
category?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Per-field parameter metadata — tells the platform where each input field
|
|
124
|
+
* goes (path, query, body, header) and what UI widget to render.
|
|
125
|
+
* @see ParamMeta from ./integration.js
|
|
126
|
+
*/
|
|
127
|
+
params?: Record<string, import("./integration.js").ParamMeta>;
|
|
59
128
|
capabilities?: Array<{
|
|
60
129
|
type: 'webhook';
|
|
61
130
|
eventTypes: string[];
|
|
@@ -121,11 +190,40 @@ export declare function generateOpenAPISpec<T extends WorkerDef>(def: T): Promis
|
|
|
121
190
|
paths: any;
|
|
122
191
|
}>;
|
|
123
192
|
/**
|
|
124
|
-
*
|
|
193
|
+
* Options for runHttpServer, including optional JWKS auth configuration.
|
|
125
194
|
*/
|
|
126
|
-
export
|
|
195
|
+
export interface HttpServerOptions {
|
|
127
196
|
port?: number;
|
|
128
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Auth issuer base URL (OIDC provider). The library fetches
|
|
199
|
+
* `{issuerBaseURL}/.well-known/jwks.json` automatically.
|
|
200
|
+
* Falls back to AUTH_ISSUER_BASE_URL env var, then to the production default.
|
|
201
|
+
*/
|
|
202
|
+
issuerBaseURL?: string;
|
|
203
|
+
/**
|
|
204
|
+
* Expected JWT audience claim.
|
|
205
|
+
* Falls back to AUTH_AUDIENCE env var, then "starfleet".
|
|
206
|
+
*/
|
|
207
|
+
audience?: string;
|
|
208
|
+
/**
|
|
209
|
+
* Paths that should NOT require a valid JWT (e.g. health checks).
|
|
210
|
+
* `/health` and `/healthcheck` are always exempt.
|
|
211
|
+
*/
|
|
212
|
+
publicPaths?: string[];
|
|
213
|
+
/**
|
|
214
|
+
* Set to true to completely disable JWKS auth (e.g. local dev without an auth server).
|
|
215
|
+
* Falls back to NOVA_SKIP_AUTH env var ("true" / "1").
|
|
216
|
+
*/
|
|
217
|
+
skipAuth?: boolean;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Enhanced HTTP server exposing each action under configurable routes.
|
|
221
|
+
* All routes are protected by JWKS-based JWT verification (RS256) via
|
|
222
|
+
* express-oauth2-jwt-bearer, matching the project-starfleet-backend pattern.
|
|
223
|
+
*
|
|
224
|
+
* Health / liveness endpoints are automatically exempted from auth.
|
|
225
|
+
*/
|
|
226
|
+
export declare function runHttpServer<T extends WorkerDef>(def: T, opts?: HttpServerOptions): void;
|
|
129
227
|
/**
|
|
130
228
|
* Run both HTTP server and queue consumer concurrently
|
|
131
229
|
* This gives you the best of both worlds: direct API access AND event processing
|
|
@@ -136,6 +234,10 @@ export declare function runDualMode<T extends WorkerDef>(def: T, opts?: {
|
|
|
136
234
|
export type { ZodTypeAny as SchemaAny, ZodTypeAny };
|
|
137
235
|
export { parseNovaSpec } from "./parseSpec.js";
|
|
138
236
|
export type { NovaSpec } from "./parseSpec.js";
|
|
237
|
+
export { defineIntegration, validateIntegration, integrationSchema, integrationEvent, integrationFunction, schema, event, IntegrationDefSchema, } from "./integration.js";
|
|
238
|
+
export type { IntegrationDef, IntegrationSchemaDef, IntegrationEventDef, IntegrationFunctionDef, ValidationResult, SchemaType, ParamMeta, ParamIn, ParamUiType, } from "./integration.js";
|
|
239
|
+
export { parseIntegrationSpec, IntegrationSpecSchema } from "./integrationSpec.js";
|
|
240
|
+
export type { IntegrationSpec } from "./integrationSpec.js";
|
|
139
241
|
export type WebhookCapability = {
|
|
140
242
|
type: 'webhook';
|
|
141
243
|
eventTypes: string[];
|
|
@@ -166,3 +268,5 @@ export type StreamCapability = {
|
|
|
166
268
|
consumerGroup?: string;
|
|
167
269
|
};
|
|
168
270
|
export type Capability = WebhookCapability | ScheduledCapability | QueueCapability | StreamCapability;
|
|
271
|
+
export { createPlatformClient, resolveCredentials, resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch, emitPlatformEvent, IntegrationNotFoundError, IntegrationDisabledError, CredentialsNotConfiguredError, ConnectionNotFoundError, TokenExchangeError, } from "./credentials.js";
|
|
272
|
+
export type { ResolvedCredentials, IntegrationConfig, AuthMode, } from "./credentials.js";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// nova-sdk-esm – Modern ESM Nova SDK with full oRPC integration (v0.4.2)
|
|
2
|
-
// =====================================================
|
|
3
|
-
// 1. Public API – action(), defineWorker(), enqueue() - SAME AS BEFORE
|
|
4
|
-
// 2. Enhanced HTTP server with REST endpoints and custom routing
|
|
5
|
-
// 3. Full oRPC integration with OpenAPI spec generation
|
|
6
|
-
// 4. Runtime harness for *worker* pipelines using Supabase RPC
|
|
7
|
-
// 5. Modern ESM architecture for future compatibility
|
|
8
|
-
// -----------------------------------------------------------
|
|
9
|
-
import { z } from "zod";
|
|
10
1
|
import dotenv from "dotenv";
|
|
11
2
|
import { createClient } from "@supabase/supabase-js";
|
|
12
3
|
import { OpenFgaClient } from "@openfga/sdk";
|
|
@@ -15,7 +6,7 @@ import { createServer } from "node:http";
|
|
|
15
6
|
import { os } from "@orpc/server";
|
|
16
7
|
import { RPCHandler } from "@orpc/server/node";
|
|
17
8
|
import { CORSPlugin } from "@orpc/server/plugins";
|
|
18
|
-
import {
|
|
9
|
+
import { createPlatformClient, resolveCredentials as _resolveCredentials, resolveCredentialsViaHttp as _resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch as _integrationFetch, } from "./credentials.js";
|
|
19
10
|
if (!process.env.RUNTIME_SUPABASE_URL) {
|
|
20
11
|
// local dev – read .env.local
|
|
21
12
|
dotenv.config({ path: ".env.local", override: true });
|
|
@@ -71,11 +62,13 @@ export function createORPCRouter(def) {
|
|
|
71
62
|
.input(actionDef.input)
|
|
72
63
|
.output(actionDef.output)
|
|
73
64
|
.handler(async ({ input, context }) => {
|
|
65
|
+
const credCtx = buildCredentialCtx(def.name);
|
|
74
66
|
const ctx = {
|
|
75
67
|
jobId: context?.jobId || `orpc-${Date.now()}`,
|
|
76
68
|
progress: (percent, meta) => {
|
|
77
69
|
console.log(`[${actionName}] Progress: ${percent}%`, meta);
|
|
78
|
-
}
|
|
70
|
+
},
|
|
71
|
+
...credCtx,
|
|
79
72
|
};
|
|
80
73
|
return await actionDef.handler(input, ctx);
|
|
81
74
|
})
|
|
@@ -258,9 +251,11 @@ export async function runWorker(def) {
|
|
|
258
251
|
}
|
|
259
252
|
try {
|
|
260
253
|
const parsedInput = act.input.parse(payload);
|
|
254
|
+
const credCtx = buildCredentialCtx(def.name);
|
|
261
255
|
const ctx = {
|
|
262
256
|
jobId,
|
|
263
257
|
progress: (percent, meta) => runtime.from("job_events").insert({ job_id: jobId, percent, meta }),
|
|
258
|
+
...credCtx,
|
|
264
259
|
};
|
|
265
260
|
const result = await act.handler(parsedInput, ctx);
|
|
266
261
|
act.output.parse(result);
|
|
@@ -323,29 +318,245 @@ export async function generateOpenAPISpec(def) {
|
|
|
323
318
|
}, {})
|
|
324
319
|
};
|
|
325
320
|
}
|
|
321
|
+
/*──────────────── Credential Context Builder ───────────────*/
|
|
322
|
+
/**
|
|
323
|
+
* Build resolveCredentials() and fetch() methods bound to this worker's slug.
|
|
324
|
+
*
|
|
325
|
+
* Automatically detects the credential resolution strategy:
|
|
326
|
+
* - **Strategy A (DB)**: PLATFORM_SUPABASE_* env vars → direct Supabase Vault access
|
|
327
|
+
* - **Strategy B (HTTP)**: AUTH_ISSUER_BASE_URL env var → call auth server with JWT
|
|
328
|
+
*
|
|
329
|
+
* @param defaultSlug - Integration slug (e.g., "adp")
|
|
330
|
+
* @param authToken - JWT Bearer token from the inbound request (for HTTP strategy)
|
|
331
|
+
*/
|
|
332
|
+
function buildCredentialCtx(defaultSlug, authToken) {
|
|
333
|
+
// Cache the resolved credentials per request to avoid duplicate round-trips
|
|
334
|
+
let _lastCreds = null;
|
|
335
|
+
const strategy = detectCredentialStrategy();
|
|
336
|
+
const ctxResolveCredentials = async (slug, userId) => {
|
|
337
|
+
const targetSlug = slug ?? defaultSlug;
|
|
338
|
+
if (strategy === "db") {
|
|
339
|
+
// Strategy A: Direct DB access via PLATFORM_SUPABASE_*
|
|
340
|
+
const platformDB = createPlatformClient();
|
|
341
|
+
const creds = await _resolveCredentials(platformDB, targetSlug, userId);
|
|
342
|
+
_lastCreds = creds;
|
|
343
|
+
return creds;
|
|
344
|
+
}
|
|
345
|
+
if (strategy === "http") {
|
|
346
|
+
// Strategy B: HTTP callback to auth server with JWT
|
|
347
|
+
const authBaseUrl = process.env.AUTH_ISSUER_BASE_URL;
|
|
348
|
+
if (!authToken) {
|
|
349
|
+
throw new Error(`[nova-sdk] HTTP credential resolution requires a JWT bearer token, ` +
|
|
350
|
+
`but no authToken was provided in the request context. ` +
|
|
351
|
+
`Ensure the request includes an Authorization: Bearer header.`);
|
|
352
|
+
}
|
|
353
|
+
const creds = await _resolveCredentialsViaHttp(authBaseUrl, targetSlug, authToken, userId);
|
|
354
|
+
_lastCreds = creds;
|
|
355
|
+
return creds;
|
|
356
|
+
}
|
|
357
|
+
// strategy === "none"
|
|
358
|
+
throw new Error(`[nova-sdk] No credential resolution strategy available. ` +
|
|
359
|
+
`Set either PLATFORM_SUPABASE_URL + PLATFORM_SUPABASE_SERVICE_ROLE_KEY (direct DB) ` +
|
|
360
|
+
`or AUTH_ISSUER_BASE_URL (HTTP callback) to enable credential resolution.`);
|
|
361
|
+
};
|
|
362
|
+
const ctxFetch = async (url, options, credentials) => {
|
|
363
|
+
const creds = credentials ?? _lastCreds ?? await ctxResolveCredentials();
|
|
364
|
+
return _integrationFetch(url, creds, options);
|
|
365
|
+
};
|
|
366
|
+
return { resolveCredentials: ctxResolveCredentials, fetch: ctxFetch };
|
|
367
|
+
}
|
|
326
368
|
/*──────────────── HTTP Server Harness (Enhanced) ───────────────*/
|
|
327
369
|
import express from "express";
|
|
328
370
|
import bodyParser from "body-parser";
|
|
371
|
+
import { auth } from "express-oauth2-jwt-bearer";
|
|
329
372
|
/**
|
|
330
|
-
* Enhanced HTTP server exposing each action under configurable routes
|
|
373
|
+
* Enhanced HTTP server exposing each action under configurable routes.
|
|
374
|
+
* All routes are protected by JWKS-based JWT verification (RS256) via
|
|
375
|
+
* express-oauth2-jwt-bearer, matching the project-starfleet-backend pattern.
|
|
376
|
+
*
|
|
377
|
+
* Health / liveness endpoints are automatically exempted from auth.
|
|
331
378
|
*/
|
|
332
379
|
export function runHttpServer(def, opts = {}) {
|
|
333
380
|
const app = express();
|
|
334
381
|
app.use(bodyParser.json());
|
|
382
|
+
// ── Determine whether auth is enabled ──
|
|
383
|
+
const skipAuth = opts.skipAuth ??
|
|
384
|
+
['true', '1'].includes((process.env.NOVA_SKIP_AUTH ?? '').toLowerCase());
|
|
385
|
+
// ── Build the set of public (unauthenticated) paths ──
|
|
386
|
+
const publicPaths = new Set([
|
|
387
|
+
'/health',
|
|
388
|
+
'/healthcheck',
|
|
389
|
+
'/healthcheck/',
|
|
390
|
+
...(opts.publicPaths ?? []),
|
|
391
|
+
]);
|
|
392
|
+
// Also exempt any action named "health"
|
|
393
|
+
for (const [actionName, act] of Object.entries(def.actions)) {
|
|
394
|
+
if (actionName === 'health') {
|
|
395
|
+
const route = act.path || `/${def.name}/${actionName}`;
|
|
396
|
+
publicPaths.add(route);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!skipAuth) {
|
|
400
|
+
// ── JWKS middleware (same pattern as project-starfleet-backend) ──
|
|
401
|
+
const issuerBaseURL = opts.issuerBaseURL ??
|
|
402
|
+
process.env.AUTH_ISSUER_BASE_URL ??
|
|
403
|
+
'https://auth.newhomeconnect.dev';
|
|
404
|
+
const audience = opts.audience ??
|
|
405
|
+
process.env.AUTH_AUDIENCE ??
|
|
406
|
+
'starfleet';
|
|
407
|
+
console.log(`[nova] 🔐 JWKS auth enabled — issuer: ${issuerBaseURL}, audience: ${audience}`);
|
|
408
|
+
console.log(`[nova] 🔓 Public (unauthenticated) paths: ${[...publicPaths].join(', ')}`);
|
|
409
|
+
const jwtCheck = auth({
|
|
410
|
+
audience,
|
|
411
|
+
issuerBaseURL,
|
|
412
|
+
tokenSigningAlg: 'RS256',
|
|
413
|
+
});
|
|
414
|
+
// Apply JWKS middleware to all routes EXCEPT public paths
|
|
415
|
+
app.use((req, res, next) => {
|
|
416
|
+
if (publicPaths.has(req.path)) {
|
|
417
|
+
return next();
|
|
418
|
+
}
|
|
419
|
+
return jwtCheck(req, res, next);
|
|
420
|
+
});
|
|
421
|
+
// ── InvalidTokenError handler (matches project-starfleet-backend) ──
|
|
422
|
+
app.use((err, _req, res, next) => {
|
|
423
|
+
if (err && (err.name === 'InvalidTokenError' || err.code === 'invalid_token')) {
|
|
424
|
+
console.warn(`[nova] 🚫 JWT rejected: ${err.message}`);
|
|
425
|
+
return res.status(401).json({
|
|
426
|
+
error: 'Unauthorized',
|
|
427
|
+
message: 'Invalid or expired token',
|
|
428
|
+
code: 'invalid_token',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (err && err.status === 401) {
|
|
432
|
+
console.warn(`[nova] 🚫 Auth failed: ${err.message}`);
|
|
433
|
+
return res.status(401).json({
|
|
434
|
+
error: 'Unauthorized',
|
|
435
|
+
message: err.message || 'Authentication failed',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
next(err);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
console.warn(`[nova] ⚠️ JWKS auth DISABLED (skipAuth=true). All routes are public.`);
|
|
443
|
+
}
|
|
444
|
+
// ── Register action routes ──
|
|
335
445
|
for (const [actionName, act] of Object.entries(def.actions)) {
|
|
336
446
|
const method = (act.method || 'POST').toLowerCase();
|
|
337
447
|
const route = act.path || `/${def.name}/${actionName}`;
|
|
338
|
-
// unified handler: parse JSON body or default to empty object
|
|
339
448
|
const handler = async (req, res) => {
|
|
340
449
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
450
|
+
// ── Smart input extraction using params metadata ──
|
|
451
|
+
// Merges path params, query params, and body based on explicit
|
|
452
|
+
// `params` metadata OR convention (GET→query, POST→body).
|
|
453
|
+
const rawInput = {};
|
|
454
|
+
const paramsMeta = act.params ?? {};
|
|
455
|
+
const hasExplicitParams = Object.keys(paramsMeta).length > 0;
|
|
456
|
+
if (hasExplicitParams) {
|
|
457
|
+
// ── Explicit params mode: use metadata to route fields ──
|
|
458
|
+
for (const [key, meta] of Object.entries(paramsMeta)) {
|
|
459
|
+
if (meta.in === 'path' && req.params?.[key] !== undefined) {
|
|
460
|
+
rawInput[key] = req.params[key];
|
|
461
|
+
}
|
|
462
|
+
else if (meta.in === 'query' && req.query?.[key] !== undefined) {
|
|
463
|
+
rawInput[key] = req.query[key];
|
|
464
|
+
}
|
|
465
|
+
else if (meta.in === 'header') {
|
|
466
|
+
const headerVal = req.headers?.[key.toLowerCase()];
|
|
467
|
+
if (headerVal !== undefined)
|
|
468
|
+
rawInput[key] = headerVal;
|
|
469
|
+
}
|
|
470
|
+
else if (meta.in === 'body' && req.body?.[key] !== undefined) {
|
|
471
|
+
rawInput[key] = req.body[key];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Also include any body fields not in params (backward compat)
|
|
475
|
+
if (req.body && typeof req.body === 'object') {
|
|
476
|
+
for (const [k, v] of Object.entries(req.body)) {
|
|
477
|
+
if (!(k in rawInput))
|
|
478
|
+
rawInput[k] = v;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// ── Convention mode: derive from HTTP method + path ──
|
|
484
|
+
// 1. Path params from Express :param matching
|
|
485
|
+
if (req.params && typeof req.params === 'object') {
|
|
486
|
+
Object.assign(rawInput, req.params);
|
|
487
|
+
}
|
|
488
|
+
// 2. For GET requests: all remaining input comes from query params
|
|
489
|
+
if (method === 'get' && req.query && typeof req.query === 'object') {
|
|
490
|
+
Object.assign(rawInput, req.query);
|
|
491
|
+
}
|
|
492
|
+
// 3. Body params (for all methods — Express won't have body for GET usually)
|
|
493
|
+
if (req.body && typeof req.body === 'object') {
|
|
494
|
+
Object.assign(rawInput, req.body);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// ── Type coercion for query/path string values ──
|
|
498
|
+
// Express delivers query and path params as strings, but Zod expects
|
|
499
|
+
// numbers/booleans. Coerce based on params metadata or Zod schema shape.
|
|
500
|
+
for (const [key, val] of Object.entries(rawInput)) {
|
|
501
|
+
if (typeof val !== 'string')
|
|
502
|
+
continue;
|
|
503
|
+
const meta = paramsMeta[key];
|
|
504
|
+
const uiType = meta?.uiType;
|
|
505
|
+
// Coerce numbers
|
|
506
|
+
if (uiType === 'number' || uiType === 'integer') {
|
|
507
|
+
const parsed = Number(val);
|
|
508
|
+
if (!isNaN(parsed))
|
|
509
|
+
rawInput[key] = parsed;
|
|
510
|
+
}
|
|
511
|
+
// Coerce booleans
|
|
512
|
+
else if (uiType === 'boolean') {
|
|
513
|
+
rawInput[key] = val === 'true' || val === '1';
|
|
514
|
+
}
|
|
515
|
+
// Auto-detect from Zod schema shape if no explicit uiType
|
|
516
|
+
else if (!uiType && act.input?._def?.shape) {
|
|
517
|
+
const fieldDef = act.input._def.shape()?.[key];
|
|
518
|
+
const innerType = fieldDef?._def?.innerType?._def?.typeName ?? fieldDef?._def?.typeName;
|
|
519
|
+
if (innerType === 'ZodNumber') {
|
|
520
|
+
const parsed = Number(val);
|
|
521
|
+
if (!isNaN(parsed))
|
|
522
|
+
rawInput[key] = parsed;
|
|
523
|
+
}
|
|
524
|
+
else if (innerType === 'ZodBoolean') {
|
|
525
|
+
rawInput[key] = val === 'true' || val === '1';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const input = act.input.parse(rawInput);
|
|
530
|
+
const jobId = `http-${Date.now()}`;
|
|
531
|
+
const authHeader = req.headers['authorization'];
|
|
532
|
+
const authToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : undefined;
|
|
533
|
+
// Extract validated JWT payload from express-oauth2-jwt-bearer
|
|
534
|
+
// The library attaches it as req.auth after successful verification
|
|
535
|
+
const jwtPayload = req.auth?.payload ?? req.auth;
|
|
536
|
+
const credCtx = buildCredentialCtx(def.name, authToken);
|
|
537
|
+
const ctx = {
|
|
538
|
+
jobId,
|
|
539
|
+
progress: (percent, meta) => {
|
|
540
|
+
console.log(`[nova][${actionName}] Progress: ${percent}%`, meta ?? '');
|
|
541
|
+
},
|
|
542
|
+
headers: req.headers,
|
|
543
|
+
authToken,
|
|
544
|
+
auth: jwtPayload,
|
|
545
|
+
...credCtx,
|
|
546
|
+
};
|
|
547
|
+
const authLabel = jwtPayload?.sub
|
|
548
|
+
? ` [auth ✓ sub=${jwtPayload.sub}]`
|
|
549
|
+
: authToken
|
|
550
|
+
? ' [auth ✓]'
|
|
551
|
+
: ' [no auth]';
|
|
552
|
+
console.log(`[nova] ▶ ${method.toUpperCase()} ${route} → ${actionName} (${jobId})${authLabel}`);
|
|
344
553
|
const out = await act.handler(input, ctx);
|
|
345
554
|
act.output.parse(out);
|
|
555
|
+
console.log(`[nova] ✅ ${actionName} completed (${jobId})`);
|
|
346
556
|
res.json(out);
|
|
347
557
|
}
|
|
348
558
|
catch (err) {
|
|
559
|
+
console.error(`[nova] ❌ ${actionName} failed:`, err.message);
|
|
349
560
|
res.status(400).json({ error: err.message });
|
|
350
561
|
}
|
|
351
562
|
};
|
|
@@ -356,13 +567,22 @@ export function runHttpServer(def, opts = {}) {
|
|
|
356
567
|
app.get('/health', handler);
|
|
357
568
|
}
|
|
358
569
|
}
|
|
570
|
+
// ── Log credential resolution strategy ──
|
|
571
|
+
const credStrategy = detectCredentialStrategy();
|
|
572
|
+
const strategyLabels = {
|
|
573
|
+
db: '🗄️ Direct DB (PLATFORM_SUPABASE_*)',
|
|
574
|
+
http: '🌐 HTTP callback (AUTH_ISSUER_BASE_URL → auth server)',
|
|
575
|
+
none: '⚠️ NONE — ctx.resolveCredentials() will throw if called',
|
|
576
|
+
};
|
|
577
|
+
console.log(`[nova] 🔑 Credential strategy: ${strategyLabels[credStrategy]}`);
|
|
359
578
|
const port = opts.port ?? (process.env.PORT ? parseInt(process.env.PORT) : 8000);
|
|
360
579
|
app.listen(port, () => {
|
|
361
580
|
console.log(`[nova] HTTP server listening on http://localhost:${port}`);
|
|
362
581
|
Object.entries(def.actions).forEach(([actionName, actionDef]) => {
|
|
363
582
|
const method = (actionDef.method || 'POST').toUpperCase();
|
|
364
583
|
const path = actionDef.path || `/${def.name}/${actionName}`;
|
|
365
|
-
|
|
584
|
+
const isPublic = publicPaths.has(path) ? ' 🔓' : ' 🔐';
|
|
585
|
+
console.log(`[nova] ${method} ${path} -> ${actionName}${isPublic}`);
|
|
366
586
|
});
|
|
367
587
|
});
|
|
368
588
|
}
|
|
@@ -392,3 +612,30 @@ export function runDualMode(def, opts = {}) {
|
|
|
392
612
|
}
|
|
393
613
|
// YAML spec parsing utility
|
|
394
614
|
export { parseNovaSpec } from "./parseSpec.js";
|
|
615
|
+
// Integration definition API
|
|
616
|
+
export { defineIntegration, validateIntegration,
|
|
617
|
+
// Verbose helpers (backward-compatible)
|
|
618
|
+
integrationSchema, integrationEvent, integrationFunction,
|
|
619
|
+
// Lean helpers (recommended)
|
|
620
|
+
schema, event, IntegrationDefSchema, } from "./integration.js";
|
|
621
|
+
// Integration spec parsing utility
|
|
622
|
+
export { parseIntegrationSpec, IntegrationSpecSchema } from "./integrationSpec.js";
|
|
623
|
+
/*──────────────── Credential Resolution (re-exports) ───────────────*/
|
|
624
|
+
// These are the first-class SDK exports for integration credential management.
|
|
625
|
+
// Every integration gets vault-backed token caching, mTLS, and OAuth for free.
|
|
626
|
+
export { createPlatformClient, resolveCredentials, resolveCredentialsViaHttp, detectCredentialStrategy, integrationFetch, emitPlatformEvent,
|
|
627
|
+
// Error classes
|
|
628
|
+
IntegrationNotFoundError, IntegrationDisabledError, CredentialsNotConfiguredError, ConnectionNotFoundError, TokenExchangeError, } from "./credentials.js";
|
|
629
|
+
// // Default export for compatibility
|
|
630
|
+
// import { parseNovaSpec as parseNovaSpecFunction } from "./parseSpec.js";
|
|
631
|
+
// export default {
|
|
632
|
+
// defineWorker,
|
|
633
|
+
// action,
|
|
634
|
+
// enqueue,
|
|
635
|
+
// runHttpServer,
|
|
636
|
+
// runORPCServer,
|
|
637
|
+
// runWorker,
|
|
638
|
+
// generateOpenAPISpec,
|
|
639
|
+
// createORPCRouter,
|
|
640
|
+
// parseNovaSpec: parseNovaSpecFunction
|
|
641
|
+
// };
|