@xcitedbs/client 0.3.1 → 0.3.3
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 +60 -1
- package/dist/client.js +345 -11
- package/dist/client.test.js +131 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +152 -0
- package/llms.txt +10 -2
- package/package.json +1 -1
- package/unquery-ai-guide.md +19 -3
- package/unquery-grammar.md +6 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, SmartDiffRef, SmartDiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult } from './types';
|
|
1
|
+
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, SmartDiffRef, SmartDiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, TriggerEventsResponse, StoredPolicyResponse, TxnRequest, TxnResponse, VerifyAppUserTokenOptions, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult } from './types';
|
|
2
2
|
import { WebSocketSubscription } from './websocket';
|
|
3
3
|
export declare class XCiteDBClient {
|
|
4
4
|
private baseUrl;
|
|
@@ -24,6 +24,9 @@ export declare class XCiteDBClient {
|
|
|
24
24
|
private userIsolation?;
|
|
25
25
|
private cachedAppUserId?;
|
|
26
26
|
private readonly requestTimeoutMs?;
|
|
27
|
+
private jwksCache?;
|
|
28
|
+
/** TTL for the JWKS cache; rotations land via `kid` mismatch + force refresh. */
|
|
29
|
+
private static readonly JWKS_TTL_MS;
|
|
27
30
|
/** Set by {@link XCiteDBClient.createTestSession} when the server returns a `bootstrap` summary. */
|
|
28
31
|
lastTestSessionBootstrap?: TestSessionBootstrapSummary;
|
|
29
32
|
constructor(options: XCiteDBClientOptions);
|
|
@@ -47,11 +50,33 @@ export declare class XCiteDBClient {
|
|
|
47
50
|
*/
|
|
48
51
|
static buildProjectGroup(projectId: string, role: 'admin' | 'editor' | 'viewer'): string;
|
|
49
52
|
private static decodeJwtPayloadJson;
|
|
53
|
+
/** Build {@link XCiteDBJwtClaims} from a decoded JWT payload, or `null` if shape is wrong. */
|
|
54
|
+
private static claimsFromPayload;
|
|
50
55
|
/**
|
|
51
56
|
* Decode `appUserAccessToken`, or `accessToken` if no app token (platform JWT), without verifying the signature.
|
|
52
57
|
* Use for debugging ABAC (compare `tenant_id` and `groups` to `project:<…>:role` in policies).
|
|
53
58
|
*/
|
|
54
59
|
getTokenClaims(): XCiteDBJwtClaims | null;
|
|
60
|
+
private static decodeJwtHeaderJson;
|
|
61
|
+
private static base64UrlToBytes;
|
|
62
|
+
/** Fetch JWKS, honoring the in-memory TTL unless `force` is set. */
|
|
63
|
+
private fetchJwks;
|
|
64
|
+
/**
|
|
65
|
+
* Verify an app-user JWT and return its claims. Validates signature, expiry, and
|
|
66
|
+
* (unless overridden in {@link VerifyAppUserTokenOptions.expectedTenantId}) tenant scope.
|
|
67
|
+
*
|
|
68
|
+
* The verification path depends on the server's `auth.app_users.jwt_algorithm`:
|
|
69
|
+
* * **`RS256`** — verifies locally against `/.well-known/jwks.json` (cached) using
|
|
70
|
+
* `crypto.subtle`. No round-trip per call after the JWKS is cached.
|
|
71
|
+
* * **`HS256`** — there's no public key the BFF can hold, so the SDK falls back to
|
|
72
|
+
* `POST /api/v1/app/auth/verify` (one round-trip). The shape is the same.
|
|
73
|
+
*
|
|
74
|
+
* Throws {@link XCiteDBAuthError} on bad signature, expired token, tenant mismatch, or
|
|
75
|
+
* malformed payload.
|
|
76
|
+
*/
|
|
77
|
+
verifyAppUserToken(token: string, options?: VerifyAppUserTokenOptions): Promise<XCiteDBJwtClaims>;
|
|
78
|
+
private verifyRs256Locally;
|
|
79
|
+
private verifyTokenViaServer;
|
|
55
80
|
private cacheAppUserIdFromPair;
|
|
56
81
|
private clearAppUserIdCache;
|
|
57
82
|
private getAppUserId;
|
|
@@ -324,6 +349,13 @@ export declare class XCiteDBClient {
|
|
|
324
349
|
listTriggers(): Promise<Record<string, TriggerDefinition>>;
|
|
325
350
|
getTrigger(name: string): Promise<StoredTriggerResponse>;
|
|
326
351
|
deleteTrigger(name: string): Promise<void>;
|
|
352
|
+
/**
|
|
353
|
+
* Recent trigger firings for the current project (`GET /api/v1/_xcitedb/trigger-events`).
|
|
354
|
+
*
|
|
355
|
+
* Newest first. `limit` is clamped server-side to `[1, 500]` (default 100). Only
|
|
356
|
+
* `admin`/`editor` roles can read this; public-key requests are rejected.
|
|
357
|
+
*/
|
|
358
|
+
listTriggerEvents(limit?: number): Promise<TriggerEventsResponse>;
|
|
327
359
|
/**
|
|
328
360
|
* Dry-run access check (`POST /api/v1/security/check`). Returns `effect` and optional `matched_policy_id`.
|
|
329
361
|
* Useful for debugging policies. Actions: `read`, `write`, `delete`, `list`, `unquery`.
|
|
@@ -838,6 +870,33 @@ export declare class XCiteDBClient {
|
|
|
838
870
|
* Best-effort batch JSON document writes (`POST /api/v1/json-documents/batch`). Each item is independent; check `results[].ok`.
|
|
839
871
|
*/
|
|
840
872
|
writeJsonDocumentsBatch(items: JsonDocumentBatchItem[]): Promise<DocumentBatchResponse>;
|
|
873
|
+
/**
|
|
874
|
+
* Atomic multi-operation transaction (`POST /api/v1/txn`). All `operations` and `preconditions`
|
|
875
|
+
* apply or none do — any failure aborts the whole txn and unexecuted ops carry
|
|
876
|
+
* `code: 0` with `error: "not executed (atomic txn aborted)"`.
|
|
877
|
+
*
|
|
878
|
+
* Operation kinds: `WriteJson`, `WriteXml`, `WriteMeta`, `ClearMetaPath`, `DeleteIdentifier`,
|
|
879
|
+
* `AddIdentifier`. Precondition kinds: `MetaEquals`, `MetaExists`, `MetaAbsent`,
|
|
880
|
+
* `DocumentExists`, `DocumentAbsent`. See {@link TxnOperation} / {@link TxnPrecondition} for
|
|
881
|
+
* field-by-field shapes.
|
|
882
|
+
*
|
|
883
|
+
* Pass `idempotency_key` to make safe retries — the server caches the response body for ~24h
|
|
884
|
+
* so a repeated call returns the original 200 result.
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* ```ts
|
|
888
|
+
* const r = await client.txn({
|
|
889
|
+
* preconditions: [{ kind: 'MetaEquals', identifier: '/songs/42', path: 'rating', value: 4 }],
|
|
890
|
+
* operations: [
|
|
891
|
+
* { kind: 'WriteMeta', identifier: '/songs/42', path: 'rating', value: 5 },
|
|
892
|
+
* { kind: 'WriteJson', identifier: '/users/u1/votes/42', data: { score: 5 } },
|
|
893
|
+
* ],
|
|
894
|
+
* idempotency_key: 'vote-u1-42-v5',
|
|
895
|
+
* });
|
|
896
|
+
* if (!r.committed) throw new Error(r.error);
|
|
897
|
+
* ```
|
|
898
|
+
*/
|
|
899
|
+
txn(req: TxnRequest): Promise<TxnResponse>;
|
|
841
900
|
readJsonDocument<T = unknown>(identifier: string): Promise<T>;
|
|
842
901
|
deleteJsonDocument(identifier: string): Promise<void>;
|
|
843
902
|
listJsonDocuments(match?: string, limit?: number, offset?: number): Promise<ListIdentifiersResult>;
|
package/dist/client.js
CHANGED
|
@@ -9,6 +9,20 @@ function joinUrl(base, path) {
|
|
|
9
9
|
const p = path.startsWith('/') ? path : `/${path}`;
|
|
10
10
|
return `${b}${p}`;
|
|
11
11
|
}
|
|
12
|
+
function pickJwk(keys, kid) {
|
|
13
|
+
if (!Array.isArray(keys) || keys.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (kid) {
|
|
16
|
+
const byKid = keys.find((k) => k.kid === kid);
|
|
17
|
+
if (byKid)
|
|
18
|
+
return byKid;
|
|
19
|
+
}
|
|
20
|
+
// No matching kid: only accept a sole RSA signing key — never silently pick from a multi-key set.
|
|
21
|
+
const rsa = keys.filter((k) => k.kty === 'RSA' && (k.use === 'sig' || k.use === undefined));
|
|
22
|
+
if (rsa.length === 1)
|
|
23
|
+
return rsa[0];
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
12
26
|
function newClientRequestId() {
|
|
13
27
|
const c = globalThis.crypto?.randomUUID?.();
|
|
14
28
|
if (c)
|
|
@@ -229,17 +243,8 @@ class XCiteDBClient {
|
|
|
229
243
|
return null;
|
|
230
244
|
}
|
|
231
245
|
}
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
* Use for debugging ABAC (compare `tenant_id` and `groups` to `project:<…>:role` in policies).
|
|
235
|
-
*/
|
|
236
|
-
getTokenClaims() {
|
|
237
|
-
const raw = this.appUserAccessToken ?? this.accessToken;
|
|
238
|
-
if (!raw)
|
|
239
|
-
return null;
|
|
240
|
-
const p = XCiteDBClient.decodeJwtPayloadJson(raw);
|
|
241
|
-
if (!p)
|
|
242
|
-
return null;
|
|
246
|
+
/** Build {@link XCiteDBJwtClaims} from a decoded JWT payload, or `null` if shape is wrong. */
|
|
247
|
+
static claimsFromPayload(p) {
|
|
243
248
|
let groups = [];
|
|
244
249
|
const g = p['groups'];
|
|
245
250
|
if (Array.isArray(g)) {
|
|
@@ -273,6 +278,209 @@ class XCiteDBClient {
|
|
|
273
278
|
jti: typeof jti === 'string' ? jti : undefined,
|
|
274
279
|
};
|
|
275
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Decode `appUserAccessToken`, or `accessToken` if no app token (platform JWT), without verifying the signature.
|
|
283
|
+
* Use for debugging ABAC (compare `tenant_id` and `groups` to `project:<…>:role` in policies).
|
|
284
|
+
*/
|
|
285
|
+
getTokenClaims() {
|
|
286
|
+
const raw = this.appUserAccessToken ?? this.accessToken;
|
|
287
|
+
if (!raw)
|
|
288
|
+
return null;
|
|
289
|
+
const p = XCiteDBClient.decodeJwtPayloadJson(raw);
|
|
290
|
+
if (!p)
|
|
291
|
+
return null;
|
|
292
|
+
return XCiteDBClient.claimsFromPayload(p);
|
|
293
|
+
}
|
|
294
|
+
static decodeJwtHeaderJson(token) {
|
|
295
|
+
const parts = token.split('.');
|
|
296
|
+
if (parts.length < 2)
|
|
297
|
+
return null;
|
|
298
|
+
try {
|
|
299
|
+
let b64 = parts[0].replace(/-/g, '+').replace(/_/g, '/');
|
|
300
|
+
const pad = (4 - (b64.length % 4)) % 4;
|
|
301
|
+
b64 += '='.repeat(pad);
|
|
302
|
+
const g = globalThis;
|
|
303
|
+
let json = null;
|
|
304
|
+
if (g.Buffer) {
|
|
305
|
+
json = g.Buffer.from(b64, 'base64').toString('utf8');
|
|
306
|
+
}
|
|
307
|
+
else if (typeof atob === 'function') {
|
|
308
|
+
json = atob(b64);
|
|
309
|
+
}
|
|
310
|
+
if (json === null)
|
|
311
|
+
return null;
|
|
312
|
+
return JSON.parse(json);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
static base64UrlToBytes(b64url) {
|
|
319
|
+
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
320
|
+
const pad = (4 - (b64.length % 4)) % 4;
|
|
321
|
+
b64 += '='.repeat(pad);
|
|
322
|
+
const g = globalThis;
|
|
323
|
+
if (g.Buffer) {
|
|
324
|
+
const buf = g.Buffer.from(b64, 'base64');
|
|
325
|
+
// Buffer extends Uint8Array; copy to a fresh view to detach from the underlying pool.
|
|
326
|
+
return new Uint8Array(buf);
|
|
327
|
+
}
|
|
328
|
+
if (typeof atob !== 'function') {
|
|
329
|
+
throw new types_1.XCiteDBAuthError('No base64 decoder available in this environment', 0, null);
|
|
330
|
+
}
|
|
331
|
+
const bin = atob(b64);
|
|
332
|
+
const out = new Uint8Array(bin.length);
|
|
333
|
+
for (let i = 0; i < bin.length; i++)
|
|
334
|
+
out[i] = bin.charCodeAt(i);
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
/** Fetch JWKS, honoring the in-memory TTL unless `force` is set. */
|
|
338
|
+
async fetchJwks(force) {
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
if (!force && this.jwksCache && now - this.jwksCache.fetchedAt < XCiteDBClient.JWKS_TTL_MS) {
|
|
341
|
+
return this.jwksCache.keys;
|
|
342
|
+
}
|
|
343
|
+
const url = joinUrl(this.baseUrl, '/.well-known/jwks.json');
|
|
344
|
+
const sig = requestTimeoutSignal(this.requestTimeoutMs);
|
|
345
|
+
const init = { method: 'GET', headers: { Accept: 'application/json' } };
|
|
346
|
+
if (sig)
|
|
347
|
+
init.signal = sig;
|
|
348
|
+
let res;
|
|
349
|
+
try {
|
|
350
|
+
res = await fetch(url, init);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
throw new types_1.XCiteDBAuthError(`JWKS fetch failed: ${e instanceof Error ? e.message : String(e)}`, 0, null);
|
|
354
|
+
}
|
|
355
|
+
if (!res.ok) {
|
|
356
|
+
throw new types_1.XCiteDBAuthError(`JWKS fetch returned HTTP ${res.status}`, res.status, null);
|
|
357
|
+
}
|
|
358
|
+
let body;
|
|
359
|
+
try {
|
|
360
|
+
body = (await res.json());
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
throw new types_1.XCiteDBAuthError('JWKS response was not JSON', 0, null);
|
|
364
|
+
}
|
|
365
|
+
const keys = Array.isArray(body?.keys) ? body.keys : [];
|
|
366
|
+
this.jwksCache = { fetchedAt: now, keys };
|
|
367
|
+
return keys;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Verify an app-user JWT and return its claims. Validates signature, expiry, and
|
|
371
|
+
* (unless overridden in {@link VerifyAppUserTokenOptions.expectedTenantId}) tenant scope.
|
|
372
|
+
*
|
|
373
|
+
* The verification path depends on the server's `auth.app_users.jwt_algorithm`:
|
|
374
|
+
* * **`RS256`** — verifies locally against `/.well-known/jwks.json` (cached) using
|
|
375
|
+
* `crypto.subtle`. No round-trip per call after the JWKS is cached.
|
|
376
|
+
* * **`HS256`** — there's no public key the BFF can hold, so the SDK falls back to
|
|
377
|
+
* `POST /api/v1/app/auth/verify` (one round-trip). The shape is the same.
|
|
378
|
+
*
|
|
379
|
+
* Throws {@link XCiteDBAuthError} on bad signature, expired token, tenant mismatch, or
|
|
380
|
+
* malformed payload.
|
|
381
|
+
*/
|
|
382
|
+
async verifyAppUserToken(token, options = {}) {
|
|
383
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
384
|
+
throw new types_1.XCiteDBAuthError('verifyAppUserToken: token must be a non-empty string', 0, null);
|
|
385
|
+
}
|
|
386
|
+
const parts = token.split('.');
|
|
387
|
+
if (parts.length !== 3) {
|
|
388
|
+
throw new types_1.XCiteDBAuthError('verifyAppUserToken: token is not a compact JWS', 0, null);
|
|
389
|
+
}
|
|
390
|
+
const header = XCiteDBClient.decodeJwtHeaderJson(token);
|
|
391
|
+
const alg = header && typeof header['alg'] === 'string' ? header['alg'] : '';
|
|
392
|
+
const kid = header && typeof header['kid'] === 'string' ? header['kid'] : '';
|
|
393
|
+
let claims;
|
|
394
|
+
if (alg === 'RS256') {
|
|
395
|
+
claims = await this.verifyRs256Locally(token, kid, options.forceRefreshJwks === true);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
// HS256 (the server default), or any unknown alg — round-trip to the server.
|
|
399
|
+
claims = await this.verifyTokenViaServer(token);
|
|
400
|
+
}
|
|
401
|
+
const skew = options.clockToleranceSeconds ?? 30;
|
|
402
|
+
if (typeof claims.exp === 'number') {
|
|
403
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
404
|
+
if (claims.exp + skew < nowSec) {
|
|
405
|
+
throw new types_1.XCiteDBAuthError('Token expired', 401, null);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const expected = options.expectedTenantId !== undefined
|
|
409
|
+
? options.expectedTenantId
|
|
410
|
+
: this.defaultContext.project_id ?? this.defaultContext.tenant_id ?? this.projectId ?? '';
|
|
411
|
+
if (expected && claims.tenant_id !== expected) {
|
|
412
|
+
throw new types_1.XCiteDBAuthError(`Token tenant_id "${claims.tenant_id}" does not match expected "${expected}"`, 401, null);
|
|
413
|
+
}
|
|
414
|
+
return claims;
|
|
415
|
+
}
|
|
416
|
+
async verifyRs256Locally(token, kid, forceRefresh) {
|
|
417
|
+
const subtle = globalThis.crypto?.subtle;
|
|
418
|
+
if (!subtle) {
|
|
419
|
+
throw new types_1.XCiteDBAuthError('verifyAppUserToken: crypto.subtle is unavailable in this environment', 0, null);
|
|
420
|
+
}
|
|
421
|
+
let keys = await this.fetchJwks(forceRefresh);
|
|
422
|
+
let jwk = pickJwk(keys, kid);
|
|
423
|
+
if (!jwk) {
|
|
424
|
+
// Possibly a key rotation since we last cached. Force-refresh once and retry.
|
|
425
|
+
keys = await this.fetchJwks(true);
|
|
426
|
+
jwk = pickJwk(keys, kid);
|
|
427
|
+
}
|
|
428
|
+
if (!jwk) {
|
|
429
|
+
// JWKS empty → server likely on HS256 (or RS256 not yet provisioned). Fall back.
|
|
430
|
+
return this.verifyTokenViaServer(token);
|
|
431
|
+
}
|
|
432
|
+
let cryptoKey;
|
|
433
|
+
try {
|
|
434
|
+
cryptoKey = await subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']);
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
throw new types_1.XCiteDBAuthError(`JWKS import failed: ${e instanceof Error ? e.message : String(e)}`, 0, null);
|
|
438
|
+
}
|
|
439
|
+
const parts = token.split('.');
|
|
440
|
+
const signed = `${parts[0]}.${parts[1]}`;
|
|
441
|
+
const sigBytes = XCiteDBClient.base64UrlToBytes(parts[2]);
|
|
442
|
+
const dataBytes = new TextEncoder().encode(signed);
|
|
443
|
+
// Copy into fresh ArrayBuffer-backed views — strict TS lib types disallow ArrayBufferLike (SAB).
|
|
444
|
+
const sigBuf = new ArrayBuffer(sigBytes.byteLength);
|
|
445
|
+
new Uint8Array(sigBuf).set(sigBytes);
|
|
446
|
+
const dataBuf = new ArrayBuffer(dataBytes.byteLength);
|
|
447
|
+
new Uint8Array(dataBuf).set(dataBytes);
|
|
448
|
+
const ok = await subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, cryptoKey, sigBuf, dataBuf);
|
|
449
|
+
if (!ok) {
|
|
450
|
+
throw new types_1.XCiteDBAuthError('Token signature is invalid', 401, null);
|
|
451
|
+
}
|
|
452
|
+
const payload = XCiteDBClient.decodeJwtPayloadJson(token);
|
|
453
|
+
if (!payload) {
|
|
454
|
+
throw new types_1.XCiteDBAuthError('Token payload is malformed', 401, null);
|
|
455
|
+
}
|
|
456
|
+
const claims = XCiteDBClient.claimsFromPayload(payload);
|
|
457
|
+
if (!claims) {
|
|
458
|
+
throw new types_1.XCiteDBAuthError('Token payload is missing required claims', 401, null);
|
|
459
|
+
}
|
|
460
|
+
return claims;
|
|
461
|
+
}
|
|
462
|
+
async verifyTokenViaServer(token) {
|
|
463
|
+
try {
|
|
464
|
+
const r = await this.request('POST', '/api/v1/app/auth/verify', this.mergeAppTenant({ token }));
|
|
465
|
+
const claims = r?.claims;
|
|
466
|
+
if (!claims || typeof claims.sub !== 'string' || typeof claims.tenant_id !== 'string') {
|
|
467
|
+
throw new types_1.XCiteDBAuthError('Server verify returned a malformed claims object', 0, r);
|
|
468
|
+
}
|
|
469
|
+
return claims;
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
if (e instanceof types_1.XCiteDBAuthError)
|
|
473
|
+
throw e;
|
|
474
|
+
if (e instanceof types_1.XCiteDBError) {
|
|
475
|
+
throw new types_1.XCiteDBAuthError(e.message, e.status, e.body, {
|
|
476
|
+
reason: e.reason,
|
|
477
|
+
serverRequestId: e.serverRequestId,
|
|
478
|
+
clientRequestId: e.clientRequestId,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
throw e;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
276
484
|
cacheAppUserIdFromPair(pair) {
|
|
277
485
|
if (pair?.user?.user_id) {
|
|
278
486
|
this.cachedAppUserId = pair.user.user_id;
|
|
@@ -1201,6 +1409,20 @@ class XCiteDBClient {
|
|
|
1201
1409
|
const q = buildQuery({ name });
|
|
1202
1410
|
await this.request('DELETE', `/api/v1/triggers${q}`);
|
|
1203
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Recent trigger firings for the current project (`GET /api/v1/_xcitedb/trigger-events`).
|
|
1414
|
+
*
|
|
1415
|
+
* Newest first. `limit` is clamped server-side to `[1, 500]` (default 100). Only
|
|
1416
|
+
* `admin`/`editor` roles can read this; public-key requests are rejected.
|
|
1417
|
+
*/
|
|
1418
|
+
async listTriggerEvents(limit) {
|
|
1419
|
+
const q = buildQuery({ limit });
|
|
1420
|
+
const r = await this.request('GET', `/api/v1/_xcitedb/trigger-events${q}`);
|
|
1421
|
+
return {
|
|
1422
|
+
events: Array.isArray(r?.events) ? r.events : [],
|
|
1423
|
+
limit: typeof r?.limit === 'number' ? r.limit : 0,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1204
1426
|
/**
|
|
1205
1427
|
* Dry-run access check (`POST /api/v1/security/check`). Returns `effect` and optional `matched_policy_id`.
|
|
1206
1428
|
* Useful for debugging policies. Actions: `read`, `write`, `delete`, `list`, `unquery`.
|
|
@@ -2127,6 +2349,8 @@ class XCiteDBClient {
|
|
|
2127
2349
|
body.date_from = q.date_from;
|
|
2128
2350
|
if (q.date_to !== undefined && q.date_to !== '')
|
|
2129
2351
|
body.date_to = q.date_to;
|
|
2352
|
+
if (q.identifiers && q.identifiers.length > 0)
|
|
2353
|
+
body.identifiers = q.identifiers;
|
|
2130
2354
|
const data = await this.request('POST', '/api/v1/search', body);
|
|
2131
2355
|
const hits = [];
|
|
2132
2356
|
if (Array.isArray(data.hits)) {
|
|
@@ -2504,6 +2728,8 @@ class XCiteDBClient {
|
|
|
2504
2728
|
body.max_tokens = options.max_tokens;
|
|
2505
2729
|
if (options.messages?.length)
|
|
2506
2730
|
body.messages = options.messages;
|
|
2731
|
+
if (options.identifiers && options.identifiers.length > 0)
|
|
2732
|
+
body.identifiers = options.identifiers;
|
|
2507
2733
|
const data = await this.request('POST', '/api/v1/rag/query', body);
|
|
2508
2734
|
if (!data || typeof data !== 'object') {
|
|
2509
2735
|
throw new types_1.XCiteDBError('Invalid RAG response', 500, data);
|
|
@@ -2530,6 +2756,9 @@ class XCiteDBClient {
|
|
|
2530
2756
|
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
2531
2757
|
...(options.max_tokens !== undefined ? { max_tokens: options.max_tokens } : {}),
|
|
2532
2758
|
...(options.messages?.length ? { messages: options.messages } : {}),
|
|
2759
|
+
...(options.identifiers && options.identifiers.length > 0
|
|
2760
|
+
? { identifiers: options.identifiers }
|
|
2761
|
+
: {}),
|
|
2533
2762
|
});
|
|
2534
2763
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2535
2764
|
const url = joinUrl(this.baseUrl, path);
|
|
@@ -2638,6 +2867,109 @@ class XCiteDBClient {
|
|
|
2638
2867
|
}
|
|
2639
2868
|
return r;
|
|
2640
2869
|
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Atomic multi-operation transaction (`POST /api/v1/txn`). All `operations` and `preconditions`
|
|
2872
|
+
* apply or none do — any failure aborts the whole txn and unexecuted ops carry
|
|
2873
|
+
* `code: 0` with `error: "not executed (atomic txn aborted)"`.
|
|
2874
|
+
*
|
|
2875
|
+
* Operation kinds: `WriteJson`, `WriteXml`, `WriteMeta`, `ClearMetaPath`, `DeleteIdentifier`,
|
|
2876
|
+
* `AddIdentifier`. Precondition kinds: `MetaEquals`, `MetaExists`, `MetaAbsent`,
|
|
2877
|
+
* `DocumentExists`, `DocumentAbsent`. See {@link TxnOperation} / {@link TxnPrecondition} for
|
|
2878
|
+
* field-by-field shapes.
|
|
2879
|
+
*
|
|
2880
|
+
* Pass `idempotency_key` to make safe retries — the server caches the response body for ~24h
|
|
2881
|
+
* so a repeated call returns the original 200 result.
|
|
2882
|
+
*
|
|
2883
|
+
* @example
|
|
2884
|
+
* ```ts
|
|
2885
|
+
* const r = await client.txn({
|
|
2886
|
+
* preconditions: [{ kind: 'MetaEquals', identifier: '/songs/42', path: 'rating', value: 4 }],
|
|
2887
|
+
* operations: [
|
|
2888
|
+
* { kind: 'WriteMeta', identifier: '/songs/42', path: 'rating', value: 5 },
|
|
2889
|
+
* { kind: 'WriteJson', identifier: '/users/u1/votes/42', data: { score: 5 } },
|
|
2890
|
+
* ],
|
|
2891
|
+
* idempotency_key: 'vote-u1-42-v5',
|
|
2892
|
+
* });
|
|
2893
|
+
* if (!r.committed) throw new Error(r.error);
|
|
2894
|
+
* ```
|
|
2895
|
+
*/
|
|
2896
|
+
async txn(req) {
|
|
2897
|
+
if (!req || !Array.isArray(req.operations) || req.operations.length === 0) {
|
|
2898
|
+
throw new types_1.XCiteDBError('txn: operations must be a non-empty array', 400, null);
|
|
2899
|
+
}
|
|
2900
|
+
const operations = req.operations.map((op) => {
|
|
2901
|
+
const out = { kind: op.kind, identifier: this.isoPrefixId(op.identifier) };
|
|
2902
|
+
switch (op.kind) {
|
|
2903
|
+
case 'WriteJson':
|
|
2904
|
+
out.data = op.data;
|
|
2905
|
+
if (op.overwrite)
|
|
2906
|
+
out.overwrite = true;
|
|
2907
|
+
break;
|
|
2908
|
+
case 'WriteXml':
|
|
2909
|
+
out.xml = this.isoApplyXmlDbIdentifier(op.xml);
|
|
2910
|
+
if (op.is_top)
|
|
2911
|
+
out.is_top = true;
|
|
2912
|
+
if (op.compare_attributes)
|
|
2913
|
+
out.compare_attributes = true;
|
|
2914
|
+
break;
|
|
2915
|
+
case 'WriteMeta':
|
|
2916
|
+
out.value = op.value;
|
|
2917
|
+
if (op.path !== undefined)
|
|
2918
|
+
out.path = op.path;
|
|
2919
|
+
if (op.mode !== undefined)
|
|
2920
|
+
out.mode = op.mode;
|
|
2921
|
+
if (op.overwrite)
|
|
2922
|
+
out.overwrite = true;
|
|
2923
|
+
break;
|
|
2924
|
+
case 'ClearMetaPath':
|
|
2925
|
+
out.path = op.path;
|
|
2926
|
+
break;
|
|
2927
|
+
case 'DeleteIdentifier':
|
|
2928
|
+
case 'AddIdentifier':
|
|
2929
|
+
break;
|
|
2930
|
+
}
|
|
2931
|
+
return out;
|
|
2932
|
+
});
|
|
2933
|
+
const body = { operations };
|
|
2934
|
+
if (req.preconditions && req.preconditions.length > 0) {
|
|
2935
|
+
body.preconditions = req.preconditions.map((p) => {
|
|
2936
|
+
const out = { kind: p.kind, identifier: this.isoPrefixId(p.identifier) };
|
|
2937
|
+
switch (p.kind) {
|
|
2938
|
+
case 'MetaEquals':
|
|
2939
|
+
out.value = p.value;
|
|
2940
|
+
if (p.path !== undefined)
|
|
2941
|
+
out.path = p.path;
|
|
2942
|
+
break;
|
|
2943
|
+
case 'MetaExists':
|
|
2944
|
+
case 'MetaAbsent':
|
|
2945
|
+
if (p.path !== undefined)
|
|
2946
|
+
out.path = p.path;
|
|
2947
|
+
break;
|
|
2948
|
+
case 'DocumentExists':
|
|
2949
|
+
case 'DocumentAbsent':
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
return out;
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
if (req.idempotency_key !== undefined)
|
|
2956
|
+
body.idempotency_key = req.idempotency_key;
|
|
2957
|
+
const r = await this.request('POST', '/api/v1/txn', body);
|
|
2958
|
+
// Un-prefix identifiers in the response so callers see what they passed in.
|
|
2959
|
+
if (Array.isArray(r?.results)) {
|
|
2960
|
+
for (const row of r.results) {
|
|
2961
|
+
if (row?.identifier)
|
|
2962
|
+
row.identifier = this.isoUnprefixId(String(row.identifier));
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
if (Array.isArray(r?.preconditions)) {
|
|
2966
|
+
for (const row of r.preconditions) {
|
|
2967
|
+
if (row?.identifier)
|
|
2968
|
+
row.identifier = this.isoUnprefixId(String(row.identifier));
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return r;
|
|
2972
|
+
}
|
|
2641
2973
|
async readJsonDocument(identifier) {
|
|
2642
2974
|
return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
2643
2975
|
}
|
|
@@ -2710,3 +3042,5 @@ class XCiteDBClient {
|
|
|
2710
3042
|
}
|
|
2711
3043
|
}
|
|
2712
3044
|
exports.XCiteDBClient = XCiteDBClient;
|
|
3045
|
+
/** TTL for the JWKS cache; rotations land via `kid` mismatch + force refresh. */
|
|
3046
|
+
XCiteDBClient.JWKS_TTL_MS = 10 * 60 * 1000;
|
package/dist/client.test.js
CHANGED
|
@@ -385,3 +385,134 @@ const types_js_1 = require("./types.js");
|
|
|
385
385
|
}
|
|
386
386
|
});
|
|
387
387
|
});
|
|
388
|
+
(0, node_test_1.describe)('txn wrapper', () => {
|
|
389
|
+
(0, node_test_1.it)('rejects empty operations array client-side', async () => {
|
|
390
|
+
const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'k' });
|
|
391
|
+
await strict_1.default.rejects(c.txn({ operations: [] }), /non-empty/);
|
|
392
|
+
});
|
|
393
|
+
(0, node_test_1.it)('serializes mixed ops + preconditions and unprefixes response identifiers', async () => {
|
|
394
|
+
const captured = { value: null };
|
|
395
|
+
const orig = globalThis.fetch;
|
|
396
|
+
globalThis.fetch = node_test_1.mock.fn(async (input, init) => {
|
|
397
|
+
captured.value = {
|
|
398
|
+
url: String(input),
|
|
399
|
+
body: JSON.parse(String(init?.body ?? '{}')),
|
|
400
|
+
};
|
|
401
|
+
return new Response(JSON.stringify({
|
|
402
|
+
committed: true,
|
|
403
|
+
preconditions: [
|
|
404
|
+
{ index: 0, identifier: '/users/u1/songs/42', kind: 'MetaEquals', ok: true },
|
|
405
|
+
],
|
|
406
|
+
results: [
|
|
407
|
+
{ index: 0, identifier: '/users/u1/songs/42', kind: 'WriteMeta', ok: true, code: 200 },
|
|
408
|
+
{ index: 1, identifier: '/users/u1/votes/42', kind: 'WriteJson', ok: true, code: 200 },
|
|
409
|
+
],
|
|
410
|
+
}), { status: 200 });
|
|
411
|
+
});
|
|
412
|
+
try {
|
|
413
|
+
const c = new client_js_1.XCiteDBClient({
|
|
414
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
415
|
+
apiKey: 'k',
|
|
416
|
+
userIsolation: { enabled: true, namespace: '/users/{userId}' },
|
|
417
|
+
});
|
|
418
|
+
c.setAppUserTokens('h.eyJzdWIiOiJ1MSJ9.s');
|
|
419
|
+
const r = await c.txn({
|
|
420
|
+
preconditions: [{ kind: 'MetaEquals', identifier: '/songs/42', path: 'rating', value: 4 }],
|
|
421
|
+
operations: [
|
|
422
|
+
{ kind: 'WriteMeta', identifier: '/songs/42', path: 'rating', value: 5 },
|
|
423
|
+
{ kind: 'WriteJson', identifier: '/votes/42', data: { score: 5 } },
|
|
424
|
+
],
|
|
425
|
+
idempotency_key: 'abc',
|
|
426
|
+
});
|
|
427
|
+
strict_1.default.equal(r.committed, true);
|
|
428
|
+
// identifiers come back un-prefixed back to caller's namespace.
|
|
429
|
+
strict_1.default.equal(r.results[0].identifier, '/songs/42');
|
|
430
|
+
strict_1.default.equal(r.results[1].identifier, '/votes/42');
|
|
431
|
+
strict_1.default.equal(r.preconditions[0].identifier, '/songs/42');
|
|
432
|
+
strict_1.default.ok(captured.value);
|
|
433
|
+
strict_1.default.match(captured.value.url, /\/api\/v1\/txn$/);
|
|
434
|
+
const body = captured.value.body;
|
|
435
|
+
strict_1.default.equal(body.idempotency_key, 'abc');
|
|
436
|
+
const ops = body.operations;
|
|
437
|
+
strict_1.default.equal(ops[0].identifier, '/users/u1/songs/42');
|
|
438
|
+
strict_1.default.equal(ops[0].kind, 'WriteMeta');
|
|
439
|
+
strict_1.default.equal(ops[0].value, 5);
|
|
440
|
+
strict_1.default.equal(ops[1].identifier, '/users/u1/votes/42');
|
|
441
|
+
const pre = body.preconditions;
|
|
442
|
+
strict_1.default.equal(pre[0].identifier, '/users/u1/songs/42');
|
|
443
|
+
strict_1.default.equal(pre[0].value, 4);
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
globalThis.fetch = orig;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
(0, node_test_1.describe)('verifyAppUserToken', () => {
|
|
451
|
+
(0, node_test_1.it)('rejects malformed tokens client-side (not a compact JWS)', async () => {
|
|
452
|
+
const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'k' });
|
|
453
|
+
await strict_1.default.rejects(c.verifyAppUserToken('only.two-segments'), /compact JWS/);
|
|
454
|
+
await strict_1.default.rejects(c.verifyAppUserToken(''), /non-empty/);
|
|
455
|
+
});
|
|
456
|
+
(0, node_test_1.it)('falls back to /api/v1/app/auth/verify for HS256 and enforces tenant', async () => {
|
|
457
|
+
// Token with header alg=HS256 — SDK should not try JWKS, it should POST /verify.
|
|
458
|
+
// header { alg: HS256 } base64url, payload arbitrary, sig dummy.
|
|
459
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
460
|
+
const payload = Buffer.from(JSON.stringify({ sub: 'u1', tenant_id: 't1' })).toString('base64url');
|
|
461
|
+
const token = `${header}.${payload}.sig`;
|
|
462
|
+
const orig = globalThis.fetch;
|
|
463
|
+
let path = '';
|
|
464
|
+
globalThis.fetch = node_test_1.mock.fn(async (input) => {
|
|
465
|
+
path = String(input);
|
|
466
|
+
return new Response(JSON.stringify({
|
|
467
|
+
claims: {
|
|
468
|
+
sub: 'u1',
|
|
469
|
+
tenant_id: 't1',
|
|
470
|
+
groups: ['g'],
|
|
471
|
+
email: 'a@b',
|
|
472
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
473
|
+
},
|
|
474
|
+
}), { status: 200 });
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
const c = new client_js_1.XCiteDBClient({
|
|
478
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
479
|
+
apiKey: 'k',
|
|
480
|
+
projectId: 't1',
|
|
481
|
+
context: { project_id: 't1' },
|
|
482
|
+
});
|
|
483
|
+
const claims = await c.verifyAppUserToken(token);
|
|
484
|
+
strict_1.default.match(path, /\/api\/v1\/app\/auth\/verify$/);
|
|
485
|
+
strict_1.default.equal(claims.sub, 'u1');
|
|
486
|
+
strict_1.default.equal(claims.tenant_id, 't1');
|
|
487
|
+
// Tenant mismatch must throw.
|
|
488
|
+
await strict_1.default.rejects(c.verifyAppUserToken(token, { expectedTenantId: 'other' }), /tenant_id/);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
globalThis.fetch = orig;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
(0, node_test_1.describe)('listTriggerEvents', () => {
|
|
496
|
+
(0, node_test_1.it)('passes limit and parses events', async () => {
|
|
497
|
+
let url = '';
|
|
498
|
+
const orig = globalThis.fetch;
|
|
499
|
+
globalThis.fetch = node_test_1.mock.fn(async (input) => {
|
|
500
|
+
url = String(input);
|
|
501
|
+
return new Response(JSON.stringify({
|
|
502
|
+
events: [{ id: '1', trigger: 't', identifier: '/x', at: '...', status: 'ok' }],
|
|
503
|
+
limit: 50,
|
|
504
|
+
}), { status: 200 });
|
|
505
|
+
});
|
|
506
|
+
try {
|
|
507
|
+
const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'k' });
|
|
508
|
+
const r = await c.listTriggerEvents(50);
|
|
509
|
+
strict_1.default.match(url, /\/api\/v1\/_xcitedb\/trigger-events\?limit=50$/);
|
|
510
|
+
strict_1.default.equal(r.limit, 50);
|
|
511
|
+
strict_1.default.equal(r.events.length, 1);
|
|
512
|
+
strict_1.default.equal(r.events[0].trigger, 't');
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
globalThis.fetch = orig;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { XCiteDBClient } from './client';
|
|
2
2
|
export { parseAssetUri, formatAssetUri, collectIdentifiersFromText, ASSET_URI_PREFIX } from './assetUri';
|
|
3
3
|
export { WebSocketSubscription } from './websocket';
|
|
4
|
-
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, SmartDiffRef, SmartDiffResult, SmartDiffStats, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListItem, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
4
|
+
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, SmartDiffRef, SmartDiffResult, SmartDiffStats, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListItem, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TriggerEvent, TriggerEventsResponse, TxnOperation, TxnOperationResult, TxnPrecondition, TxnPreconditionResult, TxnRequest, TxnResponse, JwksKey, JwksResponse, VerifyAppUserTokenOptions, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
5
5
|
export { XCiteDBError, XCiteDBForbiddenError, XCiteDBNotFoundError, XCiteDBAuthError, XCiteDBLockConflictError, } from './types';
|
package/dist/types.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface TextSearchQuery {
|
|
|
55
55
|
/** Range FTS filter: interval overlap with [date_from, date_to) (ISO dates). */
|
|
56
56
|
date_from?: string;
|
|
57
57
|
date_to?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Restrict hits to documents whose identifier matches one of these patterns.
|
|
60
|
+
* Same shape as `policies.resources.identifiers` and `triggers.resources.identifiers`:
|
|
61
|
+
* within one entry the recognized keys AND together (and `contains` arrays AND across substrings);
|
|
62
|
+
* across the array entries OR. Empty array = no filtering.
|
|
63
|
+
*/
|
|
64
|
+
identifiers?: PolicyIdentifierPattern[];
|
|
58
65
|
}
|
|
59
66
|
export interface TextSearchHit {
|
|
60
67
|
/** JSON: document id. XML: first non-xcitepath id from db:identifier, else xcitepath (xcitepath also in `xcitepath`). */
|
|
@@ -344,6 +351,11 @@ export interface RagQueryOptions {
|
|
|
344
351
|
role: 'system' | 'user' | 'assistant';
|
|
345
352
|
content: string;
|
|
346
353
|
}[];
|
|
354
|
+
/**
|
|
355
|
+
* Restrict the documents pulled into RAG context by identifier pattern.
|
|
356
|
+
* Same shape as {@link TextSearchQuery.identifiers}.
|
|
357
|
+
*/
|
|
358
|
+
identifiers?: PolicyIdentifierPattern[];
|
|
347
359
|
}
|
|
348
360
|
export interface RagQueryResult {
|
|
349
361
|
answer: string;
|
|
@@ -1115,6 +1127,146 @@ export interface RebaseUserWorkspaceResult {
|
|
|
1115
1127
|
auto_mergeable?: string[];
|
|
1116
1128
|
would_expose?: string[];
|
|
1117
1129
|
}
|
|
1130
|
+
/** One mutation within a {@link XCiteDBClient.txn} call. */
|
|
1131
|
+
export type TxnOperation = {
|
|
1132
|
+
kind: 'WriteJson';
|
|
1133
|
+
identifier: string;
|
|
1134
|
+
data: unknown;
|
|
1135
|
+
overwrite?: boolean;
|
|
1136
|
+
} | {
|
|
1137
|
+
kind: 'WriteXml';
|
|
1138
|
+
identifier: string;
|
|
1139
|
+
xml: string;
|
|
1140
|
+
is_top?: boolean;
|
|
1141
|
+
compare_attributes?: boolean;
|
|
1142
|
+
} | {
|
|
1143
|
+
kind: 'WriteMeta';
|
|
1144
|
+
identifier: string;
|
|
1145
|
+
value: unknown;
|
|
1146
|
+
path?: string;
|
|
1147
|
+
mode?: 'set' | 'append' | 'merge_append';
|
|
1148
|
+
overwrite?: boolean;
|
|
1149
|
+
} | {
|
|
1150
|
+
kind: 'ClearMetaPath';
|
|
1151
|
+
identifier: string;
|
|
1152
|
+
path: string;
|
|
1153
|
+
} | {
|
|
1154
|
+
kind: 'DeleteIdentifier';
|
|
1155
|
+
identifier: string;
|
|
1156
|
+
} | {
|
|
1157
|
+
kind: 'AddIdentifier';
|
|
1158
|
+
identifier: string;
|
|
1159
|
+
};
|
|
1160
|
+
/** One precondition checked under the same transaction (must pass for the writes to apply). */
|
|
1161
|
+
export type TxnPrecondition = {
|
|
1162
|
+
kind: 'MetaEquals';
|
|
1163
|
+
identifier: string;
|
|
1164
|
+
path?: string;
|
|
1165
|
+
value: unknown;
|
|
1166
|
+
} | {
|
|
1167
|
+
kind: 'MetaExists';
|
|
1168
|
+
identifier: string;
|
|
1169
|
+
path?: string;
|
|
1170
|
+
} | {
|
|
1171
|
+
kind: 'MetaAbsent';
|
|
1172
|
+
identifier: string;
|
|
1173
|
+
path?: string;
|
|
1174
|
+
} | {
|
|
1175
|
+
kind: 'DocumentExists';
|
|
1176
|
+
identifier: string;
|
|
1177
|
+
} | {
|
|
1178
|
+
kind: 'DocumentAbsent';
|
|
1179
|
+
identifier: string;
|
|
1180
|
+
};
|
|
1181
|
+
export interface TxnRequest {
|
|
1182
|
+
operations: TxnOperation[];
|
|
1183
|
+
preconditions?: TxnPrecondition[];
|
|
1184
|
+
/**
|
|
1185
|
+
* Optional idempotency key. The server caches the response body for ~24h; replays return the
|
|
1186
|
+
* original 200 with `committed:true` and an additional `idempotency_hit` discriminator can be
|
|
1187
|
+
* inferred from a successful repeat call.
|
|
1188
|
+
*/
|
|
1189
|
+
idempotency_key?: string;
|
|
1190
|
+
}
|
|
1191
|
+
export interface TxnPreconditionResult {
|
|
1192
|
+
index: number;
|
|
1193
|
+
identifier: string;
|
|
1194
|
+
kind: TxnPrecondition['kind'];
|
|
1195
|
+
ok: boolean;
|
|
1196
|
+
error?: string;
|
|
1197
|
+
}
|
|
1198
|
+
export interface TxnOperationResult {
|
|
1199
|
+
index: number;
|
|
1200
|
+
identifier: string;
|
|
1201
|
+
kind: TxnOperation['kind'];
|
|
1202
|
+
ok: boolean;
|
|
1203
|
+
/** HTTP-style code per op; 0 means "not executed because the txn aborted". */
|
|
1204
|
+
code: number;
|
|
1205
|
+
error?: string;
|
|
1206
|
+
}
|
|
1207
|
+
export interface TxnResponse {
|
|
1208
|
+
committed: boolean;
|
|
1209
|
+
/** Index of the failing precondition when the txn aborted on a precondition. */
|
|
1210
|
+
first_failed_precond?: number;
|
|
1211
|
+
/** Index of the failing operation when the txn aborted on an op. */
|
|
1212
|
+
first_failure_index?: number;
|
|
1213
|
+
code?: number;
|
|
1214
|
+
error?: string;
|
|
1215
|
+
preconditions: TxnPreconditionResult[];
|
|
1216
|
+
results: TxnOperationResult[];
|
|
1217
|
+
}
|
|
1218
|
+
/** A persisted trigger firing record. */
|
|
1219
|
+
export interface TriggerEvent {
|
|
1220
|
+
/** Unique id for the firing (sortable; newer first when listing). */
|
|
1221
|
+
id?: string;
|
|
1222
|
+
/** Trigger name that fired. */
|
|
1223
|
+
trigger?: string;
|
|
1224
|
+
/** Identifier whose write caused the firing. */
|
|
1225
|
+
identifier?: string;
|
|
1226
|
+
/** Server-assigned ISO timestamp. */
|
|
1227
|
+
at?: string;
|
|
1228
|
+
/** Outcome — `ok` or `error` for synchronous triggers. */
|
|
1229
|
+
status?: string;
|
|
1230
|
+
/** Free-form details (matched value, error message). */
|
|
1231
|
+
detail?: unknown;
|
|
1232
|
+
[key: string]: unknown;
|
|
1233
|
+
}
|
|
1234
|
+
export interface TriggerEventsResponse {
|
|
1235
|
+
events: TriggerEvent[];
|
|
1236
|
+
limit: number;
|
|
1237
|
+
}
|
|
1238
|
+
/** Single key entry from `GET /.well-known/jwks.json` (RFC 7517). */
|
|
1239
|
+
export interface JwksKey {
|
|
1240
|
+
kty: string;
|
|
1241
|
+
use?: string;
|
|
1242
|
+
alg?: string;
|
|
1243
|
+
kid?: string;
|
|
1244
|
+
/** RSA modulus (base64url) — present for `kty: "RSA"`. */
|
|
1245
|
+
n?: string;
|
|
1246
|
+
/** RSA public exponent (base64url) — present for `kty: "RSA"`. */
|
|
1247
|
+
e?: string;
|
|
1248
|
+
[key: string]: unknown;
|
|
1249
|
+
}
|
|
1250
|
+
export interface JwksResponse {
|
|
1251
|
+
keys: JwksKey[];
|
|
1252
|
+
}
|
|
1253
|
+
/** Options for {@link XCiteDBClient.verifyAppUserToken}. */
|
|
1254
|
+
export interface VerifyAppUserTokenOptions {
|
|
1255
|
+
/**
|
|
1256
|
+
* Override the expected `tenant_id` claim. When omitted, the client's configured
|
|
1257
|
+
* `tenant_id` / `project_id` is used. Pass an empty string to skip the check.
|
|
1258
|
+
*/
|
|
1259
|
+
expectedTenantId?: string;
|
|
1260
|
+
/**
|
|
1261
|
+
* Clock-skew tolerance for `exp` in seconds (default 30). Set to 0 for strict comparison.
|
|
1262
|
+
*/
|
|
1263
|
+
clockToleranceSeconds?: number;
|
|
1264
|
+
/**
|
|
1265
|
+
* Force a re-fetch of the JWKS even if a cached copy is fresh. Useful right after a key
|
|
1266
|
+
* rotation — normally the cache TTL handles this on its own.
|
|
1267
|
+
*/
|
|
1268
|
+
forceRefreshJwks?: boolean;
|
|
1269
|
+
}
|
|
1118
1270
|
/**
|
|
1119
1271
|
* Canonical 403 `reason` codes the server can emit. Use as a discriminator on
|
|
1120
1272
|
* `XCiteDBForbiddenError.reason` in catch sites — TypeScript will warn if you forget a case.
|
package/llms.txt
CHANGED
|
@@ -323,6 +323,7 @@ interface XCiteDBClientOptions {
|
|
|
323
323
|
- `loginAppUser(email, password)` — App end-user sign-in
|
|
324
324
|
- `XCiteDBClient.buildProjectGroup(projectId, 'admin'|'editor'|'viewer')` — Static helper: canonical `project:<tenant_id>:<role>` string
|
|
325
325
|
- `getTokenClaims()` — Decode current `appUserAccessToken` or `accessToken` payload (no signature verification); use for ABAC debugging
|
|
326
|
+
- `verifyAppUserToken(token, options?)` — **BFF token verifier.** Validates signature, expiry, and (by default) the `tenant_id` claim against the client's configured project. RS256 deployments verify locally against `/.well-known/jwks.json` (cached; key rotations land via `kid` mismatch + auto refresh). HS256 deployments fall back to `POST /api/v1/app/auth/verify` (one round-trip; the same shape comes back). Throws `XCiteDBAuthError` on bad signature, expired, or tenant mismatch. Options: `expectedTenantId` (override; pass `''` to skip), `clockToleranceSeconds` (default 30), `forceRefreshJwks`. Returns `XCiteDBJwtClaims` (sub/tenant_id/groups/email/exp/iat/iss/jti/type).
|
|
326
327
|
- `setContext(ctx)` — Update workspace/date context
|
|
327
328
|
- `setProjectId(id)` — Switch active project (platform console)
|
|
328
329
|
|
|
@@ -381,9 +382,9 @@ interface XCiteDBClientOptions {
|
|
|
381
382
|
- ABAC actions: `lock`, `unlock`, `force_unlock`; policies with `write` only still authorize all three (compat)
|
|
382
383
|
|
|
383
384
|
**Search & Analytics:**
|
|
384
|
-
- `search(query)` — Full-text search (embedded FTS). Optional **`at_date`**, **`date_from`**, **`date_to`** on the query for temporal keyword search (use **`mode: 'fts'`**). Hits may include **`valid_from`** / **`valid_to`** (7-char internal keys).
|
|
385
|
+
- `search(query)` — Full-text search (embedded FTS). Optional **`at_date`**, **`date_from`**, **`date_to`** on the query for temporal keyword search (use **`mode: 'fts'`**). Hits may include **`valid_from`** / **`valid_to`** (7-char internal keys). Optional **`identifiers`** — array of pattern objects (`exact` / `match_start` / `match_end` / `contains` / `regex`, same shape as policy `resources.identifiers`) restricts hits to matching documents (entries OR; keys within an entry AND).
|
|
385
386
|
- `reindex()` — Rebuild search index
|
|
386
|
-
- `unquery(query, unqueryDoc)` — Execute Unquery DSL
|
|
387
|
+
- `unquery(query, unqueryDoc)` — Execute Unquery DSL. Postfix slicing supported: `field[start:end]` with optional negative indices on a JSON array yields a sliced array.
|
|
387
388
|
|
|
388
389
|
**Security & Policies:**
|
|
389
390
|
- `createPolicy(id, policy)` / `listPolicies()` / `updatePolicy()` / `deletePolicy()` — ABAC policies
|
|
@@ -392,6 +393,13 @@ interface XCiteDBClientOptions {
|
|
|
392
393
|
|
|
393
394
|
**Triggers:**
|
|
394
395
|
- `upsertTrigger(id, trigger)` / `listTriggers()` / `deleteTrigger(name)` — Automation triggers
|
|
396
|
+
- `listTriggerEvents(limit?)` — `GET /api/v1/_xcitedb/trigger-events`; recent firings (newest first; clamp 1..500, default 100). Admin/editor only; public-key contexts rejected.
|
|
397
|
+
|
|
398
|
+
**Atomic transactions:**
|
|
399
|
+
- `txn({ operations, preconditions?, idempotency_key? })` — `POST /api/v1/txn`. All-or-nothing across mixed kinds (`WriteJson`, `WriteXml`, `WriteMeta`, `ClearMetaPath`, `DeleteIdentifier`, `AddIdentifier`) with optional preconditions (`MetaEquals`, `MetaExists`, `MetaAbsent`, `DocumentExists`, `DocumentAbsent`). Pass `idempotency_key` to make safe retries — server caches the response body for ~24h so a repeated call replays the original 200. Response: `{ committed, first_failed_precond?, first_failure_index?, code?, error?, preconditions[], results[] }`. Unexecuted ops on abort carry `code: 0` with `error: "not executed (atomic txn aborted)"`.
|
|
400
|
+
|
|
401
|
+
**RAG:**
|
|
402
|
+
- `ragQuery(options)` / `ragQueryStream(options, onEvent)` — Retrieval-augmented Q&A. Optional **`identifiers`** restricts the documents pulled into context (same shape as `search` filter).
|
|
395
403
|
|
|
396
404
|
### Advanced: policy expressions (ABAC)
|
|
397
405
|
|
package/package.json
CHANGED
package/unquery-ai-guide.md
CHANGED
|
@@ -85,6 +85,7 @@ A path selects a value from the current context.
|
|
|
85
85
|
| `[n]` | Array index (0-based) | `"Dependants[0].FirstName"` |
|
|
86
86
|
| `[expr]` | Computed index | `"Field1[$index+1]"` |
|
|
87
87
|
| `[]` | Whole array (projection over all elements) | `"Array1[].Field1"` |
|
|
88
|
+
| `[a:b]` | Array **slice** in a context modifier (half-open; negative counts from end) | `"scores:[0:3]"`, `"scores:[-2:]"` |
|
|
88
89
|
| `/Field` | Absolute from document root | `"/employees.$(.)"` |
|
|
89
90
|
| `../Field` | Up one path level (skips array indices) | `"../DBInstanceIdentifier"` |
|
|
90
91
|
| `<<Field` | Read same field in *previous* document context | `"<<Field1"` after a `->$file(...)` |
|
|
@@ -259,18 +260,21 @@ Equivalent to SQL `GROUP BY bin`. Combine with aggregate values for grouped stat
|
|
|
259
260
|
{ "$(Title)": "$avg(Salary)" }
|
|
260
261
|
```
|
|
261
262
|
|
|
262
|
-
### 5.4 Copy-all keys — `{}
|
|
263
|
+
### 5.4 Copy-all keys — `{}`, `{'regex'}`, and slice `{a:b}`
|
|
263
264
|
|
|
264
|
-
`{}` evaluates to *every* key in the queried object. `{'regex'}` evaluates to keys matching the regex.
|
|
265
|
+
`{}` evaluates to *every* key in the queried object. `{'regex'}` evaluates to keys matching the regex. `{a:b}` is a positional **slice** of the source-key sequence (half-open, negative values count from the end) — keys are visited in their stored sort order, so this naturally gives "first K", "last K", or a middle range.
|
|
265
266
|
|
|
266
267
|
```json
|
|
267
268
|
{ "{}": "value" } // every key gets the constant 'value' (rarely useful)
|
|
268
269
|
{ "{}:": "." } // copy every key → its value (very useful)
|
|
269
270
|
{ "{'A.*B'}:": "." } // copy only keys matching the regex
|
|
270
271
|
{ "{}:": ".?$key!='ID'" } // copy all except ID (predicate filter)
|
|
272
|
+
{ "{:3}:": "." } // first 3 keys (alphabetical)
|
|
273
|
+
{ "{-2:}:": "." } // last 2 keys
|
|
274
|
+
{ "{2:5}:": "." } // keys at positions 2..4 inclusive
|
|
271
275
|
```
|
|
272
276
|
|
|
273
|
-
The trailing `:` after `{}` is a context modifier ("for each key, switch context to that key"). Without `:`, `{}` is just the empty-key spec.
|
|
277
|
+
The trailing `:` after `{}` is a context modifier ("for each key, switch context to that key"). Without `:`, `{}` is just the empty-key spec. The slice form `{a:b}` mirrors the array slice `[a:b]` (see §4 path expressions and §6 context modifiers): `{:K}` is the first K, `{-K:}` the last K, `{a:b}` the half-open range.
|
|
274
278
|
|
|
275
279
|
### 5.5 Key utility functions
|
|
276
280
|
|
|
@@ -298,7 +302,9 @@ A modifier may be followed by `?cond` (predicate). Modifiers chain freely.
|
|
|
298
302
|
| `:` (nothing after) | Repeat the key name as the path: `"Field1:"` ≡ `"Field1:Field1"` |
|
|
299
303
|
| `:[]` | Iterate over each array element |
|
|
300
304
|
| `:[n]` | Switch to array index `n` |
|
|
305
|
+
| `:[a:b]` | Iterate over the array **slice** at indices `a..b` (half-open; negative counts from end) |
|
|
301
306
|
| `:{}` | Iterate over each object field |
|
|
307
|
+
| `:{a:b}` | Iterate over the object-key **slice** at positions `a..b` (half-open; negative counts from end) |
|
|
302
308
|
| `:{'regex'}` | Iterate over fields matching regex |
|
|
303
309
|
| `:**` | Recursive descent (every path under current) |
|
|
304
310
|
| `:.` | Stay at current path (rarely needed) |
|
|
@@ -308,8 +314,18 @@ A modifier may be followed by `?cond` (predicate). Modifiers chain freely.
|
|
|
308
314
|
{ "result:Customers[]?Balance>100000:Accounts[]": ["accountNumber"] }
|
|
309
315
|
{ "result:{}": ["."] }
|
|
310
316
|
{ "#return:**": ["$key@unique_ascending"] }
|
|
317
|
+
{ "first:scores[:3]": ["."] } // first 3 elements of `scores`
|
|
318
|
+
{ "tail:scores[-2:]": ["."] } // last 2 elements
|
|
319
|
+
{ "middle:scores[2:5]": ["."] } // elements at positions 2..4
|
|
311
320
|
```
|
|
312
321
|
|
|
322
|
+
**Slice semantics** (same shape for arrays and object-key iteration):
|
|
323
|
+
- `[:K]` / `{:K}` — first K (start defaults to 0).
|
|
324
|
+
- `[K:]` / `{K:}` — from index K to end.
|
|
325
|
+
- `[a:b]` / `{a:b}` — half-open range `[a, b)`.
|
|
326
|
+
- `[-K:]` / `{-K:}` — last K (negative values count from the end).
|
|
327
|
+
- Out-of-range bounds are clamped; `b ≤ a` yields an empty result.
|
|
328
|
+
|
|
313
329
|
### 6.2 The `:Field` shorthand (very common)
|
|
314
330
|
|
|
315
331
|
`"FirstName:"` is identical to `"FirstName:FirstName"`. Use it whenever the result key matches the source field:
|
package/unquery-grammar.md
CHANGED
|
@@ -151,7 +151,7 @@ Roughly in parse order:
|
|
|
151
151
|
|
|
152
152
|
| Prefix / form | AST | Notes |
|
|
153
153
|
|-----------------|-----|--------|
|
|
154
|
-
| `[` optional `]` | `TExprSubfield(TExprField("."), expr?, is_index)` | `[]` = whole array on current `.`; `[e]` = index. |
|
|
154
|
+
| `[` optional `]` | `TExprSubfield(TExprField("."), expr?, is_index)` | `[]` = whole array on current `.`; `[e]` = index. (Slice `[a:b]` form is currently parsed only on context modifiers, not in path expressions.) |
|
|
155
155
|
| `$` `(` `expression` `)` | `TExprField(expr)` | Evaluate / path from dynamic name (`$()`). |
|
|
156
156
|
| `$if` `(` `condition` `,` `expression` `,` `expression` `)` | `TExprITE` | |
|
|
157
157
|
| `$call` `(` IDENT `)` | `TExprCall(name)` | Zero-arg user function by name. |
|
|
@@ -294,9 +294,12 @@ key ::= '$' '(' expression ')' (* dynamic key → TQParamKey *)
|
|
|
294
294
|
| '$' IDENT ... | '%' IDENT ... (* whole thing parsed as expression() → TQParamKey *)
|
|
295
295
|
| '{' '}' (* TQRegexKey empty regex *)
|
|
296
296
|
| '{' QUOTED '}' (* TQRegexKey *)
|
|
297
|
+
| '{' slice '}' (* TQRegexKey + slice over source-key sequence *)
|
|
297
298
|
| '#' directive
|
|
298
299
|
| pathId (* TQSimpleKey, name may include quoted segments from pathId *)
|
|
299
300
|
|
|
301
|
+
slice ::= INT? ':' INT? (* INT may be `-` INT for negative-from-end *)
|
|
302
|
+
|
|
300
303
|
directive ::= 'if'
|
|
301
304
|
| 'func' IDENT ( '(' IDENT ( ',' IDENT )* ')' )? (* register arity; TQFuncDefinition *)
|
|
302
305
|
| 'var' IDENT
|
|
@@ -336,8 +339,10 @@ Parsed **after** `key()` from the **same** JSON key string. Mode parameter `Cont
|
|
|
336
339
|
| `->` … | `Arrow` | See §8.2 (`identifierExpression`, builtins, legacy JSON-field lookup, `->$each`); often `new_frame = true` |
|
|
337
340
|
| `[` `]` | `Array` | traversal |
|
|
338
341
|
| `[` INT `]` | *(string context)* | literal index path segment |
|
|
342
|
+
| `[` (INT)? `:` (INT)? `]` | `Array` + slice | half-open slice; sets `array_slice_set` and the start/end fields on the `TQContextMod`. Negative ints count from end. (`-` token + INT recognized.) |
|
|
339
343
|
| `.` | *(path)* | context string `"."` |
|
|
340
344
|
| IDENT… `[` `]`? | `Array` if trailing `[]`, else path | Empty path + no `[` → **`Reskey`** mode |
|
|
345
|
+
| IDENT… `[` slice `]` | `Array` + slice | `Field[a:b]` after a path |
|
|
341
346
|
| Optional `?` `condition` after each atom | — | Wraps inner mod chain in `TQValueWithCond` |
|
|
342
347
|
|
|
343
348
|
Recursion: `context_mod(...)` parses the **rest** of the chain; result wrapped in `TQContextMod` (expr or string form).
|