yungle-client 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hein de Wilde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # yungle-client
2
+
3
+ Typed client for the [Yungle](https://yungle.co) API — a private, EU-hosted file
4
+ transfer service. Send transfers, build client collections and sync contacts
5
+ from Node.
6
+
7
+ ```bash
8
+ npm install yungle-client
9
+ ```
10
+
11
+ ```ts
12
+ import { YungleClient } from 'yungle-client';
13
+
14
+ const yungle = new YungleClient({ apiKey: process.env.YUNGLE_API_KEY! });
15
+
16
+ // Reading is free on any account.
17
+ const { transfers } = await yungle.listTransfers();
18
+ const { downloads } = await yungle.transferDownloads(transfers[0].id);
19
+
20
+ // Sending needs a paid plan.
21
+ const draft = await yungle.createTransfer({
22
+ files: [{ name: 'final-cut.mov', size: 8_123_456_789 }],
23
+ });
24
+ // …stream the bytes to draft.tusEndpoint using draft.files[n].uploadToken…
25
+ await yungle.finalizeTransfer(draft.transfer.id, { recipients: ['client@example.com'] });
26
+ ```
27
+
28
+ Get a key from Settings → API keys. Reads are free; creating, sending and
29
+ uploading come with a paid plan.
30
+
31
+ ## What it does and doesn't do
32
+
33
+ - **Dependency-free**, built on `fetch`, so it runs unchanged on Node 22+, Bun,
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.
38
+ - **Does not upload bytes.** Endpoints that accept files return a tus endpoint
39
+ and per-file tokens; streaming to those is the caller's job. Keeping the byte
40
+ pipeline out is what lets this stay one small file. If you want that handled
41
+ for you, use [`yungle-cli`](https://www.npmjs.com/package/yungle-cli), which
42
+ does it resumably.
43
+ - **Errors carry a stable `code`.** Branch on `err.code`, never on the message.
44
+
45
+ ```ts
46
+ import { YungleApiError } from 'yungle-client';
47
+
48
+ try {
49
+ await yungle.createTransfer({ files });
50
+ } catch (err) {
51
+ if (err instanceof YungleApiError && err.code === 'upgrade_required') {
52
+ // Reads are free; this is a write.
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Not reachable
58
+
59
+ Your vault and end-to-end encrypted transfers, because their keys are derived in
60
+ the client and never sent to Yungle. See
61
+ <https://yungle.co/developers/unsupported>.
62
+
63
+ ## Related
64
+
65
+ - [`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)
68
+
69
+ MIT
@@ -0,0 +1,184 @@
1
+ import type { Collection, CollectionFile, CollectionSummary, Contact, ContactInput, DownloadEvent, FileInput, Folder, Guest, Me, RecipientStatus, Transfer, TransferFile, TransferSummary, UploadTargets } from './types';
2
+ export * from './types';
3
+ /**
4
+ * A typed client for the Yungle API.
5
+ *
6
+ * Deliberately dependency-free and built on `fetch`, so it runs unchanged in
7
+ * Node 22, Bun, Deno and a server-side edge runtime. The CLI and the MCP server
8
+ * are both thin layers over this — which is the point: one place understands
9
+ * the wire format, so neither can drift from the other.
10
+ *
11
+ * It does **not** upload bytes. `createTransfer` and `addCollectionFiles`
12
+ * return a tus endpoint and per-file tokens, and streaming to those is the
13
+ * caller's job (`yungle-cli` does it with `tus-js-client`). Keeping the byte
14
+ * pipeline out of here is what lets this stay a few hundred lines with no
15
+ * dependencies.
16
+ */
17
+ export interface YungleErrorBody {
18
+ code: string;
19
+ message: string;
20
+ details?: Record<string, unknown>;
21
+ }
22
+ /**
23
+ * A failed request. Carries the machine-readable `code` — branch on that, never
24
+ * on `message`, which is written for a human reading a log and may change.
25
+ */
26
+ export declare class YungleApiError extends Error {
27
+ readonly status: number;
28
+ readonly code: string;
29
+ readonly details?: Record<string, unknown> | undefined;
30
+ constructor(status: number, code: string, message: string, details?: Record<string, unknown> | undefined);
31
+ /** Worth trying again: throttled, or our fault. */
32
+ get retryable(): boolean;
33
+ /** Seconds to wait, when the server said. */
34
+ get retryAfterSeconds(): number | null;
35
+ }
36
+ export interface ClientOptions {
37
+ apiKey: string;
38
+ /** Defaults to production. Point it at a local server for development. */
39
+ baseUrl?: string;
40
+ fetch?: typeof globalThis.fetch;
41
+ /**
42
+ * Retries for throttles and 5xx. Set to 0 to handle it yourself.
43
+ *
44
+ * Only ever applied to requests that are safe to repeat — see `request()`.
45
+ */
46
+ maxRetries?: number;
47
+ }
48
+ export declare class YungleClient {
49
+ private readonly baseUrl;
50
+ private readonly apiKey;
51
+ private readonly doFetch;
52
+ private readonly maxRetries;
53
+ constructor(options: ClientOptions);
54
+ me(): Promise<Me>;
55
+ listTransfers(): Promise<{
56
+ transfers: TransferSummary[];
57
+ }>;
58
+ /**
59
+ * Create a draft. Nothing is live and nobody is emailed until `finalizeTransfer`.
60
+ */
61
+ createTransfer(input: {
62
+ files: FileInput[];
63
+ title?: string;
64
+ expiresInDays?: number;
65
+ }): Promise<{
66
+ transfer: {
67
+ id: string;
68
+ slug: string;
69
+ expiresAt: string;
70
+ maxBytes: number;
71
+ };
72
+ } & UploadTargets>;
73
+ getTransfer(id: string): Promise<{
74
+ transfer: Transfer;
75
+ files: TransferFile[];
76
+ recipients: RecipientStatus[];
77
+ }>;
78
+ /**
79
+ * Send it. Omit `recipients` for a link-only transfer, which costs nothing
80
+ * from the daily email budget.
81
+ *
82
+ * Idempotent server-side, so a lost response is safe to repeat.
83
+ */
84
+ finalizeTransfer(id: string, input?: {
85
+ recipients?: string[];
86
+ message?: string;
87
+ password?: string;
88
+ title?: string;
89
+ }): Promise<{
90
+ transfer: Transfer;
91
+ notified: string[];
92
+ }>;
93
+ addTransferFiles(id: string, files: FileInput[]): Promise<UploadTargets>;
94
+ removeTransferFile(id: string, fileId: string): Promise<{
95
+ deleted: boolean;
96
+ }>;
97
+ updateTransfer(id: string, input: {
98
+ expiresInDays?: number;
99
+ maxDownloads?: number | null;
100
+ }): Promise<{
101
+ transfer: Transfer;
102
+ }>;
103
+ /** Immediate and irreversible — the encryption keys are destroyed. */
104
+ revokeTransfer(id: string): Promise<{
105
+ transfer: Transfer;
106
+ }>;
107
+ transferDownloads(id: string): Promise<{
108
+ downloads: DownloadEvent[];
109
+ recipients: RecipientStatus[];
110
+ totalDownloads: number;
111
+ }>;
112
+ listCollections(): Promise<{
113
+ collections: CollectionSummary[];
114
+ }>;
115
+ createCollection(input: {
116
+ title: string;
117
+ description?: string;
118
+ }): Promise<{
119
+ collection: Collection;
120
+ }>;
121
+ getCollection(id: string): Promise<{
122
+ collection: Collection;
123
+ }>;
124
+ updateCollection(id: string, input: {
125
+ title?: string;
126
+ description?: string;
127
+ }): Promise<{
128
+ collection: Collection;
129
+ }>;
130
+ deleteCollection(id: string): Promise<{
131
+ deleted: boolean;
132
+ }>;
133
+ /** `folderId` omitted lists everything; `'root'` lists the top level only. */
134
+ listCollectionFiles(id: string, folderId?: string): Promise<{
135
+ files: CollectionFile[];
136
+ }>;
137
+ addCollectionFiles(id: string, files: FileInput[], folderId?: string | null): Promise<UploadTargets>;
138
+ deleteCollectionFiles(id: string, fileIds: string[]): Promise<{
139
+ deleted: number;
140
+ }>;
141
+ listFolders(id: string): Promise<{
142
+ folders: Folder[];
143
+ }>;
144
+ createFolder(id: string, input: {
145
+ name: string;
146
+ parentId?: string | null;
147
+ }): Promise<{
148
+ folder: Folder;
149
+ }>;
150
+ listGuests(id: string): Promise<{
151
+ guests: Guest[];
152
+ }>;
153
+ inviteGuests(id: string, emails: string[]): Promise<{
154
+ invited: string[];
155
+ }>;
156
+ removeGuest(id: string, guestId: string): Promise<{
157
+ removed: boolean;
158
+ }>;
159
+ listContacts(): Promise<{
160
+ contacts: Contact[];
161
+ }>;
162
+ createContact(input: ContactInput): Promise<{
163
+ contact: Contact;
164
+ }>;
165
+ getContact(id: string): Promise<{
166
+ contact: Contact;
167
+ }>;
168
+ /** A full replacement: any field omitted is cleared. */
169
+ updateContact(id: string, input: ContactInput): Promise<{
170
+ contact: Contact;
171
+ }>;
172
+ deleteContact(id: string): Promise<{
173
+ deleted: boolean;
174
+ }>;
175
+ /**
176
+ * `idempotent` widens what may be retried.
177
+ *
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.
182
+ */
183
+ private request;
184
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,193 @@
1
+ // src/index.ts
2
+ var YungleApiError = class extends Error {
3
+ constructor(status, code, message, details) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = "YungleApiError";
9
+ }
10
+ /** Worth trying again: throttled, or our fault. */
11
+ get retryable() {
12
+ return this.status === 429 || this.status >= 500;
13
+ }
14
+ /** Seconds to wait, when the server said. */
15
+ get retryAfterSeconds() {
16
+ const value = this.details?.retryAfterSeconds;
17
+ return typeof value === "number" ? value : null;
18
+ }
19
+ };
20
+ var DEFAULT_BASE = "https://yungle.co/api/v1";
21
+ var YungleClient = class {
22
+ baseUrl;
23
+ apiKey;
24
+ doFetch;
25
+ maxRetries;
26
+ constructor(options) {
27
+ if (!options.apiKey) throw new Error("An API key is required.");
28
+ this.apiKey = options.apiKey;
29
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
30
+ this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
31
+ this.maxRetries = options.maxRetries ?? 3;
32
+ }
33
+ // ── Account ───────────────────────────────────────────────────────────────
34
+ me() {
35
+ return this.request("GET", "/me");
36
+ }
37
+ // ── Transfers ─────────────────────────────────────────────────────────────
38
+ listTransfers() {
39
+ return this.request("GET", "/transfers");
40
+ }
41
+ /**
42
+ * Create a draft. Nothing is live and nobody is emailed until `finalizeTransfer`.
43
+ */
44
+ createTransfer(input) {
45
+ return this.request("POST", "/transfers", input);
46
+ }
47
+ getTransfer(id) {
48
+ return this.request("GET", `/transfers/${enc(id)}`);
49
+ }
50
+ /**
51
+ * Send it. Omit `recipients` for a link-only transfer, which costs nothing
52
+ * from the daily email budget.
53
+ *
54
+ * Idempotent server-side, so a lost response is safe to repeat.
55
+ */
56
+ finalizeTransfer(id, input = {}) {
57
+ return this.request("POST", `/transfers/${enc(id)}/finalize`, input, { idempotent: true });
58
+ }
59
+ addTransferFiles(id, files) {
60
+ return this.request("POST", `/transfers/${enc(id)}/files`, { files });
61
+ }
62
+ removeTransferFile(id, fileId) {
63
+ return this.request("DELETE", `/transfers/${enc(id)}/files/${enc(fileId)}`);
64
+ }
65
+ updateTransfer(id, input) {
66
+ return this.request("PATCH", `/transfers/${enc(id)}`, input);
67
+ }
68
+ /** Immediate and irreversible — the encryption keys are destroyed. */
69
+ revokeTransfer(id) {
70
+ return this.request("DELETE", `/transfers/${enc(id)}`);
71
+ }
72
+ transferDownloads(id) {
73
+ return this.request("GET", `/transfers/${enc(id)}/downloads`);
74
+ }
75
+ // ── Collections ───────────────────────────────────────────────────────────
76
+ listCollections() {
77
+ return this.request("GET", "/collections");
78
+ }
79
+ createCollection(input) {
80
+ return this.request("POST", "/collections", input);
81
+ }
82
+ getCollection(id) {
83
+ return this.request("GET", `/collections/${enc(id)}`);
84
+ }
85
+ updateCollection(id, input) {
86
+ return this.request("PATCH", `/collections/${enc(id)}`, input);
87
+ }
88
+ deleteCollection(id) {
89
+ return this.request("DELETE", `/collections/${enc(id)}`);
90
+ }
91
+ /** `folderId` omitted lists everything; `'root'` lists the top level only. */
92
+ listCollectionFiles(id, folderId) {
93
+ const query = folderId ? `?folderId=${encodeURIComponent(folderId)}` : "";
94
+ return this.request("GET", `/collections/${enc(id)}/files${query}`);
95
+ }
96
+ addCollectionFiles(id, files, folderId) {
97
+ return this.request("POST", `/collections/${enc(id)}/files`, { files, folderId });
98
+ }
99
+ deleteCollectionFiles(id, fileIds) {
100
+ return this.request("DELETE", `/collections/${enc(id)}/files`, { fileIds });
101
+ }
102
+ listFolders(id) {
103
+ return this.request("GET", `/collections/${enc(id)}/folders`);
104
+ }
105
+ createFolder(id, input) {
106
+ return this.request("POST", `/collections/${enc(id)}/folders`, input);
107
+ }
108
+ listGuests(id) {
109
+ return this.request("GET", `/collections/${enc(id)}/guests`);
110
+ }
111
+ inviteGuests(id, emails) {
112
+ return this.request("POST", `/collections/${enc(id)}/guests`, { emails });
113
+ }
114
+ removeGuest(id, guestId) {
115
+ return this.request("DELETE", `/collections/${enc(id)}/guests/${enc(guestId)}`);
116
+ }
117
+ // ── Contacts ──────────────────────────────────────────────────────────────
118
+ listContacts() {
119
+ return this.request("GET", "/contacts");
120
+ }
121
+ createContact(input) {
122
+ return this.request("POST", "/contacts", input);
123
+ }
124
+ getContact(id) {
125
+ return this.request("GET", `/contacts/${enc(id)}`);
126
+ }
127
+ /** A full replacement: any field omitted is cleared. */
128
+ updateContact(id, input) {
129
+ return this.request("PATCH", `/contacts/${enc(id)}`, input);
130
+ }
131
+ deleteContact(id) {
132
+ return this.request("DELETE", `/contacts/${enc(id)}`);
133
+ }
134
+ // ── Transport ─────────────────────────────────────────────────────────────
135
+ /**
136
+ * `idempotent` widens what may be retried.
137
+ *
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.
142
+ */
143
+ async request(method, path, body, opts) {
144
+ const safe = method === "GET" || method === "DELETE" || opts?.idempotent === true;
145
+ let attempt = 0;
146
+ for (; ; ) {
147
+ const res = await this.doFetch(`${this.baseUrl}${path}`, {
148
+ method,
149
+ headers: {
150
+ Authorization: `Bearer ${this.apiKey}`,
151
+ Accept: "application/json",
152
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
153
+ },
154
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
155
+ });
156
+ if (res.ok) return await res.json();
157
+ const error = await toError(res);
158
+ const mayRetry = error.status === 429 || safe && error.retryable;
159
+ if (!mayRetry || attempt >= this.maxRetries) throw error;
160
+ await sleep(backoffMs(attempt, error));
161
+ attempt++;
162
+ }
163
+ }
164
+ };
165
+ function enc(segment) {
166
+ return encodeURIComponent(segment);
167
+ }
168
+ async function toError(res) {
169
+ let body = null;
170
+ try {
171
+ body = await res.json();
172
+ } catch {
173
+ }
174
+ const err = body?.error;
175
+ return new YungleApiError(
176
+ res.status,
177
+ err?.code ?? "http_error",
178
+ err?.message ?? `Request failed with status ${res.status}.`,
179
+ err?.details
180
+ );
181
+ }
182
+ function backoffMs(attempt, error) {
183
+ const stated = error.retryAfterSeconds;
184
+ if (stated !== null) return stated * 1e3;
185
+ return Math.min(2 ** attempt * 500, 8e3) + Math.floor(Math.random() * 250);
186
+ }
187
+ function sleep(ms) {
188
+ return new Promise((resolve) => setTimeout(resolve, ms));
189
+ }
190
+ export {
191
+ YungleApiError,
192
+ YungleClient
193
+ };
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The wire shapes `/api/v1` emits.
3
+ *
4
+ * Hand-written rather than generated, and deliberately **not** imported by
5
+ * `apps/web`. Two reasons, both practical:
6
+ *
7
+ * - Having the server import this package would put it in the web app's
8
+ * dependency graph, which means a `COPY packages/api-client/package.json`
9
+ * line in two Dockerfiles for something production never runs. The cost of
10
+ * forgetting that line is an image build failure, not a test failure.
11
+ * - The server's own definitions live in `lib/api/serialize.ts` and are the
12
+ * authority. These are a client's *reading* of the contract.
13
+ *
14
+ * Drift is managed by the OpenAPI document at `/api/v1/openapi.json`, which is
15
+ * generated from the server's Zod schemas — and by both sides living in one
16
+ * repo and shipping in one commit. If this package ever gains a second consumer
17
+ * outside the repo, generate it from the spec instead.
18
+ */
19
+ export type Iso8601 = string;
20
+ export interface Workspace {
21
+ id: string;
22
+ email: string | null;
23
+ displayName: string | null;
24
+ handle: string | null;
25
+ }
26
+ export interface PlanInfo {
27
+ tier: string | null;
28
+ name: string | null;
29
+ status: string;
30
+ quotaBytes: number;
31
+ usedBytes: number;
32
+ }
33
+ export interface KeyInfo {
34
+ id: string;
35
+ name: string;
36
+ scopes: string[];
37
+ }
38
+ export interface Me {
39
+ workspace: Workspace;
40
+ plan: PlanInfo;
41
+ key: KeyInfo;
42
+ }
43
+ /** What you send when registering a file for upload. */
44
+ export interface FileInput {
45
+ name: string;
46
+ size: number;
47
+ type?: string;
48
+ /** Directory inside an uploaded folder, e.g. `Ceremony/Raw`. */
49
+ path?: string;
50
+ }
51
+ /** Where to put the bytes, and the token that authorizes every tus request. */
52
+ export interface UploadTarget {
53
+ id: string;
54
+ name: string;
55
+ size: number;
56
+ uploadToken: string;
57
+ }
58
+ export interface UploadTargets {
59
+ tusEndpoint: string;
60
+ files: UploadTarget[];
61
+ }
62
+ export interface Transfer {
63
+ id: string;
64
+ slug: string;
65
+ url: string;
66
+ title: string | null;
67
+ status: string;
68
+ sizeBytes: number;
69
+ fileCount?: number;
70
+ recipients: string[];
71
+ hasPassword: boolean;
72
+ downloadCount: number;
73
+ maxDownloads: number | null;
74
+ /** Null while it is still a draft. A draft is not shareable. */
75
+ finalizedAt: Iso8601 | null;
76
+ expiresAt: Iso8601 | null;
77
+ createdAt: Iso8601 | null;
78
+ }
79
+ export interface TransferSummary extends Omit<Transfer, 'hasPassword' | 'fileCount'> {
80
+ fileCount: number;
81
+ firstFileName: string | null;
82
+ }
83
+ export interface TransferFile {
84
+ id: string;
85
+ name: string;
86
+ sizeBytes: number;
87
+ mimeType: string;
88
+ path: string | null;
89
+ /** Null until the malware scan finishes, then `clean` or `infected`. */
90
+ scanResult: string | null;
91
+ createdAt: Iso8601;
92
+ }
93
+ export interface RecipientStatus {
94
+ id: string;
95
+ email: string;
96
+ notifiedAt: Iso8601 | null;
97
+ downloaded: boolean;
98
+ }
99
+ export interface DownloadEvent {
100
+ id: string;
101
+ /** Null means the whole-transfer zip rather than one file. */
102
+ fileName: string | null;
103
+ recipientEmail: string | null;
104
+ /** One page visit is one download; several files in it share this. */
105
+ sessionId: string;
106
+ ipTruncated: string | null;
107
+ createdAt: Iso8601;
108
+ }
109
+ export interface Collection {
110
+ id: string;
111
+ title: string;
112
+ slug: string;
113
+ description: string | null;
114
+ visibility: string;
115
+ /** The secret link. Null only for rows created before it was minted eagerly. */
116
+ url: string | null;
117
+ customLink: boolean;
118
+ sizeBytes: number;
119
+ coverFileId: string | null;
120
+ expiresAt: Iso8601 | null;
121
+ createdAt: Iso8601 | null;
122
+ updatedAt: Iso8601 | null;
123
+ fileCount?: number;
124
+ }
125
+ export interface CollectionSummary {
126
+ id: string;
127
+ title: string;
128
+ slug: string;
129
+ visibility: string;
130
+ fileCount: number;
131
+ sizeBytes: number;
132
+ coverFileId: string | null;
133
+ updatedAt: Iso8601 | null;
134
+ }
135
+ export interface CollectionFile {
136
+ id: string;
137
+ name: string;
138
+ sizeBytes: number;
139
+ mimeType: string;
140
+ folderId: string | null;
141
+ hasThumbnail: boolean;
142
+ createdAt: Iso8601 | null;
143
+ }
144
+ export interface Folder {
145
+ id: string;
146
+ name: string;
147
+ /** Materialized, e.g. `/Ceremony/Raw`. */
148
+ path: string;
149
+ parentId: string | null;
150
+ depth: number;
151
+ }
152
+ export interface Guest {
153
+ id: string;
154
+ email: string;
155
+ status: string;
156
+ }
157
+ export interface Contact {
158
+ id: string;
159
+ firstName: string | null;
160
+ lastName: string | null;
161
+ company: string | null;
162
+ email: string;
163
+ phone: string | null;
164
+ type: string;
165
+ createdAt: Iso8601 | null;
166
+ updatedAt: Iso8601 | null;
167
+ }
168
+ export interface ContactInput {
169
+ firstName?: string | null;
170
+ lastName?: string | null;
171
+ company?: string | null;
172
+ email: string;
173
+ phone?: string | null;
174
+ type?: string | null;
175
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "yungle-client",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.mjs",
9
+ "default": "./dist/index.mjs"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "node build.mjs && tsc -p tsconfig.build.json",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "node --import tsx --test src/*.test.ts",
16
+ "lint": "eslint .",
17
+ "prepublishOnly": "node build.mjs && tsc -p tsconfig.build.json",
18
+ "prepare": "node build.mjs && tsc -p tsconfig.build.json"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.10.5",
22
+ "tsx": "^4.19.2",
23
+ "typescript": "^5.7.3",
24
+ "esbuild": "^0.25.0"
25
+ },
26
+ "description": "Typed client for the Yungle API \u2014 send file transfers, build collections and sync contacts from Node.",
27
+ "license": "MIT",
28
+ "author": "Hein de Wilde",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/heindewilde/yungle-clients.git"
32
+ },
33
+ "homepage": "https://yungle.co/developers",
34
+ "bugs": {
35
+ "url": "https://github.com/heindewilde/yungle-clients/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "keywords": [
41
+ "yungle",
42
+ "file-transfer",
43
+ "api-client",
44
+ "sdk",
45
+ "wetransfer-alternative",
46
+ "eu",
47
+ "gdpr"
48
+ ],
49
+ "files": [
50
+ "dist",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "types": "./dist/index.d.ts"
55
+ }