@rynko/sdk 1.0.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.mjs ADDED
@@ -0,0 +1,604 @@
1
+ // src/utils/http.ts
2
+ var DEFAULT_RETRY_CONFIG = {
3
+ maxAttempts: 5,
4
+ initialDelayMs: 1e3,
5
+ maxDelayMs: 3e4,
6
+ maxJitterMs: 1e3,
7
+ retryableStatuses: [429, 503, 504]
8
+ };
9
+ var HttpClient = class {
10
+ constructor(config) {
11
+ this.config = config;
12
+ if (config.retry === false) {
13
+ this.retryConfig = null;
14
+ } else {
15
+ this.retryConfig = {
16
+ ...DEFAULT_RETRY_CONFIG,
17
+ ...config.retry
18
+ };
19
+ }
20
+ }
21
+ /**
22
+ * Calculate delay for exponential backoff with jitter
23
+ */
24
+ calculateDelay(attempt, retryAfterMs) {
25
+ if (!this.retryConfig) return 0;
26
+ if (retryAfterMs !== void 0) {
27
+ const jitter2 = Math.random() * this.retryConfig.maxJitterMs;
28
+ return Math.min(retryAfterMs + jitter2, this.retryConfig.maxDelayMs);
29
+ }
30
+ const exponentialDelay = this.retryConfig.initialDelayMs * Math.pow(2, attempt);
31
+ const jitter = Math.random() * this.retryConfig.maxJitterMs;
32
+ return Math.min(exponentialDelay + jitter, this.retryConfig.maxDelayMs);
33
+ }
34
+ /**
35
+ * Parse Retry-After header value to milliseconds
36
+ */
37
+ parseRetryAfter(retryAfter) {
38
+ if (!retryAfter) return void 0;
39
+ const seconds = parseInt(retryAfter, 10);
40
+ if (!isNaN(seconds)) {
41
+ return seconds * 1e3;
42
+ }
43
+ const date = new Date(retryAfter);
44
+ if (!isNaN(date.getTime())) {
45
+ const delayMs = date.getTime() - Date.now();
46
+ return delayMs > 0 ? delayMs : void 0;
47
+ }
48
+ return void 0;
49
+ }
50
+ /**
51
+ * Check if the status code should trigger a retry
52
+ */
53
+ shouldRetry(statusCode) {
54
+ if (!this.retryConfig) return false;
55
+ return this.retryConfig.retryableStatuses.includes(statusCode);
56
+ }
57
+ /**
58
+ * Sleep for the specified duration
59
+ */
60
+ sleep(ms) {
61
+ return new Promise((resolve) => setTimeout(resolve, ms));
62
+ }
63
+ async request(method, path, options = {}) {
64
+ const url = new URL(path, this.config.baseUrl);
65
+ if (options.query) {
66
+ Object.entries(options.query).forEach(([key, value]) => {
67
+ if (value !== void 0 && value !== null) {
68
+ if (value instanceof Date) {
69
+ url.searchParams.append(key, value.toISOString());
70
+ } else {
71
+ url.searchParams.append(key, String(value));
72
+ }
73
+ }
74
+ });
75
+ }
76
+ const headers = {
77
+ "Content-Type": "application/json",
78
+ Authorization: `Bearer ${this.config.apiKey}`,
79
+ "User-Agent": "@rynko/sdk/1.0.0",
80
+ ...this.config.headers
81
+ };
82
+ const maxAttempts = this.retryConfig?.maxAttempts ?? 1;
83
+ let lastError = null;
84
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
85
+ const controller = new AbortController();
86
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
87
+ try {
88
+ const response = await fetch(url.toString(), {
89
+ method,
90
+ headers,
91
+ body: options.body ? JSON.stringify(options.body) : void 0,
92
+ signal: controller.signal
93
+ });
94
+ clearTimeout(timeoutId);
95
+ if (!response.ok && this.shouldRetry(response.status)) {
96
+ const retryAfterMs = this.parseRetryAfter(response.headers.get("Retry-After"));
97
+ const delay = this.calculateDelay(attempt, retryAfterMs);
98
+ const data2 = await response.json().catch(() => ({}));
99
+ const error = data2;
100
+ lastError = new RynkoError(
101
+ error.message || `HTTP ${response.status}`,
102
+ error.error || "ApiError",
103
+ response.status
104
+ );
105
+ if (attempt < maxAttempts - 1) {
106
+ await this.sleep(delay);
107
+ continue;
108
+ }
109
+ }
110
+ const data = await response.json();
111
+ if (!response.ok) {
112
+ const error = data;
113
+ throw new RynkoError(
114
+ error.message || `HTTP ${response.status}`,
115
+ error.error || "ApiError",
116
+ response.status
117
+ );
118
+ }
119
+ return data;
120
+ } catch (error) {
121
+ clearTimeout(timeoutId);
122
+ if (error instanceof RynkoError) {
123
+ if (!this.shouldRetry(error.statusCode) || attempt >= maxAttempts - 1) {
124
+ throw error;
125
+ }
126
+ lastError = error;
127
+ const delay = this.calculateDelay(attempt);
128
+ await this.sleep(delay);
129
+ continue;
130
+ }
131
+ if (error instanceof Error) {
132
+ if (error.name === "AbortError") {
133
+ throw new RynkoError("Request timeout", "TimeoutError", 408);
134
+ }
135
+ throw new RynkoError(error.message, "NetworkError", 0);
136
+ }
137
+ throw new RynkoError("Unknown error", "UnknownError", 0);
138
+ }
139
+ }
140
+ if (lastError) {
141
+ throw lastError;
142
+ }
143
+ throw new RynkoError("Request failed after retries", "RetryExhausted", 0);
144
+ }
145
+ async get(path, query) {
146
+ return this.request("GET", path, { query });
147
+ }
148
+ async post(path, body) {
149
+ return this.request("POST", path, { body });
150
+ }
151
+ async put(path, body) {
152
+ return this.request("PUT", path, { body });
153
+ }
154
+ async patch(path, body) {
155
+ return this.request("PATCH", path, { body });
156
+ }
157
+ async delete(path) {
158
+ return this.request("DELETE", path);
159
+ }
160
+ };
161
+ var RynkoError = class extends Error {
162
+ constructor(message, code, statusCode) {
163
+ super(message);
164
+ this.name = "RynkoError";
165
+ this.code = code;
166
+ this.statusCode = statusCode;
167
+ }
168
+ };
169
+
170
+ // src/resources/documents.ts
171
+ var DocumentsResource = class {
172
+ constructor(http) {
173
+ this.http = http;
174
+ }
175
+ /**
176
+ * Generate a document from a template
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const result = await rynko.documents.generate({
181
+ * templateId: 'tmpl_abc123',
182
+ * format: 'pdf',
183
+ * variables: {
184
+ * customerName: 'John Doe',
185
+ * invoiceNumber: 'INV-001',
186
+ * amount: 150.00,
187
+ * },
188
+ * });
189
+ * console.log('Job ID:', result.jobId);
190
+ * console.log('Download URL:', result.downloadUrl);
191
+ * ```
192
+ */
193
+ async generate(options) {
194
+ return this.http.post(
195
+ "/api/v1/documents/generate",
196
+ options
197
+ );
198
+ }
199
+ /**
200
+ * Generate a PDF document from a template
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * const result = await rynko.documents.generatePdf({
205
+ * templateId: 'tmpl_invoice',
206
+ * variables: {
207
+ * invoiceNumber: 'INV-001',
208
+ * total: 99.99,
209
+ * },
210
+ * });
211
+ * ```
212
+ */
213
+ async generatePdf(options) {
214
+ return this.generate({ ...options, format: "pdf" });
215
+ }
216
+ /**
217
+ * Generate an Excel document from a template
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * const result = await rynko.documents.generateExcel({
222
+ * templateId: 'tmpl_report',
223
+ * variables: {
224
+ * reportDate: '2025-01-15',
225
+ * data: [{ name: 'Item 1', value: 100 }],
226
+ * },
227
+ * });
228
+ * ```
229
+ */
230
+ async generateExcel(options) {
231
+ return this.generate({ ...options, format: "excel" });
232
+ }
233
+ /**
234
+ * Generate multiple documents in a batch
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * const result = await rynko.documents.generateBatch({
239
+ * templateId: 'tmpl_invoice',
240
+ * format: 'pdf',
241
+ * documents: [
242
+ * { variables: { invoiceNumber: 'INV-001', total: 99.99 } },
243
+ * { variables: { invoiceNumber: 'INV-002', total: 149.99 } },
244
+ * ],
245
+ * });
246
+ * console.log('Batch ID:', result.batchId);
247
+ * ```
248
+ */
249
+ async generateBatch(options) {
250
+ return this.http.post(
251
+ "/api/v1/documents/generate/batch",
252
+ options
253
+ );
254
+ }
255
+ /**
256
+ * Get a document job by ID
257
+ *
258
+ * @example
259
+ * ```typescript
260
+ * const job = await rynko.documents.getJob('job_abc123');
261
+ * console.log('Status:', job.status);
262
+ * if (job.status === 'completed') {
263
+ * console.log('Download:', job.downloadUrl);
264
+ * }
265
+ * ```
266
+ */
267
+ async getJob(jobId) {
268
+ return this.http.get(`/api/v1/documents/jobs/${jobId}`);
269
+ }
270
+ /**
271
+ * List document jobs with optional filters
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const { data, meta } = await rynko.documents.listJobs({
276
+ * status: 'completed',
277
+ * limit: 10,
278
+ * });
279
+ * console.log(`Found ${meta.total} jobs`);
280
+ * ```
281
+ */
282
+ async listJobs(options = {}) {
283
+ const response = await this.http.get(
284
+ "/api/v1/documents/jobs",
285
+ {
286
+ status: options.status,
287
+ templateId: options.templateId,
288
+ workspaceId: options.workspaceId,
289
+ limit: options.limit,
290
+ offset: options.offset ?? ((options.page ?? 1) - 1) * (options.limit ?? 20)
291
+ }
292
+ );
293
+ const limit = options.limit ?? 20;
294
+ const page = options.page ?? 1;
295
+ return {
296
+ data: response.jobs,
297
+ meta: {
298
+ total: response.total,
299
+ page,
300
+ limit,
301
+ totalPages: Math.ceil(response.total / limit)
302
+ }
303
+ };
304
+ }
305
+ /**
306
+ * Wait for a document job to complete
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * const result = await rynko.documents.generate({
311
+ * templateId: 'tmpl_invoice',
312
+ * format: 'pdf',
313
+ * variables: { invoiceNumber: 'INV-001' },
314
+ * });
315
+ *
316
+ * // Wait for completion (polls every 1 second, max 30 seconds)
317
+ * const completedJob = await rynko.documents.waitForCompletion(result.jobId);
318
+ * console.log('Download URL:', completedJob.downloadUrl);
319
+ * ```
320
+ */
321
+ async waitForCompletion(jobId, options = {}) {
322
+ const pollInterval = options.pollInterval || 1e3;
323
+ const timeout = options.timeout || 3e4;
324
+ const startTime = Date.now();
325
+ while (true) {
326
+ const job = await this.getJob(jobId);
327
+ if (job.status === "completed" || job.status === "failed") {
328
+ return job;
329
+ }
330
+ if (Date.now() - startTime > timeout) {
331
+ throw new Error(`Timeout waiting for job ${jobId} to complete`);
332
+ }
333
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
334
+ }
335
+ }
336
+ };
337
+
338
+ // src/resources/templates.ts
339
+ var TemplatesResource = class {
340
+ constructor(http) {
341
+ this.http = http;
342
+ }
343
+ /**
344
+ * Get a template by ID
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * const template = await rynko.templates.get('tmpl_abc123');
349
+ * console.log('Template:', template.name);
350
+ * console.log('Variables:', template.variables);
351
+ * ```
352
+ */
353
+ async get(id) {
354
+ return this.http.get(`/api/templates/${id}`);
355
+ }
356
+ /**
357
+ * List templates with optional filters
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * // List all templates
362
+ * const { data } = await rynko.templates.list();
363
+ *
364
+ * // List with pagination
365
+ * const { data, meta } = await rynko.templates.list({ page: 1, limit: 10 });
366
+ * ```
367
+ */
368
+ async list(options = {}) {
369
+ const response = await this.http.get(
370
+ "/api/templates/attachment",
371
+ {
372
+ limit: options.limit,
373
+ page: options.page,
374
+ search: options.search
375
+ }
376
+ );
377
+ return {
378
+ data: response.data,
379
+ meta: {
380
+ total: response.total,
381
+ page: response.page,
382
+ limit: response.limit,
383
+ totalPages: response.totalPages
384
+ }
385
+ };
386
+ }
387
+ /**
388
+ * List only PDF templates
389
+ *
390
+ * Note: Filtering by type is done client-side based on outputFormats.
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * const { data } = await rynko.templates.listPdf();
395
+ * ```
396
+ */
397
+ async listPdf(options = {}) {
398
+ const result = await this.list(options);
399
+ result.data = result.data.filter(
400
+ (t) => t.outputFormats?.includes("pdf")
401
+ );
402
+ return result;
403
+ }
404
+ /**
405
+ * List only Excel templates
406
+ *
407
+ * Note: Filtering by type is done client-side based on outputFormats.
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * const { data } = await rynko.templates.listExcel();
412
+ * ```
413
+ */
414
+ async listExcel(options = {}) {
415
+ const result = await this.list(options);
416
+ result.data = result.data.filter(
417
+ (t) => t.outputFormats?.includes("xlsx") || t.outputFormats?.includes("excel")
418
+ );
419
+ return result;
420
+ }
421
+ };
422
+
423
+ // src/resources/webhooks.ts
424
+ var WebhooksResource = class {
425
+ constructor(http) {
426
+ this.http = http;
427
+ }
428
+ /**
429
+ * Get a webhook subscription by ID
430
+ *
431
+ * @example
432
+ * ```typescript
433
+ * const webhook = await rynko.webhooks.get('wh_abc123');
434
+ * console.log('Events:', webhook.events);
435
+ * ```
436
+ */
437
+ async get(id) {
438
+ return this.http.get(
439
+ `/api/v1/webhook-subscriptions/${id}`
440
+ );
441
+ }
442
+ /**
443
+ * List all webhook subscriptions
444
+ *
445
+ * @example
446
+ * ```typescript
447
+ * const { data } = await rynko.webhooks.list();
448
+ * console.log('Active webhooks:', data.filter(w => w.isActive).length);
449
+ * ```
450
+ */
451
+ async list() {
452
+ const response = await this.http.get(
453
+ "/api/v1/webhook-subscriptions"
454
+ );
455
+ return {
456
+ data: response.data,
457
+ meta: {
458
+ total: response.total,
459
+ page: 1,
460
+ limit: response.data.length,
461
+ totalPages: 1
462
+ }
463
+ };
464
+ }
465
+ };
466
+
467
+ // src/client.ts
468
+ var DEFAULT_BASE_URL = "https://api.rynko.dev";
469
+ var DEFAULT_TIMEOUT = 3e4;
470
+ var Rynko = class {
471
+ /**
472
+ * Create a new Rynko client
473
+ *
474
+ * @example
475
+ * ```typescript
476
+ * import { Rynko } from '@rynko/sdk';
477
+ *
478
+ * const rynko = new Rynko({
479
+ * apiKey: process.env.RYNKO_API_KEY!,
480
+ * });
481
+ *
482
+ * // Generate a PDF
483
+ * const result = await rynko.documents.generate({
484
+ * templateId: 'tmpl_invoice',
485
+ * format: 'pdf',
486
+ * variables: { invoiceNumber: 'INV-001' },
487
+ * });
488
+ * ```
489
+ */
490
+ constructor(config) {
491
+ if (!config.apiKey) {
492
+ throw new Error("apiKey is required");
493
+ }
494
+ this.http = new HttpClient({
495
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
496
+ apiKey: config.apiKey,
497
+ timeout: config.timeout || DEFAULT_TIMEOUT,
498
+ headers: config.headers,
499
+ retry: config.retry
500
+ });
501
+ this.documents = new DocumentsResource(this.http);
502
+ this.templates = new TemplatesResource(this.http);
503
+ this.webhooks = new WebhooksResource(this.http);
504
+ }
505
+ /**
506
+ * Get the current authenticated user
507
+ *
508
+ * @example
509
+ * ```typescript
510
+ * const user = await rynko.me();
511
+ * console.log('Authenticated as:', user.email);
512
+ * ```
513
+ */
514
+ async me() {
515
+ return this.http.get("/api/auth/verify");
516
+ }
517
+ /**
518
+ * Verify the API key is valid
519
+ *
520
+ * @example
521
+ * ```typescript
522
+ * const isValid = await rynko.verifyApiKey();
523
+ * if (!isValid) {
524
+ * throw new Error('Invalid API key');
525
+ * }
526
+ * ```
527
+ */
528
+ async verifyApiKey() {
529
+ try {
530
+ await this.me();
531
+ return true;
532
+ } catch {
533
+ return false;
534
+ }
535
+ }
536
+ };
537
+ function createClient(config) {
538
+ return new Rynko(config);
539
+ }
540
+
541
+ // src/utils/webhooks.ts
542
+ import { createHmac, timingSafeEqual } from "crypto";
543
+ function parseSignatureHeader(header) {
544
+ const parts = header.split(",");
545
+ const parsed = {};
546
+ for (const part of parts) {
547
+ const [key, value] = part.split("=");
548
+ if (key === "t") {
549
+ parsed.timestamp = parseInt(value, 10);
550
+ } else if (key === "v1") {
551
+ parsed.signature = value;
552
+ }
553
+ }
554
+ if (!parsed.timestamp || !parsed.signature) {
555
+ throw new WebhookSignatureError("Invalid signature header format");
556
+ }
557
+ return parsed;
558
+ }
559
+ function computeSignature(timestamp, payload, secret) {
560
+ const signedPayload = `${timestamp}.${payload}`;
561
+ return createHmac("sha256", secret).update(signedPayload).digest("hex");
562
+ }
563
+ function verifyWebhookSignature(options) {
564
+ const { payload, signature, secret, tolerance = 300 } = options;
565
+ const { timestamp, signature: expectedSig } = parseSignatureHeader(signature);
566
+ const now = Math.floor(Date.now() / 1e3);
567
+ if (Math.abs(now - timestamp) > tolerance) {
568
+ throw new WebhookSignatureError(
569
+ "Webhook timestamp outside tolerance window"
570
+ );
571
+ }
572
+ const computedSig = computeSignature(timestamp, payload, secret);
573
+ const sigBuffer = Buffer.from(expectedSig, "hex");
574
+ const computedBuffer = Buffer.from(computedSig, "hex");
575
+ if (sigBuffer.length !== computedBuffer.length) {
576
+ throw new WebhookSignatureError("Invalid signature");
577
+ }
578
+ if (!timingSafeEqual(sigBuffer, computedBuffer)) {
579
+ throw new WebhookSignatureError("Invalid signature");
580
+ }
581
+ try {
582
+ return JSON.parse(payload);
583
+ } catch {
584
+ throw new WebhookSignatureError("Invalid webhook payload");
585
+ }
586
+ }
587
+ var WebhookSignatureError = class extends Error {
588
+ constructor(message) {
589
+ super(message);
590
+ this.name = "WebhookSignatureError";
591
+ }
592
+ };
593
+ export {
594
+ DocumentsResource,
595
+ Rynko,
596
+ RynkoError,
597
+ TemplatesResource,
598
+ WebhookSignatureError,
599
+ WebhooksResource,
600
+ computeSignature,
601
+ createClient,
602
+ parseSignatureHeader,
603
+ verifyWebhookSignature
604
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@rynko/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official Node.js SDK for Rynko - Generate PDFs and Excel files from templates",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "lint": "eslint src --ext .ts",
22
+ "test": "jest",
23
+ "test:watch": "jest --watch",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "rynko",
28
+ "pdf",
29
+ "excel",
30
+ "document-generation",
31
+ "templates",
32
+ "sdk",
33
+ "api"
34
+ ],
35
+ "author": "Rynko <support@rynko.dev>",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/rynko-dev/sdk-node.git"
40
+ },
41
+ "homepage": "https://docs.rynko.dev/sdk/node",
42
+ "bugs": {
43
+ "url": "https://github.com/rynko-dev/sdk-node/issues"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^20.0.0",
50
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
51
+ "@typescript-eslint/parser": "^7.0.0",
52
+ "eslint": "^8.56.0",
53
+ "jest": "^29.7.0",
54
+ "ts-jest": "^29.1.0",
55
+ "tsup": "^8.0.0",
56
+ "typescript": "^5.3.0"
57
+ },
58
+ "peerDependencies": {
59
+ "typescript": ">=4.7.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "typescript": {
63
+ "optional": true
64
+ }
65
+ }
66
+ }