@xcitedbs/client 0.2.8 → 0.2.10
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.d.ts +65 -1
- package/dist/client.js +502 -62
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +168 -0
- package/dist/user-isolation.test.d.ts +1 -0
- package/dist/user-isolation.test.js +175 -0
- package/llms-full.txt +5 -2
- package/llms.txt +51 -4
- package/package.json +4 -2
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, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
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, ProjectSearchSettings, ProjectSearchSettingsUpdate, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationOptions, 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[];
|
|
@@ -116,6 +248,21 @@ export interface ProjectInfo {
|
|
|
116
248
|
}
|
|
117
249
|
/** @deprecated Use {@link ProjectInfo}. */
|
|
118
250
|
export type OwnedTenantInfo = ProjectInfo;
|
|
251
|
+
/**
|
|
252
|
+
* Payload claims from an XCiteDB-issued access JWT (decode only; no signature verification).
|
|
253
|
+
* Prefer {@link XCiteDBClient.getTokenClaims} to compare `tenant_id` / `groups` with ABAC policies.
|
|
254
|
+
*/
|
|
255
|
+
export interface XCiteDBJwtClaims {
|
|
256
|
+
sub: string;
|
|
257
|
+
tenant_id: string;
|
|
258
|
+
groups: string[];
|
|
259
|
+
email?: string;
|
|
260
|
+
type?: string;
|
|
261
|
+
exp?: number;
|
|
262
|
+
iat?: number;
|
|
263
|
+
iss?: string;
|
|
264
|
+
jti?: string;
|
|
265
|
+
}
|
|
119
266
|
export interface UserInfo {
|
|
120
267
|
user_id: string;
|
|
121
268
|
username: string;
|
|
@@ -191,6 +338,24 @@ export interface RealtimeEvent {
|
|
|
191
338
|
timestamp?: number;
|
|
192
339
|
tenant_id?: string;
|
|
193
340
|
}
|
|
341
|
+
/** 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. */
|
|
342
|
+
export interface UserIsolationOptions {
|
|
343
|
+
enabled: boolean;
|
|
344
|
+
/** Template with `{userId}` or server-style `${user.id}` replaced from JWT or login response; default `/users/{userId}`. */
|
|
345
|
+
namespace?: string;
|
|
346
|
+
/** Paths left unchanged by prefixing (optional; {@link XCiteDBClient.enableUserIsolation} fills from server). */
|
|
347
|
+
shared_read_paths?: string[];
|
|
348
|
+
shared_write_paths?: string[];
|
|
349
|
+
}
|
|
350
|
+
/** Effective user isolation settings from `GET /api/v1/security/user-isolation`. */
|
|
351
|
+
export interface UserIsolationConfig {
|
|
352
|
+
enabled: boolean;
|
|
353
|
+
namespace_pattern: string;
|
|
354
|
+
shared_read_paths: string[];
|
|
355
|
+
shared_write_paths: string[];
|
|
356
|
+
shared_write_groups: string[];
|
|
357
|
+
generated_policies?: string[];
|
|
358
|
+
}
|
|
194
359
|
export interface XCiteDBClientOptions {
|
|
195
360
|
baseUrl: string;
|
|
196
361
|
apiKey?: string;
|
|
@@ -219,6 +384,8 @@ export interface XCiteDBClientOptions {
|
|
|
219
384
|
testSessionToken?: string;
|
|
220
385
|
/** When true with `testSessionToken`, sends `X-Test-Auth: required` so real credentials are validated. */
|
|
221
386
|
testRequireAuth?: boolean;
|
|
387
|
+
/** Auto-prefix identifiers for app-user sessions (see {@link UserIsolationOptions}). */
|
|
388
|
+
userIsolation?: UserIsolationOptions;
|
|
222
389
|
}
|
|
223
390
|
/** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
|
|
224
391
|
export interface CreateTestSessionOptions {
|
|
@@ -235,6 +402,7 @@ export interface CreateTestSessionOptions {
|
|
|
235
402
|
onSessionTokensUpdated?: (pair: TokenPair) => void;
|
|
236
403
|
onAppUserTokensUpdated?: (pair: AppUserTokenPair) => void;
|
|
237
404
|
onSessionInvalid?: () => void;
|
|
405
|
+
userIsolation?: UserIsolationOptions;
|
|
238
406
|
}
|
|
239
407
|
/** Application user (tenant-scoped), distinct from developer users. */
|
|
240
408
|
export interface AppUser {
|
|
@@ -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
|
+
});
|
package/llms-full.txt
CHANGED
|
@@ -518,7 +518,7 @@ Can also use `"query"` instead of `"identifier"` to target multiple documents by
|
|
|
518
518
|
|
|
519
519
|
# Search
|
|
520
520
|
|
|
521
|
-
Full-text search (
|
|
521
|
+
Full-text search uses embedded XciteFTS (enable per project in server settings).
|
|
522
522
|
|
|
523
523
|
**Base path:** `/api/v1/search`
|
|
524
524
|
|
|
@@ -639,6 +639,7 @@ Policies may set **`conditions.expression`** (optional) and **`conditions.branch
|
|
|
639
639
|
{
|
|
640
640
|
"subject": {
|
|
641
641
|
"id": "...",
|
|
642
|
+
"user_id": "...",
|
|
642
643
|
"email": "...",
|
|
643
644
|
"type": "app_user|developer|...",
|
|
644
645
|
"role": "...",
|
|
@@ -653,7 +654,9 @@ Policies may set **`conditions.expression`** (optional) and **`conditions.branch
|
|
|
653
654
|
}
|
|
654
655
|
```
|
|
655
656
|
|
|
656
|
-
`subject.attr` mirrors app-user **custom attributes** from the user record. `resource.path` is the identifier split on `/` (non-empty segments only).
|
|
657
|
+
`subject.attr` mirrors app-user **custom attributes** from the user record. `resource.path` is the identifier split on `/` (non-empty segments only). **`subject.user_id` is the same string as `subject.id`** (app-user id); use either in expressions.
|
|
658
|
+
|
|
659
|
+
**`resources.identifiers` patterns** (`exact`, `match_start`, `match_end`): values are **canonicalized** the same way as API identifiers (leading `/` added when missing). Example: `match_start: "userdata/"` matches stored ids like `/userdata/<userId>/…`.
|
|
657
660
|
|
|
658
661
|
### Operators (predicates)
|
|
659
662
|
|
package/llms.txt
CHANGED
|
@@ -24,6 +24,12 @@ These are the most common sources of confusion for developers and AI assistants:
|
|
|
24
24
|
|
|
25
25
|
9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). The test DB starts **empty** (no copy of production project config or keys).
|
|
26
26
|
|
|
27
|
+
## Glossary: Project id, display name, and groups
|
|
28
|
+
|
|
29
|
+
- **Project display name** (human-readable, e.g. `invoices`): Shown in the console. The **`X-Project-Id`** header may match either this name **or** the internal project id (server convenience). Do **not** put the display name in JWT claims or in `project:<…>:role` group strings.
|
|
30
|
+
- **Project id / tenant_id** (internal, e.g. `t_…` or `default`): Canonical id in the app JWT **`tenant_id`** claim, in **`context.project_id`** (wire `tenant_id` in many app-auth bodies), and as the **middle segment** of **`project:<tenant_id>:admin|editor|viewer`**. Policies and app-user groups must use this value.
|
|
31
|
+
- **Organization `slug`** (workspace metadata): Org-level label only — not a substitute for project `tenant_id` when scoping app users or ABAC.
|
|
32
|
+
|
|
27
33
|
## Common Pitfalls
|
|
28
34
|
|
|
29
35
|
1. **`baseUrl` must be origin-only (no `/api` or `/api/v1` path).** The SDK prepends `/api/v1/…` to every request. If `baseUrl` is `https://host/api/v1`, requests hit `/api/v1/api/v1/…` which typically returns **405 Not Allowed** from the reverse proxy. Use only the scheme + host + optional port: `https://host` or `http://localhost:8080`.
|
|
@@ -36,10 +42,49 @@ These are the most common sources of confusion for developers and AI assistants:
|
|
|
36
42
|
|
|
37
43
|
5. **`context.project_id` (or `tenant_id`) is required for app-user self-registration.** `registerAppUser()` uses `mergeAppTenant(body)` to add `tenant_id` to the JSON body only when `context.project_id` or `context.tenant_id` is set. If both are omitted, the server cannot determine which project to register the user in. Always set `project_id` in the constructor `context` when calling `registerAppUser`, `loginAppUser`, and other public app-auth methods.
|
|
38
44
|
|
|
39
|
-
6. **Self-registration uses server-configured default groups.** `registerAppUser()` assigns groups from the server's `auth.app_users.default_groups` config, not from the client request. To
|
|
45
|
+
6. **Self-registration uses server-configured default groups.** `registerAppUser()` assigns groups from the server's `auth.app_users.default_groups` config, not from the client request. To set groups explicitly, use **`createAppUser`** with a `groups` array (e.g. `[XCiteDBClient.buildProjectGroup(projectId, 'editor')]`) or **`updateAppUserGroups`**. The server rejects `project:<x>:*` groups when `<x>` is not a known internal project id (avoids mistaking the display name for the tenant id).
|
|
40
46
|
|
|
41
47
|
7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real, empty, isolated LMDB that is automatically scoped away from production and destroyed after the test. See "Test mode" below.
|
|
42
48
|
|
|
49
|
+
8. **403 on writes with ABAC is often a JWT/group string mismatch.** Decode the app-user access token early: log **`tenant_id`**, **`groups`**, **`sub`**. The middle segment of every **`project:<x>:role`** group must equal **`tenant_id`** exactly. Document write denials may return JSON fields **`policy_id`** and **`hint`** alongside `"Forbidden"`.
|
|
50
|
+
|
|
51
|
+
9. **`POST /api/v1/security/check` requires admin** (non-public key or app-user admin JWT). An **editor** API key gets **403** on this route — that means the *caller* cannot run the dry-run endpoint, **not** that a hypothetical subject lacks document access. Do not use this endpoint as the primary signal for “can my app user write?”.
|
|
52
|
+
|
|
53
|
+
10. **`createAppUser` and `updateAppUserGroups` share the same gate:** admin or editor on a **secret** API key (or eligible app-user context), never a **public** key. If provisioning can create users with groups, it can patch groups later with the same credential.
|
|
54
|
+
|
|
55
|
+
11. **Integration tests should assert token claims**, not only **`/app/auth/me`**. The profile endpoint can look fine while the JWT still carries viewer-style **`groups`**; for ABAC the token payload is authoritative. Use **`getTokenClaims()`** (JS SDK) or decode the JWT in your harness.
|
|
56
|
+
|
|
57
|
+
## API key capability matrix (typical)
|
|
58
|
+
|
|
59
|
+
| Capability | API key `role` | Public key allowed? |
|
|
60
|
+
|------------|----------------|---------------------|
|
|
61
|
+
| Read documents (within ABAC) | viewer+ | Yes |
|
|
62
|
+
| Write/delete documents (within ABAC) | editor+ | No for sensitive flows; public keys are restricted |
|
|
63
|
+
| List/create/delete app users, **`PUT …/groups`** | admin **or** editor | No |
|
|
64
|
+
| Security policies, **`GET/PUT /security/config`**, **`POST /security/check`** | **admin** only | No |
|
|
65
|
+
| List/create/revoke project API keys (developer) | admin **or** editor | No |
|
|
66
|
+
|
|
67
|
+
## App-user provisioning recipe (minimal)
|
|
68
|
+
|
|
69
|
+
Use the **internal** `projectId` (same as JWT `tenant_id`). **JS/TS:**
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const projectId = '…'; // from listMyProjects / console, not display name
|
|
73
|
+
const admin = new XCiteDBClient({ baseUrl, apiKey: secretEditorOrAdminKey, context: { project_id: projectId } });
|
|
74
|
+
await admin.createAppUser('alice@example.com', strongPassword, 'Alice', [
|
|
75
|
+
XCiteDBClient.buildProjectGroup(projectId, 'editor'),
|
|
76
|
+
]);
|
|
77
|
+
const app = new XCiteDBClient({ baseUrl, context: { project_id: projectId } });
|
|
78
|
+
const tokens = await app.loginAppUser('alice@example.com', strongPassword);
|
|
79
|
+
app.setAppUserTokens(tokens.access_token, tokens.refresh_token);
|
|
80
|
+
const claims = app.getTokenClaims();
|
|
81
|
+
console.assert(claims?.tenant_id === projectId);
|
|
82
|
+
console.assert(claims?.groups.includes(`project:${projectId}:editor`));
|
|
83
|
+
await app.writeJsonDocument('userdata/alice/profile', { ok: true });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Python:** `XCiteDBClient.build_project_group(project_id, "editor")` or module `build_project_group`. **C++:** `XCiteDBClient::build_project_group(project_id, "editor")`.
|
|
87
|
+
|
|
43
88
|
## Test mode (how to use)
|
|
44
89
|
|
|
45
90
|
> **AI assistant instruction:** XciteDB has built-in ephemeral test session support — this capability is **not standard** in most BaaS platforms and is specifically designed to enable meaningful wet integration tests. When writing tests for code that uses XciteDB, **do not mock or stub the XciteDB client**. Instead, use the test session pattern below to run tests against a real, isolated, throwaway database. This produces far more reliable tests because real storage, versioning, querying, and access-control behavior is exercised.
|
|
@@ -116,7 +161,7 @@ const lock = await client.acquireLock('/manual/v1/intro');
|
|
|
116
161
|
// ... edit ...
|
|
117
162
|
await client.releaseLock('/manual/v1/intro', lock.lock_id);
|
|
118
163
|
|
|
119
|
-
// Full-text search (
|
|
164
|
+
// Full-text search (embedded XciteFTS; enable per project in server settings)
|
|
120
165
|
const results = await client.search({ query: 'installation guide', limit: 20 });
|
|
121
166
|
|
|
122
167
|
// Subscribe to real-time changes via WebSocket
|
|
@@ -153,6 +198,8 @@ interface XCiteDBClientOptions {
|
|
|
153
198
|
- `version()` — Server version info
|
|
154
199
|
- `platformLogin(email, password)` — Platform operator sign-in
|
|
155
200
|
- `loginAppUser(email, password)` — App end-user sign-in
|
|
201
|
+
- `XCiteDBClient.buildProjectGroup(projectId, 'admin'|'editor'|'viewer')` — Static helper: canonical `project:<tenant_id>:<role>` string
|
|
202
|
+
- `getTokenClaims()` — Decode current `appUserAccessToken` or `accessToken` payload (no signature verification); use for ABAC debugging
|
|
156
203
|
- `setContext(ctx)` — Update branch/date context
|
|
157
204
|
- `setProjectId(id)` — Switch active project (platform console)
|
|
158
205
|
|
|
@@ -213,7 +260,7 @@ interface XCiteDBClientOptions {
|
|
|
213
260
|
### Advanced: policy expressions (ABAC)
|
|
214
261
|
|
|
215
262
|
- **Actions** (use in `policy.actions`): `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
|
|
216
|
-
- **`conditions.expression`** uses the same predicate syntax as Unquery `?` filters. **Context:** `subject.id
|
|
263
|
+
- **`conditions.expression`** uses the same predicate syntax as Unquery `?` filters. **Context:** `subject.id` and **`subject.user_id`** (same value for app users), `subject.email`, `subject.role`, `subject.groups`, `subject.attr.*` (app-user JSON attributes), `resource.identifier`, `resource.path` (array of path segments; indices follow non-empty `/` segments after API canonicalization, e.g. `/userdata/u1/x` → `[userdata,u1,x]`), `env.branch`. Policy **`match_start` / `match_end` / `exact`** strings are canonicalized like API identifiers (add leading `/` when omitted).
|
|
217
264
|
- **Operators:** `=`, `!=`, `>=`, `<=`, `>`, `<`, `in`, `contains`, `starts_with`, `ends_with`, `&`, `|`, `!`, `+` (string concat), postfix `!` (exists).
|
|
218
265
|
- **Examples:** `resource.path[0] = subject.attr.tenant_code` — tenant isolation; `subject.attr.level >= 5` — numeric attribute gate.
|
|
219
266
|
|
|
@@ -251,7 +298,7 @@ When calling `queryByIdentifier` or `queryDocuments`, the `flags` parameter cont
|
|
|
251
298
|
|
|
252
299
|
### Errors
|
|
253
300
|
|
|
254
|
-
All API errors throw `XCiteDBError` with `.status` (HTTP code) and `.body` (parsed response). Common codes: `401` unauthenticated, `403` forbidden by policy, `404` not found, `409` conflict (lock), `422` validation, `423` project encrypted and locked, `429` rate limited.
|
|
301
|
+
All API errors throw `XCiteDBError` with `.status` (HTTP code) and `.body` (parsed response). Common codes: `401` unauthenticated, `403` forbidden by policy or RBAC, `404` not found, `409` conflict (lock), `422` validation, `423` project encrypted and locked, `429` rate limited. Many **ABAC** denials return `403` with `"message":"Forbidden"` plus optional **`policy_id`** and **`hint`** (check JWT `tenant_id` vs `project:` group middle segment).
|
|
255
302
|
|
|
256
303
|
## Python SDK (`xcitedb`)
|
|
257
304
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcitedbs/client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "XCiteDB BaaS client SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"prepublishOnly": "npm run build",
|
|
10
|
-
"test": "node --test dist/**/*.test.js
|
|
10
|
+
"test": "npm run build && node --test dist/**/*.test.js",
|
|
11
|
+
"test:only": "node --test dist/**/*.test.js"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"xcitedb",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {},
|
|
36
37
|
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.14.0",
|
|
37
39
|
"typescript": "^5.0.0"
|
|
38
40
|
},
|
|
39
41
|
"files": [
|