@vtriv/sdk 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/dist/index.js ADDED
@@ -0,0 +1,1073 @@
1
+ // src/errors.ts
2
+ class VtrivError extends Error {
3
+ code;
4
+ details;
5
+ status;
6
+ constructor(message, options) {
7
+ super(message);
8
+ this.name = "VtrivError";
9
+ this.code = options?.code;
10
+ this.details = options?.details;
11
+ this.status = options?.status;
12
+ }
13
+ static fromResponse(response, status) {
14
+ return new VtrivError(response.error, {
15
+ code: response.code,
16
+ details: response.details,
17
+ status
18
+ });
19
+ }
20
+ }
21
+
22
+ class AuthenticationError extends VtrivError {
23
+ constructor(message = "Authentication required") {
24
+ super(message, { status: 401 });
25
+ this.name = "AuthenticationError";
26
+ }
27
+ }
28
+
29
+ class AuthorizationError extends VtrivError {
30
+ constructor(message = "Not authorized") {
31
+ super(message, { status: 403 });
32
+ this.name = "AuthorizationError";
33
+ }
34
+ }
35
+
36
+ class NotFoundError extends VtrivError {
37
+ constructor(message = "Resource not found") {
38
+ super(message, { status: 404 });
39
+ this.name = "NotFoundError";
40
+ }
41
+ }
42
+
43
+ class ValidationError extends VtrivError {
44
+ constructor(message, details) {
45
+ super(message, { status: 400, details });
46
+ this.name = "ValidationError";
47
+ }
48
+ }
49
+
50
+ class RateLimitError extends VtrivError {
51
+ constructor(message = "Rate limit exceeded") {
52
+ super(message, { status: 429 });
53
+ this.name = "RateLimitError";
54
+ }
55
+ }
56
+
57
+ class NetworkError extends VtrivError {
58
+ constructor(message = "Network request failed", cause) {
59
+ super(message, { details: cause?.message });
60
+ this.name = "NetworkError";
61
+ this.cause = cause;
62
+ }
63
+ }
64
+
65
+ class TokenExpiredError extends VtrivError {
66
+ constructor() {
67
+ super("Token expired", { status: 401, code: "TOKEN_EXPIRED" });
68
+ this.name = "TokenExpiredError";
69
+ }
70
+ }
71
+ function createErrorFromStatus(status, response) {
72
+ switch (status) {
73
+ case 401:
74
+ return new AuthenticationError(response.error);
75
+ case 402:
76
+ case 429:
77
+ return new RateLimitError(response.error);
78
+ case 403:
79
+ return new AuthorizationError(response.error);
80
+ case 404:
81
+ return new NotFoundError(response.error);
82
+ case 400:
83
+ return new ValidationError(response.error, response.details);
84
+ default:
85
+ return VtrivError.fromResponse(response, status);
86
+ }
87
+ }
88
+
89
+ // src/utils/fetch.ts
90
+ async function request(ctx, path, options = {}) {
91
+ const url = `${ctx.baseUrl}${path}`;
92
+ const method = options.method || "GET";
93
+ const headers = {
94
+ ...options.headers
95
+ };
96
+ const authHeader = await ctx.getAuthHeader();
97
+ if (authHeader) {
98
+ headers.Authorization = authHeader;
99
+ }
100
+ if (options.body && typeof options.body === "object" && !headers["Content-Type"]) {
101
+ headers["Content-Type"] = "application/json";
102
+ }
103
+ let response;
104
+ try {
105
+ response = await fetch(url, {
106
+ method,
107
+ headers,
108
+ body: options.body ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : undefined,
109
+ signal: options.signal
110
+ });
111
+ } catch (err) {
112
+ throw new NetworkError("Network request failed", err instanceof Error ? err : undefined);
113
+ }
114
+ const contentType = response.headers.get("Content-Type") || "";
115
+ if (!contentType.includes("application/json")) {
116
+ if (!response.ok) {
117
+ throw createErrorFromStatus(response.status, {
118
+ error: `Request failed with status ${response.status}`
119
+ });
120
+ }
121
+ return response;
122
+ }
123
+ let data;
124
+ try {
125
+ const text = await response.text();
126
+ data = text ? JSON.parse(text) : {};
127
+ } catch {
128
+ throw new NetworkError("Failed to parse response");
129
+ }
130
+ if (!response.ok) {
131
+ const errorResponse = data;
132
+ throw createErrorFromStatus(response.status, errorResponse);
133
+ }
134
+ return data;
135
+ }
136
+ async function* requestStream(ctx, path, options = {}) {
137
+ const url = `${ctx.baseUrl}${path}`;
138
+ const method = options.method || "POST";
139
+ const headers = {
140
+ Accept: "text/event-stream",
141
+ ...options.headers
142
+ };
143
+ const authHeader = await ctx.getAuthHeader();
144
+ if (authHeader) {
145
+ headers.Authorization = authHeader;
146
+ }
147
+ if (options.body && typeof options.body === "object") {
148
+ headers["Content-Type"] = "application/json";
149
+ }
150
+ let response;
151
+ try {
152
+ response = await fetch(url, {
153
+ method,
154
+ headers,
155
+ body: options.body ? JSON.stringify(options.body) : undefined,
156
+ signal: options.signal
157
+ });
158
+ } catch (err) {
159
+ throw new NetworkError("Network request failed", err instanceof Error ? err : undefined);
160
+ }
161
+ if (!response.ok) {
162
+ const text = await response.text();
163
+ let errorResponse;
164
+ try {
165
+ errorResponse = JSON.parse(text);
166
+ } catch {
167
+ errorResponse = { error: text || `Request failed with status ${response.status}` };
168
+ }
169
+ throw createErrorFromStatus(response.status, errorResponse);
170
+ }
171
+ if (!response.body) {
172
+ throw new NetworkError("No response body for stream");
173
+ }
174
+ const reader = response.body.getReader();
175
+ const decoder = new TextDecoder;
176
+ let buffer = "";
177
+ try {
178
+ while (true) {
179
+ const { done, value } = await reader.read();
180
+ if (done)
181
+ break;
182
+ buffer += decoder.decode(value, { stream: true });
183
+ const lines = buffer.split(`
184
+ `);
185
+ buffer = lines.pop() || "";
186
+ for (const line of lines) {
187
+ if (line.startsWith("data: ")) {
188
+ const data = line.slice(6);
189
+ if (data === "[DONE]")
190
+ return;
191
+ yield data;
192
+ }
193
+ }
194
+ }
195
+ } finally {
196
+ reader.releaseLock();
197
+ }
198
+ }
199
+ async function uploadFile(ctx, path, file, contentType) {
200
+ const url = `${ctx.baseUrl}${path}`;
201
+ const headers = {};
202
+ const authHeader = await ctx.getAuthHeader();
203
+ if (authHeader) {
204
+ headers.Authorization = authHeader;
205
+ }
206
+ if (contentType) {
207
+ headers["Content-Type"] = contentType;
208
+ }
209
+ let response;
210
+ try {
211
+ response = await fetch(url, {
212
+ method: "PUT",
213
+ headers,
214
+ body: file
215
+ });
216
+ } catch (err) {
217
+ throw new NetworkError("Upload failed", err instanceof Error ? err : undefined);
218
+ }
219
+ if (!response.ok) {
220
+ const text = await response.text();
221
+ let errorResponse;
222
+ try {
223
+ errorResponse = JSON.parse(text);
224
+ } catch {
225
+ errorResponse = { error: text || `Upload failed with status ${response.status}` };
226
+ }
227
+ throw createErrorFromStatus(response.status, errorResponse);
228
+ }
229
+ return response;
230
+ }
231
+
232
+ // src/services/ai.ts
233
+ class AiService {
234
+ ctx;
235
+ aiBaseUrl;
236
+ constructor(ctx, options = {}) {
237
+ this.ctx = ctx;
238
+ this.aiBaseUrl = options.aiBaseUrl || ctx.baseUrl;
239
+ }
240
+ get aiCtx() {
241
+ return {
242
+ baseUrl: this.aiBaseUrl,
243
+ getAuthHeader: this.ctx.getAuthHeader
244
+ };
245
+ }
246
+ async chat(messages, options = {}) {
247
+ return request(this.aiCtx, "/v1/chat/completions", {
248
+ method: "POST",
249
+ body: {
250
+ ...options,
251
+ messages,
252
+ stream: false
253
+ }
254
+ });
255
+ }
256
+ async* chatStream(messages, options = {}) {
257
+ const stream = requestStream(this.aiCtx, "/v1/chat/completions", {
258
+ method: "POST",
259
+ body: {
260
+ ...options,
261
+ messages,
262
+ stream: true
263
+ }
264
+ });
265
+ for await (const data of stream) {
266
+ try {
267
+ const chunk = JSON.parse(data);
268
+ const content = chunk.choices[0]?.delta?.content;
269
+ if (content) {
270
+ yield content;
271
+ }
272
+ } catch {}
273
+ }
274
+ }
275
+ async* chatStreamRaw(messages, options = {}) {
276
+ const stream = requestStream(this.aiCtx, "/v1/chat/completions", {
277
+ method: "POST",
278
+ body: {
279
+ ...options,
280
+ messages,
281
+ stream: true
282
+ }
283
+ });
284
+ for await (const data of stream) {
285
+ try {
286
+ yield JSON.parse(data);
287
+ } catch {}
288
+ }
289
+ }
290
+ async embed(input, model) {
291
+ const body = { input };
292
+ if (model) {
293
+ body.model = model;
294
+ }
295
+ return request(this.aiCtx, "/v1/embeddings", {
296
+ method: "POST",
297
+ body
298
+ });
299
+ }
300
+ async getUsage(periodDays = 30) {
301
+ return request(this.aiCtx, `/usage/stats?period=${periodDays}d`);
302
+ }
303
+ async getConfig() {
304
+ return request(this.aiCtx, "/config");
305
+ }
306
+ async updateConfig(config) {
307
+ return request(this.aiCtx, "/config", {
308
+ method: "PUT",
309
+ body: config
310
+ });
311
+ }
312
+ }
313
+
314
+ // src/services/auth.ts
315
+ class AuthService {
316
+ ctx;
317
+ tokenManager;
318
+ project;
319
+ authBaseUrl;
320
+ constructor(ctx, options = {}) {
321
+ this.ctx = ctx;
322
+ this.tokenManager = options.tokenManager || null;
323
+ this.project = options.project || null;
324
+ this.authBaseUrl = options.authBaseUrl || ctx.baseUrl;
325
+ }
326
+ get authCtx() {
327
+ return {
328
+ baseUrl: this.authBaseUrl,
329
+ getAuthHeader: this.ctx.getAuthHeader
330
+ };
331
+ }
332
+ get accounts() {
333
+ return {
334
+ create: async (name) => {
335
+ return request(this.authCtx, "/accounts", {
336
+ method: "POST",
337
+ body: { name }
338
+ });
339
+ },
340
+ list: async () => {
341
+ const res = await request(this.authCtx, "/accounts");
342
+ return res.accounts;
343
+ },
344
+ get: async (id) => {
345
+ return request(this.authCtx, `/accounts/${id}`);
346
+ },
347
+ delete: async (id) => {
348
+ return request(this.authCtx, `/accounts/${id}`, {
349
+ method: "DELETE"
350
+ });
351
+ }
352
+ };
353
+ }
354
+ get projects() {
355
+ return {
356
+ create: async (name, config) => {
357
+ const res = await request(this.authCtx, "/projects", {
358
+ method: "POST",
359
+ body: { name, config }
360
+ });
361
+ return res.project;
362
+ },
363
+ list: async () => {
364
+ const res = await request(this.authCtx, "/projects");
365
+ return res.projects;
366
+ },
367
+ get: async (name) => {
368
+ return request(this.authCtx, `/projects/${name}`);
369
+ },
370
+ update: async (name, config) => {
371
+ return request(this.authCtx, `/projects/${name}`, {
372
+ method: "PUT",
373
+ body: config
374
+ });
375
+ },
376
+ delete: async (name) => {
377
+ return request(this.authCtx, `/projects/${name}`, {
378
+ method: "DELETE"
379
+ });
380
+ }
381
+ };
382
+ }
383
+ users(projectName) {
384
+ const project = projectName || this.project;
385
+ if (!project) {
386
+ throw new Error("Project name required for user management");
387
+ }
388
+ return {
389
+ create: async (data) => {
390
+ return request(this.authCtx, `/${project}/users`, {
391
+ method: "POST",
392
+ body: data
393
+ });
394
+ },
395
+ list: async () => {
396
+ const res = await request(this.authCtx, `/${project}/users`);
397
+ return res.users;
398
+ },
399
+ get: async (id) => {
400
+ return request(this.authCtx, `/${project}/users/${id}`);
401
+ },
402
+ update: async (id, data) => {
403
+ return request(this.authCtx, `/${project}/users/${id}`, {
404
+ method: "PUT",
405
+ body: data
406
+ });
407
+ },
408
+ delete: async (id) => {
409
+ return request(this.authCtx, `/${project}/users/${id}`, {
410
+ method: "DELETE"
411
+ });
412
+ }
413
+ };
414
+ }
415
+ apiKeys(projectName) {
416
+ const project = projectName || this.project;
417
+ if (!project) {
418
+ throw new Error("Project name required for API key management");
419
+ }
420
+ return {
421
+ create: async (name) => {
422
+ return request(this.authCtx, `/${project}/api-keys`, {
423
+ method: "POST",
424
+ body: { name }
425
+ });
426
+ },
427
+ list: async () => {
428
+ const res = await request(this.authCtx, `/${project}/api-keys`);
429
+ return res.api_keys;
430
+ },
431
+ delete: async (id) => {
432
+ return request(this.authCtx, `/${project}/api-keys/${id}`, {
433
+ method: "DELETE"
434
+ });
435
+ }
436
+ };
437
+ }
438
+ async mintToken(req = {}) {
439
+ if (!this.project) {
440
+ throw new Error("Project name required for minting tokens");
441
+ }
442
+ const res = await request(this.authCtx, `/${this.project}/token`, {
443
+ method: "POST",
444
+ body: req
445
+ });
446
+ return res.token;
447
+ }
448
+ async register(data) {
449
+ if (!this.project) {
450
+ throw new Error("Project name required for registration");
451
+ }
452
+ return request(this.authCtx, `/${this.project}/register`, {
453
+ method: "POST",
454
+ body: data
455
+ });
456
+ }
457
+ async login(credentials) {
458
+ if (!this.project) {
459
+ throw new Error("Project name required for login");
460
+ }
461
+ const tokens = await request(this.authCtx, `/${this.project}/login`, {
462
+ method: "POST",
463
+ body: credentials
464
+ });
465
+ if (this.tokenManager) {
466
+ this.tokenManager.setTokens(tokens);
467
+ }
468
+ return tokens;
469
+ }
470
+ async requestMagicLink(email) {
471
+ if (!this.project) {
472
+ throw new Error("Project name required for magic link");
473
+ }
474
+ return request(this.authCtx, `/${this.project}/magic-link`, {
475
+ method: "POST",
476
+ body: { email }
477
+ });
478
+ }
479
+ async refresh(refreshToken) {
480
+ if (!this.project) {
481
+ throw new Error("Project name required for token refresh");
482
+ }
483
+ const tokens = await request(this.authCtx, `/${this.project}/refresh`, {
484
+ method: "POST",
485
+ body: { refresh_token: refreshToken }
486
+ });
487
+ if (this.tokenManager) {
488
+ this.tokenManager.setTokens(tokens);
489
+ }
490
+ return tokens;
491
+ }
492
+ logout() {
493
+ if (this.tokenManager) {
494
+ this.tokenManager.clearTokens();
495
+ }
496
+ }
497
+ get me() {
498
+ if (!this.project) {
499
+ throw new Error("Project name required for user operations");
500
+ }
501
+ const project = this.project;
502
+ return {
503
+ get: async () => {
504
+ return request(this.authCtx, `/${project}/me`);
505
+ },
506
+ updateProfile: async (profile) => {
507
+ return request(this.authCtx, `/${project}/me`, {
508
+ method: "PATCH",
509
+ body: { profile }
510
+ });
511
+ },
512
+ changePassword: async (password) => {
513
+ return request(this.authCtx, `/${project}/me/password`, {
514
+ method: "POST",
515
+ body: { password }
516
+ });
517
+ }
518
+ };
519
+ }
520
+ isLoggedIn() {
521
+ return this.tokenManager?.hasTokens() ?? false;
522
+ }
523
+ }
524
+
525
+ // src/services/blob.ts
526
+ class BlobService {
527
+ ctx;
528
+ project;
529
+ blobBaseUrl;
530
+ constructor(ctx, project, options = {}) {
531
+ this.ctx = ctx;
532
+ this.project = project;
533
+ this.blobBaseUrl = options.blobBaseUrl || ctx.baseUrl;
534
+ }
535
+ get blobCtx() {
536
+ return {
537
+ baseUrl: this.blobBaseUrl,
538
+ getAuthHeader: this.ctx.getAuthHeader
539
+ };
540
+ }
541
+ async upload(path, file, contentType) {
542
+ const response = await uploadFile(this.blobCtx, `/${this.project}/${path}`, file, contentType);
543
+ return response.json();
544
+ }
545
+ async download(path) {
546
+ const response = await request(this.blobCtx, `/${this.project}/${path}`);
547
+ return response.blob();
548
+ }
549
+ async stat(path) {
550
+ try {
551
+ const response = await request(this.blobCtx, `/${this.project}/${path}`, {
552
+ method: "HEAD"
553
+ });
554
+ return {
555
+ size: Number(response.headers.get("Content-Length") || 0),
556
+ contentType: response.headers.get("Content-Type") || "application/octet-stream",
557
+ etag: response.headers.get("ETag") || undefined,
558
+ lastModified: response.headers.get("Last-Modified") || undefined
559
+ };
560
+ } catch {
561
+ return null;
562
+ }
563
+ }
564
+ async delete(path) {
565
+ return request(this.blobCtx, `/${this.project}/${path}`, {
566
+ method: "DELETE"
567
+ });
568
+ }
569
+ async getPresignedDownloadUrl(path, expiresIn = 3600) {
570
+ return request(this.blobCtx, `/${this.project}/${path}?presign=${expiresIn}`);
571
+ }
572
+ async getPresignedUploadUrl(path, options = {}) {
573
+ return request(this.blobCtx, `/${this.project}/${path}`, {
574
+ method: "POST",
575
+ body: {
576
+ expiresIn: options.expiresIn || 3600,
577
+ contentType: options.contentType || "application/octet-stream"
578
+ }
579
+ });
580
+ }
581
+ }
582
+
583
+ // src/services/cron.ts
584
+ class CronService {
585
+ ctx;
586
+ project;
587
+ cronBaseUrl;
588
+ constructor(ctx, project, options = {}) {
589
+ this.ctx = ctx;
590
+ this.project = project;
591
+ this.cronBaseUrl = options.cronBaseUrl || ctx.baseUrl;
592
+ }
593
+ get cronCtx() {
594
+ return {
595
+ baseUrl: this.cronBaseUrl,
596
+ getAuthHeader: this.ctx.getAuthHeader
597
+ };
598
+ }
599
+ async listJobs() {
600
+ const res = await request(this.cronCtx, `/${this.project}/jobs`);
601
+ return res.jobs;
602
+ }
603
+ async setJobs(jobs) {
604
+ return request(this.cronCtx, `/${this.project}/jobs`, {
605
+ method: "PUT",
606
+ body: jobs
607
+ });
608
+ }
609
+ async triggerJob(jobId) {
610
+ return request(this.cronCtx, `/${this.project}/jobs/${jobId}/trigger`, { method: "POST" });
611
+ }
612
+ async uploadScript(name, content) {
613
+ const url = `${this.cronBaseUrl}/${this.project}/scripts/${name}`;
614
+ const authHeader = await this.ctx.getAuthHeader();
615
+ const headers = {
616
+ "Content-Type": "text/plain"
617
+ };
618
+ if (authHeader) {
619
+ headers.Authorization = authHeader;
620
+ }
621
+ const response = await fetch(url, {
622
+ method: "PUT",
623
+ headers,
624
+ body: content
625
+ });
626
+ if (!response.ok) {
627
+ const error = await response.json();
628
+ throw new Error(error.error || "Upload failed");
629
+ }
630
+ return response.json();
631
+ }
632
+ async getScript(name) {
633
+ const url = `${this.cronBaseUrl}/${this.project}/scripts/${name}`;
634
+ const authHeader = await this.ctx.getAuthHeader();
635
+ const headers = {};
636
+ if (authHeader) {
637
+ headers.Authorization = authHeader;
638
+ }
639
+ const response = await fetch(url, { headers });
640
+ if (!response.ok) {
641
+ const error = await response.json();
642
+ throw new Error(error.error || "Fetch failed");
643
+ }
644
+ return response.text();
645
+ }
646
+ async deleteScript(name) {
647
+ return request(this.cronCtx, `/${this.project}/scripts/${name}`, {
648
+ method: "DELETE"
649
+ });
650
+ }
651
+ async getRuns(options = {}) {
652
+ const params = new URLSearchParams;
653
+ if (options.limit) {
654
+ params.set("limit", String(options.limit));
655
+ }
656
+ if (options.jobId) {
657
+ params.set("job_id", options.jobId);
658
+ }
659
+ const query = params.toString();
660
+ const path = query ? `/${this.project}/runs?${query}` : `/${this.project}/runs`;
661
+ const res = await request(this.cronCtx, path);
662
+ return res.runs;
663
+ }
664
+ }
665
+
666
+ // src/services/db.ts
667
+ class DbService {
668
+ ctx;
669
+ project;
670
+ dbBaseUrl;
671
+ constructor(ctx, project, options = {}) {
672
+ this.ctx = ctx;
673
+ this.project = project;
674
+ this.dbBaseUrl = options.dbBaseUrl || ctx.baseUrl;
675
+ }
676
+ get dbCtx() {
677
+ return {
678
+ baseUrl: this.dbBaseUrl,
679
+ getAuthHeader: this.ctx.getAuthHeader
680
+ };
681
+ }
682
+ collection(name) {
683
+ const basePath = `/${this.project}/${name}`;
684
+ return {
685
+ list: async (options = {}) => {
686
+ const params = new URLSearchParams;
687
+ if (options.limit !== undefined) {
688
+ params.set("_limit", String(options.limit));
689
+ }
690
+ if (options.cursor) {
691
+ params.set("_cursor", options.cursor);
692
+ }
693
+ if (options.sort) {
694
+ params.set("_sort", options.sort);
695
+ }
696
+ if (options.filter) {
697
+ for (const [key, value] of Object.entries(options.filter)) {
698
+ params.set(key, String(value));
699
+ }
700
+ }
701
+ const query = params.toString();
702
+ const path = query ? `${basePath}?${query}` : basePath;
703
+ return request(this.dbCtx, path);
704
+ },
705
+ get: async (id) => {
706
+ return request(this.dbCtx, `${basePath}/${id}`);
707
+ },
708
+ create: async (data) => {
709
+ return request(this.dbCtx, basePath, {
710
+ method: "POST",
711
+ body: data
712
+ });
713
+ },
714
+ replace: async (id, data) => {
715
+ return request(this.dbCtx, `${basePath}/${id}`, {
716
+ method: "PUT",
717
+ body: data
718
+ });
719
+ },
720
+ patch: async (id, data) => {
721
+ return request(this.dbCtx, `${basePath}/${id}`, {
722
+ method: "PATCH",
723
+ body: data
724
+ });
725
+ },
726
+ delete: async (id) => {
727
+ return request(this.dbCtx, `${basePath}/${id}`, {
728
+ method: "DELETE"
729
+ });
730
+ },
731
+ getSchema: async () => {
732
+ return request(this.dbCtx, `${basePath}/_schema`);
733
+ },
734
+ setSchema: async (schema) => {
735
+ return request(this.dbCtx, `${basePath}/_schema`, {
736
+ method: "PUT",
737
+ body: schema
738
+ });
739
+ }
740
+ };
741
+ }
742
+ async batch(operations) {
743
+ return request(this.dbCtx, `/${this.project}/_batch`, {
744
+ method: "POST",
745
+ body: { ops: operations }
746
+ });
747
+ }
748
+ }
749
+
750
+ // src/services/search.ts
751
+ class SearchService {
752
+ ctx;
753
+ project;
754
+ searchBaseUrl;
755
+ constructor(ctx, project, options = {}) {
756
+ this.ctx = ctx;
757
+ this.project = project;
758
+ this.searchBaseUrl = options.searchBaseUrl || ctx.baseUrl;
759
+ }
760
+ get searchCtx() {
761
+ return {
762
+ baseUrl: this.searchBaseUrl,
763
+ getAuthHeader: this.ctx.getAuthHeader
764
+ };
765
+ }
766
+ index(name) {
767
+ const basePath = `/${this.project}/${name}`;
768
+ return {
769
+ getConfig: async () => {
770
+ return request(this.searchCtx, `${basePath}/_config`);
771
+ },
772
+ setConfig: async (config) => {
773
+ return request(this.searchCtx, `${basePath}/_config`, {
774
+ method: "PUT",
775
+ body: config
776
+ });
777
+ },
778
+ delete: async () => {
779
+ return request(this.searchCtx, `${basePath}/_config`, {
780
+ method: "DELETE"
781
+ });
782
+ },
783
+ index: async (doc) => {
784
+ return request(this.searchCtx, basePath, {
785
+ method: "POST",
786
+ body: doc
787
+ });
788
+ },
789
+ deleteDocument: async (id) => {
790
+ return request(this.searchCtx, `${basePath}/${id}`, {
791
+ method: "DELETE"
792
+ });
793
+ },
794
+ search: async (query, options = {}) => {
795
+ const params = new URLSearchParams({ q: query });
796
+ if (options.limit !== undefined) {
797
+ params.set("limit", String(options.limit));
798
+ }
799
+ const res = await request(this.searchCtx, `${basePath}/search?${params.toString()}`);
800
+ return res.results;
801
+ }
802
+ };
803
+ }
804
+ }
805
+
806
+ // src/utils/token.ts
807
+ var REFRESH_BUFFER_MS = 5000;
808
+ function decodeJwt(token) {
809
+ try {
810
+ const parts = token.split(".");
811
+ if (parts.length !== 3)
812
+ return null;
813
+ const payload = parts[1];
814
+ const decoded = typeof atob !== "undefined" ? atob(payload.replace(/-/g, "+").replace(/_/g, "/")) : Buffer.from(payload, "base64url").toString("utf-8");
815
+ return JSON.parse(decoded);
816
+ } catch {
817
+ return null;
818
+ }
819
+ }
820
+ function isTokenExpired(token, bufferMs = REFRESH_BUFFER_MS) {
821
+ const payload = decodeJwt(token);
822
+ if (!payload?.exp)
823
+ return true;
824
+ const expiresAt = payload.exp * 1000;
825
+ const now = Date.now();
826
+ return now >= expiresAt - bufferMs;
827
+ }
828
+ function getTokenExpiresIn(token) {
829
+ const payload = decodeJwt(token);
830
+ if (!payload?.exp)
831
+ return 0;
832
+ const expiresAt = payload.exp * 1000;
833
+ return Math.max(0, expiresAt - Date.now());
834
+ }
835
+
836
+ class TokenManager {
837
+ accessToken = null;
838
+ refreshToken = null;
839
+ refreshPromise = null;
840
+ refreshFn = null;
841
+ onRefresh = null;
842
+ constructor(options) {
843
+ this.accessToken = options?.accessToken || null;
844
+ this.refreshToken = options?.refreshToken || null;
845
+ this.refreshFn = options?.refreshFn || null;
846
+ this.onRefresh = options?.onRefresh || null;
847
+ }
848
+ setTokens(tokens) {
849
+ this.accessToken = tokens.access_token;
850
+ this.refreshToken = tokens.refresh_token;
851
+ }
852
+ clearTokens() {
853
+ this.accessToken = null;
854
+ this.refreshToken = null;
855
+ }
856
+ hasTokens() {
857
+ return this.accessToken !== null;
858
+ }
859
+ getAccessToken() {
860
+ return this.accessToken;
861
+ }
862
+ async getValidAccessToken() {
863
+ if (!this.accessToken) {
864
+ throw new TokenExpiredError;
865
+ }
866
+ if (!isTokenExpired(this.accessToken)) {
867
+ return this.accessToken;
868
+ }
869
+ if (!this.refreshToken) {
870
+ throw new TokenExpiredError;
871
+ }
872
+ if (!this.refreshFn) {
873
+ throw new TokenExpiredError;
874
+ }
875
+ if (!this.refreshPromise) {
876
+ this.refreshPromise = this.doRefresh();
877
+ }
878
+ try {
879
+ const tokens = await this.refreshPromise;
880
+ return tokens.access_token;
881
+ } finally {
882
+ this.refreshPromise = null;
883
+ }
884
+ }
885
+ async doRefresh() {
886
+ if (!this.refreshToken || !this.refreshFn) {
887
+ throw new TokenExpiredError;
888
+ }
889
+ const tokens = await this.refreshFn(this.refreshToken);
890
+ this.setTokens(tokens);
891
+ if (this.onRefresh) {
892
+ this.onRefresh(tokens);
893
+ }
894
+ return tokens;
895
+ }
896
+ setRefreshFn(fn) {
897
+ this.refreshFn = fn;
898
+ }
899
+ }
900
+
901
+ // src/client.ts
902
+ function isAdminConfig(config) {
903
+ return "adminKey" in config && !!config.adminKey;
904
+ }
905
+ function isBackendConfig(config) {
906
+ return "apiKey" in config && !!config.apiKey;
907
+ }
908
+ function isClientConfig(config) {
909
+ return "project" in config && !("apiKey" in config) && !("adminKey" in config);
910
+ }
911
+
912
+ class VtrivClient {
913
+ config;
914
+ tokenManager = null;
915
+ ctx;
916
+ serviceUrls;
917
+ _auth = null;
918
+ _db = null;
919
+ _blob = null;
920
+ _search = null;
921
+ _ai = null;
922
+ _cron = null;
923
+ constructor(config, serviceUrls = {}) {
924
+ this.config = config;
925
+ this.serviceUrls = serviceUrls;
926
+ if (isClientConfig(config)) {
927
+ this.tokenManager = new TokenManager({
928
+ accessToken: config.accessToken,
929
+ refreshToken: config.refreshToken,
930
+ onRefresh: config.onTokenRefresh
931
+ });
932
+ }
933
+ this.ctx = {
934
+ baseUrl: config.baseUrl,
935
+ getAuthHeader: this.getAuthHeader.bind(this)
936
+ };
937
+ if (this.tokenManager && isClientConfig(config)) {
938
+ this.tokenManager.setRefreshFn(async (refreshToken) => {
939
+ return this.auth.refresh(refreshToken);
940
+ });
941
+ }
942
+ }
943
+ async getAuthHeader() {
944
+ if (isAdminConfig(this.config)) {
945
+ return `Bearer ${this.config.adminKey}`;
946
+ }
947
+ if (isBackendConfig(this.config)) {
948
+ return `Bearer ${this.config.apiKey}`;
949
+ }
950
+ if (this.tokenManager) {
951
+ try {
952
+ const token = await this.tokenManager.getValidAccessToken();
953
+ return `Bearer ${token}`;
954
+ } catch {
955
+ return null;
956
+ }
957
+ }
958
+ return null;
959
+ }
960
+ get project() {
961
+ if (isBackendConfig(this.config) || isClientConfig(this.config)) {
962
+ return this.config.project;
963
+ }
964
+ return null;
965
+ }
966
+ get auth() {
967
+ if (!this._auth) {
968
+ this._auth = new AuthService(this.ctx, {
969
+ tokenManager: this.tokenManager || undefined,
970
+ project: this.project || undefined,
971
+ authBaseUrl: this.serviceUrls.auth
972
+ });
973
+ }
974
+ return this._auth;
975
+ }
976
+ get db() {
977
+ const project = this.project;
978
+ if (!project) {
979
+ throw new Error("DbService requires a project. Use backend or frontend mode.");
980
+ }
981
+ if (!this._db) {
982
+ this._db = new DbService(this.ctx, project, {
983
+ dbBaseUrl: this.serviceUrls.db
984
+ });
985
+ }
986
+ return this._db;
987
+ }
988
+ get blob() {
989
+ const project = this.project;
990
+ if (!project) {
991
+ throw new Error("BlobService requires a project. Use backend or frontend mode.");
992
+ }
993
+ if (!this._blob) {
994
+ this._blob = new BlobService(this.ctx, project, {
995
+ blobBaseUrl: this.serviceUrls.blob
996
+ });
997
+ }
998
+ return this._blob;
999
+ }
1000
+ get search() {
1001
+ const project = this.project;
1002
+ if (!project) {
1003
+ throw new Error("SearchService requires a project. Use backend or frontend mode.");
1004
+ }
1005
+ if (!this._search) {
1006
+ this._search = new SearchService(this.ctx, project, {
1007
+ searchBaseUrl: this.serviceUrls.search
1008
+ });
1009
+ }
1010
+ return this._search;
1011
+ }
1012
+ get ai() {
1013
+ if (!this._ai) {
1014
+ this._ai = new AiService(this.ctx, {
1015
+ aiBaseUrl: this.serviceUrls.ai
1016
+ });
1017
+ }
1018
+ return this._ai;
1019
+ }
1020
+ get cron() {
1021
+ const project = this.project;
1022
+ if (!project) {
1023
+ throw new Error("CronService requires a project. Use backend or frontend mode.");
1024
+ }
1025
+ if (!this._cron) {
1026
+ this._cron = new CronService(this.ctx, project, {
1027
+ cronBaseUrl: this.serviceUrls.cron
1028
+ });
1029
+ }
1030
+ return this._cron;
1031
+ }
1032
+ setTokens(tokens) {
1033
+ if (!this.tokenManager) {
1034
+ throw new Error("Token management only available in frontend mode");
1035
+ }
1036
+ this.tokenManager.setTokens(tokens);
1037
+ }
1038
+ clearTokens() {
1039
+ if (this.tokenManager) {
1040
+ this.tokenManager.clearTokens();
1041
+ }
1042
+ }
1043
+ isAuthenticated() {
1044
+ if (isAdminConfig(this.config) || isBackendConfig(this.config)) {
1045
+ return true;
1046
+ }
1047
+ return this.tokenManager?.hasTokens() ?? false;
1048
+ }
1049
+ getAccessToken() {
1050
+ return this.tokenManager?.getAccessToken() ?? null;
1051
+ }
1052
+ }
1053
+ export {
1054
+ isTokenExpired,
1055
+ getTokenExpiresIn,
1056
+ decodeJwt,
1057
+ VtrivError,
1058
+ VtrivClient,
1059
+ ValidationError,
1060
+ TokenManager,
1061
+ TokenExpiredError,
1062
+ SearchService,
1063
+ RateLimitError,
1064
+ NotFoundError,
1065
+ NetworkError,
1066
+ DbService,
1067
+ CronService,
1068
+ BlobService,
1069
+ AuthorizationError,
1070
+ AuthenticationError,
1071
+ AuthService,
1072
+ AiService
1073
+ };