@xcitedbs/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/dist/client.js ADDED
@@ -0,0 +1,894 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XCiteDBClient = void 0;
4
+ const types_1 = require("./types");
5
+ const websocket_1 = require("./websocket");
6
+ function joinUrl(base, path) {
7
+ const b = base.replace(/\/+$/, '');
8
+ const p = path.startsWith('/') ? path : `/${path}`;
9
+ return `${b}${p}`;
10
+ }
11
+ function buildQuery(params) {
12
+ const sp = new URLSearchParams();
13
+ for (const [k, v] of Object.entries(params)) {
14
+ if (v === undefined || v === '')
15
+ continue;
16
+ sp.set(k, String(v));
17
+ }
18
+ const s = sp.toString();
19
+ return s ? `?${s}` : '';
20
+ }
21
+ class XCiteDBClient {
22
+ constructor(options) {
23
+ this.baseUrl = options.baseUrl.replace(/\/+$/, '');
24
+ this.apiKey = options.apiKey;
25
+ this.accessToken = options.accessToken;
26
+ this.appUserAccessToken = options.appUserAccessToken;
27
+ this.appUserRefreshToken = options.appUserRefreshToken;
28
+ this.defaultContext = options.context ?? {};
29
+ this.platformConsole = options.platformConsole === true;
30
+ this.projectId = options.projectId;
31
+ this.onSessionTokensUpdated = options.onSessionTokensUpdated;
32
+ this.onAppUserTokensUpdated = options.onAppUserTokensUpdated;
33
+ this.onSessionInvalid = options.onSessionInvalid;
34
+ }
35
+ /** True if this client would send API key or Bearer credentials on a normal request. */
36
+ sentAuthCredentials() {
37
+ return !!(this.apiKey || this.accessToken || this.appUserAccessToken);
38
+ }
39
+ /** 401 on these paths is an expected auth flow outcome, not a dead session. */
40
+ isSessionInvalidExcludedPath(path) {
41
+ const base = path.includes('?') ? path.slice(0, path.indexOf('?')) : path;
42
+ switch (base) {
43
+ case '/api/v1/platform/auth/login':
44
+ case '/api/v1/platform/auth/register':
45
+ case '/api/v1/platform/auth/refresh':
46
+ case '/api/v1/platform/auth/change-password':
47
+ case '/api/v1/app/auth/login':
48
+ case '/api/v1/app/auth/register':
49
+ case '/api/v1/app/auth/refresh':
50
+ case '/api/v1/app/auth/oauth/exchange':
51
+ case '/api/v1/app/auth/change-password':
52
+ return true;
53
+ default:
54
+ return false;
55
+ }
56
+ }
57
+ notifySessionInvalidIfNeeded(path, status) {
58
+ if (status !== 401)
59
+ return;
60
+ if (!this.sentAuthCredentials())
61
+ return;
62
+ if (this.isSessionInvalidExcludedPath(path))
63
+ return;
64
+ this.onSessionInvalid?.();
65
+ }
66
+ /** Use platform console JWT routes (`/api/v1/platform/auth/*`) for this client instance. */
67
+ setPlatformConsole(enabled) {
68
+ this.platformConsole = enabled;
69
+ }
70
+ get isPlatformConsole() {
71
+ return this.platformConsole;
72
+ }
73
+ /** Sets active project for platform console mode (`X-Project-Id`). */
74
+ setProjectId(projectId) {
75
+ this.projectId = projectId;
76
+ }
77
+ setContext(ctx) {
78
+ this.defaultContext = { ...this.defaultContext, ...ctx };
79
+ }
80
+ setTokens(access, refresh) {
81
+ this.accessToken = access;
82
+ if (refresh !== undefined)
83
+ this.refreshToken = refresh;
84
+ }
85
+ /** End-user (app) tokens. With developer `accessToken`/`apiKey`, sent as `X-App-User-Token`. */
86
+ setAppUserTokens(access, refresh) {
87
+ this.appUserAccessToken = access;
88
+ if (refresh !== undefined)
89
+ this.appUserRefreshToken = refresh;
90
+ }
91
+ clearAppUserTokens() {
92
+ this.appUserAccessToken = undefined;
93
+ this.appUserRefreshToken = undefined;
94
+ }
95
+ contextHeaders() {
96
+ const h = {};
97
+ const c = this.defaultContext;
98
+ if (c.branch)
99
+ h['X-Branch'] = c.branch;
100
+ if (c.date)
101
+ h['X-Date'] = c.date;
102
+ if (c.prefix)
103
+ h['X-Prefix'] = c.prefix;
104
+ return h;
105
+ }
106
+ /** Include `tenant_id` for public app-auth routes when using only app-user tokens (no developer key/JWT). */
107
+ mergeAppTenant(body) {
108
+ const tid = this.defaultContext.tenant_id;
109
+ if (tid)
110
+ return { ...body, tenant_id: tid };
111
+ return body;
112
+ }
113
+ authHeaders() {
114
+ const h = {};
115
+ if (this.apiKey) {
116
+ h['X-API-Key'] = this.apiKey;
117
+ if (this.appUserAccessToken)
118
+ h['X-App-User-Token'] = this.appUserAccessToken;
119
+ }
120
+ else if (this.accessToken) {
121
+ h['Authorization'] = `Bearer ${this.accessToken}`;
122
+ if (this.appUserAccessToken)
123
+ h['X-App-User-Token'] = this.appUserAccessToken;
124
+ }
125
+ else if (this.appUserAccessToken) {
126
+ h['Authorization'] = `Bearer ${this.appUserAccessToken}`;
127
+ }
128
+ if (this.platformConsole && this.projectId) {
129
+ h['X-Project-Id'] = this.projectId;
130
+ }
131
+ return h;
132
+ }
133
+ async request(method, path, body, extraHeaders, opts) {
134
+ const no401Retry = opts?.no401Retry === true;
135
+ for (let attempt = 0; attempt < 2; attempt++) {
136
+ const url = joinUrl(this.baseUrl, path);
137
+ const headers = {
138
+ ...this.authHeaders(),
139
+ ...this.contextHeaders(),
140
+ ...extraHeaders,
141
+ };
142
+ let init = { method, headers };
143
+ if (body !== undefined) {
144
+ if (typeof body === 'string') {
145
+ init.body = body;
146
+ }
147
+ else {
148
+ headers['Content-Type'] = 'application/json';
149
+ init.body = JSON.stringify(body);
150
+ }
151
+ }
152
+ const res = await fetch(url, init);
153
+ const text = await res.text();
154
+ let data;
155
+ try {
156
+ data = text ? JSON.parse(text) : null;
157
+ }
158
+ catch {
159
+ data = text;
160
+ }
161
+ if (res.ok) {
162
+ return data;
163
+ }
164
+ if (res.status === 401 &&
165
+ attempt === 0 &&
166
+ !no401Retry &&
167
+ (await this.tryRefreshSessionAfter401())) {
168
+ continue;
169
+ }
170
+ const msg = typeof data === 'object' && data !== null && 'message' in data
171
+ ? String(data.message)
172
+ : res.statusText;
173
+ this.notifySessionInvalidIfNeeded(path, res.status);
174
+ throw new types_1.XCiteDBError(msg || `HTTP ${res.status}`, res.status, data);
175
+ }
176
+ this.notifySessionInvalidIfNeeded(path, 401);
177
+ throw new types_1.XCiteDBError('Request failed after retry', 401, null);
178
+ }
179
+ /** Developer Bearer refresh first, then app-user refresh (no API key refresh). */
180
+ async tryRefreshSessionAfter401() {
181
+ if (this.accessToken && this.refreshToken) {
182
+ try {
183
+ await this.refresh();
184
+ return true;
185
+ }
186
+ catch {
187
+ /* fall through */
188
+ }
189
+ }
190
+ if (!this.apiKey && this.appUserAccessToken && this.appUserRefreshToken) {
191
+ try {
192
+ await this.refreshAppUserNoRetry();
193
+ return true;
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+ async refreshAppUserNoRetry() {
202
+ if (!this.appUserRefreshToken)
203
+ throw new Error('No app user refresh token');
204
+ const pair = await this.request('POST', '/api/v1/app/auth/refresh', this.mergeAppTenant({ refresh_token: this.appUserRefreshToken }), undefined, { no401Retry: true });
205
+ this.appUserAccessToken = pair.access_token;
206
+ this.appUserRefreshToken = pair.refresh_token;
207
+ this.onAppUserTokensUpdated?.(pair);
208
+ return pair;
209
+ }
210
+ async health() {
211
+ return this.request('GET', '/api/v1/health');
212
+ }
213
+ async version() {
214
+ return this.request('GET', '/api/v1/version');
215
+ }
216
+ /**
217
+ * Platform console sign-in. The first argument is the account **email** (e.g. `admin@localhost`).
218
+ * Legacy `/api/v1/auth/login` has been removed.
219
+ */
220
+ async login(email, password) {
221
+ return this.platformLogin(email, password);
222
+ }
223
+ /** Platform console sign-in (`email` + `password`). Project context is `X-Project-Id`, not the JWT. */
224
+ async platformLogin(email, password) {
225
+ const pair = await this.request('POST', '/api/v1/platform/auth/login', { email, password });
226
+ this.accessToken = pair.access_token;
227
+ this.refreshToken = pair.refresh_token;
228
+ return pair;
229
+ }
230
+ async refresh() {
231
+ if (!this.refreshToken)
232
+ throw new Error('No refresh token');
233
+ const pair = await this.request('POST', '/api/v1/platform/auth/refresh', {
234
+ refresh_token: this.refreshToken,
235
+ }, undefined, { no401Retry: true });
236
+ this.accessToken = pair.access_token;
237
+ this.refreshToken = pair.refresh_token;
238
+ this.onSessionTokensUpdated?.(pair);
239
+ return pair;
240
+ }
241
+ async logout() {
242
+ await this.request('POST', '/api/v1/platform/auth/logout', {
243
+ refresh_token: this.refreshToken,
244
+ });
245
+ this.accessToken = undefined;
246
+ this.refreshToken = undefined;
247
+ }
248
+ async me() {
249
+ return this.request('GET', '/api/v1/platform/auth/me');
250
+ }
251
+ async platformRegistrationConfig() {
252
+ return this.request('GET', '/api/v1/platform/auth/registration-config');
253
+ }
254
+ async platformRegister(body) {
255
+ return this.request('POST', '/api/v1/platform/auth/register', body);
256
+ }
257
+ async platformWorkspaces() {
258
+ return this.request('GET', '/api/v1/platform/auth/workspaces');
259
+ }
260
+ async listMyTenants() {
261
+ const w = await this.platformWorkspaces();
262
+ const tenants = (w.projects ?? []).map((p) => {
263
+ const r = p;
264
+ return {
265
+ tenant_id: r.tenant_id || r.project_id || '',
266
+ org_id: r.org_id,
267
+ name: r.name,
268
+ status: r.status,
269
+ created_at: r.created_at,
270
+ updated_at: r.updated_at,
271
+ config: r.config,
272
+ owner_user_id: r.owner_user_id,
273
+ };
274
+ });
275
+ if (this.platformConsole && !this.projectId && tenants.length > 0 && tenants[0].tenant_id) {
276
+ this.projectId = tenants[0].tenant_id;
277
+ }
278
+ return tenants;
279
+ }
280
+ /** Alias for {@link listMyTenants} (organization/project terminology). */
281
+ async listMyProjects() {
282
+ return this.listMyTenants();
283
+ }
284
+ /**
285
+ * Switch active tenant/project for API calls. Platform console: updates `X-Project-Id` only (no token exchange).
286
+ * Legacy `/api/v1/auth/switch-tenant` has been removed; non-platform callers should set context instead.
287
+ */
288
+ async switchTenant(tenantId) {
289
+ if (this.platformConsole) {
290
+ this.projectId = tenantId;
291
+ return;
292
+ }
293
+ throw new types_1.XCiteDBError('switchTenant is only supported for platform console clients; set context.tenant_id / use app-user tenant instead', 410, null);
294
+ }
295
+ /** Alias for {@link switchTenant}. */
296
+ async switchProject(projectId) {
297
+ return this.switchTenant(projectId);
298
+ }
299
+ async listApiKeys() {
300
+ return this.request('GET', '/api/v1/project/keys');
301
+ }
302
+ async createApiKey(name, expiresAt = 0, keyType = 'secret') {
303
+ return this.request('POST', '/api/v1/project/keys', {
304
+ name,
305
+ expires_at: expiresAt,
306
+ key_type: keyType,
307
+ });
308
+ }
309
+ async changePassword(currentPassword, newPassword) {
310
+ await this.request('POST', '/api/v1/platform/auth/change-password', {
311
+ current_password: currentPassword,
312
+ new_password: newPassword,
313
+ });
314
+ }
315
+ async revokeApiKey(keyId) {
316
+ await this.request('DELETE', `/api/v1/project/keys/${encodeURIComponent(keyId)}`);
317
+ }
318
+ // --- App user auth (requires developer API key or JWT on the same tenant) ---
319
+ async registerAppUser(email, password, displayName, groups, attributes) {
320
+ const body = { email, password };
321
+ if (displayName !== undefined)
322
+ body.display_name = displayName;
323
+ if (groups !== undefined)
324
+ body.groups = groups;
325
+ if (attributes !== undefined)
326
+ body.attributes = attributes;
327
+ return this.request('POST', '/api/v1/app/auth/register', this.mergeAppTenant(body));
328
+ }
329
+ async getOAuthProviders() {
330
+ const tid = this.defaultContext.tenant_id;
331
+ const q = buildQuery({ tenant_id: tid && String(tid).length > 0 ? String(tid) : 'default' });
332
+ return this.request('GET', `/api/v1/app/auth/oauth/providers${q}`);
333
+ }
334
+ /** Relative path + query for browser navigation to start OAuth (append to API base URL). */
335
+ oauthAuthorizePath(provider) {
336
+ const tid = this.defaultContext.tenant_id && String(this.defaultContext.tenant_id).length > 0
337
+ ? String(this.defaultContext.tenant_id)
338
+ : 'default';
339
+ return `/api/v1/app/auth/oauth/${encodeURIComponent(provider)}/authorize${buildQuery({ tenant_id: tid })}`;
340
+ }
341
+ /** Exchange one-time session code from OAuth browser redirect (public + tenant_id). */
342
+ async exchangeOAuthCode(code) {
343
+ const pair = await this.request('POST', '/api/v1/app/auth/oauth/exchange', this.mergeAppTenant({ code }));
344
+ this.appUserAccessToken = pair.access_token;
345
+ this.appUserRefreshToken = pair.refresh_token;
346
+ return pair;
347
+ }
348
+ async loginAppUser(email, password) {
349
+ const pair = await this.request('POST', '/api/v1/app/auth/login', this.mergeAppTenant({ email, password }));
350
+ this.appUserAccessToken = pair.access_token;
351
+ this.appUserRefreshToken = pair.refresh_token;
352
+ return pair;
353
+ }
354
+ async refreshAppUser() {
355
+ return this.refreshAppUserNoRetry();
356
+ }
357
+ async logoutAppUser() {
358
+ await this.request('POST', '/api/v1/app/auth/logout', this.mergeAppTenant({ refresh_token: this.appUserRefreshToken }));
359
+ this.appUserAccessToken = undefined;
360
+ this.appUserRefreshToken = undefined;
361
+ }
362
+ async appUserMe() {
363
+ return this.request('GET', '/api/v1/app/auth/me');
364
+ }
365
+ async updateAppUserProfile(displayName, attributes) {
366
+ const body = {};
367
+ if (displayName !== undefined)
368
+ body.display_name = displayName;
369
+ if (attributes !== undefined)
370
+ body.attributes = attributes;
371
+ return this.request('PUT', '/api/v1/app/auth/me', body);
372
+ }
373
+ async exchangeCustomToken(token) {
374
+ const pair = await this.request('POST', '/api/v1/app/auth/custom-token', { token });
375
+ this.appUserAccessToken = pair.access_token;
376
+ this.appUserRefreshToken = pair.refresh_token;
377
+ return pair;
378
+ }
379
+ /** Change app-user password (requires valid app-user access token). */
380
+ async changeAppUserPassword(currentPassword, newPassword) {
381
+ await this.request('POST', '/api/v1/app/auth/change-password', {
382
+ current_password: currentPassword,
383
+ new_password: newPassword,
384
+ });
385
+ }
386
+ /** Request a password-reset token (developer-authenticated). Token omitted when delivery is smtp/webhook success. */
387
+ async forgotAppUserPassword(email) {
388
+ return this.request('POST', '/api/v1/app/auth/forgot-password', { email });
389
+ }
390
+ /** Complete password reset with token from `forgotAppUserPassword` (public; set `context.tenant_id` if no developer auth). */
391
+ async resetAppUserPassword(token, newPassword) {
392
+ await this.request('POST', '/api/v1/app/auth/reset-password', this.mergeAppTenant({ token, new_password: newPassword }));
393
+ }
394
+ /** Issue email verification token (developer-authenticated). Token omitted when delivery is smtp/webhook success. */
395
+ async sendAppUserVerification(userId) {
396
+ return this.request('POST', '/api/v1/app/auth/send-verification', {
397
+ user_id: userId,
398
+ });
399
+ }
400
+ async getAppAuthConfig() {
401
+ return this.request('GET', '/api/v1/app/auth/config');
402
+ }
403
+ async getEmailConfig() {
404
+ return this.request('GET', '/api/v1/app/email/config');
405
+ }
406
+ async updateEmailConfig(config) {
407
+ return this.request('PUT', '/api/v1/app/email/config', config);
408
+ }
409
+ async getEmailTemplates() {
410
+ return this.request('GET', '/api/v1/app/email/templates');
411
+ }
412
+ async updateEmailTemplates(templates) {
413
+ return this.request('PUT', '/api/v1/app/email/templates', templates);
414
+ }
415
+ async sendTestEmail(to) {
416
+ return this.request('POST', '/api/v1/app/email/test', { to });
417
+ }
418
+ /** Verify email with token (public; set `context.tenant_id` if no developer auth). */
419
+ async verifyAppUserEmail(token) {
420
+ await this.request('POST', '/api/v1/app/auth/verify-email', this.mergeAppTenant({ token }));
421
+ }
422
+ // --- App user management (developer admin/editor) ---
423
+ async listAppUsers() {
424
+ return this.request('GET', '/api/v1/app/users');
425
+ }
426
+ async getAppUser(userId) {
427
+ return this.request('GET', `/api/v1/app/users/${encodeURIComponent(userId)}`);
428
+ }
429
+ async createAppUser(email, password, displayName, groups, attributes) {
430
+ const body = { email, password };
431
+ if (displayName !== undefined)
432
+ body.display_name = displayName;
433
+ if (groups !== undefined)
434
+ body.groups = groups;
435
+ if (attributes !== undefined)
436
+ body.attributes = attributes;
437
+ return this.request('POST', '/api/v1/app/users', body);
438
+ }
439
+ async deleteAppUser(userId) {
440
+ await this.request('DELETE', `/api/v1/app/users/${encodeURIComponent(userId)}`);
441
+ }
442
+ async updateAppUserGroups(userId, groups) {
443
+ await this.request('PUT', `/api/v1/app/users/${encodeURIComponent(userId)}/groups`, { groups });
444
+ }
445
+ async updateAppUserStatus(userId, status) {
446
+ await this.request('PUT', `/api/v1/app/users/${encodeURIComponent(userId)}/status`, { status });
447
+ }
448
+ // --- Security policies (developer admin/editor) ---
449
+ async createPolicy(policyId, policy) {
450
+ return this.request('POST', '/api/v1/security/policies', {
451
+ policy_id: policyId,
452
+ policy,
453
+ });
454
+ }
455
+ async listPolicies() {
456
+ const r = await this.request('GET', '/api/v1/security/policies');
457
+ if (r == null || typeof r !== 'object' || Array.isArray(r))
458
+ return {};
459
+ return r;
460
+ }
461
+ async getPolicy(policyId) {
462
+ return this.request('GET', `/api/v1/security/policies/${encodeURIComponent(policyId)}`);
463
+ }
464
+ async updatePolicy(policyId, policy) {
465
+ return this.request('PUT', `/api/v1/security/policies/${encodeURIComponent(policyId)}`, policy);
466
+ }
467
+ async deletePolicy(policyId) {
468
+ await this.request('DELETE', `/api/v1/security/policies/${encodeURIComponent(policyId)}`);
469
+ }
470
+ // --- Triggers (developer admin/editor) ---
471
+ async upsertTrigger(triggerId, trigger) {
472
+ return this.request('POST', '/api/v1/triggers', {
473
+ trigger_id: triggerId,
474
+ trigger,
475
+ });
476
+ }
477
+ async listTriggers() {
478
+ const r = await this.request('GET', '/api/v1/triggers');
479
+ if (r == null || typeof r !== 'object' || Array.isArray(r))
480
+ return {};
481
+ return r;
482
+ }
483
+ async getTrigger(name) {
484
+ const q = buildQuery({ name });
485
+ return this.request('GET', `/api/v1/triggers${q}`);
486
+ }
487
+ async deleteTrigger(name) {
488
+ const q = buildQuery({ name });
489
+ await this.request('DELETE', `/api/v1/triggers${q}`);
490
+ }
491
+ async checkAccess(subject, identifier, action, metaPath, branch) {
492
+ const body = { subject, identifier, action };
493
+ if (metaPath !== undefined)
494
+ body.meta_path = metaPath;
495
+ if (branch !== undefined)
496
+ body.branch = branch;
497
+ return this.request('POST', '/api/v1/security/check', body);
498
+ }
499
+ async getSecurityConfig() {
500
+ return this.request('GET', '/api/v1/security/config');
501
+ }
502
+ async updateSecurityConfig(config) {
503
+ await this.request('PUT', '/api/v1/security/config', config);
504
+ }
505
+ async createBranch(name, fromBranch, fromDate) {
506
+ const body = { name };
507
+ if (fromBranch)
508
+ body.from_branch = fromBranch;
509
+ if (fromDate)
510
+ body.from_date = fromDate;
511
+ await this.request('POST', '/api/v1/branches', body);
512
+ }
513
+ async deleteBranch(name) {
514
+ await this.request('DELETE', `/api/v1/branches/${encodeURIComponent(name)}`);
515
+ }
516
+ async deleteRevision(branch, date) {
517
+ await this.request('DELETE', `/api/v1/branches/${encodeURIComponent(branch)}/revisions/${encodeURIComponent(date)}`);
518
+ }
519
+ async listBranches() {
520
+ const r = await this.request('GET', '/api/v1/branches');
521
+ return r.branches ?? [];
522
+ }
523
+ async getBranch(name) {
524
+ const r = await this.request('GET', `/api/v1/branches/${encodeURIComponent(name)}`);
525
+ return r.branch;
526
+ }
527
+ async createCommit(message, author) {
528
+ const body = { message };
529
+ if (author)
530
+ body.author = author;
531
+ const r = await this.request('POST', '/api/v1/commits', body);
532
+ return r.commit;
533
+ }
534
+ async listCommits(options) {
535
+ const q = buildQuery({
536
+ branch: options?.branch,
537
+ limit: options?.limit,
538
+ offset: options?.offset,
539
+ });
540
+ return this.request('GET', `/api/v1/commits${q}`);
541
+ }
542
+ async getCommit(commitId) {
543
+ const r = await this.request('GET', `/api/v1/commits/${encodeURIComponent(commitId)}`);
544
+ return r.commit;
545
+ }
546
+ async rollbackToCommit(commitId, _confirm) {
547
+ return this.request('POST', `/api/v1/commits/${encodeURIComponent(commitId)}/rollback`, {
548
+ confirm: true,
549
+ });
550
+ }
551
+ async cherryPick(commitId, message, author) {
552
+ const body = {};
553
+ if (message)
554
+ body.message = message;
555
+ if (author)
556
+ body.author = author;
557
+ const r = await this.request('POST', `/api/v1/commits/${encodeURIComponent(commitId)}/cherry-pick`, body);
558
+ return r.commit;
559
+ }
560
+ async createTag(name, commitId, message, author) {
561
+ const body = { name, commit_id: commitId };
562
+ if (message)
563
+ body.message = message;
564
+ if (author)
565
+ body.author = author;
566
+ const r = await this.request('POST', '/api/v1/tags', body);
567
+ return r.tag;
568
+ }
569
+ async listTags(options) {
570
+ const q = buildQuery({ limit: options?.limit, offset: options?.offset });
571
+ return this.request('GET', `/api/v1/tags${q}`);
572
+ }
573
+ async getTag(name) {
574
+ const r = await this.request('GET', `/api/v1/tags/${encodeURIComponent(name)}`);
575
+ return r.tag;
576
+ }
577
+ async deleteTag(name) {
578
+ await this.request('DELETE', `/api/v1/tags/${encodeURIComponent(name)}`);
579
+ }
580
+ async diff(from, to, includeContent) {
581
+ return this.request('POST', '/api/v1/diff', {
582
+ from,
583
+ to,
584
+ include_content: includeContent ?? false,
585
+ });
586
+ }
587
+ async mergeBranch(targetBranch, sourceBranch, options) {
588
+ const body = { source_branch: sourceBranch };
589
+ if (options?.message)
590
+ body.message = options.message;
591
+ body.auto_resolve = options?.autoResolve ?? 'none';
592
+ return this.request('POST', `/api/v1/branches/${encodeURIComponent(targetBranch)}/merge`, body);
593
+ }
594
+ /** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeDocumentJson`. */
595
+ async writeXML(xml, _options) {
596
+ await this.request('POST', '/api/v1/documents', xml, {
597
+ 'Content-Type': 'application/xml',
598
+ });
599
+ }
600
+ async writeDocumentJson(xml, options) {
601
+ await this.request('POST', '/api/v1/documents', {
602
+ xml,
603
+ is_top: options?.is_top ?? true,
604
+ compare_attributes: options?.compare_attributes ?? false,
605
+ });
606
+ }
607
+ async queryByIdentifier(identifier, flags, filter, pathFilter) {
608
+ const q = buildQuery({
609
+ identifier,
610
+ flags: flags,
611
+ filter,
612
+ path_filter: pathFilter,
613
+ });
614
+ return this.request('GET', `/api/v1/documents/by-id${q}`);
615
+ }
616
+ async queryDocuments(query, flags, filter, pathFilter) {
617
+ const params = {
618
+ match: query.match,
619
+ match_start: query.match_start,
620
+ match_end: query.match_end,
621
+ regex: query.regex,
622
+ flags: flags,
623
+ filter,
624
+ path_filter: pathFilter,
625
+ };
626
+ if (query.contains !== undefined) {
627
+ const c = Array.isArray(query.contains) ? query.contains.join(',') : query.contains;
628
+ params.contains = c;
629
+ }
630
+ if (query.required_meta_paths !== undefined && query.required_meta_paths.length > 0) {
631
+ params.required_meta_paths = query.required_meta_paths.join(',');
632
+ }
633
+ if (query.filter_any_meta === true) {
634
+ params.any_meta = '1';
635
+ }
636
+ return this.request('GET', `/api/v1/documents${buildQuery(params)}`);
637
+ }
638
+ async deleteDocument(identifier) {
639
+ await this.request('DELETE', `/api/v1/documents/by-id${buildQuery({ identifier })}`);
640
+ }
641
+ async addIdentifier(identifier) {
642
+ const r = await this.request('POST', '/api/v1/documents/identifiers', {
643
+ identifier,
644
+ });
645
+ return r?.ok !== false;
646
+ }
647
+ async addAlias(original, alias) {
648
+ const r = await this.request('POST', '/api/v1/documents/identifiers/alias', { original, alias });
649
+ return r?.ok !== false;
650
+ }
651
+ async queryChangeDate(identifier) {
652
+ const r = await this.request('GET', `/api/v1/documents/change-date${buildQuery({ identifier })}`);
653
+ return r?.change_date ?? r?.date ?? '';
654
+ }
655
+ async getXcitepath(identifier) {
656
+ const r = await this.request('GET', `/api/v1/documents/xcitepath${buildQuery({ identifier })}`);
657
+ return r?.xcitepath ?? '';
658
+ }
659
+ async changedIdentifiers(branch, fromDate, toDate) {
660
+ return this.request('GET', `/api/v1/documents/changed${buildQuery({ branch, from_date: fromDate, to_date: toDate })}`);
661
+ }
662
+ async listIdentifiers(query) {
663
+ const params = {
664
+ match: query.match,
665
+ match_start: query.match_start,
666
+ match_end: query.match_end,
667
+ regex: query.regex,
668
+ limit: query.limit,
669
+ offset: query.offset,
670
+ };
671
+ if (query.contains !== undefined) {
672
+ params.contains = Array.isArray(query.contains)
673
+ ? query.contains.join(',')
674
+ : query.contains;
675
+ }
676
+ if (query.required_meta_paths !== undefined && query.required_meta_paths.length > 0) {
677
+ params.required_meta_paths = query.required_meta_paths.join(',');
678
+ }
679
+ if (query.filter_any_meta === true) {
680
+ params.any_meta = '1';
681
+ }
682
+ const data = await this.request('GET', `/api/v1/documents/identifiers${buildQuery(params)}`);
683
+ // Older servers returned a bare string[]; paginated API returns { identifiers, total, offset, limit }.
684
+ if (Array.isArray(data)) {
685
+ return {
686
+ identifiers: data,
687
+ total: data.length,
688
+ offset: 0,
689
+ limit: data.length,
690
+ };
691
+ }
692
+ if (data !== null && typeof data === 'object') {
693
+ const o = data;
694
+ const ids = Array.isArray(o.identifiers) ? o.identifiers : [];
695
+ return {
696
+ identifiers: ids,
697
+ total: typeof o.total === 'number' ? o.total : ids.length,
698
+ offset: typeof o.offset === 'number' ? o.offset : 0,
699
+ limit: typeof o.limit === 'number' ? o.limit : ids.length,
700
+ };
701
+ }
702
+ return { identifiers: [], total: 0, offset: 0, limit: 0 };
703
+ }
704
+ async listIdentifierChildren(parentPath) {
705
+ const params = {};
706
+ if (parentPath !== undefined && parentPath !== '') {
707
+ params.parent_path = parentPath;
708
+ }
709
+ const data = await this.request('GET', `/api/v1/documents/identifier-children${buildQuery(params)}`);
710
+ if (data !== null && typeof data === 'object') {
711
+ const o = data;
712
+ const raw = o.children;
713
+ const children = [];
714
+ if (Array.isArray(raw)) {
715
+ for (const row of raw) {
716
+ if (row !== null && typeof row === 'object') {
717
+ const r = row;
718
+ children.push({
719
+ segment: typeof r.segment === 'string' ? r.segment : '',
720
+ full_path: typeof r.full_path === 'string' ? r.full_path : '',
721
+ is_identifier: r.is_identifier === true,
722
+ has_children: r.has_children === true,
723
+ });
724
+ }
725
+ }
726
+ }
727
+ return {
728
+ parent_path: typeof o.parent_path === 'string' ? o.parent_path : '',
729
+ parent_is_identifier: o.parent_is_identifier === true,
730
+ hierarchy_index_available: o.hierarchy_index_available === true,
731
+ children,
732
+ };
733
+ }
734
+ return {
735
+ parent_path: '',
736
+ parent_is_identifier: false,
737
+ hierarchy_index_available: false,
738
+ children: [],
739
+ };
740
+ }
741
+ async queryLog(query, fromDate, toDate) {
742
+ const params = {
743
+ match_start: query.match_start,
744
+ match: query.match,
745
+ from_date: fromDate,
746
+ to_date: toDate,
747
+ };
748
+ return this.request('GET', `/api/v1/documents/log${buildQuery(params)}`);
749
+ }
750
+ async addMeta(identifier, value, path = '', opts) {
751
+ const body = { identifier, value, path };
752
+ if (opts?.mode === 'append')
753
+ body.mode = 'append';
754
+ const r = await this.request('POST', '/api/v1/meta', body);
755
+ return r?.ok !== false;
756
+ }
757
+ async addMetaByQuery(query, value, path = '', firstMatch = false, opts) {
758
+ const body = {
759
+ query,
760
+ value,
761
+ path,
762
+ first_match: firstMatch,
763
+ };
764
+ if (opts?.mode === 'append')
765
+ body.mode = 'append';
766
+ const r = await this.request('POST', '/api/v1/meta', body);
767
+ return r?.ok !== false;
768
+ }
769
+ async appendMeta(identifier, value, path = '') {
770
+ return this.addMeta(identifier, value, path, { mode: 'append' });
771
+ }
772
+ async appendMetaByQuery(query, value, path = '', firstMatch = false) {
773
+ return this.addMetaByQuery(query, value, path, firstMatch, { mode: 'append' });
774
+ }
775
+ async queryMeta(identifier, path = '') {
776
+ return this.request('GET', `/api/v1/meta${buildQuery({ identifier, path })}`);
777
+ }
778
+ async queryMetaByQuery(query, path = '') {
779
+ return this.request('POST', '/api/v1/meta/query', { query, path });
780
+ }
781
+ async clearMeta(query) {
782
+ const r = await this.request('DELETE', '/api/v1/meta', { query });
783
+ return r?.ok !== false;
784
+ }
785
+ async acquireLock(identifier, expires = 0) {
786
+ return this.request('POST', '/api/v1/locks', { identifier, expires });
787
+ }
788
+ async releaseLock(identifier, lockId) {
789
+ const r = await this.request('DELETE', '/api/v1/locks', {
790
+ identifier,
791
+ lock_id: lockId,
792
+ });
793
+ return r?.ok !== false;
794
+ }
795
+ async findLocks(identifier) {
796
+ return this.request('GET', `/api/v1/locks${buildQuery({ identifier })}`);
797
+ }
798
+ async unquery(query, unquery) {
799
+ return this.request('POST', '/api/v1/unquery', { query, unquery });
800
+ }
801
+ async search(q) {
802
+ const body = { query: q.query };
803
+ if (q.doc_types?.length)
804
+ body.doc_types = q.doc_types;
805
+ if (q.branch !== undefined)
806
+ body.branch = q.branch;
807
+ if (q.offset !== undefined)
808
+ body.offset = q.offset;
809
+ if (q.limit !== undefined)
810
+ body.limit = q.limit;
811
+ const data = await this.request('POST', '/api/v1/search', body);
812
+ const hits = [];
813
+ if (Array.isArray(data.hits)) {
814
+ for (const h of data.hits) {
815
+ if (h && typeof h === 'object') {
816
+ const o = h;
817
+ const hit = {
818
+ identifier: String(o.identifier ?? ''),
819
+ path: String(o.path ?? ''),
820
+ doc_type: o.doc_type === 'json' ? 'json' : 'xml',
821
+ branch: String(o.branch ?? ''),
822
+ snippet: String(o.snippet ?? ''),
823
+ score: typeof o.score === 'number' ? o.score : 0,
824
+ };
825
+ if (typeof o.xcitepath === 'string' && o.xcitepath.length > 0) {
826
+ hit.xcitepath = o.xcitepath;
827
+ }
828
+ hits.push(hit);
829
+ }
830
+ }
831
+ }
832
+ return {
833
+ hits,
834
+ total: typeof data.total === 'number' ? data.total : hits.length,
835
+ query: typeof data.query === 'string' ? data.query : q.query,
836
+ };
837
+ }
838
+ async reindex() {
839
+ return this.request('POST', '/api/v1/search/reindex', {});
840
+ }
841
+ async writeJsonDocument(identifier, data) {
842
+ await this.request('POST', '/api/v1/json-documents', { identifier, data });
843
+ }
844
+ async readJsonDocument(identifier) {
845
+ return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier })}`);
846
+ }
847
+ async deleteJsonDocument(identifier) {
848
+ await this.request('DELETE', `/api/v1/json-documents${buildQuery({ identifier })}`);
849
+ }
850
+ async listJsonDocuments(match, limit, offset) {
851
+ const data = await this.request('GET', `/api/v1/json-documents/list${buildQuery({ match, limit, offset })}`);
852
+ if (Array.isArray(data)) {
853
+ const identifiers = data;
854
+ return { identifiers, total: identifiers.length, offset: 0, limit: identifiers.length };
855
+ }
856
+ if (data !== null && typeof data === 'object') {
857
+ const o = data;
858
+ const ids = Array.isArray(o.identifiers) ? o.identifiers : [];
859
+ return {
860
+ identifiers: ids,
861
+ total: typeof o.total === 'number' ? o.total : ids.length,
862
+ offset: typeof o.offset === 'number' ? o.offset : 0,
863
+ limit: typeof o.limit === 'number' ? o.limit : ids.length,
864
+ };
865
+ }
866
+ return { identifiers: [], total: 0, offset: 0, limit: 0 };
867
+ }
868
+ /**
869
+ * WebSocket `/api/v1/ws` — optional initial subscription pattern.
870
+ * Uses `access_token` or `api_key` query params when headers are not available (browser).
871
+ */
872
+ subscribe(options, callback, onError) {
873
+ let httpBase = this.baseUrl;
874
+ if (!httpBase && typeof globalThis !== 'undefined') {
875
+ const loc = globalThis.location;
876
+ if (loc?.origin) {
877
+ httpBase = loc.origin;
878
+ }
879
+ }
880
+ const wsBase = httpBase.replace(/^http/, 'ws');
881
+ const headers = {
882
+ ...this.authHeaders(),
883
+ ...this.contextHeaders(),
884
+ };
885
+ const sub = new websocket_1.WebSocketSubscription(wsBase, headers);
886
+ sub.onMessage(callback);
887
+ if (onError)
888
+ sub.onError(onError);
889
+ sub.connect();
890
+ sub.subscribePattern(options);
891
+ return sub;
892
+ }
893
+ }
894
+ exports.XCiteDBClient = XCiteDBClient;