yungle-client 0.1.1 → 0.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Hein de Wilde
3
+ Copyright (c) 2026 Brainpower
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -13,11 +13,11 @@ import { YungleClient } from 'yungle-client';
13
13
 
14
14
  const yungle = new YungleClient({ apiKey: process.env.YUNGLE_API_KEY! });
15
15
 
16
- // Reading is free on any account.
16
+ // Everything here works on the free plan.
17
17
  const { transfers } = await yungle.listTransfers();
18
18
  const { downloads } = await yungle.transferDownloads(transfers[0].id);
19
19
 
20
- // Sending needs a paid plan.
20
+ // Sending works on the free plan too; collections need a paid one.
21
21
  const draft = await yungle.createTransfer({
22
22
  files: [{ name: 'final-cut.mov', size: 8_123_456_789 }],
23
23
  });
@@ -25,16 +25,18 @@ const draft = await yungle.createTransfer({
25
25
  await yungle.finalizeTransfer(draft.transfer.id, { recipients: ['client@example.com'] });
26
26
  ```
27
27
 
28
- Get a key from Settings → API keys. Reads are free; creating, sending and
29
- uploading come with a paid plan.
28
+ Get a key from Settings → API keys. Every account can use the API: transfers and contacts on the free plan,
29
+ collections on a paid one. The first 10 GB of API uploads each month are free.
30
30
 
31
31
  ## What it does and doesn't do
32
32
 
33
33
  - **Dependency-free**, built on `fetch`, so it runs unchanged on Node 22+, Bun,
34
34
  Deno and edge runtimes.
35
- - **Retries what is safe to retry** — throttles always, and 5xx only on
36
- idempotent operations. `createTransfer` is never retried automatically,
37
- because repeating it would make a second draft.
35
+ - **Retries are safe.** Throttles are always retried; 5xx and dropped connections are retried
36
+ for GET, DELETE and POST. Every POST carries an `Idempotency-Key`, reused on each retry, so a
37
+ lost response to `createTransfer` gets the first draft back instead of making a second.
38
+ - **Pages are walked for you.** Lists take `{ limit, cursor }` and return `nextCursor`;
39
+ `allTransfers()` and `allCollectionFiles(id)` are async iterators over everything.
38
40
  - **Does not upload bytes.** Endpoints that accept files return a tus endpoint
39
41
  and per-file tokens; streaming to those is the caller's job. Keeping the byte
40
42
  pipeline out is what lets this stay one small file. If you want that handled
@@ -46,14 +48,27 @@ uploading come with a paid plan.
46
48
  import { YungleApiError } from 'yungle-client';
47
49
 
48
50
  try {
49
- await yungle.createTransfer({ files });
51
+ await yungle.createCollection({ title: 'Client deliveries' });
50
52
  } catch (err) {
51
53
  if (err instanceof YungleApiError && err.code === 'upgrade_required') {
52
- // Reads are free; this is a write.
54
+ // A free account wrote to a collection; collections come with a plan.
53
55
  }
54
56
  }
55
57
  ```
56
58
 
59
+ ## Webhooks
60
+
61
+ ```ts
62
+ import { verifyWebhook } from 'yungle-client';
63
+
64
+ const { secret } = await yungle.createWebhook({ url: 'https://example.com/hooks/yungle', events: ['transfer.downloaded'] });
65
+ // The secret is returned once. In your handler, with the RAW request body:
66
+ if (!(await verifyWebhook(rawBody, req.headers['yungle-signature'], secret))) return res.status(400).end();
67
+ ```
68
+
69
+ Signed with HMAC-SHA256 over a timestamp and the body; stale signatures (over 5 minutes) are
70
+ refused. No public URL? Create the endpoint with `url: null` and poll `listWebhookEvents(id, { cursor })` instead.
71
+
57
72
  ## Not reachable
58
73
 
59
74
  Your vault and end-to-end encrypted transfers, because their keys are derived in
@@ -63,7 +78,7 @@ the client and never sent to Yungle. See
63
78
  ## Related
64
79
 
65
80
  - [`yungle-cli`](https://www.npmjs.com/package/yungle-cli) — resumable uploads from your terminal
66
- - [`yungle-mcp`](https://www.npmjs.com/package/yungle-mcp) — read your transfers from an AI assistant
67
- - [Full API reference](https://yungle.co/developers) · [OpenAPI spec](https://yungle.co/api/v1/openapi.json)
81
+ - [`yungle-mcp`](https://www.npmjs.com/package/yungle-mcp) — connect Claude or Cursor to Yungle
82
+ - [API reference](https://yungle.co/developers/reference?ref=npm) · [OpenAPI spec](https://yungle.co/api/v1/openapi.json)
68
83
 
69
84
  MIT
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Collection, CollectionFile, CollectionSummary, Contact, ContactInput, DownloadEvent, FileInput, Folder, Guest, Me, RecipientStatus, Transfer, TransferFile, TransferSummary, UploadTargets } from './types.js';
1
+ import type { Collection, CollectionFile, CollectionSummary, Contact, ContactInput, DownloadEvent, FileInput, Folder, Guest, Me, RecipientStatus, Transfer, TransferFile, TransferSummary, UploadTargets, WebhookDelivery, WebhookEndpoint, WebhookEvent, WebhookEventType } from './types.js';
2
2
  export * from './types.js';
3
3
  /**
4
4
  * A typed client for the Yungle API.
@@ -44,17 +44,38 @@ export interface ClientOptions {
44
44
  * Only ever applied to requests that are safe to repeat — see `request()`.
45
45
  */
46
46
  maxRetries?: number;
47
+ /**
48
+ * Sent as `User-Agent`. The CLI and the MCP server set their own; anything
49
+ * else is reported as this package, so Yungle can tell integrations apart.
50
+ */
51
+ userAgent?: string;
47
52
  }
53
+ export interface PageOptions {
54
+ /** Page size. The server's default applies when omitted. */
55
+ limit?: number;
56
+ /** The `nextCursor` from the previous page, unchanged. */
57
+ cursor?: string;
58
+ }
59
+ export declare const CLIENT_VERSION: string;
48
60
  export declare class YungleClient {
49
61
  private readonly baseUrl;
50
62
  private readonly apiKey;
51
63
  private readonly doFetch;
52
64
  private readonly maxRetries;
65
+ private readonly userAgent;
53
66
  constructor(options: ClientOptions);
54
67
  me(): Promise<Me>;
55
- listTransfers(): Promise<{
68
+ /**
69
+ * One page of transfers, newest first — 100 by default, up to 500. Pass the
70
+ * returned `nextCursor` back as `cursor` for the next page; it is null on the
71
+ * last one. `allTransfers()` does the walking for you.
72
+ */
73
+ listTransfers(page?: PageOptions): Promise<{
56
74
  transfers: TransferSummary[];
75
+ nextCursor: string | null;
57
76
  }>;
77
+ /** Every transfer, newest first, fetched a page at a time as you iterate. */
78
+ allTransfers(pageSize?: number): AsyncGenerator<TransferSummary>;
58
79
  /**
59
80
  * Create a draft. Nothing is live and nobody is emailed until `finalizeTransfer`.
60
81
  */
@@ -130,10 +151,17 @@ export declare class YungleClient {
130
151
  deleteCollection(id: string): Promise<{
131
152
  deleted: boolean;
132
153
  }>;
133
- /** `folderId` omitted lists everything; `'root'` lists the top level only. */
134
- listCollectionFiles(id: string, folderId?: string): Promise<{
154
+ /**
155
+ * `folderId` omitted lists everything; `'root'` lists the top level only.
156
+ * Without `page.limit` the whole collection comes back in one response; with
157
+ * it, follow `nextCursor` (or use `allCollectionFiles`).
158
+ */
159
+ listCollectionFiles(id: string, folderId?: string, page?: PageOptions): Promise<{
135
160
  files: CollectionFile[];
161
+ nextCursor: string | null;
136
162
  }>;
163
+ /** Every file in a collection, oldest first, a page at a time. */
164
+ allCollectionFiles(id: string, folderId?: string, pageSize?: number): AsyncGenerator<CollectionFile>;
137
165
  addCollectionFiles(id: string, files: FileInput[], folderId?: string | null): Promise<UploadTargets>;
138
166
  deleteCollectionFiles(id: string, fileIds: string[]): Promise<{
139
167
  deleted: number;
@@ -156,6 +184,55 @@ export declare class YungleClient {
156
184
  removeGuest(id: string, guestId: string): Promise<{
157
185
  removed: boolean;
158
186
  }>;
187
+ listWebhooks(): Promise<{
188
+ webhooks: WebhookEndpoint[];
189
+ }>;
190
+ /** `url: null` makes a pull endpoint. The `secret` is returned here only. */
191
+ createWebhook(input: {
192
+ url: string | null;
193
+ events: WebhookEventType[];
194
+ description?: string;
195
+ }): Promise<{
196
+ webhook: WebhookEndpoint;
197
+ secret: string;
198
+ }>;
199
+ getWebhook(id: string): Promise<{
200
+ webhook: WebhookEndpoint;
201
+ }>;
202
+ updateWebhook(id: string, input: {
203
+ url?: string | null;
204
+ events?: WebhookEventType[];
205
+ description?: string | null;
206
+ enabled?: boolean;
207
+ }): Promise<{
208
+ webhook: WebhookEndpoint;
209
+ }>;
210
+ deleteWebhook(id: string): Promise<{
211
+ deleted: boolean;
212
+ }>;
213
+ rotateWebhookSecret(id: string): Promise<{
214
+ secret: string;
215
+ }>;
216
+ testWebhook(id: string): Promise<{
217
+ deliveryId: string;
218
+ }>;
219
+ listWebhookDeliveries(id: string): Promise<{
220
+ deliveries: WebhookDelivery[];
221
+ }>;
222
+ retryWebhookDelivery(id: string, deliveryId: string): Promise<{
223
+ queued: boolean;
224
+ }>;
225
+ /**
226
+ * An endpoint's events, oldest first, after `cursor`. `nextCursor` comes back
227
+ * even when nothing is new, so keep the latest and poll with it.
228
+ */
229
+ listWebhookEvents(id: string, page?: PageOptions): Promise<{
230
+ events: (WebhookEvent & {
231
+ deliveryId: string;
232
+ })[];
233
+ nextCursor: string | null;
234
+ hasMore: boolean;
235
+ }>;
159
236
  listContacts(): Promise<{
160
237
  contacts: Contact[];
161
238
  }>;
@@ -173,12 +250,22 @@ export declare class YungleClient {
173
250
  deleted: boolean;
174
251
  }>;
175
252
  /**
176
- * `idempotent` widens what may be retried.
253
+ * What may be retried after a 5xx or a lost connection.
177
254
  *
178
- * GET and DELETE are safe by nature. POST is not — retrying `createTransfer`
179
- * after a 500 would make a second draft — so it is opt-in per method, and set
180
- * only where the server documents the operation as repeatable. Retrying a
181
- * *throttle* is always fine regardless: a 429 means nothing happened.
255
+ * GET and DELETE are safe by nature. A POST is safe because it carries an
256
+ * `Idempotency-Key` (see below). PATCH is not retried. Retrying a *throttle*
257
+ * is always fine regardless: a 429 means nothing happened.
182
258
  */
183
259
  private request;
184
260
  }
261
+ /**
262
+ * Check a webhook's `Yungle-Signature` header against the RAW request body.
263
+ *
264
+ * WebCrypto rather than `node:crypto`, so it runs wherever this client does:
265
+ * Node, Bun, Deno and edge runtimes. Rejects a timestamp more than
266
+ * `toleranceSeconds` away, which is what stops a captured request being replayed.
267
+ */
268
+ export declare function verifyWebhook(rawBody: string, signatureHeader: string, secret: string, { toleranceSeconds, now }?: {
269
+ toleranceSeconds?: number;
270
+ now?: number;
271
+ }): Promise<boolean>;
package/dist/index.mjs CHANGED
@@ -7,6 +7,9 @@ var YungleApiError = class extends Error {
7
7
  this.details = details;
8
8
  this.name = "YungleApiError";
9
9
  }
10
+ status;
11
+ code;
12
+ details;
10
13
  /** Worth trying again: throttled, or our fault. */
11
14
  get retryable() {
12
15
  return this.status === 429 || this.status >= 500;
@@ -18,25 +21,49 @@ var YungleApiError = class extends Error {
18
21
  }
19
22
  };
20
23
  var DEFAULT_BASE = "https://yungle.co/api/v1";
24
+ function pageQuery(page, extra = {}) {
25
+ const params = new URLSearchParams(extra);
26
+ if (page.limit !== void 0) params.set("limit", String(page.limit));
27
+ if (page.cursor) params.set("cursor", page.cursor);
28
+ const q = params.toString();
29
+ return q ? `?${q}` : "";
30
+ }
31
+ var CLIENT_VERSION = true ? "0.2.1" : "dev";
21
32
  var YungleClient = class {
22
33
  baseUrl;
23
34
  apiKey;
24
35
  doFetch;
25
36
  maxRetries;
37
+ userAgent;
26
38
  constructor(options) {
27
39
  if (!options.apiKey) throw new Error("An API key is required.");
28
40
  this.apiKey = options.apiKey;
29
41
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
30
42
  this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
31
43
  this.maxRetries = options.maxRetries ?? 3;
44
+ this.userAgent = options.userAgent ?? `yungle-client/${CLIENT_VERSION}`;
32
45
  }
33
46
  // ── Account ───────────────────────────────────────────────────────────────
34
47
  me() {
35
48
  return this.request("GET", "/me");
36
49
  }
37
50
  // ── Transfers ─────────────────────────────────────────────────────────────
38
- listTransfers() {
39
- return this.request("GET", "/transfers");
51
+ /**
52
+ * One page of transfers, newest first — 100 by default, up to 500. Pass the
53
+ * returned `nextCursor` back as `cursor` for the next page; it is null on the
54
+ * last one. `allTransfers()` does the walking for you.
55
+ */
56
+ listTransfers(page = {}) {
57
+ return this.request("GET", `/transfers${pageQuery(page)}`);
58
+ }
59
+ /** Every transfer, newest first, fetched a page at a time as you iterate. */
60
+ async *allTransfers(pageSize = 500) {
61
+ let cursor;
62
+ do {
63
+ const page = await this.listTransfers({ limit: pageSize, cursor });
64
+ yield* page.transfers;
65
+ cursor = page.nextCursor ?? void 0;
66
+ } while (cursor);
40
67
  }
41
68
  /**
42
69
  * Create a draft. Nothing is live and nobody is emailed until `finalizeTransfer`.
@@ -88,11 +115,24 @@ var YungleClient = class {
88
115
  deleteCollection(id) {
89
116
  return this.request("DELETE", `/collections/${enc(id)}`);
90
117
  }
91
- /** `folderId` omitted lists everything; `'root'` lists the top level only. */
92
- listCollectionFiles(id, folderId) {
93
- const query = folderId ? `?folderId=${encodeURIComponent(folderId)}` : "";
118
+ /**
119
+ * `folderId` omitted lists everything; `'root'` lists the top level only.
120
+ * Without `page.limit` the whole collection comes back in one response; with
121
+ * it, follow `nextCursor` (or use `allCollectionFiles`).
122
+ */
123
+ listCollectionFiles(id, folderId, page = {}) {
124
+ const query = pageQuery(page, folderId ? { folderId } : {});
94
125
  return this.request("GET", `/collections/${enc(id)}/files${query}`);
95
126
  }
127
+ /** Every file in a collection, oldest first, a page at a time. */
128
+ async *allCollectionFiles(id, folderId, pageSize = 1e3) {
129
+ let cursor;
130
+ do {
131
+ const page = await this.listCollectionFiles(id, folderId, { limit: pageSize, cursor });
132
+ yield* page.files;
133
+ cursor = page.nextCursor ?? void 0;
134
+ } while (cursor);
135
+ }
96
136
  addCollectionFiles(id, files, folderId) {
97
137
  return this.request("POST", `/collections/${enc(id)}/files`, { files, folderId });
98
138
  }
@@ -114,6 +154,42 @@ var YungleClient = class {
114
154
  removeGuest(id, guestId) {
115
155
  return this.request("DELETE", `/collections/${enc(id)}/guests/${enc(guestId)}`);
116
156
  }
157
+ // ── Webhooks ──────────────────────────────────────────────────────────────
158
+ listWebhooks() {
159
+ return this.request("GET", "/webhooks");
160
+ }
161
+ /** `url: null` makes a pull endpoint. The `secret` is returned here only. */
162
+ createWebhook(input) {
163
+ return this.request("POST", "/webhooks", input);
164
+ }
165
+ getWebhook(id) {
166
+ return this.request("GET", `/webhooks/${enc(id)}`);
167
+ }
168
+ updateWebhook(id, input) {
169
+ return this.request("PATCH", `/webhooks/${enc(id)}`, input);
170
+ }
171
+ deleteWebhook(id) {
172
+ return this.request("DELETE", `/webhooks/${enc(id)}`);
173
+ }
174
+ rotateWebhookSecret(id) {
175
+ return this.request("POST", `/webhooks/${enc(id)}/rotate-secret`);
176
+ }
177
+ testWebhook(id) {
178
+ return this.request("POST", `/webhooks/${enc(id)}/test`);
179
+ }
180
+ listWebhookDeliveries(id) {
181
+ return this.request("GET", `/webhooks/${enc(id)}/deliveries`);
182
+ }
183
+ retryWebhookDelivery(id, deliveryId) {
184
+ return this.request("POST", `/webhooks/${enc(id)}/deliveries/${enc(deliveryId)}/retry`);
185
+ }
186
+ /**
187
+ * An endpoint's events, oldest first, after `cursor`. `nextCursor` comes back
188
+ * even when nothing is new, so keep the latest and poll with it.
189
+ */
190
+ listWebhookEvents(id, page = {}) {
191
+ return this.request("GET", `/webhooks/${enc(id)}/events${pageQuery(page)}`);
192
+ }
117
193
  // ── Contacts ──────────────────────────────────────────────────────────────
118
194
  listContacts() {
119
195
  return this.request("GET", "/contacts");
@@ -133,15 +209,15 @@ var YungleClient = class {
133
209
  }
134
210
  // ── Transport ─────────────────────────────────────────────────────────────
135
211
  /**
136
- * `idempotent` widens what may be retried.
212
+ * What may be retried after a 5xx or a lost connection.
137
213
  *
138
- * GET and DELETE are safe by nature. POST is not — retrying `createTransfer`
139
- * after a 500 would make a second draft — so it is opt-in per method, and set
140
- * only where the server documents the operation as repeatable. Retrying a
141
- * *throttle* is always fine regardless: a 429 means nothing happened.
214
+ * GET and DELETE are safe by nature. A POST is safe because it carries an
215
+ * `Idempotency-Key` (see below). PATCH is not retried. Retrying a *throttle*
216
+ * is always fine regardless: a 429 means nothing happened.
142
217
  */
143
218
  async request(method, path, body, opts) {
144
- const safe = method === "GET" || method === "DELETE" || opts?.idempotent === true;
219
+ const idempotencyKey = method === "POST" ? globalThis.crypto.randomUUID() : void 0;
220
+ const safe = method === "GET" || method === "DELETE" || idempotencyKey !== void 0 || opts?.idempotent === true;
145
221
  let attempt = 0;
146
222
  for (; ; ) {
147
223
  const res = await this.doFetch(`${this.baseUrl}${path}`, {
@@ -149,6 +225,8 @@ var YungleClient = class {
149
225
  headers: {
150
226
  Authorization: `Bearer ${this.apiKey}`,
151
227
  Accept: "application/json",
228
+ "User-Agent": this.userAgent,
229
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {},
152
230
  ...body === void 0 ? {} : { "Content-Type": "application/json" }
153
231
  },
154
232
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
@@ -187,7 +265,27 @@ function backoffMs(attempt, error) {
187
265
  function sleep(ms) {
188
266
  return new Promise((resolve) => setTimeout(resolve, ms));
189
267
  }
268
+ async function verifyWebhook(rawBody, signatureHeader, secret, { toleranceSeconds = 300, now = Date.now() } = {}) {
269
+ const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
270
+ const t = Number(parts.t);
271
+ if (!Number.isInteger(t) || Math.abs(now / 1e3 - t) > toleranceSeconds || !parts.v1) return false;
272
+ const key = await globalThis.crypto.subtle.importKey(
273
+ "raw",
274
+ new TextEncoder().encode(secret),
275
+ { name: "HMAC", hash: "SHA-256" },
276
+ false,
277
+ ["sign"]
278
+ );
279
+ const mac = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`)));
280
+ const expected = [...mac].map((b) => b.toString(16).padStart(2, "0")).join("");
281
+ if (expected.length !== parts.v1.length) return false;
282
+ let diff = 0;
283
+ for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ parts.v1.charCodeAt(i);
284
+ return diff === 0;
285
+ }
190
286
  export {
287
+ CLIENT_VERSION,
191
288
  YungleApiError,
192
- YungleClient
289
+ YungleClient,
290
+ verifyWebhook
193
291
  };
package/dist/types.d.ts CHANGED
@@ -21,7 +21,8 @@ export interface Workspace {
21
21
  id: string;
22
22
  email: string | null;
23
23
  displayName: string | null;
24
- handle: string | null;
24
+ /** The workspace's verified custom domain, or null. */
25
+ customDomain: string | null;
25
26
  }
26
27
  export interface PlanInfo {
27
28
  tier: string | null;
@@ -34,6 +35,10 @@ export interface KeyInfo {
34
35
  id: string;
35
36
  name: string;
36
37
  scopes: string[];
38
+ /** Absent on servers before 2026-09-26. */
39
+ via?: 'key' | 'oauth';
40
+ /** Whether this credential may make Yungle email anyone. Absent on older servers. */
41
+ canEmail?: boolean;
37
42
  }
38
43
  export interface Me {
39
44
  workspace: Workspace;
@@ -95,6 +100,18 @@ export interface RecipientStatus {
95
100
  email: string;
96
101
  notifiedAt: Iso8601 | null;
97
102
  downloaded: boolean;
103
+ /**
104
+ * @deprecated Always false since 2026-09-26: Yungle no longer records whether
105
+ * a recipient opened the page — only whether they downloaded.
106
+ */
107
+ opened: boolean;
108
+ /**
109
+ * Set when mail to this address permanently failed. Additive since 2026-09;
110
+ * clients that predate it are unaffected.
111
+ */
112
+ bouncedAt: Iso8601 | null;
113
+ /** 'hard_bounce' | 'blocked' | 'refused', or null. */
114
+ bounceKind: string | null;
98
115
  }
99
116
  export interface DownloadEvent {
100
117
  id: string;
@@ -103,6 +120,7 @@ export interface DownloadEvent {
103
120
  recipientEmail: string | null;
104
121
  /** One page visit is one download; several files in it share this. */
105
122
  sessionId: string;
123
+ /** Always null since 2026-09 — the truncated IP is no longer recorded. */
106
124
  ipTruncated: string | null;
107
125
  createdAt: Iso8601;
108
126
  }
@@ -173,3 +191,36 @@ export interface ContactInput {
173
191
  phone?: string | null;
174
192
  type?: string | null;
175
193
  }
194
+ export type WebhookEventType = 'transfer.ready' | 'transfer.downloaded' | 'transfer.expiring' | 'transfer.expired' | 'collection.file_uploaded';
195
+ export interface WebhookEndpoint {
196
+ id: string;
197
+ /** Null for a pull endpoint. */
198
+ url: string | null;
199
+ mode: 'push' | 'pull';
200
+ description: string | null;
201
+ events: WebhookEventType[];
202
+ enabled: boolean;
203
+ disabledReason: 'failing' | 'user' | null;
204
+ lastSuccessAt: Iso8601 | null;
205
+ lastFailureAt: Iso8601 | null;
206
+ createdAt: Iso8601;
207
+ }
208
+ export interface WebhookDelivery {
209
+ id: string;
210
+ eventId: string;
211
+ type: WebhookEventType | 'webhook.test';
212
+ status: 'pending' | 'succeeded' | 'failed' | 'available';
213
+ attempts: number;
214
+ nextAttemptAt: Iso8601 | null;
215
+ lastStatusCode: number | null;
216
+ lastError: string | null;
217
+ deliveredAt: Iso8601 | null;
218
+ createdAt: Iso8601;
219
+ }
220
+ /** The JSON a push delivery sends, and what `/events` returns (plus `deliveryId`). */
221
+ export interface WebhookEvent<T = Record<string, unknown>> {
222
+ id: string;
223
+ type: WebhookEventType | 'webhook.test';
224
+ createdAt: Iso8601;
225
+ data: T;
226
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yungle-client",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -9,28 +9,20 @@
9
9
  "default": "./dist/index.mjs"
10
10
  }
11
11
  },
12
- "scripts": {
13
- "build": "node build.mjs",
14
- "typecheck": "tsc --noEmit",
15
- "test": "node --import tsx --test src/*.test.ts",
16
- "lint": "eslint .",
17
- "prepublishOnly": "node build.mjs",
18
- "prepare": "node build.mjs"
19
- },
20
12
  "devDependencies": {
21
13
  "@types/node": "^22.10.5",
22
14
  "tsx": "^4.19.2",
23
15
  "typescript": "^5.7.3",
24
- "esbuild": "^0.25.0"
16
+ "esbuild": "^0.28.1"
25
17
  },
26
- "description": "Typed client for the Yungle API \u2014 send file transfers, build collections and sync contacts from Node.",
18
+ "description": "Typed client for the Yungle API — send file transfers, build collections and sync contacts from Node.",
27
19
  "license": "MIT",
28
- "author": "Hein de Wilde",
20
+ "author": "Brainpower <hello@yungle.co> (https://yungle.co)",
29
21
  "repository": {
30
22
  "type": "git",
31
23
  "url": "git+https://github.com/heindewilde/yungle-clients.git"
32
24
  },
33
- "homepage": "https://yungle.co/developers",
25
+ "homepage": "https://yungle.co/developers/reference?ref=npm",
34
26
  "bugs": {
35
27
  "url": "https://github.com/heindewilde/yungle-clients/issues"
36
28
  },
@@ -40,8 +32,12 @@
40
32
  "keywords": [
41
33
  "yungle",
42
34
  "file-transfer",
35
+ "file-sharing",
43
36
  "api-client",
44
37
  "sdk",
38
+ "typescript",
39
+ "webhooks",
40
+ "upload",
45
41
  "wetransfer-alternative",
46
42
  "eu",
47
43
  "gdpr"
@@ -51,5 +47,10 @@
51
47
  "README.md",
52
48
  "LICENSE"
53
49
  ],
54
- "types": "./dist/index.d.ts"
55
- }
50
+ "types": "./dist/index.d.ts",
51
+ "scripts": {
52
+ "build": "node build.mjs",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "node --import tsx --test src/*.test.ts"
55
+ }
56
+ }