@darseen/vaze 2.3.0 → 2.4.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/README.md CHANGED
@@ -37,6 +37,20 @@ const vaze = new Vaze({
37
37
  });
38
38
  ```
39
39
 
40
+ # Acting as a Signed-in User
41
+
42
+ Apps that sign people in can send their session instead of an API key. Pass `headers`, an object or a function called before every request, and an optional `fetch` to replace the global one:
43
+
44
+ ```ts
45
+ const vaze = new Vaze({
46
+ vazeUrl: "http://your-server-ip:3000",
47
+ headers: async () => ({ cookie: await getSessionCookie() }),
48
+ fetch: (input, init) => fetch(input, { ...init, credentials: "omit" }),
49
+ });
50
+ ```
51
+
52
+ `vaze.authHeaders()` returns the headers the SDK sends, for clients that fetch an SDK URL themselves, such as `vaze.files.thumbnailUrl(id)` or `vaze.files.downloadUrl(id)`.
53
+
40
54
  # Using Environment Variables
41
55
 
42
56
  If you set `VAZE_URL` and `VAZE_API_KEY` in your environment, you can initialize the client without passing arguments:
@@ -50,7 +64,7 @@ const vaze = new Vaze();
50
64
 
51
65
  # API Documentation
52
66
 
53
- The SDK is divided into two main modules: `files` and `folders`. All methods are asynchronous and return a standard `ApiResponse` object containing either `data` or an `error`. Files API Access file-related methods via `vaze.files.getAll(options?)` Retrieve a paginated list of all files.
67
+ The SDK's main modules are `files` and `folders`, plus `keys`, `account`, `activity`, `users` and `settings` for signed-in users. All methods are asynchronous and return a standard `ApiResponse` object containing either `data` or an `error`. Files API Access file-related methods via `vaze.files.getAll(options?)` Retrieve a paginated list of all files.
54
68
 
55
69
  - options (optional):
56
70
  - `limit`
@@ -87,7 +101,7 @@ const { data, error } = await vaze.files.getByName("document.pdf", {
87
101
  `upload(data)` Upload one or multiple files to your Vaze instance.
88
102
 
89
103
  - data:
90
- - `files` (File[]): An array of standard web [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) objects.
104
+ - `files` (File[]): An array of standard web [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) objects, or in React Native, `{ uri, name, type }` references.
91
105
  - `folder?` (string): The path of the target folder, created if it doesn't exist.
92
106
 
93
107
  ```ts
@@ -199,6 +213,80 @@ if (data) {
199
213
  const { data, error } = await vaze.folders.delete("folder-id");
200
214
  ```
201
215
 
216
+ Keys API Manage the caller's API keys via `vaze.keys`. These calls need a client that acts as a signed-in user (`headers`) holding `api-keys:manage`; a client built with an API key gets a `403`.
217
+
218
+ `getAll()` List the caller's keys, newest first.
219
+
220
+ ```ts
221
+ const { data, error } = await vaze.keys.getAll();
222
+ ```
223
+
224
+ `create(options)` Create a key. The secret in `data.key.key` is only returned this once.
225
+
226
+ - options:
227
+ - `name`: Unique among the caller's keys.
228
+ - `scopes`: Any of `files:read`, `files:write` and `files:delete` the caller holds.
229
+ - `expiresAt` (optional): A `Date` or ISO string; leave it out for a key that never expires.
230
+
231
+ ```ts
232
+ const { data, error } = await vaze.keys.create({
233
+ name: "Backup script",
234
+ scopes: ["files:read", "files:write"],
235
+ });
236
+
237
+ console.log(data?.key.key); // "vz_..."
238
+ ```
239
+
240
+ `rename(data)` Rename a key. Scopes and expiry can't change.
241
+
242
+ ```ts
243
+ const { data, error } = await vaze.keys.rename({ id: "key-id", name: "Nightly backup" });
244
+ ```
245
+
246
+ `delete(id)` Delete a key, which stops working right away.
247
+
248
+ ```ts
249
+ const { data, error } = await vaze.keys.delete("key-id");
250
+ ```
251
+
252
+ Account API Change the signed-in user's own account via `vaze.account`. These calls need a client that acts as a signed-in user (`headers`); a client built with an API key gets a `403`.
253
+
254
+ ```ts
255
+ await vaze.account.update({ name: "Dana Scully", email: "dana@example.org" });
256
+ await vaze.account.changePassword({ currentPassword: "old", newPassword: "new-password", revokeOtherSessions: true });
257
+
258
+ const { data } = await vaze.account.getSessions(); // devices, the current one first
259
+ await vaze.account.revokeSession("session-id");
260
+ await vaze.account.revokeOtherSessions();
261
+ ```
262
+
263
+ Activity API Read and prune the caller's history of uploads, deletions and changes via `vaze.activity`. Needs a signed-in user.
264
+
265
+ ```ts
266
+ const { data } = await vaze.activity.getAll(); // the 200 most recent entries
267
+ await vaze.activity.delete(["activity-id"]);
268
+ await vaze.activity.clear();
269
+ ```
270
+
271
+ Users API Manage accounts via `vaze.users`. Needs a signed-in admin (`users:manage`). None of the changes work on the caller's own account.
272
+
273
+ ```ts
274
+ const { data } = await vaze.users.getAll();
275
+ await vaze.users.create({ name: "Dana", email: "dana@example.com", password: "a-long-password", role: "viewer" });
276
+ await vaze.users.setAccess({ id: "user-id", role: "custom", scopes: ["files:read"] });
277
+ await vaze.users.resetPassword({ id: "user-id", password: "a-new-password" });
278
+ await vaze.users.setDisabled({ id: "user-id", disabled: true });
279
+ await vaze.users.delete("user-id");
280
+ ```
281
+
282
+ Settings API Read and change instance-wide settings via `vaze.settings`. Needs a signed-in user holding `settings:manage`.
283
+
284
+ ```ts
285
+ const { data } = await vaze.settings.get(); // defaultVisibility, visibilityCounts, limits
286
+ await vaze.settings.update({ defaultVisibility: "private", limits: { activityRetentionDays: 30 } });
287
+ await vaze.settings.applyVisibility("private"); // every existing file
288
+ ```
289
+
202
290
  # Health Check
203
291
 
204
292
  `vaze.health()` pings the instance to verify connectivity.
@@ -211,6 +299,17 @@ if (data) {
211
299
  }
212
300
  ```
213
301
 
302
+ # Account and Stats
303
+
304
+ `vaze.me()` returns the caller's account and the scopes it may use. `vaze.stats()` returns the dashboard's numbers, with each section `null` when the caller lacks its scope.
305
+
306
+ ```ts
307
+ const { data } = await vaze.me();
308
+ const canUpload = data?.scopes.includes("files:write");
309
+ ```
310
+
311
+ Failed requests carry the HTTP status in `error.status`, such as `401` once a session expires.
312
+
214
313
  # Types
215
314
 
216
- This SDK is built with TypeScript and exports all necessary types to ensure type safety in your applications. Types such as `File`, `Folder`, `ApiResponse`, `ListOptions`, and `VazeOptions`.
315
+ This SDK is built with TypeScript and exports all necessary types to ensure type safety in your applications. Types such as `File`, `Folder`, `ApiKey`, `Activity`, `User`, `Session`, `InstanceSettings`, `ApiResponse`, `ListOptions`, and `VazeOptions`.
package/dist/index.cjs CHANGED
@@ -1,4 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/utils/index.ts
2
+ function env(name) {
3
+ return typeof process === "undefined" ? void 0 : _optionalChain([process, 'access', _ => _.env, 'optionalAccess', _2 => _2[name]]);
4
+ }
2
5
  function constructFileUrls({
3
6
  vazeUrl,
4
7
  files
@@ -12,34 +15,39 @@ function constructFileUrls({
12
15
  function parseContentDispositionFilename(header) {
13
16
  if (!header) return null;
14
17
  const utf8Match = header.match(/filename\*=UTF-8''([^;]+)/i);
15
- if (_optionalChain([utf8Match, 'optionalAccess', _ => _[1]])) {
18
+ if (_optionalChain([utf8Match, 'optionalAccess', _3 => _3[1]])) {
16
19
  try {
17
20
  return decodeURIComponent(utf8Match[1]);
18
21
  } catch (e) {
19
22
  }
20
23
  }
21
24
  const plainMatch = header.match(/filename="([^"]*)"/i);
22
- return _nullishCoalesce(_optionalChain([plainMatch, 'optionalAccess', _2 => _2[1]]), () => ( null));
25
+ return _nullishCoalesce(_optionalChain([plainMatch, 'optionalAccess', _4 => _4[1]]), () => ( null));
23
26
  }
24
27
 
25
28
  // src/base.ts
26
29
  var Base = class {
27
30
 
28
31
 
32
+ // wrapped so the global fetch is never called with this class as `this`
33
+
34
+
29
35
  constructor(options = {}) {
30
- const vazeUrl = options.vazeUrl || process.env.VAZE_URL;
31
- const apiKey = options.apiKey || process.env.VAZE_API_KEY;
36
+ const vazeUrl = options.vazeUrl || env("VAZE_URL");
37
+ const apiKey = options.apiKey || env("VAZE_API_KEY");
32
38
  if (!vazeUrl) {
33
39
  throw new Error(
34
40
  "You must provide a Vaze URL or set the VAZE_URL environment variable."
35
41
  );
36
42
  }
37
- if (!apiKey) {
43
+ if (!apiKey && !options.headers) {
38
44
  throw new Error(
39
- "You must provide an API key or set the VAZE_API_KEY environment variable."
45
+ "You must provide an API key or headers, or set the VAZE_API_KEY environment variable."
40
46
  );
41
47
  }
42
48
  this.apiKey = apiKey;
49
+ this.headers = options.headers;
50
+ this.fetch = _nullishCoalesce(options.fetch, () => ( ((input, init) => fetch(input, init))));
43
51
  this.vazeUrl = vazeUrl.endsWith("/") ? vazeUrl : `${vazeUrl}/`;
44
52
  }
45
53
  apiUrl(path, params) {
@@ -51,35 +59,45 @@ var Base = class {
51
59
  }
52
60
  return url.toString();
53
61
  }
62
+ /** The headers every request carries, for clients fetching an SDK URL themselves. */
63
+ async authHeaders() {
64
+ const headers = typeof this.headers === "function" ? await this.headers() : this.headers;
65
+ return this.apiKey ? { ...headers, "API-Key": this.apiKey } : { ...headers };
66
+ }
54
67
  async request(method, url, body, headers) {
55
68
  try {
56
69
  const fetchHeaders = {
57
- "API-Key": this.apiKey,
70
+ ...await this.authHeaders(),
58
71
  ...headers
59
72
  };
60
73
  const isFormData = body instanceof FormData;
61
74
  if (body !== void 0 && !isFormData && !fetchHeaders["Content-Type"]) {
62
75
  fetchHeaders["Content-Type"] = "application/json";
63
76
  }
64
- const response = await fetch(url, {
77
+ const response = await this.fetch(url, {
65
78
  method,
66
79
  headers: fetchHeaders,
67
80
  body: isFormData ? body : body !== void 0 ? JSON.stringify(body) : null
68
81
  });
82
+ const { status } = response;
69
83
  const contentType = _nullishCoalesce(response.headers.get("content-type"), () => ( ""));
70
84
  if (!contentType.includes("application/json")) {
71
85
  return {
72
86
  data: null,
73
87
  error: {
74
- message: `Unexpected non-JSON response from server (status ${response.status}).`
88
+ message: `Unexpected non-JSON response from server (status ${status}).`,
89
+ status
75
90
  }
76
91
  };
77
92
  }
78
93
  const result = await response.json();
79
- if (!response.ok && !result.error) {
94
+ if (!response.ok) {
80
95
  return {
81
96
  data: null,
82
- error: { message: `Request failed with status ${response.status}.` }
97
+ error: {
98
+ message: _nullishCoalesce(_optionalChain([result, 'access', _5 => _5.error, 'optionalAccess', _6 => _6.message]), () => ( `Request failed with status ${status}.`)),
99
+ status
100
+ }
83
101
  };
84
102
  }
85
103
  return result;
@@ -94,22 +112,25 @@ var Base = class {
94
112
  }
95
113
  async requestBlob(url, method = "GET", body) {
96
114
  try {
97
- const headers = { "API-Key": this.apiKey };
115
+ const headers = await this.authHeaders();
98
116
  if (body !== void 0) headers["Content-Type"] = "application/json";
99
- const response = await fetch(url, {
117
+ const response = await this.fetch(url, {
100
118
  method,
101
119
  headers,
102
120
  body: body !== void 0 ? JSON.stringify(body) : null
103
121
  });
122
+ const { status } = response;
104
123
  const contentType = _nullishCoalesce(response.headers.get("content-type"), () => ( ""));
105
124
  if (!response.ok) {
106
125
  if (contentType.includes("application/json")) {
107
126
  const result = await response.json();
108
- if (result.error) return { data: null, error: result.error };
127
+ if (result.error) {
128
+ return { data: null, error: { ...result.error, status } };
129
+ }
109
130
  }
110
131
  return {
111
132
  data: null,
112
- error: { message: `Request failed with status ${response.status}.` }
133
+ error: { message: `Request failed with status ${status}.`, status }
113
134
  };
114
135
  }
115
136
  return {
@@ -133,6 +154,70 @@ var Base = class {
133
154
  }
134
155
  };
135
156
 
157
+ // src/account.ts
158
+ var Account = class extends Base {
159
+ /** Changes the display name, the sign-in email, or both. */
160
+ async update(changes) {
161
+ return await this.request("PUT", this.apiUrl("/api/me"), changes);
162
+ }
163
+ async changePassword({
164
+ currentPassword,
165
+ newPassword,
166
+ revokeOtherSessions = false
167
+ }) {
168
+ return await this.request("PUT", this.apiUrl("/api/me/password"), {
169
+ currentPassword,
170
+ newPassword,
171
+ revokeOtherSessions
172
+ });
173
+ }
174
+ /** Every device signed in to the account, the current one first. */
175
+ async getSessions() {
176
+ return await this.request(
177
+ "GET",
178
+ this.apiUrl("/api/me/sessions")
179
+ );
180
+ }
181
+ /** Signs out another device. The current session ends with a sign-out instead. */
182
+ async revokeSession(id) {
183
+ return await this.request("DELETE", this.apiUrl("/api/me/sessions"), {
184
+ id
185
+ });
186
+ }
187
+ /** Signs out every device but the current one. */
188
+ async revokeOtherSessions() {
189
+ return await this.request("DELETE", this.apiUrl("/api/me/sessions"), {
190
+ others: true
191
+ });
192
+ }
193
+ };
194
+
195
+ // src/activity.ts
196
+ var Activity = class extends Base {
197
+ /** The most recent entries, newest first. */
198
+ async getAll() {
199
+ return await this.request(
200
+ "GET",
201
+ this.apiUrl("/api/activity")
202
+ );
203
+ }
204
+ async delete(ids) {
205
+ return await this.request(
206
+ "DELETE",
207
+ this.apiUrl("/api/activity"),
208
+ { ids }
209
+ );
210
+ }
211
+ /** Deletes every entry. Files, keys and accounts are not affected. */
212
+ async clear() {
213
+ return await this.request(
214
+ "DELETE",
215
+ this.apiUrl("/api/activity"),
216
+ { all: true }
217
+ );
218
+ }
219
+ };
220
+
136
221
  // src/files.ts
137
222
  var Files = class extends Base {
138
223
  async getAll(options) {
@@ -220,8 +305,15 @@ var Files = class extends Base {
220
305
  };
221
306
  }
222
307
  async download(id) {
223
- const url = this.apiUrl(`/api/files/download/${encodeURIComponent(id)}`);
224
- return await this.requestBlob(url);
308
+ return await this.requestBlob(this.downloadUrl(id));
309
+ }
310
+ /** Where `download` reads from; fetch it yourself with `authHeaders()`. */
311
+ downloadUrl(id) {
312
+ return this.apiUrl(`/api/files/download/${encodeURIComponent(id)}`);
313
+ }
314
+ /** A WebP thumbnail of an image file; fetch it with `authHeaders()`. */
315
+ thumbnailUrl(id) {
316
+ return this.apiUrl(`/api/files/thumbnail/${encodeURIComponent(id)}`);
225
317
  }
226
318
  /** Download several files as one zip, with paths below their shared folder. */
227
319
  async downloadMany(ids) {
@@ -344,14 +436,140 @@ var Folders = class extends Base {
344
436
  }
345
437
  };
346
438
 
439
+ // src/keys.ts
440
+ var Keys = class extends Base {
441
+ /** Every key the caller owns, newest first. */
442
+ async getAll() {
443
+ return await this.request(
444
+ "GET",
445
+ this.apiUrl("/api/keys")
446
+ );
447
+ }
448
+ /** The response holds the key's secret, which can't be read again later. */
449
+ async create({ name, scopes, expiresAt }) {
450
+ return await this.request(
451
+ "POST",
452
+ this.apiUrl("/api/keys"),
453
+ {
454
+ name,
455
+ scopes,
456
+ expiresAt: expiresAt instanceof Date ? expiresAt.toISOString() : expiresAt
457
+ }
458
+ );
459
+ }
460
+ async rename(data) {
461
+ return await this.request("PUT", this.apiUrl("/api/keys"), data);
462
+ }
463
+ /** Revokes the key at once for anything using it. */
464
+ async delete(id) {
465
+ return await this.request("DELETE", this.apiUrl("/api/keys"), {
466
+ id
467
+ });
468
+ }
469
+ };
470
+
471
+ // src/settings.ts
472
+ var Settings = class extends Base {
473
+ async get() {
474
+ return await this.request(
475
+ "GET",
476
+ this.apiUrl("/api/settings")
477
+ );
478
+ }
479
+ /** Limits left out keep their current values. */
480
+ async update(changes) {
481
+ return await this.request(
482
+ "PUT",
483
+ this.apiUrl("/api/settings"),
484
+ changes
485
+ );
486
+ }
487
+ /** Gives every existing file this visibility. */
488
+ async applyVisibility(visibility) {
489
+ return await this.request(
490
+ "POST",
491
+ this.apiUrl("/api/settings/apply-visibility"),
492
+ { visibility }
493
+ );
494
+ }
495
+ };
496
+
497
+ // src/users.ts
498
+ var Users = class extends Base {
499
+ /** Every account, oldest first. */
500
+ async getAll() {
501
+ return await this.request(
502
+ "GET",
503
+ this.apiUrl("/api/users")
504
+ );
505
+ }
506
+ async create(options) {
507
+ return await this.request(
508
+ "POST",
509
+ this.apiUrl("/api/users"),
510
+ options
511
+ );
512
+ }
513
+ /** Applies right away, to the user's sessions and API keys alike. */
514
+ async setAccess({
515
+ id,
516
+ role,
517
+ scopes = []
518
+ }) {
519
+ return await this.request(
520
+ "PUT",
521
+ this.apiUrl("/api/users"),
522
+ { id, role, scopes }
523
+ );
524
+ }
525
+ /** Also signs the user out of every device. */
526
+ async resetPassword({
527
+ id,
528
+ password
529
+ }) {
530
+ return await this.request(
531
+ "PUT",
532
+ this.apiUrl("/api/users"),
533
+ { id, password }
534
+ );
535
+ }
536
+ /** A disabled user is signed out and their API keys stop until re-enabled. */
537
+ async setDisabled({
538
+ id,
539
+ disabled
540
+ }) {
541
+ return await this.request(
542
+ "PUT",
543
+ this.apiUrl("/api/users"),
544
+ { id, disabled }
545
+ );
546
+ }
547
+ /** Removes the account, its API keys and its history. Files stay. */
548
+ async delete(id) {
549
+ return await this.request("DELETE", this.apiUrl("/api/users"), {
550
+ id
551
+ });
552
+ }
553
+ };
554
+
347
555
  // src/index.ts
348
556
  var Vaze = class extends Base {
349
557
 
350
558
 
559
+
560
+
561
+
562
+
563
+
351
564
  constructor(options = {}) {
352
565
  super(options);
353
566
  this.files = new Files(options);
354
567
  this.folders = new Folders(options);
568
+ this.keys = new Keys(options);
569
+ this.account = new Account(options);
570
+ this.activity = new Activity(options);
571
+ this.users = new Users(options);
572
+ this.settings = new Settings(options);
355
573
  }
356
574
  async health() {
357
575
  return await this.request(
@@ -359,6 +577,35 @@ var Vaze = class extends Base {
359
577
  this.apiUrl("/api/health")
360
578
  );
361
579
  }
580
+ /** The caller's account and the scopes it may use. */
581
+ async me() {
582
+ return await this.request("GET", this.apiUrl("/api/me"));
583
+ }
584
+ /** Dashboard numbers, with each section limited to the caller's scopes. */
585
+ async stats() {
586
+ const { data, error } = await this.request(
587
+ "GET",
588
+ this.apiUrl("/api/stats")
589
+ );
590
+ const files = _optionalChain([data, 'optionalAccess', _7 => _7.files]);
591
+ return {
592
+ error,
593
+ data: data ? {
594
+ ...data,
595
+ files: files ? {
596
+ ...files,
597
+ largest: constructFileUrls({
598
+ vazeUrl: this.vazeUrl,
599
+ files: files.largest
600
+ }),
601
+ recent: constructFileUrls({
602
+ vazeUrl: this.vazeUrl,
603
+ files: files.recent
604
+ })
605
+ } : null
606
+ } : null
607
+ };
608
+ }
362
609
  };
363
610
 
364
611