flowflex 0.1.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 +284 -0
- package/dist/browser.d.ts +27 -0
- package/dist/browser.js +52 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.js +311 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +34 -0
- package/dist/esm/browser.js +43 -0
- package/dist/esm/client.js +307 -0
- package/dist/esm/errors.js +28 -0
- package/dist/esm/file.js +113 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/file.d.ts +33 -0
- package/dist/file.js +151 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +2 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# @fantacode/flowflex
|
|
2
|
+
|
|
3
|
+
Official Node/browser SDK for firing **FlowFlex custom-integration events** and
|
|
4
|
+
attaching **private files** to them — without ever dealing with presigned URLs,
|
|
5
|
+
Basic-auth headers, or the multi-step upload dance yourself.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fantacode/flowflex
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node 18+ (for the built-in `fetch`) or any modern browser.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { FlowFlex } from "@fantacode/flowflex";
|
|
19
|
+
|
|
20
|
+
const ff = new FlowFlex({
|
|
21
|
+
apiKey: "cik_xxx", // from your custom integration
|
|
22
|
+
apiSecret: "yyy",
|
|
23
|
+
integrationCode: "ic_abc123", // the code in your event URL: /v1/events/<code>
|
|
24
|
+
baseUrl: "https://app.flowflex.com",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Plain event, no file
|
|
28
|
+
await ff.sendEvent("order.placed", {
|
|
29
|
+
payload: { name: "Ada", order_id: "ord_42" },
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
In your flow builder, the values land under `trigger` — e.g. `{{trigger.name}}`,
|
|
34
|
+
`{{trigger.order_id}}`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Attaching a private file
|
|
39
|
+
|
|
40
|
+
Wrap any file in `ff.file(...)` and drop it into the payload. The SDK uploads it
|
|
41
|
+
through a presigned URL and replaces it with its opaque `assetId` before the
|
|
42
|
+
event is sent. **The file bytes go straight to storage — they never pass through
|
|
43
|
+
the FlowFlex app server.**
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await ff.sendEvent("invoice.created", {
|
|
47
|
+
payload: {
|
|
48
|
+
assetId: ff.file("./invoice.pdf"), // ← becomes "asset_..." on the wire
|
|
49
|
+
customer_id: "cust_123",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then in your flow's **Media Message** node:
|
|
55
|
+
|
|
56
|
+
1. Set **File source** → `Private file (assetId)`
|
|
57
|
+
2. Set the **Asset ID** field → `{{trigger.assetId}}`
|
|
58
|
+
|
|
59
|
+
The key you use in the payload is the key you reference in the flow. If you send
|
|
60
|
+
`{ invoice: ff.file(...) }`, reference it as `{{trigger.invoice}}`.
|
|
61
|
+
|
|
62
|
+
### Multiple files
|
|
63
|
+
|
|
64
|
+
A flow can have as many media nodes as you like — put a `file()` anywhere in the
|
|
65
|
+
payload (top-level, nested, or in arrays) and the SDK uploads them **all in
|
|
66
|
+
parallel** and swaps each for its `assetId`. The shape is entirely up to you;
|
|
67
|
+
reference each one by its path in the flow builder.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
await ff.sendEvent("order.shipped", {
|
|
71
|
+
payload: {
|
|
72
|
+
invoice: ff.file("./invoice.pdf"), // {{trigger.invoice}}
|
|
73
|
+
label: ff.file("./label.png"), // {{trigger.label}}
|
|
74
|
+
gallery: [ff.file("./a.jpg"), ff.file("./b.jpg")], // {{trigger.gallery[0]}}, {{trigger.gallery[1]}}
|
|
75
|
+
order: { receipt: ff.file("./receipt.pdf") }, // {{trigger.order.receipt}}
|
|
76
|
+
note: "non-file values pass through untouched",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
// → { invoice: "asset_a", label: "asset_b",
|
|
80
|
+
// gallery: ["asset_c", "asset_d"],
|
|
81
|
+
// order: { receipt: "asset_e" }, note: "..." }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`result.uploadedAssets` maps each payload path to its assetId, e.g.
|
|
85
|
+
`{ "invoice": "asset_a", "gallery[0]": "asset_c", "order.receipt": "asset_e" }`.
|
|
86
|
+
|
|
87
|
+
**Reusing one file across nodes:** if you pass the *same* `file()` instance in
|
|
88
|
+
multiple places, it's uploaded only once and the same `assetId` is used
|
|
89
|
+
everywhere:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const banner = ff.file("./banner.png");
|
|
93
|
+
await ff.sendEvent("promo.sent", {
|
|
94
|
+
payload: { header: banner, footer: banner }, // one upload, same assetId in both
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Supplying file bytes other ways
|
|
101
|
+
|
|
102
|
+
`file()` accepts a path (Node), a `Buffer`/`Uint8Array`, or a `Blob`/`File`
|
|
103
|
+
(browser). When the type can't be inferred, pass `filename` and `mime`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// Buffer / Uint8Array
|
|
107
|
+
ff.file(pdfBuffer, { filename: "invoice.pdf", mime: "application/pdf" });
|
|
108
|
+
|
|
109
|
+
// Browser File input
|
|
110
|
+
const f = document.querySelector("input[type=file]").files[0];
|
|
111
|
+
ff.file(f); // name + type read from the File
|
|
112
|
+
|
|
113
|
+
// Override the stored filename
|
|
114
|
+
ff.file("./tmp-7f3a.pdf", { filename: "Invoice-2026.pdf" });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Allowed types:** PDF, DOC(X), XLS(X), PPT(X), TXT, JPEG, PNG, MP4, 3GPP, AAC,
|
|
118
|
+
AMR, MP3, OGG. **Max size:** 25 MB.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### `new FlowFlex(options)`
|
|
125
|
+
|
|
126
|
+
| Option | Type | Required | Notes |
|
|
127
|
+
| ------------------------- | -------- | -------- | --------------------------------------------------------------------- |
|
|
128
|
+
| `apiKey` | string | yes | Custom-integration key (`cik_…`). |
|
|
129
|
+
| `apiSecret` | string | yes | Custom-integration secret. |
|
|
130
|
+
| `integrationCode` | string | yes | The `<code>` in `/v1/events/<code>`. |
|
|
131
|
+
| `baseUrl` | string | yes | FlowFlex host. Must be `https` (except `localhost`). Trailing `/api` stripped. |
|
|
132
|
+
| `timeoutMs` | number | no | Per-request timeout. Default `30000`. |
|
|
133
|
+
| `maxFileBytes` | number | no | Client-side size cap. Default `26214400` (25 MB). |
|
|
134
|
+
| `fetch` | function | no | Custom fetch for runtimes without a global one. |
|
|
135
|
+
|
|
136
|
+
> **Server-only.** There is no option to run this in a browser — it throws if constructed
|
|
137
|
+
> in one, and browser bundlers resolve it to a stub that throws on import. See **Security**.
|
|
138
|
+
|
|
139
|
+
### `ff.sendEvent(event, { payload?, idempotencyKey? })`
|
|
140
|
+
|
|
141
|
+
Uploads any `file()` in `payload`, then POSTs the event. Returns:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
{
|
|
145
|
+
response: unknown, // raw body from the events endpoint
|
|
146
|
+
uploadedAssets: Record<string, string>, // payload path → assetId
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
An `idempotencyKey` is auto-generated (UUID) if you don't pass one — safe to
|
|
151
|
+
retry the same call.
|
|
152
|
+
|
|
153
|
+
### `ff.file(source, { filename?, mime?, size? })`
|
|
154
|
+
|
|
155
|
+
Returns a lazy `FileRef`. Bytes are read only when the event is sent.
|
|
156
|
+
|
|
157
|
+
### Lower-level helpers
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const { assetId, uploadUrl } = await ff.createUploadUrl({ filename, mime });
|
|
161
|
+
const assetId = await ff.uploadFile(ff.file("./x.pdf")); // upload, get assetId
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Errors
|
|
167
|
+
|
|
168
|
+
All errors extend `FlowFlexError` (`{ message, status?, code?, details? }`):
|
|
169
|
+
|
|
170
|
+
- `FlowFlexConfigError` — bad/missing constructor options.
|
|
171
|
+
- `FlowFlexUploadError` — a file couldn't be read or storage rejected it.
|
|
172
|
+
- `FlowFlexError` — API or network failure (`code` is the backend error code,
|
|
173
|
+
e.g. `MIME_NOT_ALLOWED`, `ASSET_FILE_MISSING`).
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { FlowFlexError } from "@fantacode/flowflex";
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await ff.sendEvent("invoice.created", { payload: { assetId: ff.file("./big.pdf") } });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err instanceof FlowFlexError) {
|
|
182
|
+
console.error(err.code, err.status, err.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## File lifetime & storage cleanup
|
|
190
|
+
|
|
191
|
+
> **Important — read before using file attachments in production.**
|
|
192
|
+
|
|
193
|
+
When you call `ff.file(...)`, the file is uploaded to **private Supabase storage**
|
|
194
|
+
that only the FlowFlex backend can read. It is **not** a public URL and the caller
|
|
195
|
+
cannot access it after upload.
|
|
196
|
+
|
|
197
|
+
### How long does the file stay?
|
|
198
|
+
|
|
199
|
+
| Phase | Duration |
|
|
200
|
+
| ----- | -------- |
|
|
201
|
+
| Presigned upload URL valid | **2 hours** from `createUploadUrl` |
|
|
202
|
+
| File kept in storage | **48 hours** from upload |
|
|
203
|
+
| After 48 hours | File **deleted from storage** + record removed |
|
|
204
|
+
|
|
205
|
+
After the flow delivers the WhatsApp message the file is no longer needed. The
|
|
206
|
+
backend runs an automatic cleanup job every hour that:
|
|
207
|
+
|
|
208
|
+
1. Finds assets whose 48-hour window has expired
|
|
209
|
+
2. **Deletes the file from Supabase storage first**
|
|
210
|
+
3. Then removes the database record
|
|
211
|
+
|
|
212
|
+
This means storage never accumulates — every file is cleaned up within ~1 hour
|
|
213
|
+
of its expiry.
|
|
214
|
+
|
|
215
|
+
### What this means for you
|
|
216
|
+
|
|
217
|
+
- **Do not store `assetId` long-term** expecting to reuse it. It expires in 48h.
|
|
218
|
+
- **Each event send should get a fresh `assetId`** by calling `ff.sendEvent` with
|
|
219
|
+
a new `ff.file(...)`. The SDK handles the upload automatically.
|
|
220
|
+
- If you fire the event more than 48h after uploading, the file will be gone and
|
|
221
|
+
the flow will fail with `ASSET_FILE_MISSING`. Keep your event send close to
|
|
222
|
+
the upload.
|
|
223
|
+
- **Re-sending the same message** to a different recipient after 48h requires a
|
|
224
|
+
fresh upload — call `sendEvent` again with the file, don't reuse the old assetId.
|
|
225
|
+
|
|
226
|
+
### Typical correct pattern
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// ✅ Upload + fire in the same operation — always fresh
|
|
230
|
+
await ff.sendEvent("invoice.created", {
|
|
231
|
+
payload: { assetId: ff.file("./invoice.pdf") },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ❌ Don't store assetId and reuse it hours later
|
|
235
|
+
const { uploadedAssets } = await ff.sendEvent(...);
|
|
236
|
+
// ... 50 hours later ...
|
|
237
|
+
// uploadedAssets["assetId"] is now expired and deleted
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Security
|
|
243
|
+
|
|
244
|
+
**This SDK is server-only.** Your `apiKey`/`apiSecret` are integration-wide
|
|
245
|
+
credentials. Bundling them into front-end code exposes them to anyone who opens
|
|
246
|
+
devtools — they could then fire events and upload files as you. The browser is
|
|
247
|
+
hard-blocked, with no opt-out:
|
|
248
|
+
|
|
249
|
+
- **Runtime** — the constructor **always throws** if it detects a browser.
|
|
250
|
+
- **Bundle time** — browser bundlers (webpack / Vite / Next) resolve the package to
|
|
251
|
+
a stub (via the `"browser"` export condition) that **throws on import**, so the SDK
|
|
252
|
+
can't be built into client code in the first place.
|
|
253
|
+
|
|
254
|
+
Other protections built in:
|
|
255
|
+
|
|
256
|
+
- **HTTPS enforced.** `baseUrl` must be `https://` (only `localhost` may use
|
|
257
|
+
`http`), so Basic-auth credentials are never sent in cleartext.
|
|
258
|
+
- **No credential leakage.** The `Authorization` header is never included in
|
|
259
|
+
error messages, logs, or `FlowFlexError.details`.
|
|
260
|
+
- **Header-injection safe.** The `event` name and `idempotencyKey` are rejected
|
|
261
|
+
if they contain control characters (CRLF).
|
|
262
|
+
- **Path-injection safe.** `integrationCode` is URL-encoded into the request path.
|
|
263
|
+
- **Prototype-pollution safe.** Payload keys like `__proto__` can't pollute
|
|
264
|
+
`Object.prototype` during the file-swap walk.
|
|
265
|
+
- **Per-request timeouts** (default 30 s) on every network call.
|
|
266
|
+
- **Client-side size cap** (default 25 MB) so oversized files fail before upload.
|
|
267
|
+
|
|
268
|
+
If you must call FlowFlex from a browser, proxy through your own backend: the
|
|
269
|
+
browser talks to your server, your server holds the secret and calls this SDK.
|
|
270
|
+
|
|
271
|
+
## How it works
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
ff.sendEvent("invoice.created", { payload: { assetId: file("invoice.pdf") } })
|
|
275
|
+
│
|
|
276
|
+
├─ 1. POST /v1/assets/upload-url → { assetId, uploadUrl }
|
|
277
|
+
├─ 2. PUT <uploadUrl> (file bytes straight to private storage)
|
|
278
|
+
└─ 3. POST /v1/events/<code> with { assetId: "asset_…" }
|
|
279
|
+
│
|
|
280
|
+
└─ flow runs; media node resolves assetId → WhatsApp media at send time
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The caller only ever holds the opaque `assetId`. There is no readable link to
|
|
284
|
+
the file — the backend resolves it server-side when the flow delivers the message.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser build of `flowflex` — intentionally non-functional.
|
|
3
|
+
*
|
|
4
|
+
* Bundlers that target the browser resolve the package to this module via the
|
|
5
|
+
* `"browser"` export condition (and legacy `"browser"` field) in package.json.
|
|
6
|
+
*
|
|
7
|
+
* The SDK is **server-only**: it authenticates with your integration
|
|
8
|
+
* `apiKey`/`apiSecret` over HTTP Basic auth, and those credentials must never
|
|
9
|
+
* reach client code. Importing or using it in the browser throws immediately.
|
|
10
|
+
* Call the SDK from your backend (e.g. a Next.js route handler / API route) and
|
|
11
|
+
* have the browser talk to your own server endpoint instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare class FlowFlexError extends Error {
|
|
14
|
+
constructor(message?: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class FlowFlexConfigError extends FlowFlexError {
|
|
17
|
+
}
|
|
18
|
+
export declare class FlowFlexUploadError extends FlowFlexError {
|
|
19
|
+
}
|
|
20
|
+
export declare class FlowFlex {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
export declare class FileRef {
|
|
24
|
+
constructor();
|
|
25
|
+
}
|
|
26
|
+
export declare function file(): never;
|
|
27
|
+
export default FlowFlex;
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Browser build of `flowflex` — intentionally non-functional.
|
|
4
|
+
*
|
|
5
|
+
* Bundlers that target the browser resolve the package to this module via the
|
|
6
|
+
* `"browser"` export condition (and legacy `"browser"` field) in package.json.
|
|
7
|
+
*
|
|
8
|
+
* The SDK is **server-only**: it authenticates with your integration
|
|
9
|
+
* `apiKey`/`apiSecret` over HTTP Basic auth, and those credentials must never
|
|
10
|
+
* reach client code. Importing or using it in the browser throws immediately.
|
|
11
|
+
* Call the SDK from your backend (e.g. a Next.js route handler / API route) and
|
|
12
|
+
* have the browser talk to your own server endpoint instead.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FileRef = exports.FlowFlex = exports.FlowFlexUploadError = exports.FlowFlexConfigError = exports.FlowFlexError = void 0;
|
|
16
|
+
exports.file = file;
|
|
17
|
+
const MESSAGE = "flowflex is server-only and must not be bundled into browser/client code. " +
|
|
18
|
+
"It uses your integration apiKey/apiSecret over HTTP Basic auth — exposing those in the " +
|
|
19
|
+
"browser lets anyone fire events as you. Call the SDK from your server (e.g. a Next.js " +
|
|
20
|
+
"route handler / API route) and have the browser talk to your own backend endpoint.";
|
|
21
|
+
class FlowFlexError extends Error {
|
|
22
|
+
constructor(message = MESSAGE) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "FlowFlexError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.FlowFlexError = FlowFlexError;
|
|
28
|
+
class FlowFlexConfigError extends FlowFlexError {
|
|
29
|
+
}
|
|
30
|
+
exports.FlowFlexConfigError = FlowFlexConfigError;
|
|
31
|
+
class FlowFlexUploadError extends FlowFlexError {
|
|
32
|
+
}
|
|
33
|
+
exports.FlowFlexUploadError = FlowFlexUploadError;
|
|
34
|
+
class FlowFlex {
|
|
35
|
+
constructor() {
|
|
36
|
+
throw new FlowFlexConfigError(MESSAGE);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.FlowFlex = FlowFlex;
|
|
40
|
+
class FileRef {
|
|
41
|
+
constructor() {
|
|
42
|
+
throw new FlowFlexConfigError(MESSAGE);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.FileRef = FileRef;
|
|
46
|
+
function file() {
|
|
47
|
+
throw new FlowFlexConfigError(MESSAGE);
|
|
48
|
+
}
|
|
49
|
+
exports.default = FlowFlex;
|
|
50
|
+
// Fail loudly the moment this server-only module is evaluated in a browser
|
|
51
|
+
// bundle — before any FlowFlex is even constructed.
|
|
52
|
+
throw new FlowFlexConfigError(MESSAGE);
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FlowFlexOptions, SendEventOptions, SendEventResult, UploadUrlResponse, FileOptions } from "./types.js";
|
|
2
|
+
import { FileRef, FileSource } from "./file.js";
|
|
3
|
+
export declare class FlowFlex {
|
|
4
|
+
private readonly apiKey;
|
|
5
|
+
private readonly apiSecret;
|
|
6
|
+
private readonly integrationCode;
|
|
7
|
+
private readonly baseUrl;
|
|
8
|
+
private readonly timeoutMs;
|
|
9
|
+
private readonly fetchImpl;
|
|
10
|
+
private readonly maxFileBytes;
|
|
11
|
+
constructor(options: FlowFlexOptions);
|
|
12
|
+
/** Convenience re-export so callers can do `ff.file(...)`. */
|
|
13
|
+
file(source: FileSource, options?: FileOptions): FileRef;
|
|
14
|
+
private authHeader;
|
|
15
|
+
/** fetch wrapper with timeout + JSON parsing + error normalization. */
|
|
16
|
+
private request;
|
|
17
|
+
/**
|
|
18
|
+
* Request a presigned upload URL. Most callers don't need this directly —
|
|
19
|
+
* use `file()` inside an event payload and let `sendEvent` handle uploads.
|
|
20
|
+
*/
|
|
21
|
+
createUploadUrl(input: {
|
|
22
|
+
filename: string;
|
|
23
|
+
mime: string;
|
|
24
|
+
size?: number;
|
|
25
|
+
}): Promise<UploadUrlResponse>;
|
|
26
|
+
/** Upload one FileRef and return its assetId. */
|
|
27
|
+
uploadFile(ref: FileRef): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Deep-clone a payload, collecting every FileRef anywhere inside it (nested
|
|
30
|
+
* objects, arrays, mixed) alongside its parent container + key so we can
|
|
31
|
+
* patch the assetId back in after upload. The container always exists at
|
|
32
|
+
* push time, so there's no backfill to get wrong.
|
|
33
|
+
*/
|
|
34
|
+
private cloneAndCollect;
|
|
35
|
+
/**
|
|
36
|
+
* Upload every FileRef in a payload and return a deep copy with each replaced
|
|
37
|
+
* by its assetId string, plus a map of path → assetId. Uploads run in
|
|
38
|
+
* parallel; the SAME FileRef instance reused in multiple places is uploaded
|
|
39
|
+
* only once and its assetId reused everywhere.
|
|
40
|
+
*/
|
|
41
|
+
private resolveFiles;
|
|
42
|
+
/**
|
|
43
|
+
* Fire a custom-integration event into FlowFlex. Any `file()` in the payload
|
|
44
|
+
* is uploaded first and replaced with its assetId, so a flow's media node can
|
|
45
|
+
* reference it as e.g. `{{trigger.assetId}}`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* await ff.sendEvent("invoice.created", {
|
|
49
|
+
* payload: { assetId: ff.file("./invoice.pdf"), name: "Ada" },
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
sendEvent(event: string, body?: {
|
|
53
|
+
payload?: Record<string, unknown>;
|
|
54
|
+
} & SendEventOptions): Promise<SendEventResult>;
|
|
55
|
+
}
|