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