@teamkeel/functions-runtime 0.459.0 → 0.460.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
@@ -501,20 +828,44 @@ var KEEL_INTERNAL_CHILDREN = "includeChildrenSpans";
501
828
  var import_ws = __toESM(require("ws"), 1);
502
829
  var import_node_fs = require("fs");
503
830
  var dbInstance = new import_node_async_hooks2.AsyncLocalStorage();
831
+ var fileCleanupStore = new import_node_async_hooks2.AsyncLocalStorage();
832
+ function deferFileDeletion(key) {
833
+ fileCleanupStore.getStore()?.add(key);
834
+ }
835
+ __name(deferFileDeletion, "deferFileDeletion");
836
+ async function flushFileDeletions(keys) {
837
+ if (keys.size === 0) {
838
+ return;
839
+ }
840
+ const { deleteStoredFile: deleteStoredFile2 } = await Promise.resolve().then(() => (init_File(), File_exports));
841
+ for (const key of keys) {
842
+ try {
843
+ await deleteStoredFile2(key);
844
+ } catch (e) {
845
+ console.error("failed to delete orphaned file from storage", e);
846
+ }
847
+ }
848
+ }
849
+ __name(flushFileDeletions, "flushFileDeletions");
504
850
  var vitestDb = null;
505
851
  async function withDatabase(db, requiresTransaction, cb) {
852
+ const pending = /* @__PURE__ */ new Set();
506
853
  if (requiresTransaction) {
507
- return db.transaction().execute(async (transaction) => {
508
- return dbInstance.run(transaction, async () => {
509
- return cb({ transaction });
854
+ const result2 = await db.transaction().execute(async (transaction) => {
855
+ return dbInstance.run(transaction, () => {
856
+ return fileCleanupStore.run(pending, () => cb({ transaction }));
510
857
  });
511
858
  });
859
+ await flushFileDeletions(pending);
860
+ return result2;
512
861
  }
513
- return db.connection().execute(async (sDb) => {
514
- return dbInstance.run(sDb, async () => {
515
- return cb({ sDb });
862
+ const result = await db.connection().execute(async (sDb) => {
863
+ return dbInstance.run(sDb, () => {
864
+ return fileCleanupStore.run(pending, () => cb({ sDb }));
516
865
  });
517
866
  });
867
+ await flushFileDeletions(pending);
868
+ return result;
518
869
  }
519
870
  __name(withDatabase, "withDatabase");
520
871
  function useDatabase() {
@@ -689,271 +1040,8 @@ function getDialect(connString) {
689
1040
  }
690
1041
  __name(getDialect, "getDialect");
691
1042
 
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
1043
  // src/parsing.js
1044
+ init_File();
957
1045
  var dateFormat = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
958
1046
  function parseInputs(inputs) {
959
1047
  if (inputs != null && typeof inputs === "object") {
@@ -1886,6 +1974,7 @@ var QueryBuilder = class _QueryBuilder {
1886
1974
  };
1887
1975
 
1888
1976
  // src/ModelAPI.js
1977
+ init_File();
1889
1978
  var ModelAPI = class {
1890
1979
  static {
1891
1980
  __name(this, "ModelAPI");
@@ -1895,9 +1984,10 @@ var ModelAPI = class {
1895
1984
  * @param {Function} _ Used to be a function that returns the default values for a row in this table. No longer used.
1896
1985
  * @param {TableConfigMap} tableConfigMap
1897
1986
  */
1898
- constructor(tableName, _, tableConfigMap = {}) {
1987
+ constructor(tableName, _, tableConfigMap = {}, fileFieldsMap = {}) {
1899
1988
  this._tableName = tableName;
1900
1989
  this._tableConfigMap = tableConfigMap;
1990
+ this._fileFields = fileFieldsMap[tableName] || {};
1901
1991
  this._modelName = upperCamelCase(this._tableName);
1902
1992
  }
1903
1993
  async create(values) {
@@ -1967,6 +2057,10 @@ var ModelAPI = class {
1967
2057
  const name = spanNameForModelAPI(this._modelName, "update");
1968
2058
  const db = useDatabase();
1969
2059
  return withSpan(name, async (span) => {
2060
+ const fileColumns = Object.keys(values || {}).filter(
2061
+ (k) => k in this._fileFields
2062
+ );
2063
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
1970
2064
  let builder = db.updateTable(this._tableName).returningAll();
1971
2065
  const keys = values ? Object.keys(values) : [];
1972
2066
  const row = {};
@@ -2005,7 +2099,9 @@ var ModelAPI = class {
2005
2099
  span.setAttribute("sql", builder.compile().sql);
2006
2100
  try {
2007
2101
  const row2 = await builder.executeTakeFirstOrThrow();
2008
- return transformRichDataTypes(camelCaseObject(row2));
2102
+ const result = transformRichDataTypes(camelCaseObject(row2));
2103
+ this._deferReplacedFiles(existingFileRows, fileColumns, result);
2104
+ return result;
2009
2105
  } catch (e) {
2010
2106
  throw new DatabaseError(e);
2011
2107
  }
@@ -2015,12 +2111,15 @@ var ModelAPI = class {
2015
2111
  const name = spanNameForModelAPI(this._modelName, "delete");
2016
2112
  const db = useDatabase();
2017
2113
  return withSpan(name, async (span) => {
2114
+ const fileColumns = Object.keys(this._fileFields);
2115
+ const existingFileRows = fileColumns.length > 0 ? await this._selectExistingFileValues(where, fileColumns) : [];
2018
2116
  let builder = db.deleteFrom(this._tableName).returning(["id"]);
2019
2117
  const context7 = new QueryContext([this._tableName], this._tableConfigMap);
2020
2118
  builder = applyWhereConditions(context7, builder, where);
2021
2119
  span.setAttribute("sql", builder.compile().sql);
2022
2120
  try {
2023
2121
  const row = await builder.executeTakeFirstOrThrow();
2122
+ this._deferReplacedFiles(existingFileRows, fileColumns, null);
2024
2123
  return row.id;
2025
2124
  } catch (e) {
2026
2125
  throw new DatabaseError(e);
@@ -2035,6 +2134,30 @@ var ModelAPI = class {
2035
2134
  builder = applyWhereConditions(context7, builder, where);
2036
2135
  return new QueryBuilder(this._tableName, context7, builder);
2037
2136
  }
2137
+ // Reads the current file-column values for rows matched by `where`.
2138
+ async _selectExistingFileValues(where, fileColumns) {
2139
+ if (fileColumns.length === 0) {
2140
+ return [];
2141
+ }
2142
+ const db = useDatabase();
2143
+ let builder = db.selectFrom(this._tableName).selectAll(this._tableName);
2144
+ const context7 = new QueryContext([this._tableName], this._tableConfigMap);
2145
+ builder = applyWhereConditions(context7, builder, where);
2146
+ const rows = await builder.execute();
2147
+ return rows.map((x) => transformRichDataTypes(camelCaseObject(x)));
2148
+ }
2149
+ // Defers deletion of every old file key in existingRows that is not still
2150
+ // referenced by newRow.
2151
+ _deferReplacedFiles(existingRows, fileColumns, newRow) {
2152
+ const retained = new Set(
2153
+ collectFileKeys(newRow ? [newRow] : [], fileColumns)
2154
+ );
2155
+ for (const key of collectFileKeys(existingRows, fileColumns)) {
2156
+ if (!retained.has(key)) {
2157
+ deferFileDeletion(key);
2158
+ }
2159
+ }
2160
+ }
2038
2161
  };
2039
2162
  async function create(conn, tableName, tableConfigs, values) {
2040
2163
  try {
@@ -2144,6 +2267,23 @@ async function create(conn, tableName, tableConfigs, values) {
2144
2267
  }
2145
2268
  }
2146
2269
  __name(create, "create");
2270
+ function collectFileKeys(rows, fileColumns) {
2271
+ const keys = [];
2272
+ for (const row of rows || []) {
2273
+ if (!row) continue;
2274
+ for (const col of fileColumns) {
2275
+ const v = row[col];
2276
+ if (!v) continue;
2277
+ if (Array.isArray(v)) {
2278
+ for (const f of v) if (f?.key) keys.push(f.key);
2279
+ } else if (v.key) {
2280
+ keys.push(v.key);
2281
+ }
2282
+ }
2283
+ }
2284
+ return keys;
2285
+ }
2286
+ __name(collectFileKeys, "collectFileKeys");
2147
2287
 
2148
2288
  // src/TaskAPI.js
2149
2289
  var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
@@ -2815,6 +2955,61 @@ var FlowsAPI = class _FlowsAPI {
2815
2955
  }
2816
2956
  };
2817
2957
 
2958
+ // src/integrationServer.js
2959
+ var import_jsonwebtoken3 = __toESM(require("jsonwebtoken"), 1);
2960
+ function buildHeaders3(identity) {
2961
+ const headers = { "Content-Type": "application/json" };
2962
+ const base64pk = process.env.KEEL_PRIVATE_KEY;
2963
+ if (!base64pk) {
2964
+ throw new Error(
2965
+ "KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
2966
+ );
2967
+ }
2968
+ const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
2969
+ const subject = identity && identity.id ? identity.id : "integration-proxy";
2970
+ headers["Authorization"] = "Bearer " + import_jsonwebtoken3.default.sign({}, privateKey, {
2971
+ algorithm: "RS256",
2972
+ expiresIn: 60 * 60 * 24,
2973
+ subject,
2974
+ // Scope the token to the integration proxy so it can't be mistaken for an ordinary user
2975
+ // access token; the runtime proxy verifies this audience before injecting credentials.
2976
+ audience: "integration-proxy",
2977
+ issuer: "https://keel.so"
2978
+ });
2979
+ return headers;
2980
+ }
2981
+ __name(buildHeaders3, "buildHeaders");
2982
+ function getApiUrl3() {
2983
+ const apiUrl = process.env.KEEL_API_URL;
2984
+ if (!apiUrl) {
2985
+ throw new Error("KEEL_API_URL environment variable is not set");
2986
+ }
2987
+ return apiUrl;
2988
+ }
2989
+ __name(getApiUrl3, "getApiUrl");
2990
+ function createIntegrationServer(name, identity) {
2991
+ return {
2992
+ do: /* @__PURE__ */ __name(async (request) => {
2993
+ const url = `${getApiUrl3()}/integrations/${encodeURIComponent(
2994
+ name
2995
+ )}/proxy`;
2996
+ const response = await fetch(url, {
2997
+ method: "POST",
2998
+ headers: buildHeaders3(identity ?? null),
2999
+ body: JSON.stringify(request ?? {})
3000
+ });
3001
+ if (!response.ok) {
3002
+ const text = await response.text();
3003
+ throw new Error(
3004
+ `integration "${name}" proxy request failed (${response.status}): ${text}`
3005
+ );
3006
+ }
3007
+ return await response.json();
3008
+ }, "do")
3009
+ };
3010
+ }
3011
+ __name(createIntegrationServer, "createIntegrationServer");
3012
+
2818
3013
  // src/RequestHeaders.ts
2819
3014
  var RequestHeaders = class {
2820
3015
  /**
@@ -3995,6 +4190,7 @@ var datePickerInput = /* @__PURE__ */ __name((name, options) => {
3995
4190
  }, "datePickerInput");
3996
4191
 
3997
4192
  // src/flows/ui/elements/input/file.ts
4193
+ init_File();
3998
4194
  var fileInput = /* @__PURE__ */ __name((name, options) => {
3999
4195
  return {
4000
4196
  __type: "input",
@@ -4020,6 +4216,7 @@ var fileInput = /* @__PURE__ */ __name((name, options) => {
4020
4216
  }, "fileInput");
4021
4217
 
4022
4218
  // src/flows/ui/elements/input/imageCapture.ts
4219
+ init_File();
4023
4220
  var isMultiOptions = /* @__PURE__ */ __name((opts) => opts && opts.mode === "multi", "isMultiOptions");
4024
4221
  function validateEntry(entry, requireCaption, context7) {
4025
4222
  if (!entry?.file?.key) {
@@ -4510,7 +4707,7 @@ var defaultOpts = {
4510
4707
  async function insertNewStep(db, runId, name, stage) {
4511
4708
  await db.transaction().execute(async (trx) => {
4512
4709
  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();
4710
+ 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
4711
  if (existing) {
4515
4712
  return;
4516
4713
  }
@@ -4533,6 +4730,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4533
4730
  now: ctx.now,
4534
4731
  secrets: ctx.secrets,
4535
4732
  trace: ctx.trace,
4733
+ module: ctx.module,
4536
4734
  complete: /* @__PURE__ */ __name((options) => {
4537
4735
  return {
4538
4736
  __type: "ui.complete",
@@ -4595,6 +4793,11 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4595
4793
  const raw = completedSteps[0].valueRaw;
4596
4794
  return raw == null ? void 0 : JSON.parse(raw);
4597
4795
  }
4796
+ if (runningSteps.length >= 1) {
4797
+ span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4798
+ span.setAttribute("step.status", "RUNNING" /* RUNNING */);
4799
+ throw new StepCreatedDisrupt();
4800
+ }
4598
4801
  if (newSteps.length === 1) {
4599
4802
  let result = null;
4600
4803
  const claimed = await db.updateTable("keel.flow_step").set({
@@ -4650,11 +4853,6 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4650
4853
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4651
4854
  return result;
4652
4855
  }
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
4856
  await insertNewStep(db, runId, name, options.stage);
4659
4857
  span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4660
4858
  span.setAttribute("step.status", "NEW" /* NEW */);
@@ -5034,7 +5232,174 @@ async function handleFlow(request, config) {
5034
5232
  __name(handleFlow, "handleFlow");
5035
5233
 
5036
5234
  // src/index.ts
5235
+ var import_ksuid4 = __toESM(require("ksuid"), 1);
5236
+ init_File();
5237
+
5238
+ // src/notifications/email-template.tsx
5239
+ var import_components = require("@react-email/components");
5240
+ var import_render = require("@react-email/render");
5241
+ var import_jsx_runtime = require("react/jsx-runtime");
5242
+ function renderBody(text) {
5243
+ return text.split("\n").flatMap((line, i) => i === 0 ? [line] : [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}, `br-${i}`), line]);
5244
+ }
5245
+ __name(renderBody, "renderBody");
5246
+ var StockEmailTemplate = /* @__PURE__ */ __name(({ content }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
5247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
5248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5249
+ import_components.Body,
5250
+ {
5251
+ style: {
5252
+ backgroundColor: "#f6f6f6",
5253
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
5254
+ },
5255
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
5256
+ import_components.Container,
5257
+ {
5258
+ style: {
5259
+ backgroundColor: "#ffffff",
5260
+ margin: "40px auto",
5261
+ padding: "40px",
5262
+ maxWidth: "560px",
5263
+ borderRadius: "8px"
5264
+ },
5265
+ children: [
5266
+ content.title && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5267
+ import_components.Heading,
5268
+ {
5269
+ style: {
5270
+ fontSize: "22px",
5271
+ fontWeight: 600,
5272
+ color: "#111827",
5273
+ margin: "0 0 16px"
5274
+ },
5275
+ children: content.title
5276
+ }
5277
+ ),
5278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5279
+ import_components.Text,
5280
+ {
5281
+ style: {
5282
+ fontSize: "15px",
5283
+ lineHeight: "1.6",
5284
+ color: "#374151",
5285
+ margin: "0 0 24px"
5286
+ },
5287
+ children: renderBody(content.body)
5288
+ }
5289
+ ),
5290
+ (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)(
5291
+ import_components.Button,
5292
+ {
5293
+ href: action.url,
5294
+ style: {
5295
+ backgroundColor: "#111827",
5296
+ color: "#ffffff",
5297
+ padding: "12px 24px",
5298
+ borderRadius: "6px",
5299
+ fontSize: "14px",
5300
+ fontWeight: 600,
5301
+ textDecoration: "none",
5302
+ display: "inline-block",
5303
+ // Lay buttons out side by side; space between adjacent buttons.
5304
+ marginRight: i < content.actions.length - 1 ? "12px" : "0"
5305
+ },
5306
+ children: action.label
5307
+ },
5308
+ i
5309
+ )) })
5310
+ ]
5311
+ }
5312
+ )
5313
+ }
5314
+ )
5315
+ ] }), "StockEmailTemplate");
5316
+ async function renderEmailHtml(content) {
5317
+ return (0, import_render.render)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StockEmailTemplate, { content }), { pretty: false });
5318
+ }
5319
+ __name(renderEmailHtml, "renderEmailHtml");
5320
+
5321
+ // src/notifications/notify.ts
5037
5322
  var import_ksuid3 = __toESM(require("ksuid"), 1);
5323
+ function getApiUrl4() {
5324
+ const apiUrl = process.env.KEEL_API_URL;
5325
+ if (!apiUrl) {
5326
+ throw new Error("KEEL_API_URL environment variable is not set");
5327
+ }
5328
+ return apiUrl;
5329
+ }
5330
+ __name(getApiUrl4, "getApiUrl");
5331
+ function toArray(v) {
5332
+ if (v === void 0) return [];
5333
+ return Array.isArray(v) ? v : [v];
5334
+ }
5335
+ __name(toArray, "toArray");
5336
+ function normaliseGroup(group) {
5337
+ if (!group) return [];
5338
+ const refs = [];
5339
+ for (const email of toArray(group.emails)) {
5340
+ refs.push({ kind: "email", value: email });
5341
+ }
5342
+ for (const user of toArray(group.users)) {
5343
+ refs.push({
5344
+ kind: "user",
5345
+ value: typeof user === "string" ? user : user.id
5346
+ });
5347
+ }
5348
+ for (const identity of toArray(group.identities)) {
5349
+ refs.push({
5350
+ kind: "identity",
5351
+ value: typeof identity === "string" ? identity : identity.id
5352
+ });
5353
+ }
5354
+ for (const team of toArray(group.teams)) {
5355
+ refs.push({ kind: "team", value: team });
5356
+ }
5357
+ return refs;
5358
+ }
5359
+ __name(normaliseGroup, "normaliseGroup");
5360
+ async function notifyEmail(input) {
5361
+ return withSpan("notify.email", async () => {
5362
+ const id = import_ksuid3.default.randomSync().string;
5363
+ let rendered;
5364
+ let content;
5365
+ if (typeof input.content === "string") {
5366
+ rendered = input.content;
5367
+ content = void 0;
5368
+ } else {
5369
+ rendered = await renderEmailHtml(input.content);
5370
+ content = input.content;
5371
+ }
5372
+ const body = {
5373
+ id,
5374
+ subject: input.subject,
5375
+ rendered,
5376
+ content,
5377
+ recipients: {
5378
+ to: normaliseGroup(input.recipients.to),
5379
+ cc: normaliseGroup(input.recipients.cc),
5380
+ bcc: normaliseGroup(input.recipients.bcc)
5381
+ }
5382
+ };
5383
+ const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
5384
+ method: "POST",
5385
+ headers: { "Content-Type": "application/json" },
5386
+ body: JSON.stringify(body)
5387
+ });
5388
+ if (!response.ok) {
5389
+ const errorBody = await response.json().catch(() => ({}));
5390
+ throw new Error(
5391
+ `Failed to send notification: ${response.status} ${response.statusText} - ${errorBody.message || JSON.stringify(errorBody)}`
5392
+ );
5393
+ }
5394
+ await response.body?.cancel();
5395
+ return id;
5396
+ });
5397
+ }
5398
+ __name(notifyEmail, "notifyEmail");
5399
+ function createNotifier() {
5400
+ return { email: notifyEmail };
5401
+ }
5402
+ __name(createNotifier, "createNotifier");
5038
5403
 
5039
5404
  // src/experimental.ts
5040
5405
  var experimental_exports = {};
@@ -5399,9 +5764,10 @@ __name(LlmFlowStep, "LlmFlowStep");
5399
5764
  var import_zod = require("zod");
5400
5765
  var createTraceAPI2 = createTraceAPI;
5401
5766
  function ksuid() {
5402
- return import_ksuid3.default.randomSync().string;
5767
+ return import_ksuid4.default.randomSync().string;
5403
5768
  }
5404
5769
  __name(ksuid, "ksuid");
5770
+ var notify = createNotifier();
5405
5771
  // Annotate the CommonJS export names for ESM import in node:
5406
5772
  0 && (module.exports = {
5407
5773
  Duration,
@@ -5424,6 +5790,8 @@ __name(ksuid, "ksuid");
5424
5790
  TaskAPI,
5425
5791
  checkBuiltInPermissions,
5426
5792
  createFlowContext,
5793
+ createIntegrationServer,
5794
+ createNotifier,
5427
5795
  createTraceAPI,
5428
5796
  experimental,
5429
5797
  handleFlow,
@@ -5431,7 +5799,9 @@ __name(ksuid, "ksuid");
5431
5799
  handleRequest,
5432
5800
  handleRoute,
5433
5801
  handleSubscriber,
5802
+ insertNewStep,
5434
5803
  ksuid,
5804
+ notify,
5435
5805
  tracing,
5436
5806
  useDatabase,
5437
5807
  z