@xcitedbs/client 0.2.9 → 0.2.11

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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { XCiteDBClient } from './client';
2
2
  export { WebSocketSubscription } from './websocket';
3
- export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, DatabaseContext, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
3
+ export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationOptions, WorkspaceInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
4
  export { XCiteDBError } from './types';
package/dist/types.d.ts CHANGED
@@ -41,6 +41,12 @@ export interface TextSearchQuery {
41
41
  branch?: string;
42
42
  offset?: number;
43
43
  limit?: number;
44
+ /** Default `fts`. `semantic` / `hybrid` require vector search (and hybrid requires FTS). */
45
+ mode?: 'fts' | 'semantic' | 'hybrid';
46
+ /** Minimum cosine similarity for semantic / hybrid vector leg (default 0.3). */
47
+ min_score?: number;
48
+ /** Hybrid only: weight of semantic vs FTS in weighted RRF (0–1, default 0.5). */
49
+ semantic_weight?: number;
44
50
  }
45
51
  export interface TextSearchHit {
46
52
  identifier: string;
@@ -51,12 +57,138 @@ export interface TextSearchHit {
51
57
  branch: string;
52
58
  snippet: string;
53
59
  score: number;
60
+ /** Present for semantic / hybrid search. */
61
+ source?: 'fts' | 'semantic' | 'both';
54
62
  }
55
63
  export interface TextSearchResult {
56
64
  hits: TextSearchHit[];
57
65
  total: number;
58
66
  query: string;
59
67
  }
68
+ /** Progress object when the server is indexing (FTS or vector). */
69
+ export interface SearchIndexingProgress {
70
+ done: number;
71
+ total: number;
72
+ }
73
+ /** Last finished vector re-embed job (`vector_index_job` in project settings). */
74
+ export interface VectorIndexingLastJob {
75
+ ok?: boolean;
76
+ finished_at_ms?: number;
77
+ message?: string;
78
+ detail?: string;
79
+ phase?: string;
80
+ rows_indexed?: number;
81
+ }
82
+ /**
83
+ * Vector index workload + indicative cost.
84
+ * Blocking scan: `GET /api/v1/project/settings/search/vector-index-estimate` (no `session`).
85
+ * Incremental UI: `POST` same path, then `GET` / `DELETE` with `?session=`.
86
+ */
87
+ export interface VectorIndexEstimate {
88
+ document_count?: number;
89
+ chunk_count?: number;
90
+ approx_input_tokens?: number;
91
+ approx_input_tokens_xml?: number;
92
+ approx_input_tokens_json?: number;
93
+ /** Serialized XML on disk (only docs that produced ≥1 embed chunk). */
94
+ xml_source_serialized_bytes_total?: number;
95
+ /** UTF-8 bytes of XML embed text (text nodes only; same as embedding input). */
96
+ xml_embed_text_bytes_total?: number;
97
+ /** Raw JSON document bytes (only docs that produced ≥1 embed chunk). */
98
+ json_source_bytes_total?: number;
99
+ /** UTF-8 bytes of JSON scalar chunks sent to the embedder. */
100
+ json_embed_text_bytes_total?: number;
101
+ token_estimate_note?: string;
102
+ embedding_provider?: string;
103
+ embedding_model?: string;
104
+ embedding_configured?: boolean;
105
+ pricing_info_url?: string;
106
+ /** Null when unknown (e.g. openai_compatible). */
107
+ usd_per_million_tokens_indicative?: number | null;
108
+ estimated_cost_usd_indicative?: number | null;
109
+ cost_disclaimer?: string;
110
+ /** Async session responses only (`POST` / `GET ?session=`). */
111
+ session_id?: string;
112
+ branches_done?: number;
113
+ branches_total?: number;
114
+ estimate_complete?: boolean;
115
+ estimate_cancelled?: boolean;
116
+ estimate_error?: string;
117
+ }
118
+ /** `GET /api/v1/project/settings/search` (and returned after `PUT`). */
119
+ export interface ProjectSearchSettings {
120
+ search_enabled?: boolean;
121
+ search_language?: string;
122
+ indexing_status?: 'indexing' | 'idle';
123
+ indexing_progress?: SearchIndexingProgress;
124
+ vector_search_enabled?: boolean;
125
+ embedding_provider?: string;
126
+ embedding_model?: string;
127
+ embedding_base_url?: string;
128
+ llm_provider?: string;
129
+ llm_model?: string;
130
+ llm_base_url?: string;
131
+ embedding_configured?: boolean;
132
+ llm_configured?: boolean;
133
+ vector_indexing_status?: 'indexing' | 'scheduled' | 'idle';
134
+ vector_indexing_progress?: SearchIndexingProgress | null;
135
+ vector_indexing_last_job?: VectorIndexingLastJob | null;
136
+ [key: string]: unknown;
137
+ }
138
+ /**
139
+ * `PUT /api/v1/project/settings/search` body (all keys optional).
140
+ * `embedding_api_key` / `llm_api_key` are write-only: send a new secret or empty string to clear.
141
+ * While `vector_search_enabled` is true, changing embedding provider/model/base URL triggers a full vector re-embed on the server.
142
+ */
143
+ export interface ProjectSearchSettingsUpdate {
144
+ search_enabled?: boolean;
145
+ search_language?: string;
146
+ vector_search_enabled?: boolean;
147
+ embedding_provider?: string;
148
+ embedding_model?: string;
149
+ embedding_base_url?: string | null;
150
+ embedding_api_key?: string;
151
+ llm_provider?: string;
152
+ llm_model?: string;
153
+ llm_base_url?: string | null;
154
+ llm_api_key?: string;
155
+ }
156
+ /** `POST /api/v1/rag/query` (non-streaming: set `stream: false` or omit). */
157
+ export interface RagQueryOptions {
158
+ question: string;
159
+ branch?: string;
160
+ max_context_docs?: number;
161
+ doc_types?: ('xml' | 'json')[];
162
+ /** Default `false` for JSON response with `answer` and `sources`. */
163
+ stream?: boolean;
164
+ /** Default `auto`: hybrid if FTS+vector enabled, else best available mode. */
165
+ search_mode?: 'auto' | 'hybrid' | 'semantic' | 'fts';
166
+ /** Minimum cosine similarity for semantic / hybrid vector leg (default 0.35). */
167
+ min_score?: number;
168
+ /** Hybrid retrieval only: semantic vs FTS weight in weighted RRF (0–1, default 0.5). */
169
+ semantic_weight?: number;
170
+ }
171
+ export interface RagQueryResult {
172
+ answer: string;
173
+ sources: unknown;
174
+ }
175
+ /** One SSE JSON payload from streaming RAG (`stream: true`). */
176
+ export type RagStreamEvent = {
177
+ text: string;
178
+ done: false;
179
+ sources?: undefined;
180
+ error?: undefined;
181
+ } | {
182
+ text?: string;
183
+ done: true;
184
+ sources?: unknown;
185
+ error?: undefined;
186
+ } | {
187
+ error: string;
188
+ done: true;
189
+ text?: undefined;
190
+ sources?: undefined;
191
+ };
60
192
  /** Response from `GET /api/v1/documents/identifiers` */
61
193
  export interface ListIdentifiersResult {
62
194
  identifiers: string[];
@@ -175,6 +307,12 @@ export interface ApiKeyInfo {
175
307
  }
176
308
  export type Flags = 'None' | 'FirstMatch' | 'IncludeChildren' | 'NoChildren' | 'KeepIndexNodes' | 'PrevIfDeleted' | string;
177
309
  export interface DatabaseContext {
310
+ /**
311
+ * Workspace name (isolated editing environment). Empty / unset = main timeline.
312
+ * Sent as `X-Workspace` (preferred); see also {@link DatabaseContext.branch}.
313
+ */
314
+ workspace?: string;
315
+ /** @deprecated Prefer {@link DatabaseContext.workspace}. Sent as `X-Branch` when `workspace` is unset. */
178
316
  branch?: string;
179
317
  date?: string;
180
318
  prefix?: string;
@@ -206,6 +344,24 @@ export interface RealtimeEvent {
206
344
  timestamp?: number;
207
345
  tenant_id?: string;
208
346
  }
347
+ /** When enabled, prefixes document/meta paths with the app user's namespace (e.g. `/users/{userId}/…`) for convenience; ABAC still enforces access on the server. */
348
+ export interface UserIsolationOptions {
349
+ enabled: boolean;
350
+ /** Template with `{userId}` or server-style `${user.id}` replaced from JWT or login response; default `/users/{userId}`. */
351
+ namespace?: string;
352
+ /** Paths left unchanged by prefixing (optional; {@link XCiteDBClient.enableUserIsolation} fills from server). */
353
+ shared_read_paths?: string[];
354
+ shared_write_paths?: string[];
355
+ }
356
+ /** Effective user isolation settings from `GET /api/v1/security/user-isolation`. */
357
+ export interface UserIsolationConfig {
358
+ enabled: boolean;
359
+ namespace_pattern: string;
360
+ shared_read_paths: string[];
361
+ shared_write_paths: string[];
362
+ shared_write_groups: string[];
363
+ generated_policies?: string[];
364
+ }
209
365
  export interface XCiteDBClientOptions {
210
366
  baseUrl: string;
211
367
  apiKey?: string;
@@ -234,6 +390,8 @@ export interface XCiteDBClientOptions {
234
390
  testSessionToken?: string;
235
391
  /** When true with `testSessionToken`, sends `X-Test-Auth: required` so real credentials are validated. */
236
392
  testRequireAuth?: boolean;
393
+ /** Auto-prefix identifiers for app-user sessions (see {@link UserIsolationOptions}). */
394
+ userIsolation?: UserIsolationOptions;
237
395
  }
238
396
  /** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
239
397
  export interface CreateTestSessionOptions {
@@ -250,6 +408,7 @@ export interface CreateTestSessionOptions {
250
408
  onSessionTokensUpdated?: (pair: TokenPair) => void;
251
409
  onAppUserTokensUpdated?: (pair: AppUserTokenPair) => void;
252
410
  onSessionInvalid?: () => void;
411
+ userIsolation?: UserIsolationOptions;
253
412
  }
254
413
  /** Application user (tenant-scoped), distinct from developer users. */
255
414
  export interface AppUser {
@@ -443,83 +602,115 @@ export interface StoredTriggerResponse {
443
602
  trigger_id: string;
444
603
  trigger: TriggerDefinition;
445
604
  }
446
- /** Version control / commit metadata (git-like layer). */
447
- export interface CommitRecord {
605
+ /** Named snapshot on a workspace (checkpoint). */
606
+ export interface CheckpointRecord {
448
607
  id: string;
608
+ /** Same as `id` when returned by newer APIs. */
609
+ checkpoint_id?: string;
449
610
  branch: string;
450
- /** Internal LMDB date key for this commit snapshot. */
451
- date_key: string;
452
- date_human?: string;
453
- /** Alias for human-readable date (same as date_human when present). */
611
+ /** Human-readable revision date/time. */
454
612
  date?: string;
613
+ date_human?: string;
614
+ /** @deprecated Internal key; omitted from newer API responses. */
615
+ date_key?: string;
455
616
  message: string;
456
617
  author: string;
457
618
  parent_id: string | null;
458
619
  merge_source?: {
459
620
  branch: string;
460
- commit_id: string;
621
+ checkpoint_id?: string;
622
+ commit_id?: string;
461
623
  };
462
624
  cherry_picked_from?: string;
463
625
  affected_identifiers: string[];
464
626
  status: 'active' | 'rolled_back';
465
627
  created_at: number;
466
628
  }
467
- /** Branch row from GET /api/v1/branches (also exposed as BranchListItem). */
468
- export interface BranchInfo {
629
+ /** @deprecated Use {@link CheckpointRecord}. */
630
+ export type CommitRecord = CheckpointRecord;
631
+ /** Workspace row from `GET /api/v1/workspaces` (legacy: `/api/v1/branches`). */
632
+ export interface WorkspaceInfo {
469
633
  name: string;
470
634
  from_branch: string;
471
635
  from_date: string;
472
- /** Internal LMDB fork date key when present. */
473
- from_date_key?: string;
474
- tip_commit?: CommitRecord;
475
- }
476
- export type BranchListItem = BranchInfo;
477
- export interface TagRecord {
636
+ tip_checkpoint?: CheckpointRecord;
637
+ /** @deprecated Alias of {@link WorkspaceInfo.tip_checkpoint}. */
638
+ tip_commit?: CheckpointRecord;
639
+ }
640
+ /** @deprecated Use {@link WorkspaceInfo}. */
641
+ export type BranchInfo = WorkspaceInfo;
642
+ /** @deprecated Use {@link WorkspaceInfo}. */
643
+ export type BranchListItem = WorkspaceInfo;
644
+ export interface BookmarkRecord {
478
645
  name: string;
479
- commit_id: string;
646
+ checkpoint_id: string;
647
+ /** @deprecated Same as `checkpoint_id`. */
648
+ commit_id?: string;
480
649
  branch: string;
481
- date_key: string;
650
+ date?: string;
651
+ /** @deprecated */
652
+ date_key?: string;
482
653
  message?: string;
483
654
  author?: string;
484
655
  created_at: number;
485
656
  }
486
- export interface DiffEntry {
657
+ /** @deprecated Use {@link BookmarkRecord}. */
658
+ export type TagRecord = BookmarkRecord;
659
+ export interface CompareEntry {
487
660
  identifier: string;
488
661
  action: 'added' | 'modified' | 'deleted';
489
662
  from_content?: string;
490
663
  to_content?: string;
491
664
  }
492
- export interface DiffRef {
665
+ /** @deprecated Use {@link CompareEntry}. */
666
+ export type DiffEntry = CompareEntry;
667
+ export interface CompareRef {
493
668
  branch?: string;
494
- /** Internal date key (preferred for exact revision). */
669
+ /** Human-readable date (preferred). */
670
+ date?: string;
671
+ checkpoint_id?: string;
672
+ /** @deprecated Undocumented fallback. */
495
673
  date_key?: string;
674
+ /** @deprecated Use {@link CompareRef.checkpoint_id}. */
496
675
  commit_id?: string;
497
676
  }
498
- export interface DiffResult {
499
- changes: DiffEntry[];
677
+ /** @deprecated Use {@link CompareRef}. */
678
+ export type DiffRef = CompareRef;
679
+ export interface CompareResult {
680
+ changes: CompareEntry[];
500
681
  from: {
501
682
  branch?: string;
683
+ date?: string;
502
684
  date_key?: string;
503
685
  };
504
686
  to: {
505
687
  branch?: string;
688
+ date?: string;
506
689
  date_key?: string;
507
690
  };
508
691
  total_changes: number;
509
692
  }
510
- export interface MergeConflict {
693
+ /** @deprecated Use {@link CompareResult}. */
694
+ export type DiffResult = CompareResult;
695
+ export interface PublishConflict {
511
696
  identifier: string;
512
697
  source_action: string;
513
698
  target_action: string;
514
699
  }
515
- export interface MergeResult {
700
+ /** @deprecated Use {@link PublishConflict}. */
701
+ export type MergeConflict = PublishConflict;
702
+ export interface PublishResult {
516
703
  status: 'completed' | 'conflicts';
517
- commit?: CommitRecord;
704
+ checkpoint?: CheckpointRecord;
705
+ /** @deprecated Alias of {@link PublishResult.checkpoint}. */
706
+ commit?: CheckpointRecord;
518
707
  merged_identifiers?: string[];
519
- conflicts?: MergeConflict[];
708
+ conflicts?: PublishConflict[];
520
709
  auto_mergeable?: string[];
521
710
  message?: string;
522
711
  }
712
+ /** @deprecated Use {@link PublishResult}. */
713
+ export type MergeResult = PublishResult;
523
714
  export declare class XCiteDBError extends Error {
524
715
  readonly status: number;
525
716
  readonly body?: unknown | undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Wet tests: set XCITEDB_BASE_URL, XCITEDB_ADMIN_TOKEN, XCITEDB_TENANT_ID (platform + project).
8
+ * Run: npm test (builds then runs node:test).
9
+ */
10
+ const node_test_1 = require("node:test");
11
+ const strict_1 = __importDefault(require("node:assert/strict"));
12
+ const node_crypto_1 = require("node:crypto");
13
+ const client_js_1 = require("./client.js");
14
+ function wetEnv() {
15
+ const baseUrl = process.env.XCITEDB_BASE_URL?.trim();
16
+ const accessToken = process.env.XCITEDB_ADMIN_TOKEN?.trim();
17
+ const tenantId = process.env.XCITEDB_TENANT_ID?.trim();
18
+ if (!baseUrl || !accessToken || !tenantId)
19
+ return null;
20
+ return { baseUrl, accessToken, tenantId };
21
+ }
22
+ function adminClient(e) {
23
+ return new client_js_1.XCiteDBClient({
24
+ baseUrl: e.baseUrl,
25
+ accessToken: e.accessToken,
26
+ platformConsole: true,
27
+ projectId: e.tenantId,
28
+ context: { branch: 'main', project_id: e.tenantId },
29
+ });
30
+ }
31
+ async function openTestSession(e) {
32
+ const url = `${e.baseUrl.replace(/\/+$/, '')}/api/v1/test/sessions`;
33
+ const r = await fetch(url, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ Authorization: `Bearer ${e.accessToken}`,
38
+ 'X-Project-Id': e.tenantId,
39
+ 'X-Branch': 'main',
40
+ },
41
+ body: '{}',
42
+ });
43
+ const data = (await r.json());
44
+ if (!r.ok || !data.session_token) {
45
+ throw new Error(`test session failed ${r.status}: ${JSON.stringify(data)}`);
46
+ }
47
+ const client = new client_js_1.XCiteDBClient({
48
+ baseUrl: e.baseUrl,
49
+ accessToken: e.accessToken,
50
+ platformConsole: true,
51
+ projectId: e.tenantId,
52
+ context: { branch: 'main', project_id: e.tenantId },
53
+ testSessionToken: data.session_token,
54
+ testRequireAuth: true,
55
+ });
56
+ return { client, token: data.session_token };
57
+ }
58
+ const w = wetEnv();
59
+ const wd = w ? node_test_1.describe : node_test_1.describe.skip;
60
+ wd('user isolation (wet)', () => {
61
+ (0, node_test_1.it)('setUserIsolationConfig enables isolation (round-trip)', async () => {
62
+ const e = wetEnv();
63
+ if (!e)
64
+ throw new Error('missing env');
65
+ const c = adminClient(e);
66
+ try {
67
+ const out = await c.setUserIsolationConfig({
68
+ enabled: true,
69
+ namespace_pattern: '/users/${user.id}',
70
+ });
71
+ strict_1.default.equal(out.enabled, true);
72
+ const again = await c.getUserIsolationConfig();
73
+ strict_1.default.equal(again.enabled, true);
74
+ }
75
+ finally {
76
+ await c.disableUserIsolation().catch(() => { });
77
+ }
78
+ });
79
+ (0, node_test_1.it)('getUserIsolationConfig returns disabled in fresh test session', async () => {
80
+ const e = wetEnv();
81
+ if (!e)
82
+ throw new Error('missing env');
83
+ const { client: s, token } = await openTestSession(e);
84
+ try {
85
+ const cfg = await s.getUserIsolationConfig();
86
+ strict_1.default.equal(cfg.enabled, false);
87
+ }
88
+ finally {
89
+ await fetch(`${e.baseUrl.replace(/\/+$/, '')}/api/v1/test/sessions/current`, {
90
+ method: 'DELETE',
91
+ headers: { 'X-Test-Session': token },
92
+ });
93
+ }
94
+ });
95
+ (0, node_test_1.it)('disableUserIsolation after enable', async () => {
96
+ const e = wetEnv();
97
+ if (!e)
98
+ throw new Error('missing env');
99
+ const c = adminClient(e);
100
+ try {
101
+ await c.setUserIsolationConfig({ enabled: true, namespace_pattern: '/users/${user.id}' });
102
+ await c.disableUserIsolation();
103
+ const cfg = await c.getUserIsolationConfig();
104
+ strict_1.default.equal(cfg.enabled, false);
105
+ }
106
+ finally {
107
+ await c.disableUserIsolation().catch(() => { });
108
+ }
109
+ });
110
+ (0, node_test_1.it)('document write uses transparent prefix for app user', async () => {
111
+ const e = wetEnv();
112
+ if (!e)
113
+ throw new Error('missing env');
114
+ const admin = adminClient(e);
115
+ const suffix = (0, node_crypto_1.randomUUID)().slice(0, 8);
116
+ const email = `js_iso_${suffix}@apitest.invalid`;
117
+ const password = `Js_${suffix}!aA1`;
118
+ const slug = `js-iso-doc-${suffix}`;
119
+ try {
120
+ await admin.setUserIsolationConfig({ enabled: true, namespace_pattern: '/users/${user.id}' });
121
+ const u = await admin.createAppUser(email, password, undefined, [
122
+ client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
123
+ ]);
124
+ const app = new client_js_1.XCiteDBClient({
125
+ baseUrl: e.baseUrl,
126
+ context: { branch: 'main', project_id: e.tenantId },
127
+ userIsolation: { enabled: true },
128
+ });
129
+ await app.loginAppUser(email, password);
130
+ await app.writeJsonDocument(`/${slug}`, { _xcite_json_doc: true, v: 1 });
131
+ const doc = await app.readJsonDocument(`/${slug}`);
132
+ strict_1.default.equal(doc.v, 1);
133
+ await admin.deleteJsonDocument(`/users/${u.user_id}/${slug}`);
134
+ await admin.deleteAppUser(u.user_id);
135
+ }
136
+ finally {
137
+ await admin.disableUserIsolation().catch(() => { });
138
+ }
139
+ });
140
+ (0, node_test_1.it)('shared_read_paths passthrough: app reads shared path without double-prefix', async () => {
141
+ const e = wetEnv();
142
+ if (!e)
143
+ throw new Error('missing env');
144
+ const admin = adminClient(e);
145
+ const suffix = (0, node_crypto_1.randomUUID)().slice(0, 8);
146
+ const email = `js_shr_${suffix}@apitest.invalid`;
147
+ const password = `Js_${suffix}!aA1`;
148
+ const sharedPath = `/shared-read-${suffix}`;
149
+ const docPath = `${sharedPath}/doc`;
150
+ try {
151
+ await admin.setUserIsolationConfig({
152
+ enabled: true,
153
+ namespace_pattern: '/users/${user.id}',
154
+ shared_read_paths: [sharedPath],
155
+ });
156
+ await admin.writeJsonDocument(docPath, { _xcite_json_doc: true, tag: 'shared' });
157
+ const created = await admin.createAppUser(email, password, undefined, [
158
+ client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
159
+ ]);
160
+ const app = new client_js_1.XCiteDBClient({
161
+ baseUrl: e.baseUrl,
162
+ context: { branch: 'main', project_id: e.tenantId },
163
+ userIsolation: { enabled: true, shared_read_paths: [sharedPath] },
164
+ });
165
+ await app.loginAppUser(email, password);
166
+ const doc = await app.readJsonDocument(docPath);
167
+ strict_1.default.equal(doc.tag, 'shared');
168
+ await admin.deleteJsonDocument(docPath);
169
+ await admin.deleteAppUser(created.user_id);
170
+ }
171
+ finally {
172
+ await admin.disableUserIsolation().catch(() => { });
173
+ }
174
+ });
175
+ });