@xcitedbs/client 0.2.7 → 0.2.9
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 +12 -1
- package/dist/client.js +75 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +15 -0
- package/llms-full.txt +133 -3
- package/llms.txt +56 -5
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, CommitRecord, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteQuery } from './types';
|
|
1
|
+
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, CommitRecord, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, XCiteQuery } from './types';
|
|
2
2
|
import { WebSocketSubscription } from './websocket';
|
|
3
3
|
export declare class XCiteDBClient {
|
|
4
4
|
private baseUrl;
|
|
@@ -22,6 +22,17 @@ export declare class XCiteDBClient {
|
|
|
22
22
|
* then returns a client that sends `X-Test-Session` (auth-free by default).
|
|
23
23
|
*/
|
|
24
24
|
static createTestSession(opts: CreateTestSessionOptions): Promise<XCiteDBClient>;
|
|
25
|
+
/**
|
|
26
|
+
* Canonical `project:<tenant_id>:<role>` group string. The middle segment must match the app JWT `tenant_id`
|
|
27
|
+
* (internal project id), not the human-readable project name.
|
|
28
|
+
*/
|
|
29
|
+
static buildProjectGroup(projectId: string, role: 'admin' | 'editor' | 'viewer'): string;
|
|
30
|
+
private static decodeJwtPayloadJson;
|
|
31
|
+
/**
|
|
32
|
+
* Decode `appUserAccessToken`, or `accessToken` if no app token (platform JWT), without verifying the signature.
|
|
33
|
+
* Use for debugging ABAC (compare `tenant_id` and `groups` to `project:<…>:role` in policies).
|
|
34
|
+
*/
|
|
35
|
+
getTokenClaims(): XCiteDBJwtClaims | null;
|
|
25
36
|
/** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
|
|
26
37
|
destroyTestSession(): Promise<{
|
|
27
38
|
message: string;
|
package/dist/client.js
CHANGED
|
@@ -69,6 +69,81 @@ class XCiteDBClient {
|
|
|
69
69
|
testRequireAuth: opts.testRequireAuth,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Canonical `project:<tenant_id>:<role>` group string. The middle segment must match the app JWT `tenant_id`
|
|
74
|
+
* (internal project id), not the human-readable project name.
|
|
75
|
+
*/
|
|
76
|
+
static buildProjectGroup(projectId, role) {
|
|
77
|
+
return `project:${projectId}:${role}`;
|
|
78
|
+
}
|
|
79
|
+
static decodeJwtPayloadJson(token) {
|
|
80
|
+
const parts = token.split('.');
|
|
81
|
+
if (parts.length < 2)
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
let b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
85
|
+
const pad = (4 - (b64.length % 4)) % 4;
|
|
86
|
+
b64 += '='.repeat(pad);
|
|
87
|
+
const g = globalThis;
|
|
88
|
+
let json = null;
|
|
89
|
+
if (g.Buffer) {
|
|
90
|
+
json = g.Buffer.from(b64, 'base64').toString('utf8');
|
|
91
|
+
}
|
|
92
|
+
else if (typeof atob === 'function') {
|
|
93
|
+
json = atob(b64);
|
|
94
|
+
}
|
|
95
|
+
if (json === null)
|
|
96
|
+
return null;
|
|
97
|
+
return JSON.parse(json);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Decode `appUserAccessToken`, or `accessToken` if no app token (platform JWT), without verifying the signature.
|
|
105
|
+
* Use for debugging ABAC (compare `tenant_id` and `groups` to `project:<…>:role` in policies).
|
|
106
|
+
*/
|
|
107
|
+
getTokenClaims() {
|
|
108
|
+
const raw = this.appUserAccessToken ?? this.accessToken;
|
|
109
|
+
if (!raw)
|
|
110
|
+
return null;
|
|
111
|
+
const p = XCiteDBClient.decodeJwtPayloadJson(raw);
|
|
112
|
+
if (!p)
|
|
113
|
+
return null;
|
|
114
|
+
let groups = [];
|
|
115
|
+
const g = p['groups'];
|
|
116
|
+
if (Array.isArray(g)) {
|
|
117
|
+
groups = g.filter((x) => typeof x === 'string');
|
|
118
|
+
}
|
|
119
|
+
else if (g && typeof g === 'object' && !Array.isArray(g)) {
|
|
120
|
+
groups = Object.keys(g);
|
|
121
|
+
}
|
|
122
|
+
else if (typeof g === 'string') {
|
|
123
|
+
groups = g.split(',').map((s) => s.trim()).filter(Boolean);
|
|
124
|
+
}
|
|
125
|
+
const sub = p['sub'];
|
|
126
|
+
const tenant_id = p['tenant_id'];
|
|
127
|
+
if (typeof sub !== 'string' || typeof tenant_id !== 'string')
|
|
128
|
+
return null;
|
|
129
|
+
const email = p['email'];
|
|
130
|
+
const type = p['type'];
|
|
131
|
+
const exp = p['exp'];
|
|
132
|
+
const iat = p['iat'];
|
|
133
|
+
const iss = p['iss'];
|
|
134
|
+
const jti = p['jti'];
|
|
135
|
+
return {
|
|
136
|
+
sub,
|
|
137
|
+
tenant_id,
|
|
138
|
+
groups,
|
|
139
|
+
email: typeof email === 'string' ? email : undefined,
|
|
140
|
+
type: typeof type === 'string' ? type : undefined,
|
|
141
|
+
exp: typeof exp === 'number' ? exp : undefined,
|
|
142
|
+
iat: typeof iat === 'number' ? iat : undefined,
|
|
143
|
+
iss: typeof iss === 'string' ? iss : undefined,
|
|
144
|
+
jti: typeof jti === 'string' ? jti : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
72
147
|
/** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
|
|
73
148
|
async destroyTestSession() {
|
|
74
149
|
if (!this.testSessionToken) {
|
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, 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';
|
|
4
4
|
export { XCiteDBError } from './types';
|
package/dist/types.d.ts
CHANGED
|
@@ -116,6 +116,21 @@ export interface ProjectInfo {
|
|
|
116
116
|
}
|
|
117
117
|
/** @deprecated Use {@link ProjectInfo}. */
|
|
118
118
|
export type OwnedTenantInfo = ProjectInfo;
|
|
119
|
+
/**
|
|
120
|
+
* Payload claims from an XCiteDB-issued access JWT (decode only; no signature verification).
|
|
121
|
+
* Prefer {@link XCiteDBClient.getTokenClaims} to compare `tenant_id` / `groups` with ABAC policies.
|
|
122
|
+
*/
|
|
123
|
+
export interface XCiteDBJwtClaims {
|
|
124
|
+
sub: string;
|
|
125
|
+
tenant_id: string;
|
|
126
|
+
groups: string[];
|
|
127
|
+
email?: string;
|
|
128
|
+
type?: string;
|
|
129
|
+
exp?: number;
|
|
130
|
+
iat?: number;
|
|
131
|
+
iss?: string;
|
|
132
|
+
jti?: string;
|
|
133
|
+
}
|
|
119
134
|
export interface UserInfo {
|
|
120
135
|
user_id: string;
|
|
121
136
|
username: string;
|
package/llms-full.txt
CHANGED
|
@@ -26,7 +26,7 @@ Before reading the full reference, note these critical differences from typical
|
|
|
26
26
|
|
|
27
27
|
9. **OpenAPI:** See repository `docs/openapi.yaml` for a machine-readable route map.
|
|
28
28
|
|
|
29
|
-
10. **Ephemeral test sessions.** `POST /api/v1/test/sessions` (authenticated) returns a UUID **`session_token`**. Clients send **`X-Test-Session: <token>`** on API calls to use an isolated, TTL- and quota-limited LMDB instead of production project data. Unless **`X-Test-Auth: required`** is set,
|
|
29
|
+
10. **Ephemeral test sessions.** `POST /api/v1/test/sessions` (authenticated) returns a UUID **`session_token`**. Clients send **`X-Test-Session: <token>`** on API calls to use an isolated, TTL- and quota-limited LMDB instead of production project data. Unless **`X-Test-Auth: required`** is set, **developer** JWT/API-key checks are bypassed (synthetic admin for wet tests), but **app-user** identity via **`X-App-User-Token`** or Bearer app-user JWT is still recognized. Management routes under **`/api/v1/test/*`** must not include `X-Test-Session`. The test store starts empty (no cloned production project config).
|
|
30
30
|
|
|
31
31
|
## Common Pitfalls
|
|
32
32
|
|
|
@@ -48,6 +48,8 @@ Before reading the full reference, note these critical differences from typical
|
|
|
48
48
|
|
|
49
49
|
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 assign specific groups, use the admin endpoint `createAppUser()` instead, or update groups after registration via `updateAppUserGroups()`.
|
|
50
50
|
|
|
51
|
+
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 "Ephemeral test sessions" below.
|
|
52
|
+
|
|
51
53
|
---
|
|
52
54
|
|
|
53
55
|
# Part 1: Product Overview
|
|
@@ -186,13 +188,15 @@ Browser and mobile apps can use OAuth2-style flows against `/api/v1/app/auth/oau
|
|
|
186
188
|
|
|
187
189
|
# Ephemeral test sessions
|
|
188
190
|
|
|
191
|
+
> **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. Provision a session in `beforeAll` / `setUp` / test fixture setup and destroy it in `afterAll` / `tearDown` / fixture teardown.
|
|
192
|
+
|
|
189
193
|
For **integration and wet tests** against a shared BaaS host without touching production data:
|
|
190
194
|
|
|
191
195
|
| Step | What to do |
|
|
192
196
|
|------|------------|
|
|
193
197
|
| **Create** | **`POST /api/v1/test/sessions`** with normal **`Authorization: Bearer …`** or **`X-API-Key`**. Response includes a **`session_token`** (UUID). Server enforces per-credential limits (`test.max_sessions_per_key`, `test.session_ttl_seconds`, `test.max_test_db_size_bytes` in server config). |
|
|
194
198
|
| **Use** | Send **`X-Test-Session: <session_token>`** on document and other data API requests. The server routes to a dedicated LMDB under its data root (`_test/<id>/`), not the caller’s production tenant. **`tenant_id` / `X-Project-Id` semantics do not select production** while the test header is present—the synthetic test tenant is implied. |
|
|
195
|
-
| **Auth** | **Default:**
|
|
199
|
+
| **Auth** | **Default:** developer auth (API key / platform JWT) is **bypassed** with a synthetic admin identity. However, **app-user identity is still recognized**: if `X-App-User-Token` or a Bearer app-user JWT is present, the request runs as that app user (for routes like `/app/auth/me`). **`X-Test-Auth: required`:** all auth is validated normally; ABAC applies, but data still comes from the test session DB. |
|
|
196
200
|
| **Manage** | **`GET /api/v1/test/sessions`** — list sessions for the current credential. **`DELETE /api/v1/test/sessions/current`** — destroy the session named by **`X-Test-Session`** (no other auth). **`DELETE /api/v1/test/sessions/all`** — destroy all sessions for the credential. **`DELETE /api/v1/test/sessions/{token}`** — destroy one session if owned by the credential. Do **not** send **`X-Test-Session`** on these `/api/v1/test/*` routes. |
|
|
197
201
|
| **CORS** | Browsers may need **`X-Test-Session`** and **`X-Test-Auth`** in the deployment’s allowed CORS headers (defaults include them). |
|
|
198
202
|
|
|
@@ -202,6 +206,126 @@ For **integration and wet tests** against a shared BaaS host without touching pr
|
|
|
202
206
|
- **Python:** `async with XCiteDBClient.test_session(base_url, api_key=…, …)` provisions and tears down; or pass `test_session_token` / `test_require_auth` to the constructor.
|
|
203
207
|
- **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key` (and optional `test_require_auth`); `destroy_test_session()`.
|
|
204
208
|
|
|
209
|
+
**JavaScript/TypeScript — complete test scaffold (Vitest / Jest):**
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
213
|
+
import { XCiteDBClient } from '@xcitedbs/client';
|
|
214
|
+
|
|
215
|
+
describe('XciteDB integration', () => {
|
|
216
|
+
let client: XCiteDBClient;
|
|
217
|
+
|
|
218
|
+
beforeAll(async () => {
|
|
219
|
+
// Provisions an isolated, throwaway LMDB — production data is never touched.
|
|
220
|
+
client = await XCiteDBClient.createTestSession({
|
|
221
|
+
baseUrl: process.env.XCITEDB_URL ?? 'http://localhost:8080',
|
|
222
|
+
apiKey: process.env.XCITEDB_API_KEY!,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
afterAll(async () => {
|
|
227
|
+
await client.destroyTestSession();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('writes and reads a JSON document', async () => {
|
|
231
|
+
await client.writeJsonDocument('test.config', { env: 'test' });
|
|
232
|
+
const doc = await client.readJsonDocument<{ env: string }>('test.config');
|
|
233
|
+
expect(doc).toMatchObject({ env: 'test' });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('writes and reads an XML document', async () => {
|
|
237
|
+
await client.writeXmlDocument(
|
|
238
|
+
'<doc db:identifier="/test/doc1"><title>Hello</title></doc>'
|
|
239
|
+
);
|
|
240
|
+
const xml = await client.queryByIdentifier('/test/doc1', 'FirstMatch');
|
|
241
|
+
expect(xml).toContain('<title>Hello</title>');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('creates a branch, commits, and merges', async () => {
|
|
245
|
+
await client.withBranch('feature-test', async (c) => {
|
|
246
|
+
await c.writeJsonDocument('test.feature', { active: true });
|
|
247
|
+
}, { message: 'Add feature flag', autoMerge: true });
|
|
248
|
+
const doc = await client.readJsonDocument<{ active: boolean }>('test.feature');
|
|
249
|
+
expect(doc.active).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Python — complete test scaffold (pytest + pytest-asyncio):**
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import os
|
|
258
|
+
import pytest
|
|
259
|
+
import pytest_asyncio
|
|
260
|
+
from xcitedb import XCiteDBClient
|
|
261
|
+
|
|
262
|
+
@pytest_asyncio.fixture
|
|
263
|
+
async def db():
|
|
264
|
+
# Provisions an isolated, throwaway LMDB — production data is never touched.
|
|
265
|
+
async with XCiteDBClient.test_session(
|
|
266
|
+
os.environ.get("XCITEDB_URL", "http://localhost:8080"),
|
|
267
|
+
api_key=os.environ["XCITEDB_API_KEY"],
|
|
268
|
+
) as client:
|
|
269
|
+
yield client # session is destroyed automatically on exit
|
|
270
|
+
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_json_document(db):
|
|
273
|
+
await db.write_json_document("test.config", {"env": "test"})
|
|
274
|
+
doc = await db.read_json_document("test.config")
|
|
275
|
+
assert doc == {"env": "test"}
|
|
276
|
+
|
|
277
|
+
@pytest.mark.asyncio
|
|
278
|
+
async def test_xml_document(db):
|
|
279
|
+
await db.write_xml_document(
|
|
280
|
+
'<doc db:identifier="/test/doc1"><title>Hello</title></doc>'
|
|
281
|
+
)
|
|
282
|
+
result = await db.query_by_identifier("/test/doc1", "FirstMatch")
|
|
283
|
+
assert "<title>Hello</title>" in result
|
|
284
|
+
|
|
285
|
+
@pytest.mark.asyncio
|
|
286
|
+
async def test_branch_and_merge(db):
|
|
287
|
+
async with db.with_branch("feature-test", message="Add flag", auto_merge=True):
|
|
288
|
+
await db.put("test.feature", {"active": True})
|
|
289
|
+
doc = await db.get("test.feature")
|
|
290
|
+
assert doc["active"] is True
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**C++ — complete test scaffold (Catch2):**
|
|
294
|
+
|
|
295
|
+
```cpp
|
|
296
|
+
#include <xcitedb/xcitedb.hpp>
|
|
297
|
+
#include <catch2/catch_test_macros.hpp>
|
|
298
|
+
#include <cstdlib>
|
|
299
|
+
|
|
300
|
+
// Provisions an isolated, throwaway LMDB — production data is never touched.
|
|
301
|
+
static xcitedb::XCiteDBClient make_test_client() {
|
|
302
|
+
xcitedb::XCiteDBClientOptions opt;
|
|
303
|
+
opt.base_url = std::getenv("XCITEDB_URL") ? std::getenv("XCITEDB_URL")
|
|
304
|
+
: "http://127.0.0.1:8080";
|
|
305
|
+
opt.api_key = std::getenv("XCITEDB_API_KEY");
|
|
306
|
+
return xcitedb::XCiteDBClient::create_test_session(opt);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
TEST_CASE("XciteDB JSON document round-trip") {
|
|
310
|
+
auto client = make_test_client();
|
|
311
|
+
client.write_json_document("test.config", R"({"env":"test"})");
|
|
312
|
+
auto doc = client.read_json_document("test.config");
|
|
313
|
+
REQUIRE(doc.find("\"env\"") != std::string::npos);
|
|
314
|
+
REQUIRE(doc.find("\"test\"") != std::string::npos);
|
|
315
|
+
client.destroy_test_session();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
TEST_CASE("XciteDB XML document round-trip") {
|
|
319
|
+
auto client = make_test_client();
|
|
320
|
+
client.write_xml_document(
|
|
321
|
+
R"(<doc db:identifier="/test/doc1"><title>Hello</title></doc>)"
|
|
322
|
+
);
|
|
323
|
+
auto xml = client.query_by_identifier("/test/doc1", "FirstMatch");
|
|
324
|
+
REQUIRE(xml.find("<title>Hello</title>") != std::string::npos);
|
|
325
|
+
client.destroy_test_session();
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
205
329
|
---
|
|
206
330
|
|
|
207
331
|
# Health, version & discovery
|
|
@@ -515,6 +639,7 @@ Policies may set **`conditions.expression`** (optional) and **`conditions.branch
|
|
|
515
639
|
{
|
|
516
640
|
"subject": {
|
|
517
641
|
"id": "...",
|
|
642
|
+
"user_id": "...",
|
|
518
643
|
"email": "...",
|
|
519
644
|
"type": "app_user|developer|...",
|
|
520
645
|
"role": "...",
|
|
@@ -529,7 +654,9 @@ Policies may set **`conditions.expression`** (optional) and **`conditions.branch
|
|
|
529
654
|
}
|
|
530
655
|
```
|
|
531
656
|
|
|
532
|
-
`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>/…`.
|
|
533
660
|
|
|
534
661
|
### Operators (predicates)
|
|
535
662
|
|
|
@@ -1284,6 +1411,9 @@ interface DatabaseContext {
|
|
|
1284
1411
|
- `deleteTrigger(name)` → `void`
|
|
1285
1412
|
|
|
1286
1413
|
### App User Auth
|
|
1414
|
+
|
|
1415
|
+
**App-user auth in test sessions.** The full app-user lifecycle works inside a test session: `registerAppUser` → `loginAppUser` → `appUserMe` / `updateAppUserProfile` / etc. App user records are stored in the test session’s isolated LMDB and cleaned up with the session. No `X-Test-Auth: required` is needed for this flow — the default bypass mode recognizes app-user tokens while still skipping developer auth.
|
|
1416
|
+
|
|
1287
1417
|
- `registerAppUser(email, password, displayName?, attributes?)` → `AppUser` (groups assigned from server config)
|
|
1288
1418
|
- `loginAppUser(email, password)` → `AppUserTokenPair`
|
|
1289
1419
|
- `refreshAppUser()` → `AppUserTokenPair`
|
package/llms.txt
CHANGED
|
@@ -22,7 +22,13 @@ These are the most common sources of confusion for developers and AI assistants:
|
|
|
22
22
|
|
|
23
23
|
8. **Project vs tenant id.** In the SDK, prefer `context.project_id` (and `listMyProjects` / `switchProject`). Many JSON bodies and JWT claims still use the field name `tenant_id` for the same value — the client sends that wire name automatically.
|
|
24
24
|
|
|
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
|
|
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
|
+
|
|
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.
|
|
26
32
|
|
|
27
33
|
## Common Pitfalls
|
|
28
34
|
|
|
@@ -36,13 +42,56 @@ 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).
|
|
46
|
+
|
|
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.
|
|
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")`.
|
|
40
87
|
|
|
41
88
|
## Test mode (how to use)
|
|
42
89
|
|
|
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.
|
|
91
|
+
|
|
43
92
|
1. **Provision:** `POST /api/v1/test/sessions` with `Authorization: Bearer …` or `X-API-Key` (same as normal API access). Response JSON includes the session token.
|
|
44
93
|
2. **Run tests:** Every request that should hit the throwaway DB must include **`X-Test-Session: <token>`** (and your usual `X-Branch` / `context` as needed). Data is stored under the server’s `_test/<session>/` area, not your production tenant.
|
|
45
|
-
3. **Auth behavior:**
|
|
94
|
+
3. **Auth behavior:** Developer auth (API key / platform JWT) is bypassed by default for frictionless tests. **App-user identity** (`X-App-User-Token` or Bearer app-user JWT) **is still recognized** in default mode, so `registerAppUser` → `loginAppUser` → `appUserMe` works inside a test session. To also exercise developer auth and ABAC policies, set **`X-Test-Auth: required`** and send normal credentials; the DB is still the test session’s.
|
|
46
95
|
4. **Cleanup:** `DELETE /api/v1/test/sessions/current` with `X-Test-Session` (no other auth), or `DELETE /api/v1/test/sessions/all` / `DELETE /api/v1/test/sessions/{token}` with normal auth for the owning key or JWT.
|
|
47
96
|
5. **SDKs:** **JS/TS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, … })`, optional `testRequireAuth`, then `destroyTestSession()`. **Python:** `async with XCiteDBClient.test_session(...)` or manual token + `test_session_token` / `test_require_auth` constructor args. **C++:** `XCiteDBClient::create_test_session(options)`, `destroy_test_session()`, optional `test_require_auth` in options.
|
|
48
97
|
|
|
@@ -149,6 +198,8 @@ interface XCiteDBClientOptions {
|
|
|
149
198
|
- `version()` — Server version info
|
|
150
199
|
- `platformLogin(email, password)` — Platform operator sign-in
|
|
151
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
|
|
152
203
|
- `setContext(ctx)` — Update branch/date context
|
|
153
204
|
- `setProjectId(id)` — Switch active project (platform console)
|
|
154
205
|
|
|
@@ -209,7 +260,7 @@ interface XCiteDBClientOptions {
|
|
|
209
260
|
### Advanced: policy expressions (ABAC)
|
|
210
261
|
|
|
211
262
|
- **Actions** (use in `policy.actions`): `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
|
|
212
|
-
- **`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).
|
|
213
264
|
- **Operators:** `=`, `!=`, `>=`, `<=`, `>`, `<`, `in`, `contains`, `starts_with`, `ends_with`, `&`, `|`, `!`, `+` (string concat), postfix `!` (exists).
|
|
214
265
|
- **Examples:** `resource.path[0] = subject.attr.tenant_code` — tenant isolation; `subject.attr.level >= 5` — numeric attribute gate.
|
|
215
266
|
|
|
@@ -247,7 +298,7 @@ When calling `queryByIdentifier` or `queryDocuments`, the `flags` parameter cont
|
|
|
247
298
|
|
|
248
299
|
### Errors
|
|
249
300
|
|
|
250
|
-
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).
|
|
251
302
|
|
|
252
303
|
## Python SDK (`xcitedb`)
|
|
253
304
|
|