khotan-data 0.1.0 → 0.2.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/README.md +89 -6
- package/dist/cli.js +405 -35
- package/dist/factory.cjs +1160 -106
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.cts +262 -38
- package/dist/factory.d.ts +262 -38
- package/dist/factory.js +1158 -108
- package/dist/factory.js.map +1 -1
- package/dist/templates/api-state.tsx +249 -0
- package/dist/templates/cache.example.ts +11 -0
- package/dist/templates/cache.ts +58 -0
- package/dist/templates/catch.ts +13 -1
- package/dist/templates/debug-index-page.tsx +56 -36
- package/dist/templates/hub.tsx +9 -23
- package/dist/templates/inflow.ts +5 -6
- package/dist/templates/khotan-config.ts +30 -4
- package/dist/templates/mapping-browser.tsx +773 -0
- package/dist/templates/mappings-page.tsx +9 -0
- package/dist/templates/outflow.ts +5 -5
- package/dist/templates/pass.ts +10 -0
- package/dist/templates/plug-debugger.tsx +15 -7
- package/dist/templates/relay.example.ts +11 -1
- package/dist/templates/relay.ts +16 -7
- package/dist/templates/runs-table.tsx +133 -130
- package/dist/templates/schema.ts +81 -0
- package/dist/templates/skill-plug.md +38 -15
- package/dist/templates/skill-setup.md +80 -3
- package/dist/templates/topology-canvas.tsx +19 -30
- package/dist/templates/var-panel.tsx +33 -10
- package/dist/templates/webhook-events-table.tsx +105 -102
- package/dist/templates/wire-panel.tsx +30 -8
- package/package.json +1 -1
package/dist/factory.cjs
CHANGED
|
@@ -164,7 +164,7 @@ var khotanRuns = pgCore.pgTable(
|
|
|
164
164
|
)
|
|
165
165
|
]
|
|
166
166
|
);
|
|
167
|
-
var
|
|
167
|
+
var khotanMappingsTable = pgCore.pgTable(
|
|
168
168
|
"khotan_mappings",
|
|
169
169
|
{
|
|
170
170
|
id: pgCore.text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
@@ -184,6 +184,39 @@ var khotanMappings = pgCore.pgTable(
|
|
|
184
184
|
pgCore.index("khotan_mappings_refs_gin_idx").using("gin", table.refs)
|
|
185
185
|
]
|
|
186
186
|
);
|
|
187
|
+
var khotanCaches = pgCore.pgTable(
|
|
188
|
+
"khotan_caches",
|
|
189
|
+
{
|
|
190
|
+
id: pgCore.text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
191
|
+
name: pgCore.text("name").notNull().unique(),
|
|
192
|
+
scope: pgCore.jsonb("scope").$type(),
|
|
193
|
+
ttlSeconds: pgCore.integer("ttl_seconds"),
|
|
194
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
195
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
196
|
+
},
|
|
197
|
+
(table) => [pgCore.index("khotan_caches_name_idx").on(table.name)]
|
|
198
|
+
);
|
|
199
|
+
var khotanCacheEntries = pgCore.pgTable(
|
|
200
|
+
"khotan_cache_entries",
|
|
201
|
+
{
|
|
202
|
+
id: pgCore.text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
203
|
+
cacheId: pgCore.text("cache_id").notNull(),
|
|
204
|
+
key: pgCore.text("key").notNull(),
|
|
205
|
+
value: pgCore.jsonb("value").notNull().$type(),
|
|
206
|
+
expiresAt: pgCore.timestamp("expires_at", { withTimezone: true }),
|
|
207
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
208
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
209
|
+
},
|
|
210
|
+
(table) => [
|
|
211
|
+
pgCore.unique("khotan_cache_entries_cache_id_key_unique").on(
|
|
212
|
+
table.cacheId,
|
|
213
|
+
table.key
|
|
214
|
+
),
|
|
215
|
+
pgCore.index("khotan_cache_entries_cache_id_idx").on(table.cacheId),
|
|
216
|
+
pgCore.index("khotan_cache_entries_cache_id_key_idx").on(table.cacheId, table.key),
|
|
217
|
+
pgCore.index("khotan_cache_entries_expires_at_idx").on(table.expiresAt)
|
|
218
|
+
]
|
|
219
|
+
);
|
|
187
220
|
async function deriveKey(secret) {
|
|
188
221
|
const encoded = new TextEncoder().encode(secret);
|
|
189
222
|
const hash = await crypto.subtle.digest("SHA-256", encoded);
|
|
@@ -228,6 +261,104 @@ function hexToBytes(hex) {
|
|
|
228
261
|
function bytesToHex(bytes) {
|
|
229
262
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
230
263
|
}
|
|
264
|
+
var CLI_TOKEN_SCHEME = "KhotanCLI";
|
|
265
|
+
var CLI_TOKEN_WINDOW_MS = 6e4;
|
|
266
|
+
async function deriveCliToken(secret, timestamp2) {
|
|
267
|
+
const key = await crypto.subtle.importKey(
|
|
268
|
+
"raw",
|
|
269
|
+
new TextEncoder().encode(secret),
|
|
270
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
271
|
+
false,
|
|
272
|
+
["sign"]
|
|
273
|
+
);
|
|
274
|
+
const sig = await crypto.subtle.sign(
|
|
275
|
+
"HMAC",
|
|
276
|
+
key,
|
|
277
|
+
new TextEncoder().encode(`khotan-cli:${timestamp2}`)
|
|
278
|
+
);
|
|
279
|
+
return bytesToHex(new Uint8Array(sig));
|
|
280
|
+
}
|
|
281
|
+
function timingSafeEqualHex(a, b) {
|
|
282
|
+
if (a.length !== b.length) return false;
|
|
283
|
+
let diff = 0;
|
|
284
|
+
for (let i = 0; i < a.length; i++) {
|
|
285
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
286
|
+
}
|
|
287
|
+
return diff === 0;
|
|
288
|
+
}
|
|
289
|
+
async function isCliRequestAuthorized(request, secret) {
|
|
290
|
+
if (process.env["NODE_ENV"] === "production") return false;
|
|
291
|
+
if (!secret) return false;
|
|
292
|
+
const header = request.headers.get("authorization");
|
|
293
|
+
if (!header?.startsWith(`${CLI_TOKEN_SCHEME} `)) return false;
|
|
294
|
+
const token = header.slice(CLI_TOKEN_SCHEME.length + 1).trim();
|
|
295
|
+
const dotIdx = token.indexOf(".");
|
|
296
|
+
if (dotIdx === -1) return false;
|
|
297
|
+
const timestamp2 = token.slice(0, dotIdx);
|
|
298
|
+
const provided = token.slice(dotIdx + 1);
|
|
299
|
+
const ts = Number.parseInt(timestamp2, 10);
|
|
300
|
+
if (!Number.isFinite(ts)) return false;
|
|
301
|
+
if (Math.abs(Date.now() - ts) > CLI_TOKEN_WINDOW_MS) return false;
|
|
302
|
+
const expected = await deriveCliToken(secret, timestamp2);
|
|
303
|
+
return timingSafeEqualHex(provided, expected);
|
|
304
|
+
}
|
|
305
|
+
function bindPlugWithVars(plug, vars, setVars) {
|
|
306
|
+
const opts = (extra) => ({
|
|
307
|
+
...extra,
|
|
308
|
+
vars,
|
|
309
|
+
...setVars ? { _setVars: setVars } : {}
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
get(path, extra) {
|
|
313
|
+
return plug.get(path, opts(extra));
|
|
314
|
+
},
|
|
315
|
+
post(path, extra) {
|
|
316
|
+
return plug.post(path, opts(extra));
|
|
317
|
+
},
|
|
318
|
+
put(path, extra) {
|
|
319
|
+
return plug.put(path, opts(extra));
|
|
320
|
+
},
|
|
321
|
+
patch(path, extra) {
|
|
322
|
+
return plug.patch(path, opts(extra));
|
|
323
|
+
},
|
|
324
|
+
delete(path, extra) {
|
|
325
|
+
return plug.delete(path, opts(extra));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function bindWorkflowPlug(plug, ctx, plugName = ctx.flow.plugName) {
|
|
330
|
+
const vars = plugName === ctx.flow.plugName ? ctx.vars : ctx.plugVarsByName?.[plugName] ?? {};
|
|
331
|
+
if (plugName !== ctx.flow.plugName) {
|
|
332
|
+
ctx.plugVarsByName ??= {};
|
|
333
|
+
ctx.plugVarsByName[plugName] = vars;
|
|
334
|
+
}
|
|
335
|
+
return bindPlugWithVars(plug, vars, async (updates) => {
|
|
336
|
+
Object.assign(vars, updates);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
var khotanRuntimeRegistry = /* @__PURE__ */ new Map();
|
|
340
|
+
function getWorkflowRuntimeHelpers(ctx) {
|
|
341
|
+
const helpers = khotanRuntimeRegistry.get(ctx.khotanInstanceId);
|
|
342
|
+
if (!helpers) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Khotan runtime helpers for instance "${ctx.khotanInstanceId}" are not registered`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return helpers;
|
|
348
|
+
}
|
|
349
|
+
function khotanCache(ctx, cacheName) {
|
|
350
|
+
return getWorkflowRuntimeHelpers(ctx).cache(cacheName);
|
|
351
|
+
}
|
|
352
|
+
function khotanMappings(ctx) {
|
|
353
|
+
const helpers = getWorkflowRuntimeHelpers(ctx);
|
|
354
|
+
return {
|
|
355
|
+
list: helpers.listMappings,
|
|
356
|
+
lookup: helpers.lookupMapping,
|
|
357
|
+
upsert: helpers.upsertMapping,
|
|
358
|
+
update: helpers.updateMapping,
|
|
359
|
+
delete: helpers.deleteMapping
|
|
360
|
+
};
|
|
361
|
+
}
|
|
231
362
|
function drizzleAdapter(db) {
|
|
232
363
|
return {
|
|
233
364
|
async upsertPlug(plug) {
|
|
@@ -376,27 +507,93 @@ function drizzleAdapter(db) {
|
|
|
376
507
|
async upsertResource(resource) {
|
|
377
508
|
const rows = await db.insert(khotanResources).values({
|
|
378
509
|
name: resource.name,
|
|
379
|
-
connectField: resource.connectField,
|
|
510
|
+
connectField: serializeConnectField(resource.connectField),
|
|
380
511
|
description: resource.description ?? null
|
|
381
512
|
}).onConflictDoUpdate({
|
|
382
513
|
target: khotanResources.name,
|
|
383
514
|
set: {
|
|
384
|
-
connectField: resource.connectField,
|
|
515
|
+
connectField: serializeConnectField(resource.connectField),
|
|
385
516
|
description: resource.description ?? null,
|
|
386
517
|
updatedAt: /* @__PURE__ */ new Date()
|
|
387
518
|
}
|
|
388
519
|
}).returning({ id: khotanResources.id });
|
|
389
520
|
return { id: rows[0].id };
|
|
390
521
|
},
|
|
522
|
+
async upsertCache(cache) {
|
|
523
|
+
const rows = await db.insert(khotanCaches).values({
|
|
524
|
+
name: cache.name,
|
|
525
|
+
scope: cache.scope ?? null,
|
|
526
|
+
ttlSeconds: cache.ttlSeconds ?? null
|
|
527
|
+
}).onConflictDoUpdate({
|
|
528
|
+
target: khotanCaches.name,
|
|
529
|
+
set: {
|
|
530
|
+
scope: cache.scope ?? null,
|
|
531
|
+
ttlSeconds: cache.ttlSeconds ?? null,
|
|
532
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
533
|
+
}
|
|
534
|
+
}).returning({ id: khotanCaches.id });
|
|
535
|
+
return { id: rows[0].id };
|
|
536
|
+
},
|
|
537
|
+
async getCacheByName(name) {
|
|
538
|
+
const rows = await db.select().from(khotanCaches).where(drizzleOrm.eq(khotanCaches.name, name)).limit(1);
|
|
539
|
+
return rows[0] ?? null;
|
|
540
|
+
},
|
|
541
|
+
async getCacheEntry(cacheId, key) {
|
|
542
|
+
const rows = await db.select({
|
|
543
|
+
id: khotanCacheEntries.id,
|
|
544
|
+
cacheId: khotanCacheEntries.cacheId,
|
|
545
|
+
key: khotanCacheEntries.key,
|
|
546
|
+
value: khotanCacheEntries.value,
|
|
547
|
+
expiresAt: khotanCacheEntries.expiresAt,
|
|
548
|
+
createdAt: khotanCacheEntries.createdAt,
|
|
549
|
+
updatedAt: khotanCacheEntries.updatedAt
|
|
550
|
+
}).from(khotanCacheEntries).where(
|
|
551
|
+
drizzleOrm.and(
|
|
552
|
+
drizzleOrm.eq(khotanCacheEntries.cacheId, cacheId),
|
|
553
|
+
drizzleOrm.eq(khotanCacheEntries.key, key)
|
|
554
|
+
)
|
|
555
|
+
).limit(1);
|
|
556
|
+
return rows[0] ?? null;
|
|
557
|
+
},
|
|
558
|
+
async upsertCacheEntry(entry) {
|
|
559
|
+
const existing = await db.select({ id: khotanCacheEntries.id }).from(khotanCacheEntries).where(
|
|
560
|
+
drizzleOrm.and(
|
|
561
|
+
drizzleOrm.eq(khotanCacheEntries.cacheId, entry.cacheId),
|
|
562
|
+
drizzleOrm.eq(khotanCacheEntries.key, entry.key)
|
|
563
|
+
)
|
|
564
|
+
).limit(1);
|
|
565
|
+
const rows = await db.insert(khotanCacheEntries).values({
|
|
566
|
+
cacheId: entry.cacheId,
|
|
567
|
+
key: entry.key,
|
|
568
|
+
value: entry.value,
|
|
569
|
+
expiresAt: entry.expiresAt ?? null
|
|
570
|
+
}).onConflictDoUpdate({
|
|
571
|
+
target: [khotanCacheEntries.cacheId, khotanCacheEntries.key],
|
|
572
|
+
set: {
|
|
573
|
+
value: entry.value,
|
|
574
|
+
expiresAt: entry.expiresAt ?? null,
|
|
575
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
576
|
+
}
|
|
577
|
+
}).returning({ id: khotanCacheEntries.id });
|
|
578
|
+
return { id: rows[0].id, created: existing.length === 0 };
|
|
579
|
+
},
|
|
580
|
+
async deleteCacheEntry(cacheId, key) {
|
|
581
|
+
await db.delete(khotanCacheEntries).where(
|
|
582
|
+
drizzleOrm.and(
|
|
583
|
+
drizzleOrm.eq(khotanCacheEntries.cacheId, cacheId),
|
|
584
|
+
drizzleOrm.eq(khotanCacheEntries.key, key)
|
|
585
|
+
)
|
|
586
|
+
);
|
|
587
|
+
},
|
|
391
588
|
async listResources() {
|
|
392
589
|
const flowCounts = db.select({
|
|
393
590
|
resourceId: khotanFlows.resourceId,
|
|
394
591
|
flowCount: drizzleOrm.count(khotanFlows.id).as("flow_count")
|
|
395
592
|
}).from(khotanFlows).where(drizzleOrm.sql`${khotanFlows.resourceId} is not null`).groupBy(khotanFlows.resourceId).as("flow_counts");
|
|
396
593
|
const mappingCounts = db.select({
|
|
397
|
-
resourceId:
|
|
398
|
-
mappingCount: drizzleOrm.count(
|
|
399
|
-
}).from(
|
|
594
|
+
resourceId: khotanMappingsTable.resourceId,
|
|
595
|
+
mappingCount: drizzleOrm.count(khotanMappingsTable.id).as("mapping_count")
|
|
596
|
+
}).from(khotanMappingsTable).groupBy(khotanMappingsTable.resourceId).as("mapping_counts");
|
|
400
597
|
const rows = await db.select({
|
|
401
598
|
id: khotanResources.id,
|
|
402
599
|
name: khotanResources.name,
|
|
@@ -410,57 +607,79 @@ function drizzleAdapter(db) {
|
|
|
410
607
|
mappingCounts,
|
|
411
608
|
drizzleOrm.eq(khotanResources.id, mappingCounts.resourceId)
|
|
412
609
|
);
|
|
413
|
-
return rows
|
|
610
|
+
return rows.map((row) => ({
|
|
611
|
+
...row,
|
|
612
|
+
connectField: deserializeConnectField(row.connectField)
|
|
613
|
+
}));
|
|
414
614
|
},
|
|
415
615
|
async getResource(id) {
|
|
416
616
|
const rows = await db.select().from(khotanResources).where(drizzleOrm.eq(khotanResources.id, id)).limit(1);
|
|
417
|
-
|
|
617
|
+
if (!rows[0]) return null;
|
|
618
|
+
return {
|
|
619
|
+
...rows[0],
|
|
620
|
+
connectField: deserializeConnectField(rows[0].connectField)
|
|
621
|
+
};
|
|
418
622
|
},
|
|
419
623
|
async getResourceFlows(resourceId) {
|
|
420
624
|
return db.select().from(khotanFlows).where(drizzleOrm.eq(khotanFlows.resourceId, resourceId));
|
|
421
625
|
},
|
|
422
626
|
async upsertMapping(mapping) {
|
|
423
627
|
if (mapping.id) {
|
|
424
|
-
const rows2 = await db.update(
|
|
628
|
+
const rows2 = await db.update(khotanMappingsTable).set({
|
|
425
629
|
resourceId: mapping.resourceId,
|
|
426
630
|
connectValue: mapping.connectValue,
|
|
427
631
|
refs: mapping.refs,
|
|
428
632
|
metadata: mapping.metadata ?? null,
|
|
429
633
|
updatedAt: /* @__PURE__ */ new Date()
|
|
430
|
-
}).where(drizzleOrm.eq(
|
|
634
|
+
}).where(drizzleOrm.eq(khotanMappingsTable.id, mapping.id)).returning({ id: khotanMappingsTable.id });
|
|
431
635
|
return { id: rows2[0].id, created: false };
|
|
432
636
|
}
|
|
433
|
-
const existing = await db.select({ id:
|
|
434
|
-
drizzleOrm.sql`${
|
|
637
|
+
const existing = await db.select({ id: khotanMappingsTable.id }).from(khotanMappingsTable).where(
|
|
638
|
+
drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${mapping.resourceId} and ${khotanMappingsTable.connectValue} = ${mapping.connectValue}`
|
|
435
639
|
).limit(1);
|
|
436
|
-
const rows = await db.insert(
|
|
640
|
+
const rows = await db.insert(khotanMappingsTable).values({
|
|
437
641
|
resourceId: mapping.resourceId,
|
|
438
642
|
connectValue: mapping.connectValue,
|
|
439
643
|
refs: mapping.refs,
|
|
440
644
|
metadata: mapping.metadata ?? null
|
|
441
645
|
}).onConflictDoUpdate({
|
|
442
|
-
target: [
|
|
646
|
+
target: [khotanMappingsTable.resourceId, khotanMappingsTable.connectValue],
|
|
443
647
|
set: {
|
|
444
|
-
refs: drizzleOrm.sql`${
|
|
648
|
+
refs: drizzleOrm.sql`${khotanMappingsTable.refs} || ${JSON.stringify(mapping.refs)}::jsonb`,
|
|
445
649
|
metadata: mapping.metadata ?? null,
|
|
446
650
|
updatedAt: /* @__PURE__ */ new Date()
|
|
447
651
|
}
|
|
448
|
-
}).returning({ id:
|
|
652
|
+
}).returning({ id: khotanMappingsTable.id });
|
|
449
653
|
return { id: rows[0].id, created: existing.length === 0 };
|
|
450
654
|
},
|
|
451
655
|
async getMapping(id) {
|
|
452
|
-
const rows = await db.select().from(
|
|
656
|
+
const rows = await db.select().from(khotanMappingsTable).where(drizzleOrm.eq(khotanMappingsTable.id, id)).limit(1);
|
|
453
657
|
return rows[0] ?? null;
|
|
454
658
|
},
|
|
455
|
-
async listMappings(resourceId) {
|
|
456
|
-
|
|
659
|
+
async listMappings({ resourceId, limit, offset, search }) {
|
|
660
|
+
const normalizedSearch = search?.trim();
|
|
661
|
+
const searchPattern = normalizedSearch ? `%${normalizedSearch.replace(/[%_]/g, "\\$&")}%` : null;
|
|
662
|
+
const filters = searchPattern ? drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${resourceId} and (${khotanMappingsTable.connectValue} ilike ${searchPattern} escape '\\' or ${khotanMappingsTable.refs}::text ilike ${searchPattern} escape '\\' or ${khotanMappingsTable.metadata}::text ilike ${searchPattern} escape '\\')` : drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${resourceId}`;
|
|
663
|
+
const totalRows = await db.select({ total: drizzleOrm.count(khotanMappingsTable.id) }).from(khotanMappingsTable).where(filters);
|
|
664
|
+
const rows = await db.select().from(khotanMappingsTable).where(filters).orderBy(khotanMappingsTable.connectValue, khotanMappingsTable.id).limit(limit + 1).offset(offset);
|
|
665
|
+
return {
|
|
666
|
+
items: rows.slice(0, limit),
|
|
667
|
+
hasMore: rows.length > limit,
|
|
668
|
+
total: totalRows[0]?.total ?? 0
|
|
669
|
+
};
|
|
457
670
|
},
|
|
458
671
|
async deleteMapping(id) {
|
|
459
|
-
await db.delete(
|
|
672
|
+
await db.delete(khotanMappingsTable).where(drizzleOrm.eq(khotanMappingsTable.id, id));
|
|
460
673
|
},
|
|
461
|
-
async lookupMapping(
|
|
462
|
-
|
|
463
|
-
|
|
674
|
+
async lookupMapping(params) {
|
|
675
|
+
if ("connectValue" in params) {
|
|
676
|
+
const rows2 = await db.select().from(khotanMappingsTable).where(
|
|
677
|
+
drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${params.resourceId} and ${khotanMappingsTable.connectValue} = ${params.connectValue}`
|
|
678
|
+
).limit(1);
|
|
679
|
+
return rows2[0] ?? null;
|
|
680
|
+
}
|
|
681
|
+
const rows = await db.select().from(khotanMappingsTable).where(
|
|
682
|
+
drizzleOrm.sql`${khotanMappingsTable.resourceId} = ${params.resourceId} and ${khotanMappingsTable.refs}->>${params.plugName} = ${params.ref}`
|
|
464
683
|
).limit(1);
|
|
465
684
|
return rows[0] ?? null;
|
|
466
685
|
},
|
|
@@ -793,6 +1012,149 @@ function getErrorMessage(error) {
|
|
|
793
1012
|
if (error instanceof Error) return error.message;
|
|
794
1013
|
return "Unknown error";
|
|
795
1014
|
}
|
|
1015
|
+
var CRON_MONTH_ALIASES = {
|
|
1016
|
+
jan: 1,
|
|
1017
|
+
feb: 2,
|
|
1018
|
+
mar: 3,
|
|
1019
|
+
apr: 4,
|
|
1020
|
+
may: 5,
|
|
1021
|
+
jun: 6,
|
|
1022
|
+
jul: 7,
|
|
1023
|
+
aug: 8,
|
|
1024
|
+
sep: 9,
|
|
1025
|
+
oct: 10,
|
|
1026
|
+
nov: 11,
|
|
1027
|
+
dec: 12
|
|
1028
|
+
};
|
|
1029
|
+
var CRON_DAY_ALIASES = {
|
|
1030
|
+
sun: 0,
|
|
1031
|
+
mon: 1,
|
|
1032
|
+
tue: 2,
|
|
1033
|
+
wed: 3,
|
|
1034
|
+
thu: 4,
|
|
1035
|
+
fri: 5,
|
|
1036
|
+
sat: 6
|
|
1037
|
+
};
|
|
1038
|
+
function parseCronValue(token, spec) {
|
|
1039
|
+
const normalized = token.trim().toLowerCase();
|
|
1040
|
+
if (normalized === "") {
|
|
1041
|
+
throw new Error("Cron token cannot be empty");
|
|
1042
|
+
}
|
|
1043
|
+
if (spec.aliases?.[normalized] !== void 0) {
|
|
1044
|
+
return spec.aliases[normalized];
|
|
1045
|
+
}
|
|
1046
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
1047
|
+
if (!Number.isFinite(parsed)) {
|
|
1048
|
+
throw new Error(`Invalid cron token: "${token}"`);
|
|
1049
|
+
}
|
|
1050
|
+
if (spec.aliases === CRON_DAY_ALIASES && parsed === 7) {
|
|
1051
|
+
return 0;
|
|
1052
|
+
}
|
|
1053
|
+
if (parsed < spec.min || parsed > spec.max) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`Cron value "${token}" is out of range ${spec.min}-${spec.max}`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
return parsed;
|
|
1059
|
+
}
|
|
1060
|
+
function cronFieldIsWildcard(field) {
|
|
1061
|
+
return field.trim() === "*";
|
|
1062
|
+
}
|
|
1063
|
+
function matchesCronField(field, value, spec) {
|
|
1064
|
+
return field.split(",").map((part) => part.trim()).filter(Boolean).some((part) => {
|
|
1065
|
+
if (part === "*") return true;
|
|
1066
|
+
const [baseRaw, stepRaw] = part.split("/");
|
|
1067
|
+
const base = baseRaw?.trim() ?? "";
|
|
1068
|
+
const step = stepRaw ? Number.parseInt(stepRaw.trim(), 10) : 1;
|
|
1069
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
1070
|
+
throw new Error(`Invalid cron step: "${part}"`);
|
|
1071
|
+
}
|
|
1072
|
+
let start;
|
|
1073
|
+
let end;
|
|
1074
|
+
if (base === "*" || base === "") {
|
|
1075
|
+
start = spec.min;
|
|
1076
|
+
end = spec.max;
|
|
1077
|
+
} else if (base.includes("-")) {
|
|
1078
|
+
const [startRaw, endRaw] = base.split("-");
|
|
1079
|
+
start = parseCronValue(startRaw ?? "", spec);
|
|
1080
|
+
end = parseCronValue(endRaw ?? "", spec);
|
|
1081
|
+
if (end < start) {
|
|
1082
|
+
throw new Error(`Invalid cron range: "${part}"`);
|
|
1083
|
+
}
|
|
1084
|
+
} else {
|
|
1085
|
+
start = parseCronValue(base, spec);
|
|
1086
|
+
end = stepRaw ? spec.max : start;
|
|
1087
|
+
}
|
|
1088
|
+
if (value < start || value > end) return false;
|
|
1089
|
+
return (value - start) % step === 0;
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
function matchesCronSchedule(schedule, now) {
|
|
1093
|
+
const parts = schedule.trim().split(/\s+/);
|
|
1094
|
+
if (parts.length !== 5) {
|
|
1095
|
+
throw new Error(`Cron schedule must have 5 fields: "${schedule}"`);
|
|
1096
|
+
}
|
|
1097
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
1098
|
+
const minuteMatch = matchesCronField(minute, now.getUTCMinutes(), {
|
|
1099
|
+
min: 0,
|
|
1100
|
+
max: 59
|
|
1101
|
+
});
|
|
1102
|
+
const hourMatch = matchesCronField(hour, now.getUTCHours(), {
|
|
1103
|
+
min: 0,
|
|
1104
|
+
max: 23
|
|
1105
|
+
});
|
|
1106
|
+
const monthMatch = matchesCronField(month, now.getUTCMonth() + 1, {
|
|
1107
|
+
min: 1,
|
|
1108
|
+
max: 12,
|
|
1109
|
+
aliases: CRON_MONTH_ALIASES
|
|
1110
|
+
});
|
|
1111
|
+
const dayOfMonthMatch = matchesCronField(dayOfMonth, now.getUTCDate(), {
|
|
1112
|
+
min: 1,
|
|
1113
|
+
max: 31
|
|
1114
|
+
});
|
|
1115
|
+
const dayOfWeekMatch = matchesCronField(dayOfWeek, now.getUTCDay(), {
|
|
1116
|
+
min: 0,
|
|
1117
|
+
max: 6,
|
|
1118
|
+
aliases: CRON_DAY_ALIASES
|
|
1119
|
+
});
|
|
1120
|
+
const dayOfMonthWildcard = cronFieldIsWildcard(dayOfMonth);
|
|
1121
|
+
const dayOfWeekWildcard = cronFieldIsWildcard(dayOfWeek);
|
|
1122
|
+
const dayMatches = dayOfMonthWildcard && dayOfWeekWildcard ? true : dayOfMonthWildcard ? dayOfWeekMatch : dayOfWeekWildcard ? dayOfMonthMatch : dayOfMonthMatch || dayOfWeekMatch;
|
|
1123
|
+
return minuteMatch && hourMatch && monthMatch && dayMatches;
|
|
1124
|
+
}
|
|
1125
|
+
function startOfUtcMinute(date) {
|
|
1126
|
+
return new Date(
|
|
1127
|
+
Date.UTC(
|
|
1128
|
+
date.getUTCFullYear(),
|
|
1129
|
+
date.getUTCMonth(),
|
|
1130
|
+
date.getUTCDate(),
|
|
1131
|
+
date.getUTCHours(),
|
|
1132
|
+
date.getUTCMinutes(),
|
|
1133
|
+
0,
|
|
1134
|
+
0
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
function coerceDate(value) {
|
|
1139
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
1140
|
+
return value;
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1143
|
+
const parsed = new Date(value);
|
|
1144
|
+
if (Number.isFinite(parsed.getTime())) return parsed;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
function isCronRequestAuthorized(request) {
|
|
1149
|
+
const secret = process.env["CRON_SECRET"]?.trim();
|
|
1150
|
+
if (!secret) {
|
|
1151
|
+
return process.env["NODE_ENV"] !== "production";
|
|
1152
|
+
}
|
|
1153
|
+
return request.headers.get("authorization") === `Bearer ${secret}`;
|
|
1154
|
+
}
|
|
1155
|
+
function isDebugEnabled() {
|
|
1156
|
+
return Boolean(process?.env?.["KHOTAN_DEBUG"]) && process?.env?.["NODE_ENV"] !== "production";
|
|
1157
|
+
}
|
|
796
1158
|
function isWorkflowCancelledError(error) {
|
|
797
1159
|
if (!error || typeof error !== "object") return false;
|
|
798
1160
|
const record = error;
|
|
@@ -905,31 +1267,263 @@ function serializeEndpoints(endpoints) {
|
|
|
905
1267
|
}
|
|
906
1268
|
return result;
|
|
907
1269
|
}
|
|
1270
|
+
function isPlainObject(value) {
|
|
1271
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1272
|
+
}
|
|
1273
|
+
function serializeConnectField(connectField) {
|
|
1274
|
+
return Array.isArray(connectField) ? JSON.stringify(connectField) : connectField;
|
|
1275
|
+
}
|
|
1276
|
+
function deserializeConnectField(connectField) {
|
|
1277
|
+
if (Array.isArray(connectField)) {
|
|
1278
|
+
return connectField;
|
|
1279
|
+
}
|
|
1280
|
+
if (typeof connectField !== "string") {
|
|
1281
|
+
throw new Error("Resource connectField must be a string or string array");
|
|
1282
|
+
}
|
|
1283
|
+
const trimmed = connectField.trim();
|
|
1284
|
+
if (!trimmed.startsWith("[")) {
|
|
1285
|
+
return connectField;
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
const parsed = JSON.parse(trimmed);
|
|
1289
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0)) {
|
|
1290
|
+
return parsed;
|
|
1291
|
+
}
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
return connectField;
|
|
1295
|
+
}
|
|
1296
|
+
function validateConnectField(resourceName, connectField) {
|
|
1297
|
+
if (typeof connectField === "string") {
|
|
1298
|
+
if (!connectField.trim()) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`Resource "${resourceName}" must declare a non-empty connectField`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (!Array.isArray(connectField) || connectField.length === 0) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`Resource "${resourceName}" must declare connectField as a string or non-empty ordered string array`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
for (const field of connectField) {
|
|
1311
|
+
if (typeof field !== "string" || !field.trim()) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`Resource "${resourceName}" has an invalid composite connectField entry`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function validateResourcePlugs(resource, plugNames) {
|
|
1319
|
+
if (resource.mapping.plugs === void 0) {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (!isPlainObject(resource.mapping.plugs)) {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`Resource "${resource.name}" must declare mapping.plugs as an object keyed by plug name`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
for (const [plugName, declaration] of Object.entries(resource.mapping.plugs)) {
|
|
1328
|
+
if (!plugNames.has(plugName)) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`Resource "${resource.name}" references unknown plug: "${plugName}"`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
if (!isPlainObject(declaration)) {
|
|
1334
|
+
throw new Error(
|
|
1335
|
+
`Resource "${resource.name}" has an invalid plug declaration for "${plugName}"`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
const keys = Object.keys(declaration);
|
|
1339
|
+
if (keys.length !== 1 || typeof declaration["uniqueIdentifier"] !== "string" || !declaration["uniqueIdentifier"].trim()) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`Resource "${resource.name}" must declare exactly one uniqueIdentifier for plug "${plugName}"`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function normalizeCacheScope(cacheName, scope) {
|
|
1347
|
+
if (scope === void 0) {
|
|
1348
|
+
return void 0;
|
|
1349
|
+
}
|
|
1350
|
+
if (!isPlainObject(scope)) {
|
|
1351
|
+
throw new Error(`Cache "${cacheName}" must declare scope as an object`);
|
|
1352
|
+
}
|
|
1353
|
+
const normalized = {};
|
|
1354
|
+
for (const key of ["plug", "resource", "flow"]) {
|
|
1355
|
+
const value = scope[key];
|
|
1356
|
+
if (value === void 0) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1360
|
+
throw new Error(`Cache "${cacheName}" has an invalid scope.${key} value`);
|
|
1361
|
+
}
|
|
1362
|
+
normalized[key] = value.trim();
|
|
1363
|
+
}
|
|
1364
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
1365
|
+
}
|
|
1366
|
+
function parseCacheTtlSeconds(cacheName, ttl) {
|
|
1367
|
+
if (ttl === void 0) {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
if (typeof ttl === "number") {
|
|
1371
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
1372
|
+
throw new Error(`Cache "${cacheName}" must declare a positive ttl`);
|
|
1373
|
+
}
|
|
1374
|
+
return Math.ceil(ttl);
|
|
1375
|
+
}
|
|
1376
|
+
if (typeof ttl !== "string") {
|
|
1377
|
+
throw new Error(`Cache "${cacheName}" must declare ttl as a string or number`);
|
|
1378
|
+
}
|
|
1379
|
+
const normalized = ttl.trim().toLowerCase();
|
|
1380
|
+
const match = /^(\d+)\s*(ms|s|m|h|d)$/.exec(normalized);
|
|
1381
|
+
if (!match) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`Cache "${cacheName}" has an invalid ttl "${ttl}". Use values like "30s", "15m", or "6h"`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
const amount = Number.parseInt(match[1], 10);
|
|
1387
|
+
const unit = match[2];
|
|
1388
|
+
const milliseconds = unit === "ms" ? amount : unit === "s" ? amount * 1e3 : unit === "m" ? amount * 6e4 : unit === "h" ? amount * 36e5 : amount * 864e5;
|
|
1389
|
+
return Math.max(1, Math.ceil(milliseconds / 1e3));
|
|
1390
|
+
}
|
|
1391
|
+
function validateCacheKey(key) {
|
|
1392
|
+
if (typeof key !== "string" || !key.trim()) {
|
|
1393
|
+
throw new Error("Cache key must be a non-empty string");
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
function coerceCacheEntryRecord(row) {
|
|
1397
|
+
if (typeof row["id"] !== "string" || typeof row["cacheId"] !== "string" || typeof row["key"] !== "string") {
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
return {
|
|
1401
|
+
id: row["id"],
|
|
1402
|
+
cacheId: row["cacheId"],
|
|
1403
|
+
key: row["key"],
|
|
1404
|
+
value: row["value"],
|
|
1405
|
+
expiresAt: coerceDate(row["expiresAt"]),
|
|
1406
|
+
createdAt: coerceDate(row["createdAt"]) ?? void 0,
|
|
1407
|
+
updatedAt: coerceDate(row["updatedAt"]) ?? void 0
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function isCacheEntryExpired(entry, now = /* @__PURE__ */ new Date()) {
|
|
1411
|
+
return entry.expiresAt !== null && entry.expiresAt.getTime() <= now.getTime();
|
|
1412
|
+
}
|
|
1413
|
+
function canonicalizeConnectValue(resource, connectValue) {
|
|
1414
|
+
const { connectField } = resource.mapping;
|
|
1415
|
+
if (Array.isArray(connectField)) {
|
|
1416
|
+
if (typeof connectValue === "string") {
|
|
1417
|
+
return connectValue;
|
|
1418
|
+
}
|
|
1419
|
+
if (!Array.isArray(connectValue)) {
|
|
1420
|
+
throw new Error(
|
|
1421
|
+
`Resource "${resource.name}" expects composite connectValue input matching connectField order`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
if (connectValue.length !== connectField.length) {
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
`Resource "${resource.name}" expects ${String(connectField.length)} connectValue parts in declared order`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
const parts = connectValue.map((part) => {
|
|
1430
|
+
if (typeof part === "string") return part;
|
|
1431
|
+
if (typeof part === "number" || typeof part === "boolean") {
|
|
1432
|
+
return String(part);
|
|
1433
|
+
}
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
`Resource "${resource.name}" connectValue parts must be strings, numbers, or booleans`
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
return JSON.stringify(parts);
|
|
1439
|
+
}
|
|
1440
|
+
if (typeof connectValue === "string") {
|
|
1441
|
+
return connectValue;
|
|
1442
|
+
}
|
|
1443
|
+
if (typeof connectValue === "number" || typeof connectValue === "boolean") {
|
|
1444
|
+
return String(connectValue);
|
|
1445
|
+
}
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
`Resource "${resource.name}" expects connectValue to be a string, number, or boolean`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
908
1450
|
function khotan(config) {
|
|
909
|
-
const { adapter, plugs, resources = [] } = config;
|
|
1451
|
+
const { adapter, plugs, resources = [], caches = [], authorize } = config;
|
|
1452
|
+
const instanceId = crypto.randomUUID();
|
|
1453
|
+
if (!authorize) {
|
|
1454
|
+
console.warn(
|
|
1455
|
+
"[khotan] No `authorize` hook configured: the management API (/api/khotan/*) is publicly accessible. Pass `authorize` to gate it behind your auth layer (e.g. better-auth). This is required for any deployed environment."
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
if (!(config.secret ?? process.env["KHOTAN_SECRET"])) {
|
|
1459
|
+
console.warn(
|
|
1460
|
+
"[khotan] No `secret`/`KHOTAN_SECRET` configured: plug credentials and wire metadata will not be encrypted at rest. Set KHOTAN_SECRET to a high-entropy value."
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
const plugNames = /* @__PURE__ */ new Set();
|
|
1464
|
+
for (const plug of plugs) {
|
|
1465
|
+
if (plugNames.has(plug.name)) {
|
|
1466
|
+
throw new Error(`Duplicate plug name: "${plug.name}"`);
|
|
1467
|
+
}
|
|
1468
|
+
plugNames.add(plug.name);
|
|
1469
|
+
}
|
|
910
1470
|
const resourceNames = /* @__PURE__ */ new Set();
|
|
1471
|
+
const resourceConfigByName = /* @__PURE__ */ new Map();
|
|
911
1472
|
for (const resource of resources) {
|
|
912
1473
|
if (resourceNames.has(resource.name)) {
|
|
913
1474
|
throw new Error(`Duplicate resource name: "${resource.name}"`);
|
|
914
1475
|
}
|
|
1476
|
+
validateConnectField(resource.name, resource.mapping.connectField);
|
|
1477
|
+
validateResourcePlugs(resource, plugNames);
|
|
915
1478
|
resourceNames.add(resource.name);
|
|
1479
|
+
resourceConfigByName.set(resource.name, resource);
|
|
916
1480
|
}
|
|
917
|
-
const
|
|
1481
|
+
const registeredFlowNames = /* @__PURE__ */ new Set();
|
|
918
1482
|
for (const plug of plugs) {
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
throw new Error(
|
|
927
|
-
`Flow "${flow2.name}" references unknown resource: "${flow2.resource}"`
|
|
928
|
-
);
|
|
929
|
-
}
|
|
1483
|
+
if (!plug.flows) continue;
|
|
1484
|
+
for (const flow2 of plug.flows) {
|
|
1485
|
+
registeredFlowNames.add(flow2.name);
|
|
1486
|
+
if (flow2.resource && !resourceNames.has(flow2.resource)) {
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
`Flow "${flow2.name}" references unknown resource: "${flow2.resource}"`
|
|
1489
|
+
);
|
|
930
1490
|
}
|
|
931
1491
|
}
|
|
932
1492
|
}
|
|
1493
|
+
const cacheStateByName = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const cache of caches) {
|
|
1495
|
+
if (cacheStateByName.has(cache.name)) {
|
|
1496
|
+
throw new Error(`Duplicate cache name: "${cache.name}"`);
|
|
1497
|
+
}
|
|
1498
|
+
if (typeof cache.name !== "string" || !cache.name.trim()) {
|
|
1499
|
+
throw new Error("Cache registrations must declare a non-empty name");
|
|
1500
|
+
}
|
|
1501
|
+
const normalizedScope = normalizeCacheScope(cache.name, cache.scope);
|
|
1502
|
+
if (normalizedScope?.plug && !plugNames.has(normalizedScope.plug)) {
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
`Cache "${cache.name}" references unknown plug: "${normalizedScope.plug}"`
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
if (normalizedScope?.resource && !resourceNames.has(normalizedScope.resource)) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`Cache "${cache.name}" references unknown resource: "${normalizedScope.resource}"`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (normalizedScope?.flow && !registeredFlowNames.has(normalizedScope.flow)) {
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`Cache "${cache.name}" references unknown flow: "${normalizedScope.flow}"`
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
cacheStateByName.set(cache.name, {
|
|
1518
|
+
id: "",
|
|
1519
|
+
config: {
|
|
1520
|
+
...cache,
|
|
1521
|
+
name: cache.name.trim(),
|
|
1522
|
+
...normalizedScope ? { scope: normalizedScope } : {}
|
|
1523
|
+
},
|
|
1524
|
+
ttlSeconds: parseCacheTtlSeconds(cache.name, cache.ttl)
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
933
1527
|
const registeredFlowKeys = new Set(
|
|
934
1528
|
plugs.flatMap(
|
|
935
1529
|
(plug) => (plug.flows ?? []).map(
|
|
@@ -971,16 +1565,31 @@ function khotan(config) {
|
|
|
971
1565
|
}
|
|
972
1566
|
let initialized = false;
|
|
973
1567
|
let initPromise = null;
|
|
1568
|
+
const resourceIdByName = /* @__PURE__ */ new Map();
|
|
1569
|
+
const resourceConfigById = /* @__PURE__ */ new Map();
|
|
974
1570
|
async function doInit() {
|
|
975
1571
|
if (initialized) return;
|
|
976
|
-
|
|
1572
|
+
resourceIdByName.clear();
|
|
1573
|
+
resourceConfigById.clear();
|
|
977
1574
|
for (const resource of resources) {
|
|
978
1575
|
const { id } = await adapter.upsertResource({
|
|
979
1576
|
name: resource.name,
|
|
980
|
-
connectField: resource.connectField,
|
|
1577
|
+
connectField: resource.mapping.connectField,
|
|
981
1578
|
description: resource.description ?? null
|
|
982
1579
|
});
|
|
983
|
-
|
|
1580
|
+
resourceIdByName.set(resource.name, id);
|
|
1581
|
+
resourceConfigById.set(id, resource);
|
|
1582
|
+
}
|
|
1583
|
+
for (const [cacheName, cacheState] of cacheStateByName) {
|
|
1584
|
+
const { id } = await adapter.upsertCache({
|
|
1585
|
+
name: cacheName,
|
|
1586
|
+
scope: cacheState.config.scope ?? null,
|
|
1587
|
+
ttlSeconds: cacheState.ttlSeconds
|
|
1588
|
+
});
|
|
1589
|
+
cacheStateByName.set(cacheName, {
|
|
1590
|
+
...cacheState,
|
|
1591
|
+
id
|
|
1592
|
+
});
|
|
984
1593
|
}
|
|
985
1594
|
for (const plug of plugs) {
|
|
986
1595
|
const { id: plugId } = await adapter.upsertPlug({
|
|
@@ -998,7 +1607,7 @@ function khotan(config) {
|
|
|
998
1607
|
schedule: flow2.schedule ?? null
|
|
999
1608
|
});
|
|
1000
1609
|
if (flow2.resource) {
|
|
1001
|
-
const resourceId =
|
|
1610
|
+
const resourceId = resourceIdByName.get(flow2.resource);
|
|
1002
1611
|
await adapter.updateFlowResourceId(flowId, resourceId);
|
|
1003
1612
|
}
|
|
1004
1613
|
}
|
|
@@ -1033,6 +1642,189 @@ function khotan(config) {
|
|
|
1033
1642
|
initPromise ??= doInit();
|
|
1034
1643
|
return initPromise;
|
|
1035
1644
|
}
|
|
1645
|
+
async function getRegisteredResourceById(resourceId) {
|
|
1646
|
+
await init();
|
|
1647
|
+
return resourceConfigById.get(resourceId) ?? null;
|
|
1648
|
+
}
|
|
1649
|
+
async function resolveCacheState(cacheName) {
|
|
1650
|
+
await init();
|
|
1651
|
+
const cacheState = cacheStateByName.get(cacheName);
|
|
1652
|
+
if (!cacheState || !cacheState.id) {
|
|
1653
|
+
throw new Error(`Cache "${cacheName}" is not registered`);
|
|
1654
|
+
}
|
|
1655
|
+
return cacheState;
|
|
1656
|
+
}
|
|
1657
|
+
function createCacheInstance(cacheName) {
|
|
1658
|
+
return {
|
|
1659
|
+
async get(key) {
|
|
1660
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
1661
|
+
return entry ? entry.value : null;
|
|
1662
|
+
},
|
|
1663
|
+
async set(key, value) {
|
|
1664
|
+
validateCacheKey(key);
|
|
1665
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1666
|
+
const expiresAt = cacheState.ttlSeconds !== null ? new Date(Date.now() + cacheState.ttlSeconds * 1e3) : null;
|
|
1667
|
+
await adapter.upsertCacheEntry({
|
|
1668
|
+
cacheId: cacheState.id,
|
|
1669
|
+
key,
|
|
1670
|
+
value,
|
|
1671
|
+
expiresAt
|
|
1672
|
+
});
|
|
1673
|
+
return value;
|
|
1674
|
+
},
|
|
1675
|
+
async delete(key) {
|
|
1676
|
+
validateCacheKey(key);
|
|
1677
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1678
|
+
await adapter.deleteCacheEntry(cacheState.id, key);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
async function readCacheEntry(cacheName, key) {
|
|
1683
|
+
validateCacheKey(key);
|
|
1684
|
+
const cacheState = await resolveCacheState(cacheName);
|
|
1685
|
+
const row = await adapter.getCacheEntry(cacheState.id, key);
|
|
1686
|
+
if (!row) {
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
const entry = coerceCacheEntryRecord(row);
|
|
1690
|
+
if (!entry || isCacheEntryExpired(entry)) {
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
return entry;
|
|
1694
|
+
}
|
|
1695
|
+
function decorateResourceRecord(resource) {
|
|
1696
|
+
const { connectField: storedConnectField, ...rest } = resource;
|
|
1697
|
+
const configResource = typeof resource["name"] === "string" ? resourceConfigByName.get(resource["name"]) : void 0;
|
|
1698
|
+
return {
|
|
1699
|
+
...rest,
|
|
1700
|
+
mapping: {
|
|
1701
|
+
connectField: configResource?.mapping.connectField ?? deserializeConnectField(storedConnectField),
|
|
1702
|
+
...configResource?.mapping.plugs ? { plugs: configResource.mapping.plugs } : {}
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
function buildMappingPage(params) {
|
|
1707
|
+
return {
|
|
1708
|
+
items: params.items,
|
|
1709
|
+
page: {
|
|
1710
|
+
limit: params.limit,
|
|
1711
|
+
offset: params.offset,
|
|
1712
|
+
hasMore: params.hasMore,
|
|
1713
|
+
prevOffset: Math.max(params.offset - params.limit, 0),
|
|
1714
|
+
nextOffset: params.offset + params.limit,
|
|
1715
|
+
total: params.total
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
async function validateMappingPayload(params) {
|
|
1720
|
+
if (!isPlainObject(params.refs)) {
|
|
1721
|
+
throw new Error("Mapping refs must be an object keyed by plug name");
|
|
1722
|
+
}
|
|
1723
|
+
for (const [plugName, ref] of Object.entries(params.refs)) {
|
|
1724
|
+
if (typeof ref !== "string") {
|
|
1725
|
+
throw new Error(`Mapping ref "${plugName}" must be a string`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (params.metadata !== void 0 && params.metadata !== null) {
|
|
1729
|
+
if (!isPlainObject(params.metadata)) {
|
|
1730
|
+
throw new Error("Mapping metadata must be an object when provided");
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1734
|
+
if (!resource) {
|
|
1735
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1736
|
+
}
|
|
1737
|
+
if (resource.mapping.plugs) {
|
|
1738
|
+
const invalidPlugs = Object.keys(params.refs).filter(
|
|
1739
|
+
(plugName) => !resource.mapping.plugs?.[plugName]
|
|
1740
|
+
);
|
|
1741
|
+
if (invalidPlugs.length > 0) {
|
|
1742
|
+
throw new Error(
|
|
1743
|
+
`Resource "${resource.name}" only allows refs for declared plugs. Invalid refs: ${invalidPlugs.join(", ")}`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return resource;
|
|
1748
|
+
}
|
|
1749
|
+
async function listMappings(params) {
|
|
1750
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1751
|
+
if (!resource) {
|
|
1752
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1753
|
+
}
|
|
1754
|
+
const limit = Math.min(Math.max(params.limit ?? 20, 1), 100);
|
|
1755
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
1756
|
+
const page = await adapter.listMappings({
|
|
1757
|
+
resourceId: params.resourceId,
|
|
1758
|
+
limit,
|
|
1759
|
+
offset,
|
|
1760
|
+
...params.search?.trim() ? { search: params.search.trim() } : {}
|
|
1761
|
+
});
|
|
1762
|
+
return buildMappingPage({
|
|
1763
|
+
limit,
|
|
1764
|
+
offset,
|
|
1765
|
+
hasMore: page.hasMore,
|
|
1766
|
+
total: page.total,
|
|
1767
|
+
items: page.items
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
async function lookupMapping(params) {
|
|
1771
|
+
const resource = await getRegisteredResourceById(params.resourceId);
|
|
1772
|
+
if (!resource) {
|
|
1773
|
+
throw new Error(`Resource "${params.resourceId}" is not registered`);
|
|
1774
|
+
}
|
|
1775
|
+
if ("connectValue" in params) {
|
|
1776
|
+
return adapter.lookupMapping({
|
|
1777
|
+
resourceId: params.resourceId,
|
|
1778
|
+
connectValue: canonicalizeConnectValue(resource, params.connectValue)
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
if (resource.mapping.plugs && !resource.mapping.plugs[params.plugName]) {
|
|
1782
|
+
throw new Error(
|
|
1783
|
+
`Resource "${resource.name}" does not declare plug "${params.plugName}"`
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
return adapter.lookupMapping(params);
|
|
1787
|
+
}
|
|
1788
|
+
async function upsertMapping(mapping) {
|
|
1789
|
+
const resource = await validateMappingPayload(mapping);
|
|
1790
|
+
const result = await adapter.upsertMapping({
|
|
1791
|
+
resourceId: mapping.resourceId,
|
|
1792
|
+
connectValue: canonicalizeConnectValue(resource, mapping.connectValue),
|
|
1793
|
+
refs: mapping.refs,
|
|
1794
|
+
metadata: mapping.metadata ?? null
|
|
1795
|
+
});
|
|
1796
|
+
const saved = await adapter.getMapping(result.id);
|
|
1797
|
+
if (!saved) {
|
|
1798
|
+
throw new Error("Mapping was saved but could not be reloaded");
|
|
1799
|
+
}
|
|
1800
|
+
return saved;
|
|
1801
|
+
}
|
|
1802
|
+
async function updateMapping(id, mapping) {
|
|
1803
|
+
const existing = await adapter.getMapping(id);
|
|
1804
|
+
if (!existing) {
|
|
1805
|
+
throw new Error(`Mapping "${id}" not found`);
|
|
1806
|
+
}
|
|
1807
|
+
const resource = await validateMappingPayload(mapping);
|
|
1808
|
+
await adapter.upsertMapping({
|
|
1809
|
+
id,
|
|
1810
|
+
resourceId: mapping.resourceId,
|
|
1811
|
+
connectValue: canonicalizeConnectValue(resource, mapping.connectValue),
|
|
1812
|
+
refs: mapping.refs,
|
|
1813
|
+
metadata: mapping.metadata ?? null
|
|
1814
|
+
});
|
|
1815
|
+
const saved = await adapter.getMapping(id);
|
|
1816
|
+
if (!saved) {
|
|
1817
|
+
throw new Error(`Mapping "${id}" disappeared after update`);
|
|
1818
|
+
}
|
|
1819
|
+
return saved;
|
|
1820
|
+
}
|
|
1821
|
+
async function deleteMapping(id) {
|
|
1822
|
+
const existing = await adapter.getMapping(id);
|
|
1823
|
+
if (!existing) {
|
|
1824
|
+
throw new Error(`Mapping "${id}" not found`);
|
|
1825
|
+
}
|
|
1826
|
+
await adapter.deleteMapping(id);
|
|
1827
|
+
}
|
|
1036
1828
|
function wire(plugName) {
|
|
1037
1829
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
1038
1830
|
if (!plugReg) {
|
|
@@ -1043,29 +1835,7 @@ function khotan(config) {
|
|
|
1043
1835
|
}
|
|
1044
1836
|
const wireConfig = plugReg.wires[0];
|
|
1045
1837
|
function createBoundPlug(vars, _setVars) {
|
|
1046
|
-
|
|
1047
|
-
const opts = (extra) => ({
|
|
1048
|
-
...extra,
|
|
1049
|
-
vars,
|
|
1050
|
-
..._setVars && { _setVars }
|
|
1051
|
-
});
|
|
1052
|
-
return {
|
|
1053
|
-
get(path, extra) {
|
|
1054
|
-
return plug.get(path, opts(extra));
|
|
1055
|
-
},
|
|
1056
|
-
post(path, extra) {
|
|
1057
|
-
return plug.post(path, opts(extra));
|
|
1058
|
-
},
|
|
1059
|
-
put(path, extra) {
|
|
1060
|
-
return plug.put(path, opts(extra));
|
|
1061
|
-
},
|
|
1062
|
-
patch(path, extra) {
|
|
1063
|
-
return plug.patch(path, opts(extra));
|
|
1064
|
-
},
|
|
1065
|
-
delete(path, extra) {
|
|
1066
|
-
return plug.delete(path, opts(extra));
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1838
|
+
return bindPlugWithVars(plugReg.plug, vars, _setVars);
|
|
1069
1839
|
}
|
|
1070
1840
|
async function getWireVars(wireId) {
|
|
1071
1841
|
const raw = await adapter.getWireMetadata(wireId);
|
|
@@ -1255,31 +2025,21 @@ function khotan(config) {
|
|
|
1255
2025
|
const setFlowVars = async (updates) => {
|
|
1256
2026
|
await setVars(plugName, { ...vars, ...updates });
|
|
1257
2027
|
};
|
|
1258
|
-
const
|
|
1259
|
-
|
|
2028
|
+
const boundPlug = bindPlugWithVars(
|
|
2029
|
+
plugReg.plug,
|
|
1260
2030
|
vars,
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
return plugReg.plug.get(path, opts(extra));
|
|
1266
|
-
},
|
|
1267
|
-
post(path, extra) {
|
|
1268
|
-
return plugReg.plug.post(path, opts(extra));
|
|
1269
|
-
},
|
|
1270
|
-
put(path, extra) {
|
|
1271
|
-
return plugReg.plug.put(path, opts(extra));
|
|
1272
|
-
},
|
|
1273
|
-
patch(path, extra) {
|
|
1274
|
-
return plugReg.plug.patch(path, opts(extra));
|
|
1275
|
-
},
|
|
1276
|
-
delete(path, extra) {
|
|
1277
|
-
return plugReg.plug.delete(path, opts(extra));
|
|
1278
|
-
}
|
|
2031
|
+
secret ? setFlowVars : void 0
|
|
2032
|
+
);
|
|
2033
|
+
const plugVarsByName = {
|
|
2034
|
+
[plugName]: vars
|
|
1279
2035
|
};
|
|
2036
|
+
if (flowReg.to && plugNames.has(flowReg.to)) {
|
|
2037
|
+
plugVarsByName[flowReg.to] = secret ? await getVars(flowReg.to).catch(() => ({})) : {};
|
|
2038
|
+
}
|
|
1280
2039
|
const flowContext = {
|
|
1281
2040
|
id: flowId,
|
|
1282
2041
|
name: flowReg.name,
|
|
2042
|
+
plugName,
|
|
1283
2043
|
type: flowReg.type,
|
|
1284
2044
|
resource: flowReg.resource ?? null,
|
|
1285
2045
|
to: flowReg.to ?? null
|
|
@@ -1292,7 +2052,9 @@ function khotan(config) {
|
|
|
1292
2052
|
runType,
|
|
1293
2053
|
body: requestBody["body"],
|
|
1294
2054
|
vars,
|
|
1295
|
-
|
|
2055
|
+
plugVarsByName,
|
|
2056
|
+
khotanRunId: runId,
|
|
2057
|
+
khotanInstanceId: instanceId
|
|
1296
2058
|
}
|
|
1297
2059
|
]);
|
|
1298
2060
|
const workflowRunId = getWorkflowRunId(result2);
|
|
@@ -1317,7 +2079,8 @@ function khotan(config) {
|
|
|
1317
2079
|
runType,
|
|
1318
2080
|
body: requestBody["body"],
|
|
1319
2081
|
vars,
|
|
1320
|
-
setVars: setFlowVars
|
|
2082
|
+
setVars: setFlowVars,
|
|
2083
|
+
cache: createCacheInstance
|
|
1321
2084
|
});
|
|
1322
2085
|
const runResult = toFlowRunResult(result);
|
|
1323
2086
|
const { counters, status } = await completeRunOk(runResult);
|
|
@@ -1338,6 +2101,112 @@ function khotan(config) {
|
|
|
1338
2101
|
);
|
|
1339
2102
|
}
|
|
1340
2103
|
}
|
|
2104
|
+
async function wasFlowTriggeredInMinuteWindow(flowId, slotStart, slotEnd) {
|
|
2105
|
+
const runs = await adapter.listRuns(flowId);
|
|
2106
|
+
return runs.some((run) => {
|
|
2107
|
+
const startedAt = coerceDate(run["startedAt"]);
|
|
2108
|
+
if (!startedAt) return false;
|
|
2109
|
+
const startedAtMs = startedAt.getTime();
|
|
2110
|
+
return startedAtMs >= slotStart.getTime() && startedAtMs < slotEnd.getTime();
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
async function dispatchScheduledFlows(options = {}) {
|
|
2114
|
+
await init();
|
|
2115
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2116
|
+
const slotStart = startOfUtcMinute(now);
|
|
2117
|
+
const slotEnd = new Date(slotStart.getTime() + 6e4);
|
|
2118
|
+
const runType = options.runType ?? "full";
|
|
2119
|
+
const registeredFlows = (await adapter.listFlows()).filter(
|
|
2120
|
+
(flow2) => isRegisteredFlowRecord(flow2)
|
|
2121
|
+
);
|
|
2122
|
+
const scheduledFlows = registeredFlows.filter(
|
|
2123
|
+
(flow2) => typeof flow2["schedule"] === "string" && flow2["schedule"].trim()
|
|
2124
|
+
);
|
|
2125
|
+
const triggered = [];
|
|
2126
|
+
const skipped = [];
|
|
2127
|
+
for (const flow2 of scheduledFlows) {
|
|
2128
|
+
const flowId = typeof flow2["id"] === "string" ? flow2["id"] : null;
|
|
2129
|
+
const flowName = typeof flow2["name"] === "string" ? flow2["name"] : null;
|
|
2130
|
+
const plugName = typeof flow2["plugName"] === "string" ? flow2["plugName"] : null;
|
|
2131
|
+
const schedule = typeof flow2["schedule"] === "string" ? flow2["schedule"].trim() : "";
|
|
2132
|
+
if (!flowId || !flowName || !plugName || !schedule) continue;
|
|
2133
|
+
if (flow2["enabled"] === false) {
|
|
2134
|
+
skipped.push({
|
|
2135
|
+
flowId,
|
|
2136
|
+
flowName,
|
|
2137
|
+
plugName,
|
|
2138
|
+
schedule,
|
|
2139
|
+
reason: "disabled"
|
|
2140
|
+
});
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
let isDue = false;
|
|
2144
|
+
try {
|
|
2145
|
+
isDue = matchesCronSchedule(schedule, now);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
skipped.push({
|
|
2148
|
+
flowId,
|
|
2149
|
+
flowName,
|
|
2150
|
+
plugName,
|
|
2151
|
+
schedule,
|
|
2152
|
+
reason: "invalid_schedule",
|
|
2153
|
+
detail: getErrorMessage(error)
|
|
2154
|
+
});
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (!isDue) {
|
|
2158
|
+
skipped.push({
|
|
2159
|
+
flowId,
|
|
2160
|
+
flowName,
|
|
2161
|
+
plugName,
|
|
2162
|
+
schedule,
|
|
2163
|
+
reason: "not_due"
|
|
2164
|
+
});
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (await wasFlowTriggeredInMinuteWindow(flowId, slotStart, slotEnd)) {
|
|
2168
|
+
skipped.push({
|
|
2169
|
+
flowId,
|
|
2170
|
+
flowName,
|
|
2171
|
+
plugName,
|
|
2172
|
+
schedule,
|
|
2173
|
+
reason: "already_triggered"
|
|
2174
|
+
});
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
const response = await triggerFlowRun(flowId, { runType });
|
|
2178
|
+
const payload = await response.json().catch(() => ({}));
|
|
2179
|
+
if (!response.ok) {
|
|
2180
|
+
skipped.push({
|
|
2181
|
+
flowId,
|
|
2182
|
+
flowName,
|
|
2183
|
+
plugName,
|
|
2184
|
+
schedule,
|
|
2185
|
+
reason: "trigger_failed",
|
|
2186
|
+
status: response.status,
|
|
2187
|
+
detail: typeof payload["error"] === "string" ? payload["error"] : response.statusText
|
|
2188
|
+
});
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
triggered.push({
|
|
2192
|
+
flowId,
|
|
2193
|
+
flowName,
|
|
2194
|
+
plugName,
|
|
2195
|
+
schedule,
|
|
2196
|
+
runId: payload["id"] ?? null,
|
|
2197
|
+
workflowRunId: payload["workflowRunId"] ?? null,
|
|
2198
|
+
status: typeof payload["status"] === "string" ? payload["status"] : "running"
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
return {
|
|
2202
|
+
ok: true,
|
|
2203
|
+
tickAt: slotStart.toISOString(),
|
|
2204
|
+
runType,
|
|
2205
|
+
evaluated: scheduledFlows.length,
|
|
2206
|
+
triggered,
|
|
2207
|
+
skipped
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
1341
2210
|
async function resolveFlowId(flowNameOrId, options = {}) {
|
|
1342
2211
|
await init();
|
|
1343
2212
|
const byId = await adapter.getFlow(flowNameOrId);
|
|
@@ -1409,13 +2278,32 @@ function khotan(config) {
|
|
|
1409
2278
|
const plugsIdx = segments.indexOf("plugs");
|
|
1410
2279
|
const flowsIdx = segments.indexOf("flows");
|
|
1411
2280
|
const resourcesIdx = segments.indexOf("resources");
|
|
2281
|
+
const cachesIdx = segments.indexOf("caches");
|
|
1412
2282
|
const mappingsIdx = segments.indexOf("mappings");
|
|
1413
2283
|
const runsIdx = segments.indexOf("runs");
|
|
1414
2284
|
const wiresIdx = segments.indexOf("wires");
|
|
1415
2285
|
const webhookHandlersIdx = segments.indexOf("webhook-handlers");
|
|
1416
2286
|
const webhookEventsIdx = segments.indexOf("webhook-events");
|
|
1417
2287
|
const variablesIdx = segments.indexOf("variables");
|
|
2288
|
+
const cronIdx = segments.indexOf("cron");
|
|
2289
|
+
const webhookIdx = segments.indexOf("webhook");
|
|
1418
2290
|
const debugIdx = segments.indexOf("debug");
|
|
2291
|
+
const isInboundWebhook = webhookIdx !== -1 && webhookIdx === segments.length - 2;
|
|
2292
|
+
const isCronRoute = cronIdx !== -1 && cronIdx === segments.length - 1;
|
|
2293
|
+
const isDebugRoute = debugIdx !== -1;
|
|
2294
|
+
if (authorize && !isInboundWebhook && !isCronRoute && !isDebugRoute) {
|
|
2295
|
+
let allowed = await isCliRequestAuthorized(request, secret);
|
|
2296
|
+
if (!allowed) {
|
|
2297
|
+
try {
|
|
2298
|
+
allowed = await authorize(request);
|
|
2299
|
+
} catch {
|
|
2300
|
+
allowed = false;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (!allowed) {
|
|
2304
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
1419
2307
|
const limit = Math.min(
|
|
1420
2308
|
Math.max(
|
|
1421
2309
|
Number.parseInt(url.searchParams.get("limit") ?? "20", 10) || 20,
|
|
@@ -1427,17 +2315,43 @@ function khotan(config) {
|
|
|
1427
2315
|
Number.parseInt(url.searchParams.get("offset") ?? "0", 10) || 0,
|
|
1428
2316
|
0
|
|
1429
2317
|
);
|
|
2318
|
+
const search = url.searchParams.get("search")?.trim() || void 0;
|
|
2319
|
+
const wantsMappingPage = url.searchParams.has("limit") || url.searchParams.has("offset") || url.searchParams.has("search");
|
|
1430
2320
|
if (request.method === "GET") {
|
|
2321
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
2322
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
2323
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
2324
|
+
try {
|
|
2325
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
2326
|
+
if (!entry) {
|
|
2327
|
+
return Response.json({ error: "Cache entry not found" }, { status: 404 });
|
|
2328
|
+
}
|
|
2329
|
+
return Response.json({
|
|
2330
|
+
cache: cacheName,
|
|
2331
|
+
key: entry.key,
|
|
2332
|
+
value: entry.value,
|
|
2333
|
+
expiresAt: entry.expiresAt
|
|
2334
|
+
});
|
|
2335
|
+
} catch (error) {
|
|
2336
|
+
const message = error instanceof Error ? error.message : "Invalid cache request";
|
|
2337
|
+
return Response.json({ error: message }, { status: 400 });
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (cronIdx !== -1 && cronIdx === segments.length - 1) {
|
|
2341
|
+
if (!isCronRequestAuthorized(request)) {
|
|
2342
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2343
|
+
}
|
|
2344
|
+
const result = await dispatchScheduledFlows();
|
|
2345
|
+
return Response.json(result);
|
|
2346
|
+
}
|
|
1431
2347
|
if (debugIdx !== -1 && debugIdx === segments.length - 1) {
|
|
1432
|
-
|
|
1433
|
-
if (!debugActive) {
|
|
2348
|
+
if (!isDebugEnabled()) {
|
|
1434
2349
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
1435
2350
|
}
|
|
1436
2351
|
return Response.json({ enabled: true });
|
|
1437
2352
|
}
|
|
1438
2353
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
1439
|
-
|
|
1440
|
-
if (!debugActive) {
|
|
2354
|
+
if (!isDebugEnabled()) {
|
|
1441
2355
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
1442
2356
|
}
|
|
1443
2357
|
const plugName = segments[debugIdx + 1];
|
|
@@ -1659,12 +2573,27 @@ function khotan(config) {
|
|
|
1659
2573
|
const filtered = data.filter(
|
|
1660
2574
|
(r) => typeof r["name"] === "string" && resourceNames.has(r["name"])
|
|
1661
2575
|
);
|
|
1662
|
-
return Response.json(filtered);
|
|
2576
|
+
return Response.json(filtered.map(decorateResourceRecord));
|
|
1663
2577
|
}
|
|
1664
2578
|
if (resourcesIdx !== -1 && resourcesIdx === segments.length - 3 && segments[resourcesIdx + 2] === "mappings") {
|
|
1665
2579
|
const resourceId = segments[resourcesIdx + 1];
|
|
1666
|
-
const
|
|
1667
|
-
|
|
2580
|
+
const resource = await getRegisteredResourceById(resourceId);
|
|
2581
|
+
if (!resource) {
|
|
2582
|
+
return Response.json(
|
|
2583
|
+
{ error: "Resource not found" },
|
|
2584
|
+
{ status: 404 }
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
const page = await listMappings({
|
|
2588
|
+
resourceId,
|
|
2589
|
+
limit,
|
|
2590
|
+
offset,
|
|
2591
|
+
...search ? { search } : {}
|
|
2592
|
+
});
|
|
2593
|
+
if (!wantsMappingPage) {
|
|
2594
|
+
return Response.json(page.items);
|
|
2595
|
+
}
|
|
2596
|
+
return Response.json(page);
|
|
1668
2597
|
}
|
|
1669
2598
|
if (resourcesIdx !== -1 && resourcesIdx === segments.length - 2) {
|
|
1670
2599
|
const resourceId = segments[resourcesIdx + 1];
|
|
@@ -1676,7 +2605,7 @@ function khotan(config) {
|
|
|
1676
2605
|
);
|
|
1677
2606
|
}
|
|
1678
2607
|
const flows = await adapter.getResourceFlows(resourceId);
|
|
1679
|
-
return Response.json({ ...resource, flows });
|
|
2608
|
+
return Response.json({ ...decorateResourceRecord(resource), flows });
|
|
1680
2609
|
}
|
|
1681
2610
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
1682
2611
|
const mappingId = segments[mappingsIdx + 1];
|
|
@@ -1688,7 +2617,42 @@ function khotan(config) {
|
|
|
1688
2617
|
}
|
|
1689
2618
|
}
|
|
1690
2619
|
if (request.method === "POST") {
|
|
1691
|
-
|
|
2620
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
2621
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
2622
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
2623
|
+
const body = await request.json().catch(() => ({}));
|
|
2624
|
+
if (!("value" in body)) {
|
|
2625
|
+
return Response.json({ error: "Cache writes require a value" }, { status: 400 });
|
|
2626
|
+
}
|
|
2627
|
+
try {
|
|
2628
|
+
const cacheHandle = createCacheInstance(cacheName);
|
|
2629
|
+
await cacheHandle.set(key, body.value);
|
|
2630
|
+
const entry = await readCacheEntry(cacheName, key);
|
|
2631
|
+
return Response.json({
|
|
2632
|
+
cache: cacheName,
|
|
2633
|
+
key,
|
|
2634
|
+
value: body.value,
|
|
2635
|
+
expiresAt: entry?.expiresAt ?? null
|
|
2636
|
+
});
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
const message = error instanceof Error ? error.message : "Invalid cache payload";
|
|
2639
|
+
return Response.json({ error: message }, { status: 400 });
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (cronIdx !== -1 && cronIdx === segments.length - 1) {
|
|
2643
|
+
if (!isCronRequestAuthorized(request)) {
|
|
2644
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2645
|
+
}
|
|
2646
|
+
let body = {};
|
|
2647
|
+
try {
|
|
2648
|
+
body = await request.json();
|
|
2649
|
+
} catch {
|
|
2650
|
+
body = {};
|
|
2651
|
+
}
|
|
2652
|
+
const runType = typeof body["runType"] === "string" ? body["runType"] : "full";
|
|
2653
|
+
const result = await dispatchScheduledFlows({ runType });
|
|
2654
|
+
return Response.json(result);
|
|
2655
|
+
}
|
|
1692
2656
|
if (webhookIdx !== -1 && webhookIdx === segments.length - 2) {
|
|
1693
2657
|
const plugName = segments[webhookIdx + 1];
|
|
1694
2658
|
const plugReg = plugs.find((p) => p.name === plugName);
|
|
@@ -1802,7 +2766,13 @@ function khotan(config) {
|
|
|
1802
2766
|
}
|
|
1803
2767
|
try {
|
|
1804
2768
|
const result = await startWorkflow(c.workflow, [
|
|
1805
|
-
{
|
|
2769
|
+
{
|
|
2770
|
+
event,
|
|
2771
|
+
eventType,
|
|
2772
|
+
headers,
|
|
2773
|
+
khotanRunId,
|
|
2774
|
+
khotanInstanceId: instanceId
|
|
2775
|
+
}
|
|
1806
2776
|
]);
|
|
1807
2777
|
const workflowRunId = result && typeof result === "object" ? "runId" in result ? String(result.runId) : "id" in result ? String(result.id) : null : null;
|
|
1808
2778
|
if (workflowRunId) {
|
|
@@ -1865,7 +2835,14 @@ function khotan(config) {
|
|
|
1865
2835
|
}
|
|
1866
2836
|
try {
|
|
1867
2837
|
const result = await startWorkflow(p.workflow, [
|
|
1868
|
-
{
|
|
2838
|
+
{
|
|
2839
|
+
event,
|
|
2840
|
+
eventType,
|
|
2841
|
+
headers,
|
|
2842
|
+
destVars,
|
|
2843
|
+
khotanRunId,
|
|
2844
|
+
khotanInstanceId: instanceId
|
|
2845
|
+
}
|
|
1869
2846
|
]);
|
|
1870
2847
|
const workflowRunId = result && typeof result === "object" ? "runId" in result ? String(result.runId) : "id" in result ? String(result.id) : null : null;
|
|
1871
2848
|
if (workflowRunId) {
|
|
@@ -1892,8 +2869,7 @@ function khotan(config) {
|
|
|
1892
2869
|
return Response.json({ received: true }, { status: 202 });
|
|
1893
2870
|
}
|
|
1894
2871
|
if (debugIdx !== -1 && debugIdx === segments.length - 2) {
|
|
1895
|
-
|
|
1896
|
-
if (!debugActive) {
|
|
2872
|
+
if (!isDebugEnabled()) {
|
|
1897
2873
|
return Response.json({ error: "Not found" }, { status: 404 });
|
|
1898
2874
|
}
|
|
1899
2875
|
const plugName = segments[debugIdx + 1];
|
|
@@ -2093,7 +3069,38 @@ function khotan(config) {
|
|
|
2093
3069
|
}
|
|
2094
3070
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2 && segments[mappingsIdx + 1] === "lookup") {
|
|
2095
3071
|
const body = await request.json();
|
|
2096
|
-
|
|
3072
|
+
if (!body || typeof body !== "object" || typeof body["resourceId"] !== "string") {
|
|
3073
|
+
return Response.json(
|
|
3074
|
+
{
|
|
3075
|
+
error: "Lookup requires resourceId plus either connectValue or plugName with ref"
|
|
3076
|
+
},
|
|
3077
|
+
{ status: 400 }
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
const hasConnectValue = "connectValue" in body;
|
|
3081
|
+
const hasPlugRef = "plugName" in body && typeof body.plugName === "string" && "ref" in body && typeof body.ref === "string";
|
|
3082
|
+
if (!hasConnectValue && !hasPlugRef) {
|
|
3083
|
+
return Response.json(
|
|
3084
|
+
{
|
|
3085
|
+
error: "Lookup requires either connectValue or plugName with ref"
|
|
3086
|
+
},
|
|
3087
|
+
{ status: 400 }
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
let mapping;
|
|
3091
|
+
try {
|
|
3092
|
+
mapping = hasConnectValue ? await lookupMapping({
|
|
3093
|
+
resourceId: body.resourceId,
|
|
3094
|
+
connectValue: body.connectValue
|
|
3095
|
+
}) : await lookupMapping({
|
|
3096
|
+
resourceId: body.resourceId,
|
|
3097
|
+
plugName: body.plugName,
|
|
3098
|
+
ref: body.ref
|
|
3099
|
+
});
|
|
3100
|
+
} catch (error) {
|
|
3101
|
+
const message = error instanceof Error ? error.message : "Invalid lookup request";
|
|
3102
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3103
|
+
}
|
|
2097
3104
|
if (!mapping) {
|
|
2098
3105
|
return Response.json({ error: "Mapping not found" }, { status: 404 });
|
|
2099
3106
|
}
|
|
@@ -2101,11 +3108,17 @@ function khotan(config) {
|
|
|
2101
3108
|
}
|
|
2102
3109
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 1) {
|
|
2103
3110
|
const body = await request.json();
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
3111
|
+
try {
|
|
3112
|
+
const existing = await lookupMapping({
|
|
3113
|
+
resourceId: body.resourceId,
|
|
3114
|
+
connectValue: body.connectValue
|
|
3115
|
+
});
|
|
3116
|
+
const saved = await upsertMapping(body);
|
|
3117
|
+
return Response.json(saved, { status: existing ? 200 : 201 });
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
const message = error instanceof Error ? error.message : "Invalid mapping payload";
|
|
3120
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3121
|
+
}
|
|
2109
3122
|
}
|
|
2110
3123
|
}
|
|
2111
3124
|
if (request.method === "PATCH") {
|
|
@@ -2143,11 +3156,30 @@ function khotan(config) {
|
|
|
2143
3156
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
2144
3157
|
const mappingId = segments[mappingsIdx + 1];
|
|
2145
3158
|
const body = await request.json();
|
|
2146
|
-
|
|
2147
|
-
|
|
3159
|
+
try {
|
|
3160
|
+
const saved = await updateMapping(mappingId, body);
|
|
3161
|
+
return Response.json(saved);
|
|
3162
|
+
} catch (error) {
|
|
3163
|
+
const message = error instanceof Error ? error.message : "Invalid mapping payload";
|
|
3164
|
+
return Response.json(
|
|
3165
|
+
{ error: message },
|
|
3166
|
+
{ status: message.includes("not found") ? 404 : 400 }
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
2148
3169
|
}
|
|
2149
3170
|
}
|
|
2150
3171
|
if (request.method === "DELETE") {
|
|
3172
|
+
if (cachesIdx !== -1 && cachesIdx === segments.length - 3) {
|
|
3173
|
+
const cacheName = decodeURIComponent(segments[cachesIdx + 1]);
|
|
3174
|
+
const key = decodeURIComponent(segments[cachesIdx + 2]);
|
|
3175
|
+
try {
|
|
3176
|
+
await createCacheInstance(cacheName).delete(key);
|
|
3177
|
+
return new Response(null, { status: 204 });
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
const message = error instanceof Error ? error.message : "Invalid cache request";
|
|
3180
|
+
return Response.json({ error: message }, { status: 400 });
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
2151
3183
|
if (variablesIdx !== -1 && variablesIdx === segments.length - 2) {
|
|
2152
3184
|
const plugName = segments[variablesIdx + 1];
|
|
2153
3185
|
if (!plugNames.has(plugName)) {
|
|
@@ -2179,6 +3211,10 @@ function khotan(config) {
|
|
|
2179
3211
|
}
|
|
2180
3212
|
if (mappingsIdx !== -1 && mappingsIdx === segments.length - 2) {
|
|
2181
3213
|
const mappingId = segments[mappingsIdx + 1];
|
|
3214
|
+
const existing = await adapter.getMapping(mappingId);
|
|
3215
|
+
if (!existing) {
|
|
3216
|
+
return Response.json({ error: "Mapping not found" }, { status: 404 });
|
|
3217
|
+
}
|
|
2182
3218
|
await adapter.deleteMapping(mappingId);
|
|
2183
3219
|
return new Response(null, { status: 204 });
|
|
2184
3220
|
}
|
|
@@ -2282,11 +3318,25 @@ function khotan(config) {
|
|
|
2282
3318
|
}
|
|
2283
3319
|
return plugReg.plug;
|
|
2284
3320
|
}
|
|
3321
|
+
khotanRuntimeRegistry.set(instanceId, {
|
|
3322
|
+
cache: createCacheInstance,
|
|
3323
|
+
listMappings,
|
|
3324
|
+
lookupMapping,
|
|
3325
|
+
upsertMapping,
|
|
3326
|
+
updateMapping,
|
|
3327
|
+
deleteMapping
|
|
3328
|
+
});
|
|
2285
3329
|
return {
|
|
2286
3330
|
handler,
|
|
2287
3331
|
init,
|
|
2288
3332
|
flow,
|
|
2289
3333
|
wire,
|
|
3334
|
+
cache: createCacheInstance,
|
|
3335
|
+
listMappings,
|
|
3336
|
+
lookupMapping,
|
|
3337
|
+
upsertMapping,
|
|
3338
|
+
updateMapping,
|
|
3339
|
+
deleteMapping,
|
|
2290
3340
|
getVars,
|
|
2291
3341
|
setVars,
|
|
2292
3342
|
clearVars,
|
|
@@ -2311,8 +3361,12 @@ function toNextJsHandler(factoryHandler) {
|
|
|
2311
3361
|
exports.__setWorkflowGetRunForTests = __setWorkflowGetRunForTests;
|
|
2312
3362
|
exports.__setWorkflowGetWritableForTests = __setWorkflowGetWritableForTests;
|
|
2313
3363
|
exports.__setWorkflowStartForTests = __setWorkflowStartForTests;
|
|
3364
|
+
exports.bindWorkflowPlug = bindWorkflowPlug;
|
|
3365
|
+
exports.deriveCliToken = deriveCliToken;
|
|
2314
3366
|
exports.drizzleAdapter = drizzleAdapter;
|
|
2315
3367
|
exports.khotan = khotan;
|
|
3368
|
+
exports.khotanCache = khotanCache;
|
|
3369
|
+
exports.khotanMappings = khotanMappings;
|
|
2316
3370
|
exports.sendUpdate = sendUpdate;
|
|
2317
3371
|
exports.toNextJsHandler = toNextJsHandler;
|
|
2318
3372
|
//# sourceMappingURL=factory.cjs.map
|