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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { FileRef, file as makeFile } from "./file.js";
|
|
2
|
+
import { FlowFlexConfigError, FlowFlexError, FlowFlexUploadError } from "./errors.js";
|
|
3
|
+
/** 25 MB — mirrors the backend's asset size limit. */
|
|
4
|
+
const DEFAULT_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
5
|
+
/** Base64-encode `key:secret` for HTTP Basic auth, in Node or the browser. */
|
|
6
|
+
function basicAuth(key, secret) {
|
|
7
|
+
const raw = `${key}:${secret}`;
|
|
8
|
+
if (typeof Buffer !== "undefined")
|
|
9
|
+
return Buffer.from(raw).toString("base64");
|
|
10
|
+
// Browser fallback.
|
|
11
|
+
return btoa(raw);
|
|
12
|
+
}
|
|
13
|
+
/** True when running in a browser-like environment (has window + document). */
|
|
14
|
+
function isBrowser() {
|
|
15
|
+
return (typeof window !== "undefined" &&
|
|
16
|
+
typeof window.document !== "undefined");
|
|
17
|
+
}
|
|
18
|
+
/** A loopback host is safe to use over plain HTTP in development. */
|
|
19
|
+
function isLoopbackHost(host) {
|
|
20
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reject values that would break out of an HTTP header (CRLF / control chars)
|
|
24
|
+
* — prevents header-injection via the event name or idempotency key.
|
|
25
|
+
*/
|
|
26
|
+
function assertSafeHeaderValue(label, value) {
|
|
27
|
+
if (/[\r\n\x00-\x1f\x7f]/.test(value)) {
|
|
28
|
+
throw new FlowFlexConfigError(`${label} contains invalid control characters`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Assign onto a plain object without the `__proto__` prototype-pollution
|
|
33
|
+
* footgun (and treats `constructor`/`prototype` as ordinary own keys too).
|
|
34
|
+
*/
|
|
35
|
+
function safeSet(obj, key, value) {
|
|
36
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
37
|
+
Object.defineProperty(obj, key, {
|
|
38
|
+
value,
|
|
39
|
+
enumerable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
obj[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** RFC4122-ish UUID using the platform crypto, with a non-crypto fallback. */
|
|
49
|
+
function uuid() {
|
|
50
|
+
const c = globalThis.crypto;
|
|
51
|
+
if (c?.randomUUID)
|
|
52
|
+
return c.randomUUID();
|
|
53
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
54
|
+
const r = Math.floor(Math.random() * 16);
|
|
55
|
+
const v = ch === "x" ? r : (r & 0x3) | 0x8;
|
|
56
|
+
return v.toString(16);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export class FlowFlex {
|
|
60
|
+
constructor(options) {
|
|
61
|
+
const { apiKey, apiSecret, integrationCode, baseUrl } = options;
|
|
62
|
+
if (!apiKey)
|
|
63
|
+
throw new FlowFlexConfigError("apiKey is required");
|
|
64
|
+
if (!apiSecret)
|
|
65
|
+
throw new FlowFlexConfigError("apiSecret is required");
|
|
66
|
+
if (!integrationCode)
|
|
67
|
+
throw new FlowFlexConfigError("integrationCode is required");
|
|
68
|
+
if (!baseUrl)
|
|
69
|
+
throw new FlowFlexConfigError("baseUrl is required");
|
|
70
|
+
// Hard block: the SDK is server-only. The api key/secret are
|
|
71
|
+
// integration-wide credentials and must never reach client code.
|
|
72
|
+
// (Browser bundles can't even import this module — see package.json's
|
|
73
|
+
// "browser" export condition, which resolves to a throwing stub.)
|
|
74
|
+
if (isBrowser()) {
|
|
75
|
+
throw new FlowFlexConfigError("FlowFlex is server-only and cannot run in a browser. It authenticates with " +
|
|
76
|
+
"your integration apiKey/apiSecret over HTTP Basic auth — exposing those in " +
|
|
77
|
+
"client code lets anyone fire events as you. Call the SDK from your server " +
|
|
78
|
+
"and have the browser talk to your own backend endpoint.");
|
|
79
|
+
}
|
|
80
|
+
// Validate baseUrl and enforce HTTPS off-loopback so Basic-auth
|
|
81
|
+
// credentials are never sent in cleartext.
|
|
82
|
+
const stripped = baseUrl.replace(/\/api\/?$/, "").replace(/\/$/, "");
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(stripped);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new FlowFlexConfigError(`baseUrl is not a valid URL: "${baseUrl}"`);
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "https:" && !isLoopbackHost(parsed.hostname)) {
|
|
91
|
+
throw new FlowFlexConfigError(`baseUrl must use https (got "${parsed.protocol}//${parsed.hostname}"). ` +
|
|
92
|
+
"Basic-auth credentials would otherwise be sent in cleartext. " +
|
|
93
|
+
"Plain http is only allowed for localhost.");
|
|
94
|
+
}
|
|
95
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
96
|
+
if (typeof f !== "function") {
|
|
97
|
+
throw new FlowFlexConfigError("No fetch implementation found — upgrade to Node 18+ or pass { fetch }");
|
|
98
|
+
}
|
|
99
|
+
this.apiKey = apiKey;
|
|
100
|
+
this.apiSecret = apiSecret;
|
|
101
|
+
this.integrationCode = integrationCode;
|
|
102
|
+
this.baseUrl = stripped;
|
|
103
|
+
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
104
|
+
this.maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
105
|
+
// Bind to the global scope. The browser's native `fetch` throws
|
|
106
|
+
// "Illegal invocation" if called as a method on another object
|
|
107
|
+
// (i.e. `this.fetchImpl(...)`), so we detach it from `this`.
|
|
108
|
+
this.fetchImpl = f.bind(globalThis);
|
|
109
|
+
}
|
|
110
|
+
/** Convenience re-export so callers can do `ff.file(...)`. */
|
|
111
|
+
file(source, options) {
|
|
112
|
+
return makeFile(source, options);
|
|
113
|
+
}
|
|
114
|
+
authHeader() {
|
|
115
|
+
return `Basic ${basicAuth(this.apiKey, this.apiSecret)}`;
|
|
116
|
+
}
|
|
117
|
+
/** fetch wrapper with timeout + JSON parsing + error normalization. */
|
|
118
|
+
async request(url, init) {
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
121
|
+
let res;
|
|
122
|
+
try {
|
|
123
|
+
res = await this.fetchImpl(url, { ...init, signal: controller.signal });
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (err?.name === "AbortError") {
|
|
127
|
+
throw new FlowFlexError(`Request to ${url} timed out after ${this.timeoutMs}ms`);
|
|
128
|
+
}
|
|
129
|
+
throw new FlowFlexError(`Network error calling ${url}: ${err?.message || err}`);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
const text = await res.text();
|
|
135
|
+
let body = text;
|
|
136
|
+
if (text && (res.headers.get("content-type") || "").includes("application/json")) {
|
|
137
|
+
try {
|
|
138
|
+
body = JSON.parse(text);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* leave as text */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const message = (body && typeof body === "object" && (body.error || body.message)) ||
|
|
146
|
+
`Request to ${url} failed with status ${res.status}`;
|
|
147
|
+
throw new FlowFlexError(String(message), {
|
|
148
|
+
status: res.status,
|
|
149
|
+
code: body?.code || body?.name,
|
|
150
|
+
details: body,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Request a presigned upload URL. Most callers don't need this directly —
|
|
157
|
+
* use `file()` inside an event payload and let `sendEvent` handle uploads.
|
|
158
|
+
*/
|
|
159
|
+
async createUploadUrl(input) {
|
|
160
|
+
return this.request(`${this.baseUrl}/v1/assets/upload-url`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: this.authHeader(),
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify(input),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** Upload one FileRef and return its assetId. */
|
|
170
|
+
async uploadFile(ref) {
|
|
171
|
+
const blob = await ref.toBlob();
|
|
172
|
+
// Fail fast on oversized files instead of uploading then being rejected.
|
|
173
|
+
if (blob.size > this.maxFileBytes) {
|
|
174
|
+
throw new FlowFlexUploadError(`"${ref.filename}" is ${blob.size} bytes, over the ${this.maxFileBytes}-byte limit`);
|
|
175
|
+
}
|
|
176
|
+
const { assetId, uploadUrl } = await this.createUploadUrl({
|
|
177
|
+
filename: ref.filename,
|
|
178
|
+
mime: ref.mime,
|
|
179
|
+
size: ref.size,
|
|
180
|
+
});
|
|
181
|
+
// PUT the raw bytes straight to storage. No auth header — the presigned
|
|
182
|
+
// URL carries its own token, and an extra Authorization breaks it.
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
185
|
+
let res;
|
|
186
|
+
try {
|
|
187
|
+
res = await this.fetchImpl(uploadUrl, {
|
|
188
|
+
method: "PUT",
|
|
189
|
+
headers: { "Content-Type": ref.mime },
|
|
190
|
+
body: blob,
|
|
191
|
+
signal: controller.signal,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
throw new FlowFlexUploadError(`Failed to upload "${ref.filename}": ${err?.message || err}`);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
}
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
const detail = await res.text().catch(() => "");
|
|
202
|
+
throw new FlowFlexUploadError(`Storage rejected upload of "${ref.filename}" (status ${res.status})`, { status: res.status, details: detail });
|
|
203
|
+
}
|
|
204
|
+
return assetId;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Deep-clone a payload, collecting every FileRef anywhere inside it (nested
|
|
208
|
+
* objects, arrays, mixed) alongside its parent container + key so we can
|
|
209
|
+
* patch the assetId back in after upload. The container always exists at
|
|
210
|
+
* push time, so there's no backfill to get wrong.
|
|
211
|
+
*/
|
|
212
|
+
cloneAndCollect(value, path, refs) {
|
|
213
|
+
// A bare FileRef (e.g. the whole payload is a file) — caller decides.
|
|
214
|
+
if (value instanceof FileRef)
|
|
215
|
+
return value;
|
|
216
|
+
if (Array.isArray(value)) {
|
|
217
|
+
const arr = new Array(value.length);
|
|
218
|
+
value.forEach((v, i) => {
|
|
219
|
+
const p = `${path}[${i}]`;
|
|
220
|
+
if (v instanceof FileRef) {
|
|
221
|
+
arr[i] = null; // placeholder, patched after upload
|
|
222
|
+
refs.push({ container: arr, key: i, ref: v, path: p });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
arr[i] = this.cloneAndCollect(v, p, refs);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return arr;
|
|
229
|
+
}
|
|
230
|
+
if (value && typeof value === "object") {
|
|
231
|
+
const out = {};
|
|
232
|
+
for (const [k, v] of Object.entries(value)) {
|
|
233
|
+
const p = path ? `${path}.${k}` : k;
|
|
234
|
+
if (v instanceof FileRef) {
|
|
235
|
+
refs.push({ container: out, key: k, ref: v, path: p });
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
safeSet(out, k, this.cloneAndCollect(v, p, refs));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Upload every FileRef in a payload and return a deep copy with each replaced
|
|
247
|
+
* by its assetId string, plus a map of path → assetId. Uploads run in
|
|
248
|
+
* parallel; the SAME FileRef instance reused in multiple places is uploaded
|
|
249
|
+
* only once and its assetId reused everywhere.
|
|
250
|
+
*/
|
|
251
|
+
async resolveFiles(payload) {
|
|
252
|
+
const refs = [];
|
|
253
|
+
const resolved = this.cloneAndCollect(payload, "", refs);
|
|
254
|
+
// Dedupe by FileRef identity so reusing one file() across nodes uploads once.
|
|
255
|
+
const uniqueRefs = [...new Set(refs.map((r) => r.ref))];
|
|
256
|
+
const idByRef = new Map();
|
|
257
|
+
await Promise.all(uniqueRefs.map(async (ref) => {
|
|
258
|
+
idByRef.set(ref, await this.uploadFile(ref));
|
|
259
|
+
}));
|
|
260
|
+
const uploaded = {};
|
|
261
|
+
for (const { container, key, ref, path } of refs) {
|
|
262
|
+
const assetId = idByRef.get(ref);
|
|
263
|
+
if (typeof key === "number") {
|
|
264
|
+
container[key] = assetId; // array index — safe
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
safeSet(container, key, assetId);
|
|
268
|
+
}
|
|
269
|
+
uploaded[path] = assetId;
|
|
270
|
+
}
|
|
271
|
+
return { resolved, uploaded };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Fire a custom-integration event into FlowFlex. Any `file()` in the payload
|
|
275
|
+
* is uploaded first and replaced with its assetId, so a flow's media node can
|
|
276
|
+
* reference it as e.g. `{{trigger.assetId}}`.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* await ff.sendEvent("invoice.created", {
|
|
280
|
+
* payload: { assetId: ff.file("./invoice.pdf"), name: "Ada" },
|
|
281
|
+
* });
|
|
282
|
+
*/
|
|
283
|
+
async sendEvent(event, body = {}) {
|
|
284
|
+
if (!event)
|
|
285
|
+
throw new FlowFlexError("event name is required");
|
|
286
|
+
const { payload = {}, idempotencyKey } = body;
|
|
287
|
+
// These flow into HTTP headers — reject control characters (CRLF) to
|
|
288
|
+
// prevent header injection.
|
|
289
|
+
assertSafeHeaderValue("event", event);
|
|
290
|
+
const idemKey = idempotencyKey || uuid();
|
|
291
|
+
assertSafeHeaderValue("idempotencyKey", idemKey);
|
|
292
|
+
const { resolved, uploaded } = await this.resolveFiles(payload);
|
|
293
|
+
const response = await this.request(
|
|
294
|
+
// Encode the code so it can't break out of the URL path.
|
|
295
|
+
`${this.baseUrl}/v1/events/${encodeURIComponent(this.integrationCode)}`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: {
|
|
298
|
+
Authorization: this.authHeader(),
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
"x-event": event,
|
|
301
|
+
"x-idempotency-key": idemKey,
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify(resolved),
|
|
304
|
+
});
|
|
305
|
+
return { response, uploadedAssets: uploaded };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error for every failure raised by the SDK. Carries the HTTP status (when
|
|
3
|
+
* the failure came from an API call) and the machine-readable code returned by
|
|
4
|
+
* the backend (e.g. "MIME_NOT_ALLOWED", "ASSET_FILE_MISSING").
|
|
5
|
+
*/
|
|
6
|
+
export class FlowFlexError extends Error {
|
|
7
|
+
constructor(message, opts = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "FlowFlexError";
|
|
10
|
+
this.status = opts.status;
|
|
11
|
+
this.code = opts.code;
|
|
12
|
+
this.details = opts.details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Thrown when the SDK is constructed with missing/invalid configuration. */
|
|
16
|
+
export class FlowFlexConfigError extends FlowFlexError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "FlowFlexConfigError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Thrown when a file attachment cannot be uploaded. */
|
|
23
|
+
export class FlowFlexUploadError extends FlowFlexError {
|
|
24
|
+
constructor(message, opts = {}) {
|
|
25
|
+
super(message, opts);
|
|
26
|
+
this.name = "FlowFlexUploadError";
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/esm/file.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { FlowFlexUploadError } from "./errors.js";
|
|
2
|
+
/** Maps common extensions → MIME types accepted by the FlowFlex assets API. */
|
|
3
|
+
const EXT_TO_MIME = {
|
|
4
|
+
pdf: "application/pdf",
|
|
5
|
+
doc: "application/msword",
|
|
6
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
7
|
+
xls: "application/vnd.ms-excel",
|
|
8
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
9
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
10
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
11
|
+
txt: "text/plain",
|
|
12
|
+
jpg: "image/jpeg",
|
|
13
|
+
jpeg: "image/jpeg",
|
|
14
|
+
png: "image/png",
|
|
15
|
+
mp4: "video/mp4",
|
|
16
|
+
"3gp": "video/3gpp",
|
|
17
|
+
aac: "audio/aac",
|
|
18
|
+
amr: "audio/amr",
|
|
19
|
+
mp3: "audio/mpeg",
|
|
20
|
+
ogg: "audio/ogg",
|
|
21
|
+
};
|
|
22
|
+
function inferMimeFromName(name) {
|
|
23
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
24
|
+
return ext ? EXT_TO_MIME[ext] : undefined;
|
|
25
|
+
}
|
|
26
|
+
function basename(p) {
|
|
27
|
+
return p.split(/[\\/]/).pop() || p;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A lazy reference to a file the caller wants to attach to an event. The bytes
|
|
31
|
+
* are not read until the event is sent. Create one with `FlowFlex.file()` or
|
|
32
|
+
* the standalone `file()` helper.
|
|
33
|
+
*/
|
|
34
|
+
export class FileRef {
|
|
35
|
+
constructor(source, options = {}) {
|
|
36
|
+
this.source = source;
|
|
37
|
+
this.options = options;
|
|
38
|
+
}
|
|
39
|
+
/** Resolve the filename, inferring from a path source when possible. */
|
|
40
|
+
get filename() {
|
|
41
|
+
if (this.options.filename)
|
|
42
|
+
return this.options.filename;
|
|
43
|
+
if (typeof this.source === "string")
|
|
44
|
+
return basename(this.source);
|
|
45
|
+
const blobName = this.source?.name;
|
|
46
|
+
if (blobName)
|
|
47
|
+
return blobName;
|
|
48
|
+
throw new FlowFlexUploadError("Cannot determine filename for attachment — pass { filename } to file()");
|
|
49
|
+
}
|
|
50
|
+
/** Resolve the MIME type, inferring from the filename when possible. */
|
|
51
|
+
get mime() {
|
|
52
|
+
if (this.options.mime)
|
|
53
|
+
return this.options.mime;
|
|
54
|
+
const blobType = this.source?.type;
|
|
55
|
+
if (blobType)
|
|
56
|
+
return blobType;
|
|
57
|
+
const inferred = inferMimeFromName(this.filename);
|
|
58
|
+
if (inferred)
|
|
59
|
+
return inferred;
|
|
60
|
+
throw new FlowFlexUploadError(`Cannot determine MIME type for "${this.filename}" — pass { mime } to file()`);
|
|
61
|
+
}
|
|
62
|
+
get size() {
|
|
63
|
+
if (typeof this.options.size === "number")
|
|
64
|
+
return this.options.size;
|
|
65
|
+
if (this.source instanceof Uint8Array)
|
|
66
|
+
return this.source.byteLength;
|
|
67
|
+
if (typeof Blob !== "undefined" && this.source instanceof Blob)
|
|
68
|
+
return this.source.size;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read the file bytes as a Blob, ready to PUT to storage. On Node a string
|
|
73
|
+
* source is read from disk via `fs`; Buffers/Uint8Arrays/Blobs are wrapped.
|
|
74
|
+
*/
|
|
75
|
+
async toBlob() {
|
|
76
|
+
const { source, mime } = { source: this.source, mime: this.mime };
|
|
77
|
+
if (typeof source === "string") {
|
|
78
|
+
// Node-only: read from disk. The indirect specifier + @vite-ignore
|
|
79
|
+
// stops browser bundlers (Vite/esbuild) from trying to resolve
|
|
80
|
+
// "fs/promises" at build time — this branch never runs in a browser.
|
|
81
|
+
let fs;
|
|
82
|
+
const fsSpecifier = "fs/promises";
|
|
83
|
+
try {
|
|
84
|
+
fs = await import(/* @vite-ignore */ /* webpackIgnore: true */ fsSpecifier);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new FlowFlexUploadError("File paths are only supported on Node.js — pass a Blob/File in the browser");
|
|
88
|
+
}
|
|
89
|
+
const buf = await fs.readFile(source);
|
|
90
|
+
return new Blob([new Uint8Array(buf)], { type: mime });
|
|
91
|
+
}
|
|
92
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
93
|
+
return source;
|
|
94
|
+
}
|
|
95
|
+
if (source instanceof Uint8Array) {
|
|
96
|
+
// Covers Buffer too (Buffer extends Uint8Array).
|
|
97
|
+
return new Blob([source], { type: mime });
|
|
98
|
+
}
|
|
99
|
+
throw new FlowFlexUploadError("Unsupported file source");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Wrap a file so it can be attached to an event payload. The SDK uploads it via
|
|
104
|
+
* a presigned URL when the event is sent and swaps it for its `assetId`.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ff.sendEvent("invoice.created", {
|
|
108
|
+
* payload: { assetId: file("./invoice.pdf"), customer_id: "cust_123" },
|
|
109
|
+
* });
|
|
110
|
+
*/
|
|
111
|
+
export function file(source, options) {
|
|
112
|
+
return new FileRef(source, options);
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/file.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FileOptions } from "./types.js";
|
|
2
|
+
/** Accepted file sources. */
|
|
3
|
+
export type FileSource = string | Buffer | Uint8Array | Blob;
|
|
4
|
+
/**
|
|
5
|
+
* A lazy reference to a file the caller wants to attach to an event. The bytes
|
|
6
|
+
* are not read until the event is sent. Create one with `FlowFlex.file()` or
|
|
7
|
+
* the standalone `file()` helper.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FileRef {
|
|
10
|
+
readonly source: FileSource;
|
|
11
|
+
private readonly options;
|
|
12
|
+
constructor(source: FileSource, options?: FileOptions);
|
|
13
|
+
/** Resolve the filename, inferring from a path source when possible. */
|
|
14
|
+
get filename(): string;
|
|
15
|
+
/** Resolve the MIME type, inferring from the filename when possible. */
|
|
16
|
+
get mime(): string;
|
|
17
|
+
get size(): number | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Read the file bytes as a Blob, ready to PUT to storage. On Node a string
|
|
20
|
+
* source is read from disk via `fs`; Buffers/Uint8Arrays/Blobs are wrapped.
|
|
21
|
+
*/
|
|
22
|
+
toBlob(): Promise<Blob>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wrap a file so it can be attached to an event payload. The SDK uploads it via
|
|
26
|
+
* a presigned URL when the event is sent and swaps it for its `assetId`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ff.sendEvent("invoice.created", {
|
|
30
|
+
* payload: { assetId: file("./invoice.pdf"), customer_id: "cust_123" },
|
|
31
|
+
* });
|
|
32
|
+
*/
|
|
33
|
+
export declare function file(source: FileSource, options?: FileOptions): FileRef;
|
package/dist/file.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileRef = void 0;
|
|
37
|
+
exports.file = file;
|
|
38
|
+
const errors_js_1 = require("./errors.js");
|
|
39
|
+
/** Maps common extensions → MIME types accepted by the FlowFlex assets API. */
|
|
40
|
+
const EXT_TO_MIME = {
|
|
41
|
+
pdf: "application/pdf",
|
|
42
|
+
doc: "application/msword",
|
|
43
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
44
|
+
xls: "application/vnd.ms-excel",
|
|
45
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
46
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
47
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
48
|
+
txt: "text/plain",
|
|
49
|
+
jpg: "image/jpeg",
|
|
50
|
+
jpeg: "image/jpeg",
|
|
51
|
+
png: "image/png",
|
|
52
|
+
mp4: "video/mp4",
|
|
53
|
+
"3gp": "video/3gpp",
|
|
54
|
+
aac: "audio/aac",
|
|
55
|
+
amr: "audio/amr",
|
|
56
|
+
mp3: "audio/mpeg",
|
|
57
|
+
ogg: "audio/ogg",
|
|
58
|
+
};
|
|
59
|
+
function inferMimeFromName(name) {
|
|
60
|
+
const ext = name.split(".").pop()?.toLowerCase();
|
|
61
|
+
return ext ? EXT_TO_MIME[ext] : undefined;
|
|
62
|
+
}
|
|
63
|
+
function basename(p) {
|
|
64
|
+
return p.split(/[\\/]/).pop() || p;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A lazy reference to a file the caller wants to attach to an event. The bytes
|
|
68
|
+
* are not read until the event is sent. Create one with `FlowFlex.file()` or
|
|
69
|
+
* the standalone `file()` helper.
|
|
70
|
+
*/
|
|
71
|
+
class FileRef {
|
|
72
|
+
constructor(source, options = {}) {
|
|
73
|
+
this.source = source;
|
|
74
|
+
this.options = options;
|
|
75
|
+
}
|
|
76
|
+
/** Resolve the filename, inferring from a path source when possible. */
|
|
77
|
+
get filename() {
|
|
78
|
+
if (this.options.filename)
|
|
79
|
+
return this.options.filename;
|
|
80
|
+
if (typeof this.source === "string")
|
|
81
|
+
return basename(this.source);
|
|
82
|
+
const blobName = this.source?.name;
|
|
83
|
+
if (blobName)
|
|
84
|
+
return blobName;
|
|
85
|
+
throw new errors_js_1.FlowFlexUploadError("Cannot determine filename for attachment — pass { filename } to file()");
|
|
86
|
+
}
|
|
87
|
+
/** Resolve the MIME type, inferring from the filename when possible. */
|
|
88
|
+
get mime() {
|
|
89
|
+
if (this.options.mime)
|
|
90
|
+
return this.options.mime;
|
|
91
|
+
const blobType = this.source?.type;
|
|
92
|
+
if (blobType)
|
|
93
|
+
return blobType;
|
|
94
|
+
const inferred = inferMimeFromName(this.filename);
|
|
95
|
+
if (inferred)
|
|
96
|
+
return inferred;
|
|
97
|
+
throw new errors_js_1.FlowFlexUploadError(`Cannot determine MIME type for "${this.filename}" — pass { mime } to file()`);
|
|
98
|
+
}
|
|
99
|
+
get size() {
|
|
100
|
+
if (typeof this.options.size === "number")
|
|
101
|
+
return this.options.size;
|
|
102
|
+
if (this.source instanceof Uint8Array)
|
|
103
|
+
return this.source.byteLength;
|
|
104
|
+
if (typeof Blob !== "undefined" && this.source instanceof Blob)
|
|
105
|
+
return this.source.size;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Read the file bytes as a Blob, ready to PUT to storage. On Node a string
|
|
110
|
+
* source is read from disk via `fs`; Buffers/Uint8Arrays/Blobs are wrapped.
|
|
111
|
+
*/
|
|
112
|
+
async toBlob() {
|
|
113
|
+
const { source, mime } = { source: this.source, mime: this.mime };
|
|
114
|
+
if (typeof source === "string") {
|
|
115
|
+
// Node-only: read from disk. The indirect specifier + @vite-ignore
|
|
116
|
+
// stops browser bundlers (Vite/esbuild) from trying to resolve
|
|
117
|
+
// "fs/promises" at build time — this branch never runs in a browser.
|
|
118
|
+
let fs;
|
|
119
|
+
const fsSpecifier = "fs/promises";
|
|
120
|
+
try {
|
|
121
|
+
fs = await Promise.resolve(`${fsSpecifier}`).then(s => __importStar(require(s)));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new errors_js_1.FlowFlexUploadError("File paths are only supported on Node.js — pass a Blob/File in the browser");
|
|
125
|
+
}
|
|
126
|
+
const buf = await fs.readFile(source);
|
|
127
|
+
return new Blob([new Uint8Array(buf)], { type: mime });
|
|
128
|
+
}
|
|
129
|
+
if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
130
|
+
return source;
|
|
131
|
+
}
|
|
132
|
+
if (source instanceof Uint8Array) {
|
|
133
|
+
// Covers Buffer too (Buffer extends Uint8Array).
|
|
134
|
+
return new Blob([source], { type: mime });
|
|
135
|
+
}
|
|
136
|
+
throw new errors_js_1.FlowFlexUploadError("Unsupported file source");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.FileRef = FileRef;
|
|
140
|
+
/**
|
|
141
|
+
* Wrap a file so it can be attached to an event payload. The SDK uploads it via
|
|
142
|
+
* a presigned URL when the event is sent and swaps it for its `assetId`.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ff.sendEvent("invoice.created", {
|
|
146
|
+
* payload: { assetId: file("./invoice.pdf"), customer_id: "cust_123" },
|
|
147
|
+
* });
|
|
148
|
+
*/
|
|
149
|
+
function file(source, options) {
|
|
150
|
+
return new FileRef(source, options);
|
|
151
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { FlowFlex } from "./client.js";
|
|
2
|
+
export { file, FileRef } from "./file.js";
|
|
3
|
+
export type { FileSource } from "./file.js";
|
|
4
|
+
export { FlowFlexError, FlowFlexConfigError, FlowFlexUploadError, } from "./errors.js";
|
|
5
|
+
export type { FlowFlexOptions, FileOptions, SendEventOptions, SendEventResult, UploadUrlResponse, } from "./types.js";
|
|
6
|
+
import { FlowFlex } from "./client.js";
|
|
7
|
+
export default FlowFlex;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowFlexUploadError = exports.FlowFlexConfigError = exports.FlowFlexError = exports.FileRef = exports.file = exports.FlowFlex = void 0;
|
|
4
|
+
var client_js_1 = require("./client.js");
|
|
5
|
+
Object.defineProperty(exports, "FlowFlex", { enumerable: true, get: function () { return client_js_1.FlowFlex; } });
|
|
6
|
+
var file_js_1 = require("./file.js");
|
|
7
|
+
Object.defineProperty(exports, "file", { enumerable: true, get: function () { return file_js_1.file; } });
|
|
8
|
+
Object.defineProperty(exports, "FileRef", { enumerable: true, get: function () { return file_js_1.FileRef; } });
|
|
9
|
+
var errors_js_1 = require("./errors.js");
|
|
10
|
+
Object.defineProperty(exports, "FlowFlexError", { enumerable: true, get: function () { return errors_js_1.FlowFlexError; } });
|
|
11
|
+
Object.defineProperty(exports, "FlowFlexConfigError", { enumerable: true, get: function () { return errors_js_1.FlowFlexConfigError; } });
|
|
12
|
+
Object.defineProperty(exports, "FlowFlexUploadError", { enumerable: true, get: function () { return errors_js_1.FlowFlexUploadError; } });
|
|
13
|
+
const client_js_2 = require("./client.js");
|
|
14
|
+
exports.default = client_js_2.FlowFlex;
|