@stack0/sdk 0.5.17 → 0.6.1

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.js CHANGED
@@ -432,11 +432,19 @@ var Mailboxes = class {
432
432
  }
433
433
  http;
434
434
  /**
435
- * Create a new mailbox
435
+ * Create a new mailbox. The result carries `webhookSecret`, which signs every
436
+ * inbound delivery; store it now (only `create` and `rotateSecret` return it).
436
437
  */
437
438
  async create(request) {
438
439
  return this.http.post("/mail/mailboxes", request);
439
440
  }
441
+ /**
442
+ * Replace a mailbox's webhook secret and return the new one. Deliveries are
443
+ * signed with the new secret from then on.
444
+ */
445
+ async rotateSecret(id) {
446
+ return this.http.post(`/mail/mailboxes/${id}/rotate-secret`, {});
447
+ }
440
448
  /**
441
449
  * Get a mailbox by ID
442
450
  */
@@ -1898,6 +1906,7 @@ var CDN = class {
1898
1906
  const params = new URLSearchParams();
1899
1907
  if (request.projectSlug) params.set("projectSlug", request.projectSlug);
1900
1908
  if (request.environment) params.set("environment", request.environment);
1909
+ if (request.folder) params.set("folder", request.folder);
1901
1910
  if (request.periodStart) {
1902
1911
  const date = request.periodStart instanceof Date ? request.periodStart.toISOString() : request.periodStart;
1903
1912
  params.set("periodStart", date);
@@ -1957,6 +1966,36 @@ var CDN = class {
1957
1966
  const query = params.toString();
1958
1967
  return this.http.get(`/cdn/usage/storage-breakdown${query ? `?${query}` : ""}`);
1959
1968
  }
1969
+ /**
1970
+ * Get total stored bytes for a project or folder, derived assets included.
1971
+ *
1972
+ * Totals uploads plus everything the pipelines derived from them — video renditions, HLS
1973
+ * segments and manifests, thumbnails, GIFs — in one call. Use this to meter a plan limit:
1974
+ * paging `list()` and summing `size` counts uploads only, and an HLS ladder typically runs
1975
+ * well past the size of the source it came from.
1976
+ *
1977
+ * Check `unmeasuredAssets`. A nonzero value means some assets have derivatives whose bytes
1978
+ * have not been measured, so `totalBytes` is a floor rather than the full number.
1979
+ *
1980
+ * @example
1981
+ * ```typescript
1982
+ * const usage = await cdn.getStorageUsage({
1983
+ * projectSlug: 'my-project',
1984
+ * folder: '/customers/acme',
1985
+ * });
1986
+ * console.log(`${usage.totalFormatted} across ${usage.objectCount} objects`);
1987
+ * console.log(` uploads: ${usage.breakdown.originals.bytesFormatted}`);
1988
+ * console.log(` derived: ${usage.breakdown.derived.bytesFormatted}`);
1989
+ * ```
1990
+ */
1991
+ async getStorageUsage(request = {}) {
1992
+ const params = new URLSearchParams();
1993
+ if (request.projectSlug) params.set("projectSlug", request.projectSlug);
1994
+ if (request.environment) params.set("environment", request.environment);
1995
+ if (request.folder) params.set("folder", request.folder);
1996
+ const query = params.toString();
1997
+ return this.http.get(`/cdn/usage/storage${query ? `?${query}` : ""}`);
1998
+ }
1960
1999
  convertUsageDates(usage) {
1961
2000
  if (typeof usage.periodStart === "string") {
1962
2001
  usage.periodStart = new Date(usage.periodStart);
@@ -2233,6 +2272,112 @@ var CDN = class {
2233
2272
  return this.convertMergeJobDates(job);
2234
2273
  }
2235
2274
  // ============================================================================
2275
+ // Video Render Methods
2276
+ // ============================================================================
2277
+ /**
2278
+ * Render a declarative video spec into a video asset
2279
+ *
2280
+ * The spec describes scenes, layers, timing and media as data. There are no
2281
+ * compositions to write and no code to upload — Stack0 owns the renderer.
2282
+ *
2283
+ * `@stack0/video-spec` has the full types, validation and worked examples;
2284
+ * validating locally before you POST turns a round trip into a type error.
2285
+ *
2286
+ * @example
2287
+ * ```typescript
2288
+ * const job = await cdn.createRenderJob({
2289
+ * projectSlug: 'my-project',
2290
+ * spec: {
2291
+ * canvas: { width: 1080, height: 1920, fps: 30 },
2292
+ * audio: [{ src: musicAssetId, volume: 0.34 }],
2293
+ * scenes: [
2294
+ * {
2295
+ * id: 'hook',
2296
+ * duration: 2.5,
2297
+ * layers: [
2298
+ * { type: 'image', src: afterImageAssetId, zoom: { from: 1, to: 1.06 } },
2299
+ * { type: 'caption', text: 'i told my mom\nthe kitchen exploded', position: 'upperThird' },
2300
+ * ],
2301
+ * },
2302
+ * {
2303
+ * // No duration: the thread derives its own length from the messages
2304
+ * id: 'payoff',
2305
+ * layers: [
2306
+ * {
2307
+ * type: 'messageThread',
2308
+ * recipient: 'Mom',
2309
+ * imageSrc: afterImageAssetId,
2310
+ * messages: [
2311
+ * { sender: 'recipient', text: 'you home?', history: true },
2312
+ * { sender: 'user', text: '[IMAGE]' },
2313
+ * { sender: 'recipient', text: 'WHAT IS THAT' },
2314
+ * ],
2315
+ * },
2316
+ * ],
2317
+ * },
2318
+ * ],
2319
+ * },
2320
+ * webhookUrl: 'https://your-app.com/webhook',
2321
+ * });
2322
+ * console.log(job.durationInFrames); // known before rendering starts
2323
+ * ```
2324
+ */
2325
+ async createRenderJob(request) {
2326
+ const response = await this.http.post("/cdn/video/render", request);
2327
+ return this.convertRenderJobDates(response);
2328
+ }
2329
+ /**
2330
+ * Get a render job by ID with output asset details
2331
+ *
2332
+ * @example
2333
+ * ```typescript
2334
+ * const job = await cdn.getRenderJob('job-id');
2335
+ * if (job.status === 'completed' && job.outputAsset) {
2336
+ * console.log(`Output video: ${job.outputAsset.cdnUrl}`);
2337
+ * }
2338
+ * ```
2339
+ */
2340
+ async getRenderJob(jobId) {
2341
+ const response = await this.http.get(`/cdn/video/render/${jobId}`);
2342
+ return this.convertRenderJobDates(response);
2343
+ }
2344
+ /**
2345
+ * List render jobs with optional filters
2346
+ */
2347
+ async listRenderJobs(request) {
2348
+ const params = new URLSearchParams();
2349
+ params.set("projectSlug", request.projectSlug);
2350
+ if (request.status) params.set("status", request.status);
2351
+ if (request.limit) params.set("limit", request.limit.toString());
2352
+ if (request.offset) params.set("offset", request.offset.toString());
2353
+ const response = await this.http.get(`/cdn/video/render?${params.toString()}`);
2354
+ return {
2355
+ ...response,
2356
+ jobs: response.jobs.map((job) => this.convertRenderJobDates(job))
2357
+ };
2358
+ }
2359
+ /**
2360
+ * Cancel a pending or processing render job
2361
+ */
2362
+ async cancelRenderJob(jobId) {
2363
+ return this.http.post(`/cdn/video/render/${jobId}/cancel`, {});
2364
+ }
2365
+ convertRenderJobDates(job) {
2366
+ if (typeof job.createdAt === "string") {
2367
+ job.createdAt = new Date(job.createdAt);
2368
+ }
2369
+ if (job.updatedAt && typeof job.updatedAt === "string") {
2370
+ job.updatedAt = new Date(job.updatedAt);
2371
+ }
2372
+ if (job.startedAt && typeof job.startedAt === "string") {
2373
+ job.startedAt = new Date(job.startedAt);
2374
+ }
2375
+ if (job.completedAt && typeof job.completedAt === "string") {
2376
+ job.completedAt = new Date(job.completedAt);
2377
+ }
2378
+ return job;
2379
+ }
2380
+ // ============================================================================
2236
2381
  // S3 Import Methods
2237
2382
  // ============================================================================
2238
2383
  /**
@@ -5629,6 +5774,92 @@ var Workflows = class {
5629
5774
  }
5630
5775
  };
5631
5776
 
5777
+ // src/mail/webhooks.ts
5778
+ var WebhookVerificationError = class extends Error {
5779
+ constructor(message) {
5780
+ super(message);
5781
+ this.name = "WebhookVerificationError";
5782
+ }
5783
+ };
5784
+ function header(headers, name) {
5785
+ if (typeof headers.get === "function") return headers.get(name) ?? void 0;
5786
+ const record = headers;
5787
+ for (const key of Object.keys(record)) {
5788
+ if (key.toLowerCase() === name) {
5789
+ const v = record[key];
5790
+ return Array.isArray(v) ? v[0] : v;
5791
+ }
5792
+ }
5793
+ return void 0;
5794
+ }
5795
+ function base64ToBytes(b64) {
5796
+ const bin = atob(b64);
5797
+ const out = new Uint8Array(bin.length);
5798
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
5799
+ return out;
5800
+ }
5801
+ function bytesToBase64(bytes) {
5802
+ let bin = "";
5803
+ for (const b of bytes) bin += String.fromCharCode(b);
5804
+ return btoa(bin);
5805
+ }
5806
+ async function subtle() {
5807
+ const global = globalThis.crypto;
5808
+ if (global?.subtle) return global.subtle;
5809
+ const specifier = "node:crypto";
5810
+ const nodeCrypto = await import(
5811
+ /* webpackIgnore: true */
5812
+ /* @vite-ignore */
5813
+ specifier
5814
+ );
5815
+ return nodeCrypto.webcrypto.subtle;
5816
+ }
5817
+ function timingSafeEqual(a, b) {
5818
+ if (a.length !== b.length) return false;
5819
+ let diff = 0;
5820
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
5821
+ return diff === 0;
5822
+ }
5823
+ async function verifyInboundWebhook(options) {
5824
+ const id = header(options.headers, "webhook-id");
5825
+ const timestamp = header(options.headers, "webhook-timestamp");
5826
+ const signatures = header(options.headers, "webhook-signature");
5827
+ if (!id || !timestamp || !signatures) {
5828
+ throw new WebhookVerificationError("missing webhook-id, webhook-timestamp, or webhook-signature header");
5829
+ }
5830
+ const seconds = Number(timestamp);
5831
+ if (!Number.isInteger(seconds)) throw new WebhookVerificationError("webhook-timestamp is not a whole number");
5832
+ const now = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
5833
+ const tolerance = options.toleranceSeconds ?? 300;
5834
+ if (Math.abs(now - seconds) > tolerance) {
5835
+ throw new WebhookVerificationError(`webhook-timestamp is more than ${tolerance} seconds from now`);
5836
+ }
5837
+ const body = typeof options.payload === "string" ? options.payload : new TextDecoder().decode(options.payload instanceof ArrayBuffer ? new Uint8Array(options.payload) : options.payload);
5838
+ const secret = options.secret.startsWith("whsec_") ? options.secret.slice(6) : options.secret;
5839
+ const webcrypto = await subtle();
5840
+ const key = await webcrypto.importKey(
5841
+ "raw",
5842
+ base64ToBytes(secret),
5843
+ { name: "HMAC", hash: "SHA-256" },
5844
+ false,
5845
+ ["sign"]
5846
+ );
5847
+ const mac = new Uint8Array(
5848
+ await webcrypto.sign("HMAC", key, new TextEncoder().encode(`${id}.${seconds}.${body}`))
5849
+ );
5850
+ const expected = bytesToBase64(mac);
5851
+ const matched = signatures.split(" ").map((s) => s.split(",", 2)).some(([version, sig]) => version === "v1" && sig !== void 0 && timingSafeEqual(sig, expected));
5852
+ if (!matched) throw new WebhookVerificationError("no webhook-signature matches");
5853
+ let payload;
5854
+ try {
5855
+ payload = JSON.parse(body);
5856
+ } catch {
5857
+ throw new WebhookVerificationError("the payload is not JSON");
5858
+ }
5859
+ if (payload.id !== id) throw new WebhookVerificationError("the payload id does not match webhook-id");
5860
+ return payload;
5861
+ }
5862
+
5632
5863
  // src/index.ts
5633
5864
  var Stack0 = class {
5634
5865
  http;
@@ -5710,7 +5941,9 @@ exports.Sequences = Sequences;
5710
5941
  exports.Stack0 = Stack0;
5711
5942
  exports.Templates = Templates;
5712
5943
  exports.Webdata = Webdata;
5944
+ exports.WebhookVerificationError = WebhookVerificationError;
5713
5945
  exports.Workflows = Workflows;
5714
5946
  exports.default = src_default;
5947
+ exports.verifyInboundWebhook = verifyInboundWebhook;
5715
5948
  //# sourceMappingURL=index.js.map
5716
5949
  //# sourceMappingURL=index.js.map