@spooled/sdk 1.0.11 → 1.0.13

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 CHANGED
@@ -14,7 +14,13 @@ Official Node.js/TypeScript SDK for [Spooled Cloud](https://spooled.cloud) - a m
14
14
  - **Realtime events** via WebSocket and SSE
15
15
  - **Worker runtime** for processing jobs
16
16
  - **Workflow DAGs** for complex job dependencies
17
- - **gRPC streaming** for high-performance workers
17
+ - **gRPC streaming** for high-performance workers (with automatic limit enforcement)
18
+ - **Tier-based limits** with automatic enforcement across all endpoints
19
+ - **Organization management** with usage tracking
20
+ - **Webhook management** with automatic retries
21
+ - **Dead Letter Queue (DLQ)** operations
22
+ - **API Key management** with Redis caching (~28x faster authentication)
23
+ - **Billing integration** via Stripe
18
24
  - **Automatic case conversion** between camelCase (SDK) and snake_case (API)
19
25
 
20
26
  ## Installation
@@ -165,6 +171,161 @@ await realtime.connect();
165
171
  await realtime.subscribe({ queueName: 'my-queue' });
166
172
  ```
167
173
 
174
+ ### Organization Management
175
+
176
+ Manage your organization and track usage:
177
+
178
+ ```typescript
179
+ // Get current usage and limits
180
+ const usage = await client.organizations.getUsage();
181
+ console.log(`Active jobs: ${usage.active_jobs.current}/${usage.active_jobs.limit}`);
182
+ console.log(`Plan: ${usage.plan.tier}`);
183
+
184
+ // Generate a unique slug for a new organization
185
+ const { slug } = await client.organizations.generateSlug('My Company');
186
+
187
+ // Check if a slug is available
188
+ const { available } = await client.organizations.checkSlug('my-company');
189
+ ```
190
+
191
+ ### Webhooks
192
+
193
+ Configure outgoing webhooks for job events:
194
+
195
+ ```typescript
196
+ // Create webhook
197
+ const webhook = await client.webhooks.create({
198
+ url: 'https://your-app.com/webhooks/spooled',
199
+ events: ['job.completed', 'job.failed'],
200
+ queueName: 'my-queue',
201
+ secret: 'webhook_secret_key'
202
+ });
203
+
204
+ // Retry a failed delivery
205
+ await client.webhooks.retryDelivery(webhookId, deliveryId);
206
+
207
+ // Get delivery history
208
+ const deliveries = await client.webhooks.getDeliveries(webhookId);
209
+ ```
210
+
211
+ ### Dead Letter Queue (DLQ)
212
+
213
+ Manage jobs that have exhausted all retries:
214
+
215
+ ```typescript
216
+ // List DLQ jobs
217
+ const dlqJobs = await client.jobs.dlq.list({ limit: 100 });
218
+
219
+ // Retry specific jobs from DLQ
220
+ await client.jobs.dlq.retry({
221
+ jobIds: ['job-1', 'job-2'],
222
+ });
223
+
224
+ // Retry jobs by queue
225
+ await client.jobs.dlq.retry({
226
+ queueName: 'my-queue',
227
+ limit: 50
228
+ });
229
+
230
+ // Purge DLQ (requires confirmation)
231
+ await client.jobs.dlq.purge({
232
+ queueName: 'my-queue',
233
+ confirm: true
234
+ });
235
+ ```
236
+
237
+ ### API Key Management
238
+
239
+ Manage API keys programmatically:
240
+
241
+ ```typescript
242
+ // Create a new API key
243
+ const apiKey = await client.apiKeys.create({
244
+ name: 'Production Worker',
245
+ queues: ['emails', 'notifications'],
246
+ rateLimit: 1000
247
+ });
248
+
249
+ console.log(`Save this key: ${apiKey.key}`); // Only shown once!
250
+
251
+ // List all API keys
252
+ const keys = await client.apiKeys.list();
253
+
254
+ // Revoke a key
255
+ await client.apiKeys.revoke(keyId);
256
+ ```
257
+
258
+ ### Billing & Subscriptions
259
+
260
+ Manage billing via Stripe integration:
261
+
262
+ ```typescript
263
+ // Get billing status
264
+ const status = await client.billing.getStatus();
265
+
266
+ // Create customer portal session
267
+ const { url } = await client.billing.createPortal({
268
+ returnUrl: 'https://your-app.com/settings'
269
+ });
270
+
271
+ // Redirect user to: url
272
+ ```
273
+
274
+ ### Authentication
275
+
276
+ Email-based passwordless authentication:
277
+
278
+ ```typescript
279
+ // Start email login flow
280
+ await client.auth.startEmailLogin('user@example.com');
281
+ // User receives email with login link
282
+
283
+ // Check if email exists
284
+ const { exists } = await client.auth.checkEmail('user@example.com');
285
+
286
+ // Exchange API key for JWT
287
+ const { accessToken, refreshToken } = await client.auth.login({
288
+ apiKey: 'sk_live_...'
289
+ });
290
+
291
+ // Use JWT for subsequent requests
292
+ const jwtClient = new SpooledClient({ accessToken });
293
+ ```
294
+
295
+ ## Plan Limits
296
+
297
+ All operations automatically enforce tier-based limits:
298
+
299
+ | Tier | Active Jobs | Daily Jobs | Queues | Workers | Webhooks |
300
+ |------|-------------|------------|--------|---------|----------|
301
+ | **Free** | 10 | 1,000 | 5 | 3 | 2 |
302
+ | **Starter** | 100 | 100,000 | 25 | 25 | 10 |
303
+ | **Enterprise** | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
304
+
305
+ **Limits are enforced on:**
306
+ - ✅ HTTP job creation (`POST /jobs`, `POST /jobs/bulk`)
307
+ - ✅ gRPC job enqueue
308
+ - ✅ Workflow creation (counts all jobs in workflow)
309
+ - ✅ Schedule triggers
310
+ - ✅ DLQ retry operations
311
+ - ✅ Worker registration
312
+ - ✅ Queue creation
313
+ - ✅ Webhook creation
314
+
315
+ When limits are exceeded, you'll receive a `403 Forbidden` response with details:
316
+
317
+ ```typescript
318
+ try {
319
+ await client.jobs.create({ /* ... */ });
320
+ } catch (error) {
321
+ if (error.statusCode === 403 && error.code === 'limit_exceeded') {
322
+ console.log(`Limit: ${error.message}`);
323
+ console.log(`Current: ${error.current}, Max: ${error.limit}`);
324
+ console.log(`Upgrade to: ${error.upgradeTo}`);
325
+ }
326
+ }
327
+ ```
328
+
168
329
  ## Error Handling
169
330
 
170
331
  All errors extend `SpooledError` with specific subclasses:
@@ -174,6 +335,7 @@ import {
174
335
  NotFoundError,
175
336
  RateLimitError,
176
337
  ValidationError,
338
+ LimitExceededError,
177
339
  isSpooledError,
178
340
  } from '@spooled/sdk';
179
341
 
@@ -182,6 +344,9 @@ try {
182
344
  } catch (error) {
183
345
  if (error instanceof NotFoundError) {
184
346
  console.log('Job not found');
347
+ } else if (error instanceof LimitExceededError) {
348
+ console.log(`Plan limit: ${error.message}`);
349
+ console.log(`Upgrade to ${error.upgradeTo} for more capacity`);
185
350
  } else if (error instanceof RateLimitError) {
186
351
  console.log(`Retry after ${error.getRetryAfter()} seconds`);
187
352
  } else if (isSpooledError(error)) {
@@ -192,35 +357,57 @@ try {
192
357
 
193
358
  ## gRPC API
194
359
 
195
- For high-throughput workers, use the native gRPC API:
360
+ For high-throughput workers, use the native gRPC API with the included gRPC client:
196
361
 
197
362
  ```typescript
198
- import * as grpc from '@grpc/grpc-js';
199
- import * as protoLoader from '@grpc/proto-loader';
363
+ import { SpooledGrpcClient } from '@spooled/sdk';
200
364
 
201
- const packageDefinition = protoLoader.loadSync('spooled.proto');
202
- const spooled = grpc.loadPackageDefinition(packageDefinition).spooled.v1;
203
-
204
- const client = new spooled.QueueService(
365
+ // Connect to gRPC server
366
+ const grpcClient = new SpooledGrpcClient(
205
367
  'grpc.spooled.cloud:443',
206
- grpc.credentials.createSsl()
368
+ 'sk_live_your_key'
207
369
  );
208
370
 
209
- const metadata = new grpc.Metadata();
210
- metadata.add('x-api-key', 'sk_live_your_key');
371
+ // Register worker
372
+ const { workerId } = await grpcClient.workers.register({
373
+ queueName: 'emails',
374
+ hostname: 'worker-1',
375
+ maxConcurrency: 10
376
+ });
211
377
 
212
- // Stream jobs
213
- const stream = client.StreamJobs({
214
- queue_name: 'emails',
215
- worker_id: 'worker-1',
216
- lease_duration_secs: 300,
217
- }, metadata);
378
+ // Enqueue job (enforces plan limits automatically)
379
+ const { jobId } = await grpcClient.queue.enqueue({
380
+ queueName: 'emails',
381
+ payload: { to: 'user@example.com' },
382
+ priority: 5
383
+ });
218
384
 
219
- stream.on('data', (job) => {
220
- console.log('Received job:', job.id);
385
+ // Dequeue jobs in batch
386
+ const { jobs } = await grpcClient.queue.dequeue({
387
+ queueName: 'emails',
388
+ workerId,
389
+ batchSize: 10,
390
+ leaseDurationSecs: 300
221
391
  });
392
+
393
+ // Process and complete
394
+ for (const job of jobs) {
395
+ await processJob(job);
396
+ await grpcClient.queue.complete({
397
+ jobId: job.id,
398
+ workerId,
399
+ result: { success: true }
400
+ });
401
+ }
222
402
  ```
223
403
 
404
+ **gRPC Features:**
405
+ - ⚡ **~28x faster** than HTTP (with Redis cache: ~50ms vs 1400ms)
406
+ - 🛡️ **Automatic plan limit enforcement** on enqueue operations
407
+ - 📦 **Batch operations** for higher throughput
408
+ - 🔄 **Streaming support** for real-time job processing
409
+ - 🔐 **Secure authentication** via API key metadata
410
+
224
411
  See [gRPC Guide](docs/grpc.md) for complete documentation.
225
412
 
226
413
  ## TypeScript Support
@@ -107,11 +107,11 @@ declare const DEFAULT_CONFIG: {
107
107
  successThreshold: number;
108
108
  timeout: number;
109
109
  };
110
- readonly userAgent: "@spooled/sdk-nodejs/1.0.11";
110
+ readonly userAgent: "@spooled/sdk-nodejs/1.0.13";
111
111
  readonly autoRefreshToken: true;
112
112
  };
113
113
  /** SDK version */
114
- declare const SDK_VERSION = "1.0.11";
114
+ declare const SDK_VERSION = "1.0.13";
115
115
  /** API version prefix */
116
116
  declare const API_VERSION = "v1";
117
117
  /**
@@ -422,6 +422,19 @@ declare class AuthResource {
422
422
  * Validate a token
423
423
  */
424
424
  validate(params: ValidateTokenParams): Promise<ValidateTokenResponse>;
425
+ /**
426
+ * Start email-based login flow (sends magic link)
427
+ */
428
+ startEmailLogin(email: string): Promise<{
429
+ success: boolean;
430
+ message: string;
431
+ }>;
432
+ /**
433
+ * Check if an email address exists in the system
434
+ */
435
+ checkEmail(email: string): Promise<{
436
+ exists: boolean;
437
+ }>;
425
438
  }
426
439
 
427
440
  /**
@@ -1388,6 +1401,13 @@ declare class WebhooksResource {
1388
1401
  * Get webhook delivery history
1389
1402
  */
1390
1403
  getDeliveries(id: string): Promise<OutgoingWebhookDelivery[]>;
1404
+ /**
1405
+ * Retry a specific webhook delivery
1406
+ */
1407
+ retryDelivery(webhookId: string, deliveryId: string): Promise<{
1408
+ success: boolean;
1409
+ message: string;
1410
+ }>;
1391
1411
  }
1392
1412
 
1393
1413
  /**
@@ -1633,6 +1653,12 @@ declare class OrganizationsResource {
1633
1653
  * Check slug availability
1634
1654
  */
1635
1655
  checkSlug(slug: string): Promise<CheckSlugResponse>;
1656
+ /**
1657
+ * Generate a unique slug from an organization name
1658
+ */
1659
+ generateSlug(name: string): Promise<{
1660
+ slug: string;
1661
+ }>;
1636
1662
  }
1637
1663
 
1638
1664
  /**
@@ -107,11 +107,11 @@ declare const DEFAULT_CONFIG: {
107
107
  successThreshold: number;
108
108
  timeout: number;
109
109
  };
110
- readonly userAgent: "@spooled/sdk-nodejs/1.0.11";
110
+ readonly userAgent: "@spooled/sdk-nodejs/1.0.13";
111
111
  readonly autoRefreshToken: true;
112
112
  };
113
113
  /** SDK version */
114
- declare const SDK_VERSION = "1.0.11";
114
+ declare const SDK_VERSION = "1.0.13";
115
115
  /** API version prefix */
116
116
  declare const API_VERSION = "v1";
117
117
  /**
@@ -422,6 +422,19 @@ declare class AuthResource {
422
422
  * Validate a token
423
423
  */
424
424
  validate(params: ValidateTokenParams): Promise<ValidateTokenResponse>;
425
+ /**
426
+ * Start email-based login flow (sends magic link)
427
+ */
428
+ startEmailLogin(email: string): Promise<{
429
+ success: boolean;
430
+ message: string;
431
+ }>;
432
+ /**
433
+ * Check if an email address exists in the system
434
+ */
435
+ checkEmail(email: string): Promise<{
436
+ exists: boolean;
437
+ }>;
425
438
  }
426
439
 
427
440
  /**
@@ -1388,6 +1401,13 @@ declare class WebhooksResource {
1388
1401
  * Get webhook delivery history
1389
1402
  */
1390
1403
  getDeliveries(id: string): Promise<OutgoingWebhookDelivery[]>;
1404
+ /**
1405
+ * Retry a specific webhook delivery
1406
+ */
1407
+ retryDelivery(webhookId: string, deliveryId: string): Promise<{
1408
+ success: boolean;
1409
+ message: string;
1410
+ }>;
1391
1411
  }
1392
1412
 
1393
1413
  /**
@@ -1633,6 +1653,12 @@ declare class OrganizationsResource {
1633
1653
  * Check slug availability
1634
1654
  */
1635
1655
  checkSlug(slug: string): Promise<CheckSlugResponse>;
1656
+ /**
1657
+ * Generate a unique slug from an organization name
1658
+ */
1659
+ generateSlug(name: string): Promise<{
1660
+ slug: string;
1661
+ }>;
1636
1662
  }
1637
1663
 
1638
1664
  /**
package/dist/index.cjs CHANGED
@@ -56,10 +56,10 @@ var DEFAULT_CONFIG = {
56
56
  successThreshold: 3,
57
57
  timeout: 3e4
58
58
  },
59
- userAgent: "@spooled/sdk-nodejs/1.0.11",
59
+ userAgent: "@spooled/sdk-nodejs/1.0.13",
60
60
  autoRefreshToken: true
61
61
  };
62
- var SDK_VERSION = "1.0.11";
62
+ var SDK_VERSION = "1.0.13";
63
63
  var API_VERSION = "v1";
64
64
  var API_BASE_PATH = `/api/${API_VERSION}`;
65
65
  function resolveConfig(options) {
@@ -947,6 +947,20 @@ var AuthResource = class {
947
947
  async validate(params) {
948
948
  return this.http.post("/auth/validate", params);
949
949
  }
950
+ /**
951
+ * Start email-based login flow (sends magic link)
952
+ */
953
+ async startEmailLogin(email) {
954
+ return this.http.post("/auth/email/start", { email });
955
+ }
956
+ /**
957
+ * Check if an email address exists in the system
958
+ */
959
+ async checkEmail(email) {
960
+ return this.http.get("/auth/check-email", {
961
+ params: { email }
962
+ });
963
+ }
950
964
  };
951
965
 
952
966
  // src/resources/jobs.ts
@@ -1310,6 +1324,14 @@ var WebhooksResource = class {
1310
1324
  async getDeliveries(id) {
1311
1325
  return this.http.get(`/outgoing-webhooks/${id}/deliveries`);
1312
1326
  }
1327
+ /**
1328
+ * Retry a specific webhook delivery
1329
+ */
1330
+ async retryDelivery(webhookId, deliveryId) {
1331
+ return this.http.post(
1332
+ `/outgoing-webhooks/${webhookId}/retry/${deliveryId}`
1333
+ );
1334
+ }
1313
1335
  };
1314
1336
 
1315
1337
  // src/resources/api-keys.ts
@@ -1404,6 +1426,12 @@ var OrganizationsResource = class {
1404
1426
  params: { slug }
1405
1427
  });
1406
1428
  }
1429
+ /**
1430
+ * Generate a unique slug from an organization name
1431
+ */
1432
+ async generateSlug(name) {
1433
+ return this.http.post("/organizations/generate-slug", { name });
1434
+ }
1407
1435
  };
1408
1436
 
1409
1437
  // src/resources/billing.ts
@@ -2940,7 +2968,7 @@ var SpooledWorker = class {
2940
2968
  ...DEFAULT_OPTIONS,
2941
2969
  hostname: os.hostname(),
2942
2970
  workerType: "nodejs",
2943
- version: "1.0.11",
2971
+ version: "1.0.13",
2944
2972
  metadata: {},
2945
2973
  ...options
2946
2974
  };