@flixly/sdk 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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @flixly/sdk — Official SDK for the Flixly AI API.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { Flixly } from "@flixly/sdk";
7
+ *
8
+ * const flixly = new Flixly({ apiKey: process.env.FLIXLY_API_KEY! });
9
+ *
10
+ * // Quick: kick off a generation and wait for it.
11
+ * const result = await flixly.generateAndWait({
12
+ * model: "flux-dev",
13
+ * prompt: "A cat wearing a top hat, oil painting style",
14
+ * type: "TEXT_TO_IMAGE",
15
+ * input: { aspect_ratio: "1:1", resolution: "1K" },
16
+ * });
17
+ * console.log(result.output_url);
18
+ *
19
+ * // Or: subscribe via webhook (preferred for video / slow models).
20
+ * await flixly.generate({
21
+ * model: "veo-3-fast",
22
+ * prompt: "Cinematic shot of mountains at dawn",
23
+ * webhook_url: "https://example.com/flixly-webhook",
24
+ * });
25
+ *
26
+ * // Verify incoming webhooks:
27
+ * const valid = await Flixly.verifyWebhookSignature({
28
+ * secret: process.env.FLIXLY_WEBHOOK_SECRET!,
29
+ * timestamp: req.headers["x-flixly-timestamp"]!,
30
+ * signature: req.headers["x-flixly-signature"]!,
31
+ * body: req.rawBody,
32
+ * });
33
+ * if (!valid) throw new Error("invalid signature");
34
+ * ```
35
+ */
36
+ declare const BASE_URL = "https://www.flixly.ai";
37
+ type TaskType = "TEXT_TO_IMAGE" | "IMAGE_TO_IMAGE" | "TEXT_TO_VIDEO" | "IMAGE_TO_VIDEO" | "VIDEO_TO_VIDEO" | "TEXT_TO_SPEECH" | "VOICE_CLONE" | "MUSIC_GENERATION" | "UPSCALE" | "REMOVE_BACKGROUND";
38
+ type GenerationStatus = "pending" | "processing" | "completed" | "failed";
39
+ interface GenerateInput {
40
+ aspect_ratio?: string;
41
+ resolution?: string;
42
+ duration?: string | number;
43
+ image_url?: string;
44
+ video_url?: string;
45
+ audio_url?: string;
46
+ [key: string]: unknown;
47
+ }
48
+ interface GenerateRequest {
49
+ /** Model id from `listModels()`. */
50
+ model: string;
51
+ /** Text prompt. */
52
+ prompt: string;
53
+ /** Task type. Inferred from the model when omitted. */
54
+ type?: TaskType;
55
+ /** Model-specific parameters (aspect ratio, resolution, image_url, etc.). */
56
+ input?: GenerateInput;
57
+ /**
58
+ * If set, we POST a signed event to this URL when the generation
59
+ * finishes. Overrides the key's default. Verify with
60
+ * `Flixly.verifyWebhookSignature()`.
61
+ */
62
+ webhook_url?: string;
63
+ }
64
+ interface Generation {
65
+ id: string;
66
+ status: GenerationStatus;
67
+ type: string;
68
+ model: string | null;
69
+ output_url: string | null;
70
+ status_url: string;
71
+ webhook_url?: string | null;
72
+ credits_charged: number;
73
+ error?: string | null;
74
+ created_at: string;
75
+ completed_at: string | null;
76
+ message?: string;
77
+ }
78
+ interface ChatMessage {
79
+ role: "system" | "user" | "assistant";
80
+ content: string | Array<{
81
+ type: string;
82
+ text?: string;
83
+ image_url?: {
84
+ url: string;
85
+ };
86
+ }>;
87
+ }
88
+ interface ChatRequest {
89
+ model: string;
90
+ messages: ChatMessage[];
91
+ stream?: boolean;
92
+ max_tokens?: number;
93
+ temperature?: number;
94
+ }
95
+ interface ChatResponse {
96
+ id: string;
97
+ object: "chat.completion";
98
+ created: number;
99
+ model: string;
100
+ choices: Array<{
101
+ index: number;
102
+ message: {
103
+ role: string;
104
+ content: string;
105
+ };
106
+ finish_reason: string;
107
+ }>;
108
+ usage: {
109
+ prompt_tokens: number;
110
+ completion_tokens: number;
111
+ total_tokens: number;
112
+ };
113
+ credits_charged: number;
114
+ }
115
+ interface FlixlyModel {
116
+ id: string;
117
+ name: string;
118
+ type: string;
119
+ capabilities: string[];
120
+ estimated_credits?: number | null;
121
+ estimated_credits_per_1k_tokens?: number | null;
122
+ }
123
+ interface Account {
124
+ credits: {
125
+ subscription: number;
126
+ bonus: number;
127
+ total: number;
128
+ };
129
+ plan: {
130
+ name: string;
131
+ parallel_limit: number;
132
+ rate_limit: number;
133
+ renews_at: string | null;
134
+ };
135
+ usage: {
136
+ total_requests: number;
137
+ total_credits_used: number;
138
+ completed: number;
139
+ failed: number;
140
+ pending: number;
141
+ };
142
+ }
143
+ interface RateLimit {
144
+ /** Requests allowed per minute on this key's plan. */
145
+ limit: number;
146
+ /** Requests left in the current 60s window at the time of the response. */
147
+ remaining: number;
148
+ /** Unix epoch seconds when the window resets. */
149
+ resetAtSec: number;
150
+ }
151
+ /** A successful API response, with rate-limit headers parsed out. */
152
+ interface FlixlyResponse<T> {
153
+ data: T;
154
+ rateLimit: RateLimit | null;
155
+ }
156
+ declare class FlixlyError extends Error {
157
+ readonly status: number;
158
+ readonly code: string;
159
+ readonly details?: Record<string, unknown>;
160
+ readonly rateLimit: RateLimit | null;
161
+ constructor(args: {
162
+ status: number;
163
+ code: string;
164
+ message: string;
165
+ details?: Record<string, unknown>;
166
+ rateLimit: RateLimit | null;
167
+ });
168
+ }
169
+ interface FlixlyOptions {
170
+ apiKey: string;
171
+ /** Override the API base URL (defaults to https://www.flixly.ai). */
172
+ baseUrl?: string;
173
+ /** Custom fetch implementation. Defaults to global `fetch`. */
174
+ fetch?: typeof fetch;
175
+ /** Per-request timeout in ms. Defaults to 120_000 (2 minutes). */
176
+ timeoutMs?: number;
177
+ }
178
+ declare class Flixly {
179
+ private readonly apiKey;
180
+ private readonly baseUrl;
181
+ private readonly fetchImpl;
182
+ private readonly timeoutMs;
183
+ constructor(opts: FlixlyOptions);
184
+ /** Submit a generation. Returns immediately with `processing` for async models, or `completed` for fast sync models. */
185
+ generate(req: GenerateRequest): Promise<FlixlyResponse<Generation>>;
186
+ /** Fetch the current state of a generation. */
187
+ getGeneration(id: string): Promise<FlixlyResponse<Generation>>;
188
+ /**
189
+ * Submit a generation and poll until it completes or fails.
190
+ * Useful for short pipelines / one-shot scripts. Prefer
191
+ * `generate({ webhook_url })` in production.
192
+ */
193
+ generateAndWait(req: GenerateRequest, opts?: {
194
+ pollIntervalMs?: number;
195
+ maxWaitMs?: number;
196
+ }): Promise<FlixlyResponse<Generation>>;
197
+ /** List all available models. Includes per-model estimated_credits when authenticated. */
198
+ listModels(): Promise<FlixlyResponse<{
199
+ models: FlixlyModel[];
200
+ total: number;
201
+ }>>;
202
+ /** OpenAI-compatible chat completion (non-streaming). */
203
+ chat(req: Omit<ChatRequest, "stream"> & {
204
+ stream?: false;
205
+ }): Promise<FlixlyResponse<ChatResponse>>;
206
+ /**
207
+ * OpenAI-compatible streaming chat completion. Returns an async
208
+ * iterable of `chat.completion.chunk` events as parsed JSON. The
209
+ * stream terminates on `[DONE]`.
210
+ */
211
+ chatStream(req: Omit<ChatRequest, "stream">): AsyncIterable<any>;
212
+ /** Credit balance, plan, parallel + rate limits, and aggregate usage. */
213
+ getAccount(): Promise<FlixlyResponse<Account>>;
214
+ /**
215
+ * Verify a Flixly webhook delivery. Returns true if and only if the
216
+ * signature is valid AND the timestamp is within `tolerance` seconds.
217
+ *
218
+ * Call this on EVERY webhook POST you receive — without it, anyone
219
+ * who guesses the URL can forge a delivery.
220
+ */
221
+ static verifyWebhookSignature(args: VerifyWebhookArgs): Promise<boolean>;
222
+ private headers;
223
+ private request;
224
+ }
225
+ interface VerifyWebhookArgs {
226
+ /** The HMAC secret you stored from your API key's webhook settings. */
227
+ secret: string;
228
+ /** The `X-Flixly-Timestamp` request header value (unix seconds, string). */
229
+ timestamp: string;
230
+ /** The `X-Flixly-Signature` request header value (`sha256=<hex>`). */
231
+ signature: string;
232
+ /** Raw request body bytes or string. MUST be unmodified — JSON.parse + re-stringify will break verification. */
233
+ body: string | Uint8Array;
234
+ /** Reject signatures older than this many seconds. Defaults to 300 (5 minutes) to prevent replays. */
235
+ tolerance?: number;
236
+ }
237
+
238
+ export { type Account, BASE_URL, type ChatMessage, type ChatRequest, type ChatResponse, Flixly, FlixlyError, type FlixlyModel, type FlixlyOptions, type FlixlyResponse, type GenerateInput, type GenerateRequest, type Generation, type GenerationStatus, type RateLimit, type TaskType, type VerifyWebhookArgs };
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ // src/index.ts
2
+ var BASE_URL = "https://www.flixly.ai";
3
+ var FlixlyError = class extends Error {
4
+ constructor(args) {
5
+ super(args.message);
6
+ this.name = "FlixlyError";
7
+ this.status = args.status;
8
+ this.code = args.code;
9
+ this.details = args.details;
10
+ this.rateLimit = args.rateLimit;
11
+ }
12
+ };
13
+ var Flixly = class {
14
+ constructor(opts) {
15
+ if (!opts?.apiKey) throw new Error("Flixly: `apiKey` is required");
16
+ this.apiKey = opts.apiKey;
17
+ this.baseUrl = (opts.baseUrl || BASE_URL).replace(/\/$/, "");
18
+ this.fetchImpl = opts.fetch || globalThis.fetch;
19
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
20
+ if (!this.fetchImpl) {
21
+ throw new Error("Flixly: no fetch implementation. Pass `fetch` in options on Node < 18.");
22
+ }
23
+ }
24
+ // ─── Generations ───────────────────────────────────────────────
25
+ /** Submit a generation. Returns immediately with `processing` for async models, or `completed` for fast sync models. */
26
+ async generate(req) {
27
+ return this.request("POST", "/api/v1/generate", req);
28
+ }
29
+ /** Fetch the current state of a generation. */
30
+ async getGeneration(id) {
31
+ return this.request("GET", `/api/v1/generations/${encodeURIComponent(id)}`);
32
+ }
33
+ /**
34
+ * Submit a generation and poll until it completes or fails.
35
+ * Useful for short pipelines / one-shot scripts. Prefer
36
+ * `generate({ webhook_url })` in production.
37
+ */
38
+ async generateAndWait(req, opts = {}) {
39
+ const start = Date.now();
40
+ const interval = opts.pollIntervalMs ?? 2e3;
41
+ const maxWait = opts.maxWaitMs ?? 10 * 6e4;
42
+ const initial = await this.generate(req);
43
+ if (initial.data.status === "completed" || initial.data.status === "failed") {
44
+ return initial;
45
+ }
46
+ let last = initial;
47
+ while (Date.now() - start < maxWait) {
48
+ await sleep(interval);
49
+ last = await this.getGeneration(initial.data.id);
50
+ if (last.data.status === "completed" || last.data.status === "failed") {
51
+ return last;
52
+ }
53
+ }
54
+ throw new FlixlyError({
55
+ status: 408,
56
+ code: "timeout",
57
+ message: `Generation ${initial.data.id} did not finish within ${Math.round(maxWait / 1e3)}s. Last status: ${last.data.status}.`,
58
+ rateLimit: last.rateLimit
59
+ });
60
+ }
61
+ // ─── Models ────────────────────────────────────────────────────
62
+ /** List all available models. Includes per-model estimated_credits when authenticated. */
63
+ async listModels() {
64
+ return this.request("GET", "/api/v1/models");
65
+ }
66
+ // ─── Chat ──────────────────────────────────────────────────────
67
+ /** OpenAI-compatible chat completion (non-streaming). */
68
+ async chat(req) {
69
+ return this.request("POST", "/api/v1/chat/completions", { ...req, stream: false });
70
+ }
71
+ /**
72
+ * OpenAI-compatible streaming chat completion. Returns an async
73
+ * iterable of `chat.completion.chunk` events as parsed JSON. The
74
+ * stream terminates on `[DONE]`.
75
+ */
76
+ async *chatStream(req) {
77
+ const url = `${this.baseUrl}/api/v1/chat/completions`;
78
+ const ac = new AbortController();
79
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
80
+ let res;
81
+ try {
82
+ res = await this.fetchImpl(url, {
83
+ method: "POST",
84
+ headers: this.headers({ "Content-Type": "application/json" }),
85
+ body: JSON.stringify({ ...req, stream: true }),
86
+ signal: ac.signal
87
+ });
88
+ } finally {
89
+ clearTimeout(timer);
90
+ }
91
+ if (!res.ok) {
92
+ throw await responseToError(res);
93
+ }
94
+ if (!res.body) throw new Error("Flixly: streaming response has no body");
95
+ const reader = res.body.getReader();
96
+ const decoder = new TextDecoder();
97
+ let buf = "";
98
+ while (true) {
99
+ const { done, value } = await reader.read();
100
+ if (done) return;
101
+ buf += decoder.decode(value, { stream: true });
102
+ const lines = buf.split("\n");
103
+ buf = lines.pop() || "";
104
+ for (const line of lines) {
105
+ const trimmed = line.trim();
106
+ if (!trimmed || !trimmed.startsWith("data:")) continue;
107
+ const payload = trimmed.slice(5).trim();
108
+ if (payload === "[DONE]") return;
109
+ try {
110
+ yield JSON.parse(payload);
111
+ } catch {
112
+ }
113
+ }
114
+ }
115
+ }
116
+ // ─── Account ───────────────────────────────────────────────────
117
+ /** Credit balance, plan, parallel + rate limits, and aggregate usage. */
118
+ async getAccount() {
119
+ return this.request("GET", "/api/v1/account");
120
+ }
121
+ // ─── Webhook signature verification (static) ───────────────────
122
+ /**
123
+ * Verify a Flixly webhook delivery. Returns true if and only if the
124
+ * signature is valid AND the timestamp is within `tolerance` seconds.
125
+ *
126
+ * Call this on EVERY webhook POST you receive — without it, anyone
127
+ * who guesses the URL can forge a delivery.
128
+ */
129
+ static async verifyWebhookSignature(args) {
130
+ if (!args.signature?.startsWith("sha256=")) return false;
131
+ const provided = args.signature.slice(7).toLowerCase();
132
+ const ts = Number(args.timestamp);
133
+ if (!Number.isFinite(ts)) return false;
134
+ const now = Math.floor(Date.now() / 1e3);
135
+ const tolerance = args.tolerance ?? 300;
136
+ if (Math.abs(now - ts) > tolerance) return false;
137
+ const bodyStr = typeof args.body === "string" ? args.body : new TextDecoder().decode(args.body);
138
+ const enc = new TextEncoder();
139
+ const key = await crypto.subtle.importKey(
140
+ "raw",
141
+ enc.encode(args.secret),
142
+ { name: "HMAC", hash: "SHA-256" },
143
+ false,
144
+ ["sign"]
145
+ );
146
+ const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(`${args.timestamp}.${bodyStr}`));
147
+ const expectedHex = bufferToHex(sigBuf);
148
+ return timingSafeEqual(expectedHex, provided);
149
+ }
150
+ // ─── Internals ─────────────────────────────────────────────────
151
+ headers(extra = {}) {
152
+ return {
153
+ Authorization: `Bearer ${this.apiKey}`,
154
+ "User-Agent": "flixly-js/0.1.0",
155
+ ...extra
156
+ };
157
+ }
158
+ async request(method, path, body) {
159
+ const url = `${this.baseUrl}${path}`;
160
+ const ac = new AbortController();
161
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
162
+ let res;
163
+ try {
164
+ res = await this.fetchImpl(url, {
165
+ method,
166
+ headers: this.headers(body ? { "Content-Type": "application/json" } : {}),
167
+ body: body ? JSON.stringify(body) : void 0,
168
+ signal: ac.signal
169
+ });
170
+ } finally {
171
+ clearTimeout(timer);
172
+ }
173
+ const rateLimit = parseRateLimit(res.headers);
174
+ if (!res.ok) {
175
+ throw await responseToError(res, rateLimit);
176
+ }
177
+ const data = await res.json();
178
+ return { data, rateLimit };
179
+ }
180
+ };
181
+ function parseRateLimit(h) {
182
+ const limit = Number(h.get("X-RateLimit-Limit"));
183
+ const remaining = Number(h.get("X-RateLimit-Remaining"));
184
+ const resetAtSec = Number(h.get("X-RateLimit-Reset"));
185
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining) || !Number.isFinite(resetAtSec)) {
186
+ return null;
187
+ }
188
+ return { limit, remaining, resetAtSec };
189
+ }
190
+ async function responseToError(res, rateLimit) {
191
+ const rl = rateLimit ?? parseRateLimit(res.headers);
192
+ let code = "internal_error";
193
+ let message = `HTTP ${res.status}`;
194
+ let details;
195
+ try {
196
+ const body = await res.json();
197
+ if (body?.error) {
198
+ code = body.error.code || code;
199
+ message = body.error.message || message;
200
+ details = body.error.details;
201
+ }
202
+ } catch {
203
+ }
204
+ return new FlixlyError({ status: res.status, code, message, details, rateLimit: rl });
205
+ }
206
+ function sleep(ms) {
207
+ return new Promise((r) => setTimeout(r, ms));
208
+ }
209
+ function bufferToHex(buf) {
210
+ const bytes = new Uint8Array(buf);
211
+ let hex = "";
212
+ for (const b of bytes) hex += b.toString(16).padStart(2, "0");
213
+ return hex;
214
+ }
215
+ function timingSafeEqual(a, b) {
216
+ if (a.length !== b.length) return false;
217
+ let diff = 0;
218
+ for (let i = 0; i < a.length; i++) {
219
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
220
+ }
221
+ return diff === 0;
222
+ }
223
+ export {
224
+ BASE_URL,
225
+ Flixly,
226
+ FlixlyError
227
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@flixly/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official Flixly AI SDK for Node.js and the browser. Generate images, video, audio, and chat completions through Flixly's public API.",
5
+ "keywords": [
6
+ "flixly",
7
+ "ai",
8
+ "image-generation",
9
+ "video-generation",
10
+ "chat",
11
+ "openai-compatible"
12
+ ],
13
+ "homepage": "https://github.com/Softforge-Digital/flixly-js-sdk#readme",
14
+ "bugs": "https://github.com/Softforge-Digital/flixly-js-sdk/issues",
15
+ "license": "MIT",
16
+ "author": "Flixly <support@flixly.ai>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Softforge-Digital/flixly-js-sdk.git"
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
40
+ "lint": "tsc --noEmit",
41
+ "test": "node --experimental-vm-modules --test test/*.test.mjs"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "devDependencies": {
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.4.0"
49
+ },
50
+ "sideEffects": false
51
+ }