@teamkeel/functions-runtime 0.459.0 → 0.460.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,6 +6,9 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
9
12
  var __export = (target, all) => {
10
13
  for (var name in all)
11
14
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -28,6 +31,323 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
31
  ));
29
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
33
 
34
+ // src/File.ts
35
+ var File_exports = {};
36
+ __export(File_exports, {
37
+ File: () => File,
38
+ InlineFile: () => InlineFile,
39
+ buildContentDisposition: () => buildContentDisposition,
40
+ deleteStoredFile: () => deleteStoredFile,
41
+ rewriteFilesDomain: () => rewriteFilesDomain
42
+ });
43
+ function rewriteFilesDomain(url) {
44
+ const domain = process.env.KEEL_FILES_DOMAIN;
45
+ if (domain) {
46
+ const override = new URL(domain);
47
+ url.protocol = override.protocol;
48
+ url.hostname = override.hostname;
49
+ url.port = override.port;
50
+ const overridePath = override.pathname.replace(/\/+$/, "");
51
+ if (overridePath && overridePath !== "/") {
52
+ url.pathname = overridePath + url.pathname;
53
+ }
54
+ }
55
+ return url;
56
+ }
57
+ function encodeRFC5987(value) {
58
+ return encodeURIComponent(value).replace(
59
+ /['()*]/g,
60
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
61
+ );
62
+ }
63
+ function buildContentDisposition(disposition, filename) {
64
+ const type = disposition === "attachment" ? "attachment" : "inline";
65
+ if (!filename || filename.trim() === "") {
66
+ return type;
67
+ }
68
+ const asciiFallback = filename.replace(/[^\x20-\x7e]|["\\]/g, "_");
69
+ return `${type}; filename="${asciiFallback}"; filename*=UTF-8''${encodeRFC5987(
70
+ filename
71
+ )}`;
72
+ }
73
+ async function storeFile(contents, key, filename, contentType, size, expires) {
74
+ if (!s3Client) {
75
+ throw new Error("S3 client is required");
76
+ }
77
+ const params = {
78
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
79
+ Key: "files/" + key,
80
+ Body: contents,
81
+ ContentType: contentType,
82
+ ContentDisposition: `attachment; filename="${encodeURIComponent(
83
+ filename
84
+ )}"`,
85
+ Metadata: {
86
+ filename
87
+ },
88
+ ACL: "private"
89
+ };
90
+ if (expires) {
91
+ if (expires instanceof Date) {
92
+ params.Expires = expires;
93
+ } else {
94
+ console.warn("Invalid expires value. Skipping Expires parameter.");
95
+ }
96
+ }
97
+ const command = new import_client_s3.PutObjectCommand(params);
98
+ try {
99
+ await s3Client.send(command);
100
+ } catch (error) {
101
+ console.error("Error uploading file:", error);
102
+ throw error;
103
+ }
104
+ }
105
+ async function deleteStoredFile(key) {
106
+ if (!s3Client) {
107
+ throw new Error("S3 client is required");
108
+ }
109
+ await s3Client.send(
110
+ new import_client_s3.DeleteObjectCommand({
111
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
112
+ Key: "files/" + key
113
+ })
114
+ );
115
+ }
116
+ var import_client_s3, import_credential_providers, import_s3_request_presigner, import_ksuid, s3Client, InlineFile, File;
117
+ var init_File = __esm({
118
+ "src/File.ts"() {
119
+ "use strict";
120
+ import_client_s3 = require("@aws-sdk/client-s3");
121
+ import_credential_providers = require("@aws-sdk/credential-providers");
122
+ import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
123
+ import_ksuid = __toESM(require("ksuid"), 1);
124
+ s3Client = (() => {
125
+ if (!process.env.KEEL_FILES_BUCKET_NAME) {
126
+ return null;
127
+ }
128
+ const endpoint = process.env.KEEL_S3_ENDPOINT;
129
+ if (endpoint) {
130
+ return new import_client_s3.S3Client({
131
+ region: process.env.KEEL_REGION,
132
+ credentials: {
133
+ accessKeyId: "keelstorage",
134
+ secretAccessKey: "keelstorage"
135
+ },
136
+ endpoint
137
+ });
138
+ }
139
+ const testEndpoint = process.env.TEST_AWS_ENDPOINT;
140
+ if (testEndpoint) {
141
+ return new import_client_s3.S3Client({
142
+ region: process.env.KEEL_REGION,
143
+ credentials: {
144
+ accessKeyId: "test",
145
+ secretAccessKey: "test"
146
+ },
147
+ endpointProvider: /* @__PURE__ */ __name(() => {
148
+ return {
149
+ url: new URL(testEndpoint)
150
+ };
151
+ }, "endpointProvider")
152
+ });
153
+ }
154
+ return new import_client_s3.S3Client({
155
+ region: process.env.KEEL_REGION,
156
+ credentials: (0, import_credential_providers.fromEnv)()
157
+ });
158
+ })();
159
+ __name(rewriteFilesDomain, "rewriteFilesDomain");
160
+ __name(encodeRFC5987, "encodeRFC5987");
161
+ __name(buildContentDisposition, "buildContentDisposition");
162
+ InlineFile = class _InlineFile {
163
+ static {
164
+ __name(this, "InlineFile");
165
+ }
166
+ constructor(input) {
167
+ this._filename = input.filename;
168
+ this._contentType = input.contentType;
169
+ this._contents = null;
170
+ }
171
+ static fromDataURL(dataURL) {
172
+ const info = dataURL.split(",")[0].split(":")[1];
173
+ const data = dataURL.split(",")[1];
174
+ const mimeType = info.split(";")[0];
175
+ const name = info.split(";")[1].split("=")[1] || "file";
176
+ const buffer = Buffer.from(data, "base64");
177
+ const file2 = new _InlineFile({ filename: name, contentType: mimeType });
178
+ file2.write(buffer);
179
+ return file2;
180
+ }
181
+ // Gets size of the file's contents in bytes
182
+ get size() {
183
+ if (this._contents) {
184
+ return this._contents.size;
185
+ }
186
+ return 0;
187
+ }
188
+ // Gets the media type of the file contents
189
+ get contentType() {
190
+ return this._contentType;
191
+ }
192
+ // Gets the name of the file
193
+ get filename() {
194
+ return this._filename;
195
+ }
196
+ // Write the files contents from a buffer
197
+ write(buffer) {
198
+ this._contents = new Blob([
199
+ new Uint8Array(
200
+ buffer.buffer,
201
+ buffer.byteOffset,
202
+ buffer.byteLength
203
+ )
204
+ ]);
205
+ }
206
+ // Reads the contents of the file as a buffer
207
+ async read() {
208
+ if (!this._contents) {
209
+ throw new Error("No contents to read");
210
+ }
211
+ const arrayBuffer = await this._contents.arrayBuffer();
212
+ return Buffer.from(arrayBuffer);
213
+ }
214
+ // Persists the file
215
+ async store(expires = null) {
216
+ const content = await this.read();
217
+ const key = import_ksuid.default.randomSync().string;
218
+ await storeFile(
219
+ content,
220
+ key,
221
+ this._filename,
222
+ this._contentType,
223
+ this.size,
224
+ expires
225
+ );
226
+ return new File({
227
+ key,
228
+ size: this.size,
229
+ filename: this.filename,
230
+ contentType: this.contentType
231
+ });
232
+ }
233
+ };
234
+ File = class _File extends InlineFile {
235
+ static {
236
+ __name(this, "File");
237
+ }
238
+ constructor(input) {
239
+ super({
240
+ filename: input.filename || "",
241
+ contentType: input.contentType || ""
242
+ });
243
+ this._key = input.key || "";
244
+ this._size = input.size || 0;
245
+ }
246
+ // Creates a new instance from the database record
247
+ static fromDbRecord(input) {
248
+ return new _File({
249
+ key: input.key,
250
+ filename: input.filename,
251
+ size: input.size,
252
+ contentType: input.contentType
253
+ });
254
+ }
255
+ get size() {
256
+ return this._size;
257
+ }
258
+ // Gets the stored key
259
+ get key() {
260
+ return this._key;
261
+ }
262
+ get isPublic() {
263
+ return false;
264
+ }
265
+ async read() {
266
+ if (this._contents) {
267
+ const arrayBuffer = await this._contents.arrayBuffer();
268
+ return Buffer.from(arrayBuffer);
269
+ }
270
+ if (!s3Client) {
271
+ throw new Error("S3 client is required");
272
+ }
273
+ const params = {
274
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
275
+ Key: "files/" + this.key
276
+ };
277
+ const command = new import_client_s3.GetObjectCommand(params);
278
+ const response = await s3Client.send(command);
279
+ const blob = await response.Body.transformToByteArray();
280
+ return Buffer.from(blob);
281
+ }
282
+ async store(expires = null) {
283
+ if (this._contents) {
284
+ const contents = await this.read();
285
+ await storeFile(
286
+ contents,
287
+ this.key,
288
+ this.filename,
289
+ this.contentType,
290
+ this.size,
291
+ expires
292
+ );
293
+ }
294
+ return this;
295
+ }
296
+ // Generates a presigned download URL.
297
+ //
298
+ // By default the browser previews the file inline and, when saved, uses the
299
+ // file's own filename. Pass `contentDisposition: "attachment"` to force a
300
+ // download, or `filename` to override the suggested name.
301
+ async getPresignedUrl(options) {
302
+ if (!s3Client) {
303
+ throw new Error("S3 client is required");
304
+ }
305
+ const disposition = options?.contentDisposition ?? "inline";
306
+ const filename = options?.filename ?? this.filename;
307
+ const command = new import_client_s3.GetObjectCommand({
308
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
309
+ Key: "files/" + this.key,
310
+ ResponseContentDisposition: buildContentDisposition(
311
+ disposition,
312
+ filename
313
+ )
314
+ });
315
+ const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
316
+ return rewriteFilesDomain(new URL(url));
317
+ }
318
+ // Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
319
+ async getPresignedUploadUrl() {
320
+ if (!s3Client) {
321
+ throw new Error("S3 client is required");
322
+ }
323
+ if (!this.key) {
324
+ this._key = import_ksuid.default.randomSync().string;
325
+ }
326
+ const command = new import_client_s3.PutObjectCommand({
327
+ Bucket: process.env.KEEL_FILES_BUCKET_NAME,
328
+ Key: "files/" + this.key
329
+ });
330
+ const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
331
+ return rewriteFilesDomain(new URL(url));
332
+ }
333
+ // Persists the file
334
+ toDbRecord() {
335
+ return {
336
+ key: this.key,
337
+ filename: this.filename,
338
+ contentType: this.contentType,
339
+ size: this.size
340
+ };
341
+ }
342
+ toJSON() {
343
+ return this.toDbRecord();
344
+ }
345
+ };
346
+ __name(storeFile, "storeFile");
347
+ __name(deleteStoredFile, "deleteStoredFile");
348
+ }
349
+ });
350
+
31
351
  // src/index.ts
32
352
  var index_exports = {};
33
353
  __export(index_exports, {
@@ -36,7 +356,7 @@ __export(index_exports, {
36
356
  File: () => File,
37
357
  FlowsAPI: () => FlowsAPI,
38
358
  InlineFile: () => InlineFile,
39
- KSUID: () => import_ksuid3.default,
359
+ KSUID: () => import_ksuid4.default,
40
360
  ModelAPI: () => ModelAPI,
41
361
  NonRetriableError: () => NonRetriableError,
42
362
  PERMISSION_STATE: () => PERMISSION_STATE,
@@ -51,6 +371,8 @@ __export(index_exports, {
51
371
  TaskAPI: () => TaskAPI,
52
372
  checkBuiltInPermissions: () => checkBuiltInPermissions,
53
373
  createFlowContext: () => createFlowContext,
374
+ createIntegrationServer: () => createIntegrationServer,
375
+ createNotifier: () => createNotifier,
54
376
  createTraceAPI: () => createTraceAPI2,
55
377
  experimental: () => experimental_exports,
56
378
  handleFlow: () => handleFlow,
@@ -58,7 +380,9 @@ __export(index_exports, {
58
380
  handleRequest: () => handleRequest,
59
381
  handleRoute: () => handleRoute,
60
382
  handleSubscriber: () => handleSubscriber,
383
+ insertNewStep: () => insertNewStep,
61
384
  ksuid: () => ksuid,
385
+ notify: () => notify,
62
386
  tracing: () => tracing_exports,
63
387
  useDatabase: () => useDatabase,
64
388
  z: () => import_zod.z
@@ -132,6 +456,9 @@ var AuditContextPlugin = class {
132
456
  const rawNode = import_kysely.sql`set_trace_id(${audit.traceId})`.as(this.traceIdAlias).toOperationNode();
133
457
  returning.selections.push(import_kysely.SelectionNode.create(rawNode));
134
458
  }
459
+ if (returning.selections.length === 0) {
460
+ return { ...args.node };
461
+ }
135
462
  return {
136
463
  ...args.node,
137
464
  returning
@@ -203,6 +530,20 @@ function isRichType(obj) {
203
530
  }
204
531
  __name(isRichType, "isRichType");
205
532
 
533
+ // src/jsonColumnValue.js
534
+ var JsonColumnValue = class {
535
+ static {
536
+ __name(this, "JsonColumnValue");
537
+ }
538
+ constructor(value) {
539
+ this.value = value;
540
+ }
541
+ };
542
+ function parseJsonColumnValue(value) {
543
+ return new JsonColumnValue(JSON.parse(value));
544
+ }
545
+ __name(parseJsonColumnValue, "parseJsonColumnValue");
546
+
206
547
  // src/camelCasePlugin.js
207
548
  var KeelCamelCasePlugin = class {
208
549
  static {
@@ -210,41 +551,77 @@ var KeelCamelCasePlugin = class {
210
551
  }
211
552
  constructor(opt) {
212
553
  this.opt = opt;
554
+ this.auditQueryIds = /* @__PURE__ */ new WeakSet();
213
555
  this.CamelCasePlugin = new import_kysely2.CamelCasePlugin({
214
556
  ...opt,
215
557
  underscoreBeforeDigits: true
216
558
  });
217
559
  }
218
560
  transformQuery(args) {
561
+ if (args.queryId && referencesTable(args.node, "keel_audit")) {
562
+ this.auditQueryIds.add(args.queryId);
563
+ }
219
564
  return this.CamelCasePlugin.transformQuery(args);
220
565
  }
221
566
  async transformResult(args) {
222
567
  if (args.result.rows && Array.isArray(args.result.rows)) {
568
+ const mapAuditJson = args.queryId && this.auditQueryIds.has(args.queryId);
223
569
  return {
224
570
  ...args.result,
225
- rows: args.result.rows.map((row) => this.mapRow(row))
571
+ rows: args.result.rows.map((row) => this.mapRow(row, { mapAuditJson }))
226
572
  };
227
573
  }
228
574
  return args.result;
229
575
  }
230
- mapRow(row) {
576
+ mapRow(row, context7 = {}) {
231
577
  return Object.keys(row).reduce((obj, key) => {
232
578
  if (key.endsWith("__sequence")) {
233
579
  return obj;
234
580
  }
235
581
  let value = row[key];
582
+ if (value instanceof JsonColumnValue) {
583
+ value = shouldMapJsonColumn(row, key, context7) && canMap(value.value, this.opt) ? this.mapRow(value.value, context7) : value.value;
584
+ obj[this.CamelCasePlugin.camelCase(key)] = value;
585
+ return obj;
586
+ }
236
587
  if (Array.isArray(value)) {
237
588
  value = value.map(
238
- (it) => canMap(it, this.opt) ? this.mapRow(it) : it
589
+ (it) => canMap(it, this.opt) ? this.mapRow(it, context7) : it
239
590
  );
240
591
  } else if (canMap(value, this.opt)) {
241
- value = this.mapRow(value);
592
+ value = this.mapRow(value, context7);
242
593
  }
243
594
  obj[this.CamelCasePlugin.camelCase(key)] = value;
244
595
  return obj;
245
596
  }, {});
246
597
  }
247
598
  };
599
+ function shouldMapJsonColumn(row, key, context7) {
600
+ return context7.mapAuditJson && key === "data" && "id" in row && "table_name" in row && "op" in row && "identity_id" in row && "trace_id" in row && "created_at" in row && "event_processed_at" in row;
601
+ }
602
+ __name(shouldMapJsonColumn, "shouldMapJsonColumn");
603
+ function referencesTable(node, tableName) {
604
+ if (!node || typeof node !== "object") {
605
+ return false;
606
+ }
607
+ if (node.kind === "IdentifierNode" && (node.name === tableName || node.name === "keelAudit")) {
608
+ return true;
609
+ }
610
+ if (node.kind === "RawNode" && Array.isArray(node.sqlFragments) && node.sqlFragments.some(
611
+ (fragment) => new RegExp(`(^|[^A-Za-z0-9_])${tableName}($|[^A-Za-z0-9_])`, "i").test(
612
+ fragment
613
+ )
614
+ )) {
615
+ return true;
616
+ }
617
+ return Object.values(node).some((value) => {
618
+ if (Array.isArray(value)) {
619
+ return value.some((item) => referencesTable(item, tableName));
620
+ }
621
+ return referencesTable(value, tableName);
622
+ });
623
+ }
624
+ __name(referencesTable, "referencesTable");
248
625
  function canMap(obj, opt) {
249
626
  return isPlainObject(obj) && !opt?.maintainNestedObjectKeys && !isRichType(obj);
250
627
  }
@@ -501,20 +878,44 @@ var KEEL_INTERNAL_CHILDREN = "includeChildrenSpans";
501
878
  var import_ws = __toESM(require("ws"), 1);
502
879
  var import_node_fs = require("fs");
503
880
  var dbInstance = new import_node_async_hooks2.AsyncLocalStorage();
881
+ var fileCleanupStore = new import_node_async_hooks2.AsyncLocalStorage();
882
+ function deferFileDeletion(key) {
883
+ fileCleanupStore.getStore()?.add(key);
884
+ }
885
+ __name(deferFileDeletion, "deferFileDeletion");
886
+ async function flushFileDeletions(keys) {
887
+ if (keys.size === 0) {
888
+ return;
889
+ }
890
+ const { deleteStoredFile: deleteStoredFile2 } = await Promise.resolve().then(() => (init_File(), File_exports));
891
+ for (const key of keys) {
892
+ try {
893
+ await deleteStoredFile2(key);
894
+ } catch (e) {
895
+ console.error("failed to delete orphaned file from storage", e);
896
+ }
897
+ }
898
+ }
899
+ __name(flushFileDeletions, "flushFileDeletions");
504
900
  var vitestDb = null;
505
901
  async function withDatabase(db, requiresTransaction, cb) {
902
+ const pending = /* @__PURE__ */ new Set();
506
903
  if (requiresTransaction) {
507
- return db.transaction().execute(async (transaction) => {
508
- return dbInstance.run(transaction, async () => {
509
- return cb({ transaction });
904
+ const result2 = await db.transaction().execute(async (transaction) => {
905
+ return dbInstance.run(transaction, () => {
906
+ return fileCleanupStore.run(pending, () => cb({ transaction }));
510
907
  });
511
908
  });
909
+ await flushFileDeletions(pending);
910
+ return result2;
512
911
  }
513
- return db.connection().execute(async (sDb) => {
514
- return dbInstance.run(sDb, async () => {
515
- return cb({ sDb });
912
+ const result = await db.connection().execute(async (sDb) => {
913
+ return dbInstance.run(sDb, () => {
914
+ return fileCleanupStore.run(pending, () => cb({ sDb }));
516
915
  });
517
916
  });
917
+ await flushFileDeletions(pending);
918
+ return result;
518
919
  }
519
920
  __name(withDatabase, "withDatabase");
520
921
  function useDatabase() {
@@ -621,6 +1022,8 @@ function getDialect(connString) {
621
1022
  import_pg.types.builtins.INTERVAL,
622
1023
  (val) => new Duration(val)
623
1024
  );
1025
+ import_pg.types.setTypeParser(import_pg.types.builtins.JSON, parseJsonColumnValue);
1026
+ import_pg.types.setTypeParser(import_pg.types.builtins.JSONB, parseJsonColumnValue);
624
1027
  const poolConfig = {
625
1028
  Client: InstrumentedClient,
626
1029
  // Increased idle time before closing a connection in the local pool (from 10s default).
@@ -655,6 +1058,8 @@ function getDialect(connString) {
655
1058
  import_pg.types.builtins.INTERVAL,
656
1059
  (val) => new Duration(val)
657
1060
  );
1061
+ neon.types.setTypeParser(import_pg.types.builtins.JSON, parseJsonColumnValue);
1062
+ neon.types.setTypeParser(import_pg.types.builtins.JSONB, parseJsonColumnValue);
658
1063
  neon.neonConfig.webSocketConstructor = import_ws.default;
659
1064
  const pool = new InstrumentedNeonServerlessPool({
660
1065
  // If connString is not passed fall back to reading from env var
@@ -689,271 +1094,8 @@ function getDialect(connString) {
689
1094
  }
690
1095
  __name(getDialect, "getDialect");
691
1096
 
692
- // src/File.ts
693
- var import_client_s3 = require("@aws-sdk/client-s3");
694
- var import_credential_providers = require("@aws-sdk/credential-providers");
695
- var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
696
- var import_ksuid = __toESM(require("ksuid"), 1);
697
- var s3Client = (() => {
698
- if (!process.env.KEEL_FILES_BUCKET_NAME) {
699
- return null;
700
- }
701
- const endpoint = process.env.KEEL_S3_ENDPOINT;
702
- if (endpoint) {
703
- return new import_client_s3.S3Client({
704
- region: process.env.KEEL_REGION,
705
- credentials: {
706
- accessKeyId: "keelstorage",
707
- secretAccessKey: "keelstorage"
708
- },
709
- endpoint
710
- });
711
- }
712
- const testEndpoint = process.env.TEST_AWS_ENDPOINT;
713
- if (testEndpoint) {
714
- return new import_client_s3.S3Client({
715
- region: process.env.KEEL_REGION,
716
- credentials: {
717
- accessKeyId: "test",
718
- secretAccessKey: "test"
719
- },
720
- endpointProvider: /* @__PURE__ */ __name(() => {
721
- return {
722
- url: new URL(testEndpoint)
723
- };
724
- }, "endpointProvider")
725
- });
726
- }
727
- return new import_client_s3.S3Client({
728
- region: process.env.KEEL_REGION,
729
- credentials: (0, import_credential_providers.fromEnv)()
730
- });
731
- })();
732
- function rewriteFilesDomain(url) {
733
- const domain = process.env.KEEL_FILES_DOMAIN;
734
- if (domain) {
735
- const override = new URL(domain);
736
- url.protocol = override.protocol;
737
- url.hostname = override.hostname;
738
- url.port = override.port;
739
- const overridePath = override.pathname.replace(/\/+$/, "");
740
- if (overridePath && overridePath !== "/") {
741
- url.pathname = overridePath + url.pathname;
742
- }
743
- }
744
- return url;
745
- }
746
- __name(rewriteFilesDomain, "rewriteFilesDomain");
747
- var InlineFile = class _InlineFile {
748
- static {
749
- __name(this, "InlineFile");
750
- }
751
- constructor(input) {
752
- this._filename = input.filename;
753
- this._contentType = input.contentType;
754
- this._contents = null;
755
- }
756
- static fromDataURL(dataURL) {
757
- const info = dataURL.split(",")[0].split(":")[1];
758
- const data = dataURL.split(",")[1];
759
- const mimeType = info.split(";")[0];
760
- const name = info.split(";")[1].split("=")[1] || "file";
761
- const buffer = Buffer.from(data, "base64");
762
- const file2 = new _InlineFile({ filename: name, contentType: mimeType });
763
- file2.write(buffer);
764
- return file2;
765
- }
766
- // Gets size of the file's contents in bytes
767
- get size() {
768
- if (this._contents) {
769
- return this._contents.size;
770
- }
771
- return 0;
772
- }
773
- // Gets the media type of the file contents
774
- get contentType() {
775
- return this._contentType;
776
- }
777
- // Gets the name of the file
778
- get filename() {
779
- return this._filename;
780
- }
781
- // Write the files contents from a buffer
782
- write(buffer) {
783
- this._contents = new Blob([
784
- new Uint8Array(
785
- buffer.buffer,
786
- buffer.byteOffset,
787
- buffer.byteLength
788
- )
789
- ]);
790
- }
791
- // Reads the contents of the file as a buffer
792
- async read() {
793
- if (!this._contents) {
794
- throw new Error("No contents to read");
795
- }
796
- const arrayBuffer = await this._contents.arrayBuffer();
797
- return Buffer.from(arrayBuffer);
798
- }
799
- // Persists the file
800
- async store(expires = null) {
801
- const content = await this.read();
802
- const key = import_ksuid.default.randomSync().string;
803
- await storeFile(
804
- content,
805
- key,
806
- this._filename,
807
- this._contentType,
808
- this.size,
809
- expires
810
- );
811
- return new File({
812
- key,
813
- size: this.size,
814
- filename: this.filename,
815
- contentType: this.contentType
816
- });
817
- }
818
- };
819
- var File = class _File extends InlineFile {
820
- static {
821
- __name(this, "File");
822
- }
823
- constructor(input) {
824
- super({
825
- filename: input.filename || "",
826
- contentType: input.contentType || ""
827
- });
828
- this._key = input.key || "";
829
- this._size = input.size || 0;
830
- }
831
- // Creates a new instance from the database record
832
- static fromDbRecord(input) {
833
- return new _File({
834
- key: input.key,
835
- filename: input.filename,
836
- size: input.size,
837
- contentType: input.contentType
838
- });
839
- }
840
- get size() {
841
- return this._size;
842
- }
843
- // Gets the stored key
844
- get key() {
845
- return this._key;
846
- }
847
- get isPublic() {
848
- return false;
849
- }
850
- async read() {
851
- if (this._contents) {
852
- const arrayBuffer = await this._contents.arrayBuffer();
853
- return Buffer.from(arrayBuffer);
854
- }
855
- if (!s3Client) {
856
- throw new Error("S3 client is required");
857
- }
858
- const params = {
859
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
860
- Key: "files/" + this.key
861
- };
862
- const command = new import_client_s3.GetObjectCommand(params);
863
- const response = await s3Client.send(command);
864
- const blob = await response.Body.transformToByteArray();
865
- return Buffer.from(blob);
866
- }
867
- async store(expires = null) {
868
- if (this._contents) {
869
- const contents = await this.read();
870
- await storeFile(
871
- contents,
872
- this.key,
873
- this.filename,
874
- this.contentType,
875
- this.size,
876
- expires
877
- );
878
- }
879
- return this;
880
- }
881
- // Generates a presigned download URL
882
- async getPresignedUrl() {
883
- if (!s3Client) {
884
- throw new Error("S3 client is required");
885
- }
886
- const command = new import_client_s3.GetObjectCommand({
887
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
888
- Key: "files/" + this.key,
889
- ResponseContentDisposition: "inline"
890
- });
891
- const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
892
- return rewriteFilesDomain(new URL(url));
893
- }
894
- // Generates a presigned upload URL. If the file doesn't have a key, a new one will be generated
895
- async getPresignedUploadUrl() {
896
- if (!s3Client) {
897
- throw new Error("S3 client is required");
898
- }
899
- if (!this.key) {
900
- this._key = import_ksuid.default.randomSync().string;
901
- }
902
- const command = new import_client_s3.PutObjectCommand({
903
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
904
- Key: "files/" + this.key
905
- });
906
- const url = await (0, import_s3_request_presigner.getSignedUrl)(s3Client, command, { expiresIn: 60 * 60 });
907
- return rewriteFilesDomain(new URL(url));
908
- }
909
- // Persists the file
910
- toDbRecord() {
911
- return {
912
- key: this.key,
913
- filename: this.filename,
914
- contentType: this.contentType,
915
- size: this.size
916
- };
917
- }
918
- toJSON() {
919
- return this.toDbRecord();
920
- }
921
- };
922
- async function storeFile(contents, key, filename, contentType, size, expires) {
923
- if (!s3Client) {
924
- throw new Error("S3 client is required");
925
- }
926
- const params = {
927
- Bucket: process.env.KEEL_FILES_BUCKET_NAME,
928
- Key: "files/" + key,
929
- Body: contents,
930
- ContentType: contentType,
931
- ContentDisposition: `attachment; filename="${encodeURIComponent(
932
- filename
933
- )}"`,
934
- Metadata: {
935
- filename
936
- },
937
- ACL: "private"
938
- };
939
- if (expires) {
940
- if (expires instanceof Date) {
941
- params.Expires = expires;
942
- } else {
943
- console.warn("Invalid expires value. Skipping Expires parameter.");
944
- }
945
- }
946
- const command = new import_client_s3.PutObjectCommand(params);
947
- try {
948
- await s3Client.send(command);
949
- } catch (error) {
950
- console.error("Error uploading file:", error);
951
- throw error;
952
- }
953
- }
954
- __name(storeFile, "storeFile");
955
-
956
1097
  // src/parsing.js
1098
+ init_File();
957
1099
  var dateFormat = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
958
1100
  function parseInputs(inputs) {
959
1101
  if (inputs != null && typeof inputs === "object") {
@@ -1886,6 +2028,7 @@ var QueryBuilder = class _QueryBuilder {
1886
2028
  };
1887
2029
 
1888
2030
  // src/ModelAPI.js
2031
+ init_File();
1889
2032
  var ModelAPI = class {
1890
2033
  static {
1891
2034
  __name(this, "ModelAPI");
@@ -1895,9 +2038,10 @@ var ModelAPI = class {
1895
2038
  * @param {Function} _ Used to be a function that returns the default values for a row in this table. No longer used.
1896
2039
  * @param {TableConfigMap} tableConfigMap
1897
2040
  */
1898
- constructor(tableName, _, tableConfigMap = {}) {
2041
+ constructor(tableName, _, tableConfigMap = {}, fileFieldsMap = {}) {
1899
2042
  this._tableName = tableName;
1900
2043
  this._tableConfigMap = tableConfigMap;
2044
+ this._fileFields = fileFieldsMap[tableName] || {};
1901
2045
  this._modelName = upperCamelCase(this._tableName);
1902
2046
  }
1903
2047
  async create(values) {
@@ -1967,6 +2111,10 @@ var ModelAPI = class {
1967
2111
  const name = spanNameForModelAPI(this._modelName, "update");
1968
2112
  const db = useDatabase();
1969
2113
  return withSpan(name, async (span) => {
2114
+ const fileColumns = Object.keys(values || {}).filter(
2115
+ (k) => k in this._fileFields
2116
+ );
2117
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
1970
2118
  let builder = db.updateTable(this._tableName).returningAll();
1971
2119
  const keys = values ? Object.keys(values) : [];
1972
2120
  const row = {};
@@ -2005,7 +2153,9 @@ var ModelAPI = class {
2005
2153
  span.setAttribute("sql", builder.compile().sql);
2006
2154
  try {
2007
2155
  const row2 = await builder.executeTakeFirstOrThrow();
2008
- return transformRichDataTypes(camelCaseObject(row2));
2156
+ const result = transformRichDataTypes(camelCaseObject(row2));
2157
+ this._deferReplacedFiles(existingFileRows, fileColumns, result);
2158
+ return result;
2009
2159
  } catch (e) {
2010
2160
  throw new DatabaseError(e);
2011
2161
  }
@@ -2015,12 +2165,15 @@ var ModelAPI = class {
2015
2165
  const name = spanNameForModelAPI(this._modelName, "delete");
2016
2166
  const db = useDatabase();
2017
2167
  return withSpan(name, async (span) => {
2168
+ const fileColumns = Object.keys(this._fileFields);
2169
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
2018
2170
  let builder = db.deleteFrom(this._tableName).returning(["id"]);
2019
2171
  const context7 = new QueryContext([this._tableName], this._tableConfigMap);
2020
2172
  builder = applyWhereConditions(context7, builder, where);
2021
2173
  span.setAttribute("sql", builder.compile().sql);
2022
2174
  try {
2023
2175
  const row = await builder.executeTakeFirstOrThrow();
2176
+ this._deferReplacedFiles(existingFileRows, fileColumns, null);
2024
2177
  return row.id;
2025
2178
  } catch (e) {
2026
2179
  throw new DatabaseError(e);
@@ -2035,6 +2188,30 @@ var ModelAPI = class {
2035
2188
  builder = applyWhereConditions(context7, builder, where);
2036
2189
  return new QueryBuilder(this._tableName, context7, builder);
2037
2190
  }
2191
+ // Reads the current file-column values for rows matched by `where`.
2192
+ async _selectExistingFileValues(where, fileColumns) {
2193
+ if (fileColumns.length === 0) {
2194
+ return [];
2195
+ }
2196
+ const db = useDatabase();
2197
+ let builder = db.selectFrom(this._tableName).selectAll(this._tableName);
2198
+ const context7 = new QueryContext([this._tableName], this._tableConfigMap);
2199
+ builder = applyWhereConditions(context7, builder, where);
2200
+ const rows = await builder.execute();
2201
+ return rows.map((x) => transformRichDataTypes(camelCaseObject(x)));
2202
+ }
2203
+ // Defers deletion of every old file key in existingRows that is not still
2204
+ // referenced by newRow.
2205
+ _deferReplacedFiles(existingRows, fileColumns, newRow) {
2206
+ const retained = new Set(
2207
+ collectFileKeys(newRow ? [newRow] : [], fileColumns)
2208
+ );
2209
+ for (const key of collectFileKeys(existingRows, fileColumns)) {
2210
+ if (!retained.has(key)) {
2211
+ deferFileDeletion(key);
2212
+ }
2213
+ }
2214
+ }
2038
2215
  };
2039
2216
  async function create(conn, tableName, tableConfigs, values) {
2040
2217
  try {
@@ -2144,6 +2321,23 @@ async function create(conn, tableName, tableConfigs, values) {
2144
2321
  }
2145
2322
  }
2146
2323
  __name(create, "create");
2324
+ function collectFileKeys(rows, fileColumns) {
2325
+ const keys = [];
2326
+ for (const row of rows || []) {
2327
+ if (!row) continue;
2328
+ for (const col of fileColumns) {
2329
+ const v = row[col];
2330
+ if (!v) continue;
2331
+ if (Array.isArray(v)) {
2332
+ for (const f of v) if (f?.key) keys.push(f.key);
2333
+ } else if (v.key) {
2334
+ keys.push(v.key);
2335
+ }
2336
+ }
2337
+ }
2338
+ return keys;
2339
+ }
2340
+ __name(collectFileKeys, "collectFileKeys");
2147
2341
 
2148
2342
  // src/TaskAPI.js
2149
2343
  var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
@@ -2815,6 +3009,61 @@ var FlowsAPI = class _FlowsAPI {
2815
3009
  }
2816
3010
  };
2817
3011
 
3012
+ // src/integrationServer.js
3013
+ var import_jsonwebtoken3 = __toESM(require("jsonwebtoken"), 1);
3014
+ function buildHeaders3(identity) {
3015
+ const headers = { "Content-Type": "application/json" };
3016
+ const base64pk = process.env.KEEL_PRIVATE_KEY;
3017
+ if (!base64pk) {
3018
+ throw new Error(
3019
+ "KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
3020
+ );
3021
+ }
3022
+ const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
3023
+ const subject = identity && identity.id ? identity.id : "integration-proxy";
3024
+ headers["Authorization"] = "Bearer " + import_jsonwebtoken3.default.sign({}, privateKey, {
3025
+ algorithm: "RS256",
3026
+ expiresIn: 60 * 60 * 24,
3027
+ subject,
3028
+ // Scope the token to the integration proxy so it can't be mistaken for an ordinary user
3029
+ // access token; the runtime proxy verifies this audience before injecting credentials.
3030
+ audience: "integration-proxy",
3031
+ issuer: "https://keel.so"
3032
+ });
3033
+ return headers;
3034
+ }
3035
+ __name(buildHeaders3, "buildHeaders");
3036
+ function getApiUrl3() {
3037
+ const apiUrl = process.env.KEEL_API_URL;
3038
+ if (!apiUrl) {
3039
+ throw new Error("KEEL_API_URL environment variable is not set");
3040
+ }
3041
+ return apiUrl;
3042
+ }
3043
+ __name(getApiUrl3, "getApiUrl");
3044
+ function createIntegrationServer(name, identity) {
3045
+ return {
3046
+ do: /* @__PURE__ */ __name(async (request) => {
3047
+ const url = `${getApiUrl3()}/integrations/${encodeURIComponent(
3048
+ name
3049
+ )}/proxy`;
3050
+ const response = await fetch(url, {
3051
+ method: "POST",
3052
+ headers: buildHeaders3(identity ?? null),
3053
+ body: JSON.stringify(request ?? {})
3054
+ });
3055
+ if (!response.ok) {
3056
+ const text = await response.text();
3057
+ throw new Error(
3058
+ `integration "${name}" proxy request failed (${response.status}): ${text}`
3059
+ );
3060
+ }
3061
+ return await response.json();
3062
+ }, "do")
3063
+ };
3064
+ }
3065
+ __name(createIntegrationServer, "createIntegrationServer");
3066
+
2818
3067
  // src/RequestHeaders.ts
2819
3068
  var RequestHeaders = class {
2820
3069
  /**
@@ -3563,12 +3812,29 @@ async function applyElementGetData(content, data) {
3563
3812
  return data;
3564
3813
  }
3565
3814
  __name(applyElementGetData, "applyElementGetData");
3566
- async function callbackFn(elements, elementName, callbackName, data) {
3567
- const element = elements.find(
3568
- (el) => el.uiConfig && el.uiConfig.name === elementName
3815
+ var ITERATOR_SCOPED_ELEMENT = /^([^[]+)\[(\d+)\]:(.+)$/;
3816
+ async function callbackFn(elements, elementPath, callbackName, data) {
3817
+ const scoped = ITERATOR_SCOPED_ELEMENT.exec(elementPath);
3818
+ let searchScope = elements;
3819
+ let elementName = elementPath;
3820
+ let iteratorName = null;
3821
+ if (scoped) {
3822
+ iteratorName = scoped[1];
3823
+ elementName = scoped[3];
3824
+ const iter = elements.find(
3825
+ (el) => el?.uiConfig?.__type === "ui.iterator" && el.uiConfig.name === iteratorName
3826
+ );
3827
+ if (!iter) {
3828
+ throw new Error(`Iterator with name ${iteratorName} not found`);
3829
+ }
3830
+ searchScope = iter.uiConfig.content;
3831
+ }
3832
+ const element = searchScope.find(
3833
+ (el) => el?.uiConfig && el.uiConfig.name === elementName
3569
3834
  );
3570
3835
  if (!element) {
3571
- throw new Error(`Element with name ${elementName} not found`);
3836
+ const where = iteratorName ? ` in iterator ${iteratorName}` : "";
3837
+ throw new Error(`Element with name ${elementName} not found${where}`);
3572
3838
  }
3573
3839
  const cb = element[callbackName];
3574
3840
  if (typeof cb !== "function") {
@@ -3995,6 +4261,7 @@ var datePickerInput = /* @__PURE__ */ __name((name, options) => {
3995
4261
  }, "datePickerInput");
3996
4262
 
3997
4263
  // src/flows/ui/elements/input/file.ts
4264
+ init_File();
3998
4265
  var fileInput = /* @__PURE__ */ __name((name, options) => {
3999
4266
  return {
4000
4267
  __type: "input",
@@ -4020,6 +4287,7 @@ var fileInput = /* @__PURE__ */ __name((name, options) => {
4020
4287
  }, "fileInput");
4021
4288
 
4022
4289
  // src/flows/ui/elements/input/imageCapture.ts
4290
+ init_File();
4023
4291
  var isMultiOptions = /* @__PURE__ */ __name((opts) => opts && opts.mode === "multi", "isMultiOptions");
4024
4292
  function validateEntry(entry, requireCaption, context7) {
4025
4293
  if (!entry?.file?.key) {
@@ -4510,7 +4778,7 @@ var defaultOpts = {
4510
4778
  async function insertNewStep(db, runId, name, stage) {
4511
4779
  await db.transaction().execute(async (trx) => {
4512
4780
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
4513
- const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "=", "NEW" /* NEW */).executeTakeFirst();
4781
+ const existing = await trx.selectFrom("keel.flow_step").select("id").where("run_id", "=", runId).where("name", "=", name).where("status", "in", ["NEW" /* NEW */, "RUNNING" /* RUNNING */]).executeTakeFirst();
4514
4782
  if (existing) {
4515
4783
  return;
4516
4784
  }
@@ -4533,6 +4801,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4533
4801
  now: ctx.now,
4534
4802
  secrets: ctx.secrets,
4535
4803
  trace: ctx.trace,
4804
+ module: ctx.module,
4536
4805
  complete: /* @__PURE__ */ __name((options) => {
4537
4806
  return {
4538
4807
  __type: "ui.complete",
@@ -4595,6 +4864,11 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4595
4864
  const raw = completedSteps[0].valueRaw;
4596
4865
  return raw == null ? void 0 : JSON.parse(raw);
4597
4866
  }
4867
+ if (runningSteps.length >= 1) {
4868
+ span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4869
+ span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4870
+ throw new StepCreatedDisrupt();
4871
+ }
4598
4872
  if (newSteps.length === 1) {
4599
4873
  let result = null;
4600
4874
  const claimed = await db.updateTable("keel.flow_step").set({
@@ -4650,11 +4924,6 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4650
4924
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4651
4925
  return result;
4652
4926
  }
4653
- if (runningSteps.length >= 1) {
4654
- span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4655
- span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4656
- throw new StepCreatedDisrupt();
4657
- }
4658
4927
  await insertNewStep(db, runId, name, options.stage);
4659
4928
  span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4660
4929
  span.setAttribute("step.status", "NEW" /* NEW */);
@@ -5034,7 +5303,174 @@ async function handleFlow(request, config) {
5034
5303
  __name(handleFlow, "handleFlow");
5035
5304
 
5036
5305
  // src/index.ts
5306
+ var import_ksuid4 = __toESM(require("ksuid"), 1);
5307
+ init_File();
5308
+
5309
+ // src/notifications/email-template.tsx
5310
+ var import_components = require("@react-email/components");
5311
+ var import_render = require("@react-email/render");
5312
+ var import_jsx_runtime = require("react/jsx-runtime");
5313
+ function renderBody(text) {
5314
+ return text.split("\n").flatMap((line, i) => i === 0 ? [line] : [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}, `br-${i}`), line]);
5315
+ }
5316
+ __name(renderBody, "renderBody");
5317
+ var StockEmailTemplate = /* @__PURE__ */ __name(({ content }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
5318
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
5319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5320
+ import_components.Body,
5321
+ {
5322
+ style: {
5323
+ backgroundColor: "#f6f6f6",
5324
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
5325
+ },
5326
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
5327
+ import_components.Container,
5328
+ {
5329
+ style: {
5330
+ backgroundColor: "#ffffff",
5331
+ margin: "40px auto",
5332
+ padding: "40px",
5333
+ maxWidth: "560px",
5334
+ borderRadius: "8px"
5335
+ },
5336
+ children: [
5337
+ content.title && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5338
+ import_components.Heading,
5339
+ {
5340
+ style: {
5341
+ fontSize: "22px",
5342
+ fontWeight: 600,
5343
+ color: "#111827",
5344
+ margin: "0 0 16px"
5345
+ },
5346
+ children: content.title
5347
+ }
5348
+ ),
5349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5350
+ import_components.Text,
5351
+ {
5352
+ style: {
5353
+ fontSize: "15px",
5354
+ lineHeight: "1.6",
5355
+ color: "#374151",
5356
+ margin: "0 0 24px"
5357
+ },
5358
+ children: renderBody(content.body)
5359
+ }
5360
+ ),
5361
+ (content.actions ?? []).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Section, { style: { margin: "0 0 12px" }, children: content.actions.map((action, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5362
+ import_components.Button,
5363
+ {
5364
+ href: action.url,
5365
+ style: {
5366
+ backgroundColor: "#111827",
5367
+ color: "#ffffff",
5368
+ padding: "12px 24px",
5369
+ borderRadius: "6px",
5370
+ fontSize: "14px",
5371
+ fontWeight: 600,
5372
+ textDecoration: "none",
5373
+ display: "inline-block",
5374
+ // Lay buttons out side by side; space between adjacent buttons.
5375
+ marginRight: i < content.actions.length - 1 ? "12px" : "0"
5376
+ },
5377
+ children: action.label
5378
+ },
5379
+ i
5380
+ )) })
5381
+ ]
5382
+ }
5383
+ )
5384
+ }
5385
+ )
5386
+ ] }), "StockEmailTemplate");
5387
+ async function renderEmailHtml(content) {
5388
+ return (0, import_render.render)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StockEmailTemplate, { content }), { pretty: false });
5389
+ }
5390
+ __name(renderEmailHtml, "renderEmailHtml");
5391
+
5392
+ // src/notifications/notify.ts
5037
5393
  var import_ksuid3 = __toESM(require("ksuid"), 1);
5394
+ function getApiUrl4() {
5395
+ const apiUrl = process.env.KEEL_API_URL;
5396
+ if (!apiUrl) {
5397
+ throw new Error("KEEL_API_URL environment variable is not set");
5398
+ }
5399
+ return apiUrl;
5400
+ }
5401
+ __name(getApiUrl4, "getApiUrl");
5402
+ function toArray(v) {
5403
+ if (v === void 0) return [];
5404
+ return Array.isArray(v) ? v : [v];
5405
+ }
5406
+ __name(toArray, "toArray");
5407
+ function normaliseGroup(group) {
5408
+ if (!group) return [];
5409
+ const refs = [];
5410
+ for (const email of toArray(group.emails)) {
5411
+ refs.push({ kind: "email", value: email });
5412
+ }
5413
+ for (const user of toArray(group.users)) {
5414
+ refs.push({
5415
+ kind: "user",
5416
+ value: typeof user === "string" ? user : user.id
5417
+ });
5418
+ }
5419
+ for (const identity of toArray(group.identities)) {
5420
+ refs.push({
5421
+ kind: "identity",
5422
+ value: typeof identity === "string" ? identity : identity.id
5423
+ });
5424
+ }
5425
+ for (const team of toArray(group.teams)) {
5426
+ refs.push({ kind: "team", value: team });
5427
+ }
5428
+ return refs;
5429
+ }
5430
+ __name(normaliseGroup, "normaliseGroup");
5431
+ async function notifyEmail(input) {
5432
+ return withSpan("notify.email", async () => {
5433
+ const id = import_ksuid3.default.randomSync().string;
5434
+ let rendered;
5435
+ let content;
5436
+ if (typeof input.content === "string") {
5437
+ rendered = input.content;
5438
+ content = void 0;
5439
+ } else {
5440
+ rendered = await renderEmailHtml(input.content);
5441
+ content = input.content;
5442
+ }
5443
+ const body = {
5444
+ id,
5445
+ subject: input.subject,
5446
+ rendered,
5447
+ content,
5448
+ recipients: {
5449
+ to: normaliseGroup(input.recipients.to),
5450
+ cc: normaliseGroup(input.recipients.cc),
5451
+ bcc: normaliseGroup(input.recipients.bcc)
5452
+ }
5453
+ };
5454
+ const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
5455
+ method: "POST",
5456
+ headers: { "Content-Type": "application/json" },
5457
+ body: JSON.stringify(body)
5458
+ });
5459
+ if (!response.ok) {
5460
+ const errorBody = await response.json().catch(() => ({}));
5461
+ throw new Error(
5462
+ `Failed to send notification: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
5463
+ );
5464
+ }
5465
+ await response.body?.cancel();
5466
+ return id;
5467
+ });
5468
+ }
5469
+ __name(notifyEmail, "notifyEmail");
5470
+ function createNotifier() {
5471
+ return { email: notifyEmail };
5472
+ }
5473
+ __name(createNotifier, "createNotifier");
5038
5474
 
5039
5475
  // src/experimental.ts
5040
5476
  var experimental_exports = {};
@@ -5399,9 +5835,10 @@ __name(LlmFlowStep, "LlmFlowStep");
5399
5835
  var import_zod = require("zod");
5400
5836
  var createTraceAPI2 = createTraceAPI;
5401
5837
  function ksuid() {
5402
- return import_ksuid3.default.randomSync().string;
5838
+ return import_ksuid4.default.randomSync().string;
5403
5839
  }
5404
5840
  __name(ksuid, "ksuid");
5841
+ var notify = createNotifier();
5405
5842
  // Annotate the CommonJS export names for ESM import in node:
5406
5843
  0 && (module.exports = {
5407
5844
  Duration,
@@ -5424,6 +5861,8 @@ __name(ksuid, "ksuid");
5424
5861
  TaskAPI,
5425
5862
  checkBuiltInPermissions,
5426
5863
  createFlowContext,
5864
+ createIntegrationServer,
5865
+ createNotifier,
5427
5866
  createTraceAPI,
5428
5867
  experimental,
5429
5868
  handleFlow,
@@ -5431,7 +5870,9 @@ __name(ksuid, "ksuid");
5431
5870
  handleRequest,
5432
5871
  handleRoute,
5433
5872
  handleSubscriber,
5873
+ insertNewStep,
5434
5874
  ksuid,
5875
+ notify,
5435
5876
  tracing,
5436
5877
  useDatabase,
5437
5878
  z