@korajs/core 1.0.0-beta.12 → 1.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { R as RandomSource, B as BlobRef, C as CollectionDefinition, A as AtomicOpType, a as AtomicOp, F as FieldBuilder, b as CustomResolver, M as MigrationDefinition, S as SchemaDefinition, c as RelationDefinition, O as OperationType, E as EnumFieldBuilder, J as JsonFieldBuilder, d as ArrayFieldBuilder, e as FieldKind, f as Operation, V as VersionVector, g as MigrationStep, h as StateMachineConstraint, T as TransitionMap, i as TransitionValidationResult } from './events-D7JCtQSD.js';
2
- export { j as CONNECTION_QUALITIES, k as ConnectionQuality, l as Constraint, m as FieldDescriptor, n as FieldMergeStrategy, H as HLCTimestamp, K as KoraEvent, o as KoraEventByType, p as KoraEventEmitter, q as KoraEventListener, r as KoraEventType, s as MERGE_STRATEGIES, t as MergeStrategy, u as MergeTrace, v as MigrationBuilder, w as OnDeleteAction, x as OperationInput, y as RelationType, z as RollbackBuilder, D as SecretMode, G as SequenceConfig, I as StateMachineDefinition, L as SyncDiagnosticsSnapshot, N as SyncRuleDefinition, P as TimeSource, Q as migrate, U as t } from './events-D7JCtQSD.js';
3
- export { H as HybridLogicalClock, M as MAX_LOGICAL, c as createOperation, i as isValidOperation, v as verifyOperationIntegrity } from './operation-BX4ok7U4.js';
4
- export { S as ScopeMap, b as buildScopeMap } from './build-scope-map-Cv5HfCQP.js';
1
+ import { R as RandomSource, B as BlobRef, C as CollectionDefinition, O as OperationInput, A as AtomicOp, a as Operation, H as HLCTimestamp, S as SchemaDefinition, M as MergeTrace, F as FieldBuilder, b as CustomResolver, c as MigrationDefinition, d as FieldDescriptor, e as OperationType, V as VersionVector, f as MigrationStep, g as StateMachineConstraint, T as TransitionMap, h as TransitionValidationResult } from './events-BSU-0z5O.js';
2
+ export { i as ArrayAtomicOpSentinel, j as ArrayFieldBuilder, k as AtomicOpSentinel, l as AtomicOpType, m as BackfillOptions, n as CONNECTION_QUALITIES, o as ConnectionQuality, p as Constraint, E as EncryptedEnvelopeField, q as EncryptedOperationEnvelope, r as EnumFieldBuilder, s as FieldInput, t as FieldKind, u as FieldKindToType, v as FieldMap, w as FieldMergeStrategy, x as FieldOutput, y as FieldTypeInfo, I as InferFieldInput, z as InferFieldType, D as InferInsert, G as InferInsertInput, J as InferRecord, K as InferRecordField, L as InferUpdate, N as InferUpdateField, P as InferUpdateInput, Q as JsonFieldBuilder, U as KoraEvent, W as KoraEventByType, X as KoraEventEmitter, Y as KoraEventListener, Z as KoraEventType, _ as MERGE_STRATEGIES, $ as MergeStrategy, a0 as MigrationBuilder, a1 as NumericAtomicOpSentinel, a2 as ObjectFieldBuilder, a3 as ObjectFieldValue, a4 as OnDeleteAction, a5 as RelationDefinition, a6 as RelationType, a7 as RichtextInput, a8 as RollbackBuilder, a9 as ScalarFieldBuilder, aa as ScalarFieldKind, ab as SecretFieldBuilder, ac as SecretMode, ad as SequenceConfig, ae as Simplify, af as StateMachineDefinition, ag as SyncDiagnosticsSnapshot, ah as SyncRuleDefinition, ai as TimeSource, aj as applyAtomicOp, ak as isAtomicOp, al as migrate, am as op, an as resolveAtomicOp, ao as t, ap as toAtomicOp } from './events-BSU-0z5O.js';
3
+ import { F as FoldState, a as FoldOptions, b as FoldRecordResult, c as FoldFieldVersions, M as MergeOpResult, O as OperationTransform, d as FoldTrace } from './sql-gen-ByOZIQU-.js';
4
+ export { C as CounterFieldState, e as CreateOperationOptions, D as DEFAULT_OPERATION_HASH_VERSION, E as ElementSetFieldState, f as ElementState, g as ExtremumFieldState, h as FOLD_STATE_VERSION, i as FieldLogEntry, j as FieldState, k as FoldFieldKind, l as FoldTraceMode, H as HashableOperation, m as HybridLogicalClock, K as KeyMapFieldState, n as KeyState, o as MAX_LOGICAL, p as OperationHashVersion, R as RegisterFieldState, q as ResolverFieldState, r as RichtextFieldState, s as RichtextSubsumes, t as RichtextUpdateMerger, S as Stamp, u as computeOperationId, v as createOperation, w as generateFullDDL, x as generateSQL, y as isValidOperation, z as verifyOperationId, A as verifyOperationIntegrity } from './sql-gen-ByOZIQU-.js';
5
+ import { S as ScopeMap } from './build-scope-map-CIFa58oA.js';
6
+ export { b as buildScopeMap } from './build-scope-map-CIFa58oA.js';
5
7
 
6
8
  /**
7
9
  * Base error class for all Kora errors.
@@ -28,6 +30,18 @@ declare class SchemaValidationError extends KoraError {
28
30
  declare class OperationError extends KoraError {
29
31
  constructor(message: string, context?: Record<string, unknown>);
30
32
  }
33
+ /**
34
+ * Thrown by the local write path when an operation would exceed `maxOperationBytes`
35
+ * (RT-86): the server refuses such an operation, so the write is refused before it is
36
+ * accepted, with nothing written.
37
+ */
38
+ declare class OperationTooLargeError extends KoraError {
39
+ readonly collection: string;
40
+ readonly recordId: string;
41
+ readonly bytes: number;
42
+ readonly maxBytes: number;
43
+ constructor(collection: string, recordId: string, bytes: number, maxBytes: number);
44
+ }
31
45
  /**
32
46
  * Thrown when a merge conflict cannot be automatically resolved.
33
47
  */
@@ -325,131 +339,113 @@ declare function isApplyFailure(result: ApplyResult): boolean;
325
339
  declare function defaultApplyFailureReason(result: Exclude<ApplyResult, 'applied' | 'duplicate'>, overrides?: Partial<ApplyFailureReason>): ApplyFailureReason;
326
340
 
327
341
  /**
328
- * Atomic field operations for intent-preserving updates.
329
- *
330
- * Instead of read-modify-write patterns, developers express intent:
331
- * ```typescript
332
- * await app.stock.update(id, { quantity: op.increment(-5) })
333
- * ```
334
- *
335
- * The operation intent is preserved in the Operation for merge:
336
- * concurrent increments compose (sum of deltas) instead of LWW.
337
- */
338
-
339
- /**
340
- * Sentinel marker for detecting atomic op objects in update data.
341
- * Uses Symbol.for() so the sentinel survives module boundary crossings.
342
- */
343
- declare const KORA_ATOMIC_OP: unique symbol;
344
- /** String key used for the sentinel symbol — exported for internal use */
345
- declare const KORA_ATOMIC_OP_KEY: typeof KORA_ATOMIC_OP;
346
- /**
347
- * Sentinel object returned by op.* helpers.
348
- * Detected by Collection.update() and resolved to concrete values.
342
+ * Thrown when an operation (or a record value) holds a value that has no canonical
343
+ * JSON form: the op log and the wire are JSON, so such a value would be stored and
344
+ * synced as something other than what was written and hashed.
349
345
  */
350
- interface AtomicOpSentinel {
351
- readonly [KORA_ATOMIC_OP_KEY]: true;
352
- readonly type: AtomicOpType;
353
- readonly value: unknown;
346
+ declare class NonCanonicalValueError extends KoraError {
347
+ readonly path: string;
348
+ readonly receivedType: string;
349
+ constructor(message: string, path: string, receivedType: string);
354
350
  }
355
351
  /**
356
- * Type guard: checks whether a value is an atomic op sentinel.
357
- *
358
- * @param value - Any value to check
359
- * @returns true if the value is an AtomicOpSentinel
360
- */
361
- declare function isAtomicOp(value: unknown): value is AtomicOpSentinel;
362
- /**
363
- * Apply a plain atomic op (`{ type, value }`) to a current value, producing the
364
- * concrete result. This is the single source of truth for atomic-op semantics:
365
- * {@link resolveAtomicOp} (sentinel form, used on the write path) delegates here,
366
- * and the server's materialization replays stored atomic ops through it, so the
367
- * server's materialized view composes concurrent atomic writes identically to the
368
- * client. Folding this over a field's writes in HLC order yields, for a same-type
369
- * atomic chain, exactly the composed intent (sum of increments, max of maxes, ...).
370
- *
371
- * @param currentValue - The current value of the field
372
- * @param atomicOp - The atomic op to apply (type + operand)
373
- * @returns The resulting concrete value
374
- */
375
- declare function applyAtomicOp(currentValue: unknown, atomicOp: AtomicOp): unknown;
376
- /**
377
- * Resolve an atomic op sentinel against a current value to produce a concrete result.
378
- *
379
- * @param currentValue - The current value of the field in the database
380
- * @param sentinel - The atomic op sentinel from the developer's update call
381
- * @returns The resolved concrete value
382
- */
383
- declare function resolveAtomicOp(currentValue: unknown, sentinel: AtomicOpSentinel): unknown;
384
- /**
385
- * Atomic operation helpers. Use these in collection.update() calls
386
- * to express intent-preserving mutations.
387
- *
388
- * @example
389
- * ```typescript
390
- * import { op } from 'korajs'
391
- *
392
- * // Increment a counter (works correctly with concurrent updates)
393
- * await app.stock.update(id, { quantity: op.increment(-5) })
394
- *
395
- * // Keep the maximum value
396
- * await app.scores.update(id, { highScore: op.max(newScore) })
397
- *
398
- * // Append to an array
399
- * await app.todos.update(id, { tags: op.append('urgent') })
400
- * ```
401
- */
402
- declare const op: {
403
- /**
404
- * Increment a number field by the given amount.
405
- * Concurrent increments compose: the sum of all deltas is applied to the base.
406
- *
407
- * @param n - The amount to increment (use negative values to decrement)
408
- */
409
- increment(n: number): AtomicOpSentinel;
410
- /**
411
- * Decrement a number field by the given amount.
412
- * Syntactic sugar for `op.increment(-n)`.
413
- *
414
- * @param n - The amount to decrement
415
- */
416
- decrement(n: number): AtomicOpSentinel;
417
- /**
418
- * Set the field to the maximum of the current value and the given value.
419
- * Concurrent max operations take the maximum of all values.
420
- *
421
- * @param n - The value to compare against the current value
422
- */
423
- max(n: number): AtomicOpSentinel;
424
- /**
425
- * Set the field to the minimum of the current value and the given value.
426
- * Concurrent min operations take the minimum of all values.
427
- *
428
- * @param n - The value to compare against the current value
429
- */
430
- min(n: number): AtomicOpSentinel;
431
- /**
432
- * Append an item to an array field.
433
- * Concurrent appends include all appended items.
434
- *
435
- * @param item - The item to append to the array
436
- */
437
- append(item: unknown): AtomicOpSentinel;
438
- /**
439
- * Remove an item from an array field (by value equality).
440
- * Concurrent removes of the same item are idempotent.
441
- *
442
- * @param item - The item to remove from the array
443
- */
444
- remove(item: unknown): AtomicOpSentinel;
445
- };
352
+ * The one canonical form of an operation body (RT-72 family: RT-79, RT-80, RT-83).
353
+ *
354
+ * The op log, the wire (JSON and protobuf, whose data is JSON) and every store hold an
355
+ * operation as JSON. An operation's id is a hash of its body. So the body is put in its
356
+ * canonical form ONCE, when the operation is created (and when a legacy operation is
357
+ * ingested or rebuilt), and the canonical body is what is hashed, stored, sent and
358
+ * folded: the four can never disagree, by construction.
359
+ *
360
+ * | Value | Canonical form |
361
+ * |-----------------------------------------|-------------------------------------------------|
362
+ * | `undefined` member of an insert's data | absent |
363
+ * | `undefined` member of an update's data | `null`: the field is CLEARED (beta.12 meaning) |
364
+ * | `undefined` member of an update's `previousData` | `null` (the field held no value) |
365
+ * | `undefined` member inside an object value | absent (as JSON drops it); a `t.json()` field refuses it at validation |
366
+ * | `undefined` array element | `null` (as JSON writes it) |
367
+ * | `undefined` atomic op | absent |
368
+ * | `-0` | `0` (JSON writes `0`) |
369
+ * | valid `Date` | its ISO 8601 string (`toISOString()`), as JSON |
370
+ * | `Uint8Array` / `ArrayBuffer` | `{ $koraBytes: <base64> }` (the op-log binary form) |
371
+ * | plain object (`Object.prototype` or `null` prototype) | plain object, members canonical |
372
+ * | array (dense) | array, elements canonical |
373
+ * | string, finite number, boolean, `null` | itself |
374
+ * | invalid `Date`, `NaN`, `±Infinity`, `BigInt`, function, symbol, `Map`, `Set`, `WeakMap`, `RegExp`, `Promise`, any other class instance, an object with a `toJSON` method, an own `__proto__` key, a circular reference | refused: {@link NonCanonicalValueError} naming the path and the fix |
375
+ *
376
+ * Canonicalization is idempotent: a canonical body is returned unchanged (same
377
+ * references where nothing changed).
378
+ *
379
+ * @param input - The operation body (as given to `createOperation`, or a stored operation)
380
+ * @returns The body with `data`, `previousData` and `atomicOps` canonical
381
+ * @throws {NonCanonicalValueError} For a value with no canonical form
382
+ */
383
+ declare function canonicalizeOperationBody<T extends CanonicalizableBody>(input: T): T;
384
+ /** The members of an operation body that {@link canonicalizeOperationBody} reads. */
385
+ interface CanonicalizableBody {
386
+ type: OperationInput['type'];
387
+ collection: string;
388
+ recordId: string;
389
+ data: Record<string, unknown> | null;
390
+ previousData: Record<string, unknown> | null;
391
+ atomicOps?: Record<string, AtomicOp>;
392
+ }
446
393
  /**
447
- * Extract the serializable AtomicOp from a sentinel (strips the Symbol marker).
448
- *
449
- * @param sentinel - The atomic op sentinel
450
- * @returns A plain object suitable for JSON serialization
451
- */
452
- declare function toAtomicOp(sentinel: AtomicOpSentinel): AtomicOp;
394
+ * The canonical form of one value inside an operation body or a record field (see
395
+ * {@link canonicalizeOperationBody} for the table). Exported so schema validation
396
+ * produces exactly the values an operation will carry.
397
+ *
398
+ * @param value - Any value
399
+ * @param path - Where the value is, for the error message (e.g. `notes.extra.when`)
400
+ * @returns The canonical value (the same reference when it already is)
401
+ * @throws {NonCanonicalValueError} For a value with no canonical form
402
+ */
403
+ declare function canonicalValue(value: unknown, path: string): unknown;
404
+ /**
405
+ * A legacy update in its canonical body when its id PROVES the beta.12 clear (RT-85):
406
+ * the id is the content hash of the body with every `previousData` key absent from
407
+ * `data` restored as `null` (version 1, or version 2 with a lost declaration), and not
408
+ * of the body as it is. Such an id can only come from a writer that hashed the clear,
409
+ * so the restored body is the one its author wrote and applied; a body a schema
410
+ * transform or anything else rewrote never proves it.
411
+ *
412
+ * Used where a legacy body enters a replica without a known provenance: a device's
413
+ * inbound delivery, and the one-time canonicalization of stored logs. The server's
414
+ * ingest of a protocol-1 upload applies the same rule (plus the unverifiable
415
+ * protocol-1 case, for the session's own node).
416
+ *
417
+ * @param op - Any operation
418
+ * @returns The canonical operation, or `op` itself when nothing is proven
419
+ */
420
+ declare function canonicalizeProvenLegacyClear<T extends Operation>(op: T): Promise<T>;
421
+ /**
422
+ * The canonical body of a legacy (version-1, beta.12) update (RT-71, RT-83): every
423
+ * `previousData` key absent from `data` is a clear, restored as `null`. beta.12 wrote
424
+ * `previousData[key]` for every key it validated, so such a key held `undefined` in
425
+ * `data`: beta.12 hashed it as `null`, applied it as a cleared field, and its JSON log
426
+ * and upload dropped it. `null` and `undefined` hash identically under version 1, so
427
+ * the id is unchanged.
428
+ *
429
+ * Applied ONCE, where a genuine beta.12 body is identified (RT-85), never at fold time:
430
+ * the server's ingest of a protocol-1 upload, a device's own beta.12 log at the
431
+ * upgrade, and any body whose id proves the clear
432
+ * ({@link canonicalizeProvenLegacyClear}). The result is stored, so the server, peers
433
+ * and the upgraded writer fold the same body. A no-op
434
+ * for anything else (a declared version 2, an envelope, an insert or delete, an update
435
+ * whose data covers its previousData). Idempotent.
436
+ *
437
+ * @param op - Any operation
438
+ * @returns The operation with the restored data, or `op` itself
439
+ */
440
+ declare function canonicalizeLegacyOperation<T extends LegacyBody>(op: T): T;
441
+ /** The members of an operation {@link canonicalizeLegacyOperation} reads. */
442
+ interface LegacyBody {
443
+ type: Operation['type'];
444
+ data: Record<string, unknown> | null;
445
+ previousData: Record<string, unknown> | null;
446
+ hashVersion?: Operation['hashVersion'];
447
+ encrypted?: Operation['encrypted'];
448
+ }
453
449
 
454
450
  /** One operation's contribution to a record's materialized state. */
455
451
  interface ReplayOperation {
@@ -462,7 +458,35 @@ interface ReplayOperation {
462
458
  * last-write-wins.
463
459
  */
464
460
  atomicOps?: Record<string, AtomicOp> | null;
461
+ /**
462
+ * For updates: the values the writer saw before its change. Array fields use it
463
+ * as the base of a three-way set merge (see {@link mergeArraySet}). Absent means
464
+ * array fields resolve by last-write-wins.
465
+ */
466
+ previousData?: Record<string, unknown> | null;
465
467
  }
468
+ /**
469
+ * Three-way set merge for array fields, shared by the client merge engine
470
+ * (`addWinsSet`) and the record fold, so client and server agree.
471
+ *
472
+ * result = (local ∩ remote) ∪ (local − base) ∪ (remote − base)
473
+ *
474
+ * A base element is kept only if BOTH sides kept it (a one-sided removal is never
475
+ * undone by the other side's unchanged copy); a non-base element is kept if EITHER
476
+ * side has it. Elements are compared by `JSON.stringify`.
477
+ *
478
+ * Order is role-independent: kept base elements first, in base order, then every
479
+ * other kept element sorted by its serialized form. Swapping `local` and `remote`
480
+ * yields the identical array.
481
+ *
482
+ * Interim (S1): W7 replaces this pairwise rule with a per-element LWW set.
483
+ *
484
+ * @param local - One side's array after its modifications
485
+ * @param remote - The other side's array after its modifications
486
+ * @param base - The common ancestor both sides edited from
487
+ * @returns The merged array
488
+ */
489
+ declare function mergeArraySet(local: unknown[], remote: unknown[], base: unknown[]): unknown[];
466
490
  /**
467
491
  * Replay a list of operations (MUST be pre-sorted in HLC total order —
468
492
  * wallTime, then logical, then nodeId) to produce the current state of a single
@@ -476,6 +500,10 @@ interface ReplayOperation {
476
500
  * the current op always wins that last-write-wins comparison, so the result is
477
501
  * deterministic and identical on every replica that folds the same operation set.
478
502
  *
503
+ * Plain array writes that carry `previousData` are merged as a set against that
504
+ * base (see {@link mergeArraySet}) when a concurrent writer changed the field since
505
+ * the base, so the fold and the client merge agree on one-sided removals.
506
+ *
479
507
  * This is the single definition of atomic materialization shared by the client
480
508
  * (`@korajs/store` apply pipeline) and the server (`@korajs/server` materialization),
481
509
  * so both converge to the same value for any number of concurrent atomic writers.
@@ -484,6 +512,446 @@ interface ReplayOperation {
484
512
  * or never inserted.
485
513
  */
486
514
  declare function replayOperationsForRecord(ops: ReplayOperation[]): Record<string, unknown> | null;
515
+ /** A {@link ReplayOperation} with the HLC timestamp it was written at. */
516
+ interface VersionedReplayOperation {
517
+ type: string;
518
+ data: Record<string, unknown> | null;
519
+ timestamp: HLCTimestamp;
520
+ }
521
+ /** Per-field versions of a live record, as folded by {@link replayFieldVersionsForRecord}. */
522
+ interface RecordFieldVersions {
523
+ /** Field name -> HLC of the write that produced the field's current value. */
524
+ fields: Record<string, HLCTimestamp>;
525
+ /** The record's first operation (its creation; the server's `_created_at`). */
526
+ created: HLCTimestamp;
527
+ /** The record's newest operation. */
528
+ latest: HLCTimestamp;
529
+ }
530
+ /**
531
+ * Fold the per-field versions of one record, mirroring {@link replayOperationsForRecord}
532
+ * exactly (RT-27): an insert resets the field set and stamps every field it carries,
533
+ * an update stamps the fields it writes (composed atomic and array writes included:
534
+ * the field's version is its last writer's), a delete hides the record until a later
535
+ * write revives it. Operations MUST be pre-sorted in HLC total order.
536
+ *
537
+ * @param ops - The record's operations in HLC order
538
+ * @returns The live record's field versions, or null when it is deleted or absent
539
+ */
540
+ declare function replayFieldVersionsForRecord(ops: readonly VersionedReplayOperation[]): RecordFieldVersions | null;
541
+ /**
542
+ * Expand operations for a record fold (RT-27). A scope-entry insert carrying
543
+ * per-field versions restates the record's CURRENT values, each produced at its own
544
+ * version; folding it as one insert at a single timestamp would misplace every field
545
+ * relative to the receiver's own writes. It is expanded into an insert at its
546
+ * `timestamp` (the record's creation) followed by one update per newer field version,
547
+ * so a fold in HLC order puts each field exactly where its writer did. Other
548
+ * operations pass through unchanged. The result is sorted in HLC total order (stable,
549
+ * so an expansion keeps insert-before-update on equal timestamps).
550
+ *
551
+ * @param ops - Operations of one record, in any order
552
+ * @returns The operations to fold, in HLC order
553
+ */
554
+ declare function expandFieldVersionedOperations(ops: readonly Operation[]): Operation[];
555
+
556
+ /** `FoldTrace.field` of a record-level (delete vs write) decision. */
557
+ declare const FOLD_RECORD_TRACE_FIELD = "*";
558
+ /**
559
+ * An empty fold state for one record: no operation merged yet.
560
+ *
561
+ * @param collection - The record's collection
562
+ * @param recordId - The record's id
563
+ */
564
+ declare function createFoldState(collection: string, recordId: string): FoldState;
565
+ /**
566
+ * Merge one operation into a record's fold state.
567
+ *
568
+ * The merge is a join on a per-field CRDT, so it is commutative, associative and
569
+ * idempotent: any replica that has merged the same set of operations, in any
570
+ * order, any number of times, holds an identical state. Cost is O(fields the
571
+ * operation touches) except for the out-of-order cases documented per kind (a late
572
+ * write into an atomic or resolver chain refolds that field's chain).
573
+ *
574
+ * Record-level semantics:
575
+ * - The record exists once an insert has been merged.
576
+ * - Delete vs write is last-write-wins on the record: the record is live iff its
577
+ * newest insert/update is later (HLC, then op id) than its newest delete. A
578
+ * delete keeps the field states; a later write revives the record with every
579
+ * field's merged value (the same visible result as the pre-W7 replay for an
580
+ * update after a delete).
581
+ * - An insert onto an existing row merges per field; it is not a reset.
582
+ *
583
+ * @param state - The record's current state (not mutated)
584
+ * @param op - The operation (must belong to the same collection and record)
585
+ * @param schema - The schema; resolvers and merge strategies come from it
586
+ * @param options - Exclusion, richtext merger (traces only), trace mode, schema
587
+ * transforms (the operation is merged as `operationSchemaView` reads it for
588
+ * `schema.version`)
589
+ * @returns The new state, the merge traces, and whether anything changed
590
+ */
591
+ declare function mergeOp(state: FoldState, input: Operation, schema: SchemaDefinition, options?: FoldOptions): MergeOpResult;
592
+ /**
593
+ * Fold a record's operations from scratch: the reference definition of the
594
+ * record's state. Equal to merging the operations one by one with
595
+ * {@link mergeOp} in any order.
596
+ *
597
+ * @param ops - The record's operations, in any order, duplicates allowed
598
+ * @param schema - The schema
599
+ * @param options - `exclude` leaves out ops the server terminally rejected; traces
600
+ * default to `'none'` here (pass `traces` to get them)
601
+ * @returns The state (null if no op remains after exclusion) and the traces
602
+ */
603
+ declare function foldRecord(ops: readonly Operation[], schema: SchemaDefinition, options?: FoldOptions): FoldRecordResult;
604
+ /**
605
+ * The record's materialized field values (without `id`), or null when it does not
606
+ * exist (no insert merged) or is deleted. Keys are in sorted order.
607
+ *
608
+ * @param state - The record's fold state
609
+ * @param options - `richtext` is required when a richtext field has concurrent updates
610
+ */
611
+ declare function materialize(state: FoldState, options?: Pick<FoldOptions, 'richtext'>): Record<string, unknown> | null;
612
+ /** True when the record exists and its newest write is later than its newest delete. */
613
+ declare function isFoldStateLive(state: FoldState): boolean;
614
+ /**
615
+ * Join two replicas' states of the same record (state-based CRDT merge):
616
+ * `joinStates(foldRecord(A).state, foldRecord(B).state) = foldRecord(A ∪ B).state`.
617
+ * Commutative, associative and idempotent. Used to merge a restored backup or a
618
+ * peer's snapshot without replaying its operations.
619
+ *
620
+ * @param a - One state
621
+ * @param b - The other state (same collection and record)
622
+ * @param schema - The schema (resolver fields refold through their resolver)
623
+ */
624
+ declare function joinStates(a: FoldState, b: FoldState, schema: SchemaDefinition): FoldState;
625
+ /**
626
+ * Each field's newest affecting write as a serialized HLC (`HybridLogicalClock.
627
+ * serialize`), regardless of liveness: the `_field_versions` column of a stored row,
628
+ * without a deserialize/serialize round trip. Keys in sorted order.
629
+ */
630
+ declare function getFoldFieldVersionStrings(state: FoldState): Record<string, string>;
631
+ /**
632
+ * Per-field versions of a live record (the fold's `_field_versions`): each field's
633
+ * newest affecting write, plus the record's creation and newest operation. null
634
+ * when the record is not live.
635
+ */
636
+ declare function getFoldFieldVersions(state: FoldState): FoldFieldVersions | null;
637
+
638
+ /**
639
+ * Serialize a record's fold state for storage (the client `_fold_state` column,
640
+ * the server stores' fold-record rows, backups, compaction snapshots).
641
+ *
642
+ * The output is canonical JSON (sorted keys), so two replicas holding the same
643
+ * operations write byte-identical strings. Format, version 2 (version 1, Stage A,
644
+ * keyed array elements by value only and is refused: such a state is re-folded):
645
+ *
646
+ * ```
647
+ * {
648
+ * "v": 2, // format version
649
+ * "c": "todos", "r": "<id>", // collection, record id
650
+ * "cr": Stamp|null, // oldest insert (creation)
651
+ * "w": Stamp|null, // newest insert/update
652
+ * "d": Stamp|null, // newest delete (tombstone register)
653
+ * "u": Stamp|null, // newest operation of any type
654
+ * "f": { "<field>": FieldState }
655
+ * }
656
+ * Stamp = { "t": "<HLC serialized>", "o": "<op id>", "c"?: 1 } // c: authority class
657
+ * FieldState =
658
+ * { "k":"reg", "e":[{ "s":Stamp, "v":value, "a"?:AtomicOp }], "val":value }
659
+ * { "k":"set", "ao":bool, "sh":{ "s":Stamp, "arr":bool, "v"?:value }|null, "clr":Stamp|null,
660
+ * "el":{ "<canonical JSON>#<k>":{ "v":value, "n":k, "a":Stamp|null, "f":{ "s":Stamp, "i":n }|null, "r":Stamp|null } } }
661
+ * { "k":"map", "sh":{ "s":Stamp, "obj":bool, "v"?:value }|null, "clr":Stamp|null,
662
+ * "keys":{ "<key>":{ "s":Stamp, "del":bool, "v"?:value } } }
663
+ * { "k":"ctr", "base":{ "s":Stamp, "v":value }|null, "d":[{ "s":Stamp, "n":number }], "val":value }
664
+ * { "k":"max"|"min", "best":{ "s":Stamp, "v":number }|null, "reg":{ "s":Stamp, "v":value }|null }
665
+ * { "k":"res", "e":[{ "s":Stamp, "v":value, "b"?:value, "z"?:1 }], "val":value, "err"?:string }
666
+ * { "k":"rt", "reset":{ "s":Stamp, "v":value }|null, "u":{ "<base64 Yjs update>":Stamp } }
667
+ * ```
668
+ *
669
+ * Values are JSON-safe (binary as `{ "$koraBytes": base64 }`). The state is
670
+ * self-contained: kinds that need history embed exactly the history they need
671
+ * ('reg' keeps the atomic chain since the newest plain write, 'res' its whole
672
+ * write log, 'ctr' the deltas since its base), so a compactor may treat the state
673
+ * as the record's snapshot and drop operations it covers.
674
+ *
675
+ * @param state - The fold state
676
+ * @returns Canonical JSON
677
+ */
678
+ declare function serializeFoldState(state: FoldState): string;
679
+ /**
680
+ * Parse a stored fold state. Throws {@link FoldStateError} for an unknown format
681
+ * version or a malformed state; the caller then re-folds the record from its log.
682
+ *
683
+ * @param json - Output of {@link serializeFoldState}
684
+ */
685
+ declare function deserializeFoldState(json: string): FoldState;
686
+
687
+ /** A materialized row and its versions, the input of {@link createSnapshotState}. */
688
+ interface FoldSnapshotInput {
689
+ collection: string;
690
+ recordId: string;
691
+ /** Field values of the row (record or op-data form; binary is normalized). */
692
+ values: Record<string, unknown>;
693
+ /** Per-field version: the HLC of the newest write of each field. */
694
+ fieldVersions: Record<string, HLCTimestamp>;
695
+ /** HLC of the row's creation. */
696
+ created: HLCTimestamp;
697
+ /** HLC of the newest operation the row reflects (its `_version`). */
698
+ latest: HLCTimestamp;
699
+ /** The row is a domain tombstone. */
700
+ deleted: boolean;
701
+ }
702
+ /** Options of {@link createSnapshotState}. */
703
+ interface FoldSnapshotOptions {
704
+ /**
705
+ * A fold state this replica still holds for the record (its stored
706
+ * `_kora_fold_state`, or the base of a compacted record). Every field whose stored
707
+ * kind still matches the schema is taken from it as-is (exact: element, delta and
708
+ * update stamps included); only the other fields are rebuilt from the row.
709
+ */
710
+ seed?: FoldState | null;
711
+ /**
712
+ * Legacy authoritative node ids (`FoldOptions.authoritativeNodeIds`). A
713
+ * `merge('server-authoritative')` field whose version was written by an
714
+ * authoritative node (a `kora:server:` node or one of these) is stamped with
715
+ * authority class 1, so a client write it beat cannot overturn it when merged again.
716
+ */
717
+ authoritativeNodeIds?: ReadonlySet<string>;
718
+ }
719
+ /**
720
+ * Build a record's fold state from a materialized row: the base snapshot used when
721
+ * the operations that produced the row are no longer all in the log (a database
722
+ * compacted before W7, or a record that owns quarantined log rows).
723
+ *
724
+ * Exact where the replica still has the data: fields of `options.seed` (a stored
725
+ * fold state) whose kind is unchanged are used verbatim, and the record stamps come
726
+ * from it. Every other field is written at its own version with a sentinel
727
+ * operation id that sorts after any real id, so the snapshot dominates every
728
+ * operation it reflects: re-merging such an operation changes nothing (the record's
729
+ * remaining log is folded on top of it again on every re-fold), while a later
730
+ * operation merges on top as usual. Arrays and objects also clear elements / keys
731
+ * written before the snapshot (so an older add the row no longer shows is not
732
+ * resurrected), richtext hides older updates, counters drop older deltas and
733
+ * custom-resolver fields start a fresh log at the snapshot value. A
734
+ * `merge('server-authoritative')` field whose version is a server node's write
735
+ * keeps its authority class.
736
+ *
737
+ * Approximate for a row-only field (RT-68 residual): a concurrent write OLDER than
738
+ * the field's version that arrives later folds against the snapshot value, not
739
+ * against the original operations (a late counter delta or array add is dropped).
740
+ * It cannot be otherwise without the original operations: the row alone does not
741
+ * say whether an older write is already in its value. The store marks such records
742
+ * approximate, and the server's fold state replaces the snapshot when the record
743
+ * next enters the device's scope.
744
+ *
745
+ * @param input - The row's values and versions
746
+ * @param schema - The schema (selects each field's fold kind)
747
+ * @param options - Seed state and authoritative node ids
748
+ */
749
+ declare function createSnapshotState(input: FoldSnapshotInput, schema: SchemaDefinition, options?: FoldSnapshotOptions): FoldState;
750
+ /** Result of {@link adaptFoldState}. */
751
+ interface AdaptedFoldState {
752
+ state: FoldState;
753
+ /** Fields rebuilt as snapshots (their stored kind no longer matched the schema). */
754
+ adapted: string[];
755
+ }
756
+ /**
757
+ * Make a stored fold state mergeable under the current schema after a change of a
758
+ * field's fold kind (RT-63): every field whose stored kind no longer matches the
759
+ * plan is replaced by a snapshot of its materialized value at its newest write (as
760
+ * {@link createSnapshotState} builds from a row), keeping its authority class.
761
+ * Fields whose kind is unchanged are kept exactly. Used for compacted base states,
762
+ * whose operations are no longer in the log to re-fold with the new plan.
763
+ *
764
+ * @param state - A stored fold state (base or record state)
765
+ * @param schema - The current schema
766
+ * @param options - `richtext` merger, to materialize a richtext field with several updates
767
+ */
768
+ declare function adaptFoldState(state: FoldState, schema: SchemaDefinition, options?: Pick<FoldOptions, 'richtext'>): AdaptedFoldState;
769
+
770
+ /**
771
+ * Fingerprint of how a schema folds records (W7): the fold-state format version
772
+ * plus every collection's {@link collectionFoldPlanFingerprint}. When it changes
773
+ * (a field becomes `merge('counter')`, an array becomes append-only, a resolver is
774
+ * added or edited), stored fold states may hold fields of another kind and must be
775
+ * re-folded. The same definition as the server stores' `fold_plan_fingerprint`.
776
+ *
777
+ * Schema transforms (RT-84) are part of the plan when any is registered: an operation
778
+ * of another schema version folds as its transformed view, so changing a transform or
779
+ * the target version re-folds. Without transforms the fingerprint is unchanged.
780
+ *
781
+ * @param schema - The schema
782
+ * @param transforms - The schema transforms the replica folds with
783
+ */
784
+ declare function foldPlanFingerprint(schema: SchemaDefinition, transforms?: readonly OperationTransform[]): string;
785
+ /**
786
+ * Per-collection fold plan fingerprints, to find which collections a schema change
787
+ * re-plans (only their records need re-folding).
788
+ *
789
+ * @param schema - The schema
790
+ * @param transforms - The schema transforms the replica folds with (see {@link foldPlanFingerprint})
791
+ * @returns Collection name -> fingerprint (format version included)
792
+ */
793
+ declare function foldPlanFingerprints(schema: SchemaDefinition, transforms?: readonly OperationTransform[]): Record<string, string>;
794
+ /**
795
+ * The fields of a stored fold state whose kind no longer matches the schema (empty
796
+ * when the state can be merged into as-is).
797
+ *
798
+ * @param state - A stored fold state
799
+ * @param schema - The current schema
800
+ */
801
+ declare function mismatchedFoldFields(state: FoldState, schema: SchemaDefinition): string[];
802
+
803
+ /**
804
+ * Node-id namespaces Kora reserves for itself.
805
+ *
806
+ * Every id starting with {@link RESERVED_NODE_ID_PREFIX} belongs to Kora: devices
807
+ * never author under one (a client refuses to adopt such an id, and the server
808
+ * refuses it at the handshake). Inside it, {@link SERVER_NODE_ID_PREFIX} names the
809
+ * server's own nodes (`kora:server:<stableId>`): their writes are authoritative for
810
+ * `merge('server-authoritative')` fields on every replica, whatever list of node
811
+ * ids a given handshake advertised. That keeps authority stable across server
812
+ * restarts and instances without any replica having to learn the ids first.
813
+ */
814
+ declare const RESERVED_NODE_ID_PREFIX = "kora:";
815
+ /** Prefix of every server-authored node id: `kora:server:<stableId>`. */
816
+ declare const SERVER_NODE_ID_PREFIX = "kora:server:";
817
+ /** True when `nodeId` is in Kora's reserved `kora:` namespace. */
818
+ declare function isReservedNodeId(nodeId: string): boolean;
819
+ /** True when `nodeId` names a server node (`kora:server:<stableId>`). */
820
+ declare function isServerNodeId(nodeId: string): boolean;
821
+ /**
822
+ * Whether a node's writes are authoritative for `merge('server-authoritative')`
823
+ * fields: a server node by prefix, or one of `explicit` (legacy, randomly generated
824
+ * server node ids a server keeps advertising in its handshake).
825
+ *
826
+ * @param nodeId - The writing node
827
+ * @param explicit - Additional authoritative node ids (`FoldOptions.authoritativeNodeIds`)
828
+ */
829
+ declare function isAuthoritativeNodeId(nodeId: string, explicit?: ReadonlySet<string>): boolean;
830
+
831
+ /**
832
+ * Turn a {@link FoldTrace} into the {@link MergeTrace} DevTools and `merge:*`
833
+ * events consume. `operationA` is the operation that last wrote the field before
834
+ * the merge; the fold state only keeps its id (`trace.priorOperationId`), so the
835
+ * caller looks it up in its operation log. When it is unavailable (compacted, or
836
+ * the field had no prior value) the incoming operation stands in for it, and
837
+ * `inputA` still carries the prior value.
838
+ *
839
+ * @param trace - A trace from `mergeOp`
840
+ * @param operationA - The operation that produced the field's prior value, if known
841
+ */
842
+ declare function toMergeTrace(trace: FoldTrace, operationA?: Operation | null): MergeTrace;
843
+
844
+ /**
845
+ * Lossless string encoding for SQL text columns (RT-65), shared by every store that
846
+ * binds JavaScript strings into a database: the server's Postgres and SQLite stores
847
+ * and the client's SQLite stores (better-sqlite3, SQLite WASM on OPFS, and the
848
+ * IndexedDB fallback, which runs SQLite WASM in memory).
849
+ *
850
+ * A JavaScript string is a sequence of UTF-16 code units. SQL text is UTF-8, which
851
+ * cannot hold two kinds of code unit sequence a JS string can:
852
+ *
853
+ * - A lone UTF-16 surrogate: better-sqlite3, SQLite WASM (TextEncoder) and the
854
+ * Postgres driver all encode it as U+FFFD: silent corruption.
855
+ * - U+0000: Postgres TEXT refuses it; SQLite WASM reads a TEXT column up to the first
856
+ * NUL.
857
+ *
858
+ * The codec maps those code units to an escape introduced by U+FFFF, a Unicode
859
+ * noncharacter (valid UTF-8, never assigned):
860
+ *
861
+ * | Input | Stored |
862
+ * |---|---|
863
+ * | U+0000 | U+FFFF `0` |
864
+ * | U+FFFF | U+FFFF `F` |
865
+ * | lone surrogate `\uXXXX` | U+FFFF `s` + 4 lowercase hex digits |
866
+ *
867
+ * Every other string is stored unchanged, so existing rows, equality filters and
868
+ * ordering are unaffected for ordinary text. The map is injective, and
869
+ * `decodeStoredText(encodeStoredText(s)) === s` for every JS string. Equality is
870
+ * preserved: `encode(a) === encode(b)` iff `a === b`, so an equality filter binds the
871
+ * encoded value.
872
+ *
873
+ * Ordering of escaped strings: range filters (`$gt`, `$lt`, ...) and `orderBy` compare
874
+ * the stored (encoded) form, in the database's collation (UTF-8 byte order on SQLite and
875
+ * on Postgres with `C.UTF-8`). A string containing U+0000, U+FFFF or a lone surrogate
876
+ * therefore sorts by its escape, U+FFFF, at the position of the first such code unit:
877
+ * after every other BMP character there, and before supplementary-plane characters.
878
+ * For example `"a\u0000"` sorts after `"ab"` (in JavaScript it sorts before), and a
879
+ * lone surrogate sorts after `"\uFFFE"`. Such strings are rare and equality is exact;
880
+ * do not rely on their relative order in range queries.
881
+ *
882
+ * Columns holding `JSON.stringify` output need no codec: well-formed `JSON.stringify`
883
+ * escapes U+0000 and lone surrogates as `\u` sequences.
884
+ */
885
+ /** The escape introducer: U+FFFF, a noncharacter. */
886
+ declare const STORED_TEXT_ESCAPE = "\uFFFF";
887
+ /** True when `s` contains a code unit a SQL text column cannot hold as is. */
888
+ declare function needsStoredTextEncoding(s: string): boolean;
889
+ /**
890
+ * Encode a JS string for a SQL TEXT column (or a string inside a JSONB value).
891
+ *
892
+ * @param s - Any JavaScript string
893
+ * @returns `s` itself when it holds no NUL, U+FFFF or lone surrogate; otherwise the escaped form
894
+ */
895
+ declare function encodeStoredText(s: string): string;
896
+ /**
897
+ * Decode a string written by {@link encodeStoredText}.
898
+ *
899
+ * @param s - A stored string
900
+ * @returns The original JavaScript string. A U+FFFF not followed by a valid tag is kept as is.
901
+ */
902
+ declare function decodeStoredText(s: string): string;
903
+ /**
904
+ * Apply {@link encodeStoredText} to every string (and object key) inside a JSON value.
905
+ *
906
+ * @param value - A JSON-compatible value
907
+ */
908
+ declare function encodeStoredJsonValue(value: unknown): unknown;
909
+ /**
910
+ * Apply {@link decodeStoredText} to every string (and object key) inside a JSON value.
911
+ *
912
+ * @param value - A JSON-compatible value
913
+ */
914
+ declare function decodeStoredJsonValue(value: unknown): unknown;
915
+
916
+ /**
917
+ * Deterministic id for a side-effect operation (a referential cascade, a
918
+ * set-null, a constraint correction) generated while applying `parentOpId`.
919
+ *
920
+ * Every replica that applies the same parent operation and generates the same
921
+ * effect on the same record derives the same id, so the effect is stored once and
922
+ * deduplicates everywhere instead of multiplying (one copy per replica) and racing
923
+ * (W7 step 3). Cascades keep working offline: a client derives the same id the
924
+ * server will.
925
+ *
926
+ * Callers must also make the side-effect's CONTENT deterministic (timestamp and
927
+ * data derived from the parent), because dedup by id keeps whichever copy arrives
928
+ * first. Ids are SHA-256 over canonical JSON of
929
+ * `{ domain, parent, rule, target }`, hex encoded.
930
+ *
931
+ * @param parentOpId - Id of the operation whose application produced the effect
932
+ * @param ruleId - Stable id of the rule (for example `relation:<name>:cascade`)
933
+ * @param targetRecordId - Id of the record the effect writes
934
+ * @returns 64-char hex id
935
+ */
936
+ declare function deriveSideEffectOpId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
937
+
938
+ /**
939
+ * A fold state cannot be used: unknown format version, malformed JSON, a state
940
+ * for a different record, or a field whose stored kind no longer matches the
941
+ * schema. The fix is always the same: re-fold the record from its operation log
942
+ * (`foldRecord`), which is the source of truth.
943
+ */
944
+ declare class FoldStateError extends KoraError {
945
+ constructor(message: string, context?: Record<string, unknown>);
946
+ }
947
+ /**
948
+ * The fold was asked for something its configuration cannot provide, for example
949
+ * materializing a richtext field with several concurrent Yjs updates without a
950
+ * richtext merger.
951
+ */
952
+ declare class FoldConfigurationError extends KoraError {
953
+ constructor(message: string, context?: Record<string, unknown>);
954
+ }
487
955
 
488
956
  /**
489
957
  * Canonical JSON-safe encoding of binary bytes stored inside `op.data` /
@@ -578,7 +1046,7 @@ interface StateMachineInput {
578
1046
  onInvalidTransition: 'reject' | 'last-valid-state';
579
1047
  }
580
1048
  interface CollectionInput {
581
- fields: Record<string, FieldBuilder<any, any, any>>;
1049
+ fields: Record<string, FieldBuilder>;
582
1050
  indexes?: string[];
583
1051
  constraints?: ConstraintInput[];
584
1052
  resolve?: Record<string, CustomResolver>;
@@ -636,23 +1104,6 @@ type TypedSchemaDefinition<T extends SchemaInput = SchemaInput> = SchemaDefiniti
636
1104
  };
637
1105
  declare function defineSchema<const T extends SchemaInput>(input: T): TypedSchemaDefinition<T>;
638
1106
 
639
- /**
640
- * Generate CREATE TABLE and CREATE INDEX SQL for a single collection.
641
- *
642
- * @param collectionName - The collection name
643
- * @param collection - The collection definition
644
- * @param relations - Optional relations for FK references
645
- * @returns An array of SQL statements (CREATE TABLE + CREATE INDEX)
646
- */
647
- declare function generateSQL(collectionName: string, collection: CollectionDefinition, relations?: Record<string, RelationDefinition>): string[];
648
- /**
649
- * Generate the full DDL for all collections plus metadata tables.
650
- *
651
- * @param schema - The complete schema definition
652
- * @returns An array of all SQL statements needed to initialize the database
653
- */
654
- declare function generateFullDDL(schema: SchemaDefinition): string[];
655
-
656
1107
  /**
657
1108
  * Quote a SQL identifier (table or column name) for safe interpolation into
658
1109
  * generated SQL.
@@ -697,76 +1148,105 @@ declare function quoteIdent(name: string): string;
697
1148
  * @throws {SchemaValidationError} If validation fails
698
1149
  */
699
1150
  declare function validateRecord(collection: string, collectionDef: CollectionDefinition, data: Record<string, unknown>, operationType: OperationType): Record<string, unknown>;
1151
+ /**
1152
+ * The domain check of one resolved field value (value-domain.ts), for values produced
1153
+ * after validation: an atomic op's resolved result (`op.increment(0.5)` on a timestamp,
1154
+ * an increment past the largest finite number).
1155
+ *
1156
+ * @param collection - The collection (for the message)
1157
+ * @param fieldName - The field
1158
+ * @param descriptor - The field's descriptor
1159
+ * @param value - The resolved value
1160
+ * @throws {SchemaValidationError} When the value is outside the field's domain
1161
+ */
1162
+ declare function assertResolvedFieldValue(collection: string, fieldName: string, descriptor: FieldDescriptor, value: unknown): void;
700
1163
 
701
1164
  /**
702
- * Type-level inference utilities for Kora schemas.
1165
+ * One value domain (RT-86, RT-87): every value the local API accepts must be storable and
1166
+ * transmissible, unchanged, by every built-in store (client SQLite and IndexedDB; server
1167
+ * memory, SQLite and Postgres) and every transport (JSON and protobuf). The domain is
1168
+ * defined here once, enforced where values are written (`validateRecord`, after atomic
1169
+ * ops resolve) and re-checked by the server on ingest (`operationValueViolation`), so a
1170
+ * value outside it is refused with a clear error before anything stores it, never by a
1171
+ * database half-way through a sync.
703
1172
  *
704
- * These mapped types convert FieldBuilder type parameters into concrete
705
- * TypeScript types, enabling full autocomplete and type checking from
706
- * defineSchema() through createApp() to collection methods.
1173
+ * | Field type | Domain | Client SQLite / IndexedDB | Server SQLite | Server Postgres | Wire (JSON / protobuf) |
1174
+ * |----------------|-----------------------------------------------------------|---------------------------|---------------|--------------------------|------------------------|
1175
+ * | `string` | any string (U+0000 and lone surrogates included) | TEXT / string | TEXT | TEXT, lossless text codec (RT-65) | JSON string (escaped) |
1176
+ * | `enum` | one of the declared values | TEXT / string | TEXT | TEXT | JSON string |
1177
+ * | `secret` | string (stored hashed or encrypted) | TEXT | TEXT | TEXT | JSON string |
1178
+ * | `number` | finite double; `-0` is `0` | REAL / number | REAL | DOUBLE PRECISION | JSON number |
1179
+ * | `boolean` | `true` / `false` | INTEGER 0/1 / boolean | INTEGER | INTEGER | JSON boolean |
1180
+ * | `timestamp` | integer milliseconds in [-8.64e15, 8.64e15] (the `Date` range; a safe integer). Fractions are refused, not rounded | INTEGER / number | INTEGER | BIGINT | JSON number |
1181
+ * | `array` | dense array of the item kind's domain, nesting <= {@link MAX_VALUE_DEPTH} | TEXT (JSON) / array | TEXT (JSON) | JSONB (text codec) | JSON array |
1182
+ * | `object` | plain object; declared keys in their kind's domain | TEXT (JSON) / object | TEXT (JSON) | JSONB | JSON object |
1183
+ * | `json` | any JSON value: finite numbers, no `__proto__` key, nesting <= {@link MAX_VALUE_DEPTH}, not exactly `{ __kora_bytes__ }` (the wire's binary form) | TEXT (JSON) / value | TEXT (JSON) | JSONB | JSON |
1184
+ * | `blob` | a `BlobRef` | TEXT (JSON) | TEXT | TEXT | JSON object |
1185
+ * | `richtext` | Yjs update bytes (`Uint8Array` / `ArrayBuffer`) or a string | BLOB / bytes | BLOB | BYTEA | `{ $koraBytes }` / bytes |
1186
+ * | any operation | serialized size <= `maxOperationBytes` (default {@link DEFAULT_MAX_OPERATION_BYTES}) | | | | |
707
1187
  *
708
- * Zero runtime cost — these are purely compile-time constructs.
1188
+ * Every value additionally has a canonical JSON form (core canonical-body): no `NaN`,
1189
+ * `±Infinity`, `BigInt`, `Map`, `Set`, class instances or cycles.
709
1190
  */
710
-
1191
+ /** Smallest `t.timestamp()` value: the earliest instant a JavaScript `Date` can hold. */
1192
+ declare const TIMESTAMP_MIN_MS = -8640000000000000;
1193
+ /** Largest `t.timestamp()` value: the latest instant a JavaScript `Date` can hold. */
1194
+ declare const TIMESTAMP_MAX_MS = 8640000000000000;
711
1195
  /**
712
- * Maps a FieldKind string literal to its corresponding TypeScript type.
713
- */
714
- interface FieldKindToType {
715
- string: string;
716
- number: number;
717
- boolean: boolean;
718
- timestamp: number;
719
- richtext: string;
720
- enum: string;
721
- array: unknown[];
722
- object: Record<string, unknown>;
723
- json: unknown;
724
- blob: BlobRef;
725
- secret: string;
726
- }
1196
+ * Deepest nesting of arrays and objects inside one field value. Postgres parses JSONB
1197
+ * recursively with a stack limit, and every replica folds values recursively; 64 levels
1198
+ * is far beyond any real document.
1199
+ */
1200
+ declare const MAX_VALUE_DEPTH = 64;
1201
+ /** Default largest serialized operation, shared by the local write path and the server. */
1202
+ declare const DEFAULT_MAX_OPERATION_BYTES: number;
727
1203
  /**
728
- * Infers the TypeScript type for a single field descriptor or builder.
729
- * Supports both FieldBuilder (class instances) and FieldDescriptor (compiled objects).
730
- */
731
- type InferFieldType<F> = F extends EnumFieldBuilder<infer V, any, any> ? V[number] : F extends JsonFieldBuilder<infer T, any, any> ? T : F extends ArrayFieldBuilder<infer K, any, any> ? FieldKindToType[K][] : F extends FieldBuilder<infer K, any, any> ? K extends keyof FieldKindToType ? FieldKindToType[K] : unknown : F extends {
732
- kind: 'enum';
733
- enumValues: infer V;
734
- } ? V extends readonly (infer S)[] ? S : string : F extends {
735
- kind: 'array';
736
- itemKind: infer K extends FieldKind;
737
- } ? FieldKindToType[K][] : F extends {
738
- kind: infer K extends FieldKind;
739
- } ? FieldKindToType[K] : unknown;
740
- /**
741
- * Infers the full record type returned from queries.
742
- * Includes `id`, `createdAt`, `updatedAt` metadata fields.
743
- * Optional/defaulted fields include `| null` in their type.
744
- */
745
- type InferRecord<Fields extends Record<string, FieldBuilder<any, any, any>>> = {
746
- readonly id: string;
747
- readonly createdAt: number;
748
- readonly updatedAt: number;
749
- } & {
750
- readonly [K in keyof Fields]: Fields[K] extends FieldBuilder<any, true, any> ? InferFieldType<Fields[K]> : InferFieldType<Fields[K]> | null;
751
- };
1204
+ * Why `value` is not a valid `t.timestamp()` value, or null when it is.
1205
+ *
1206
+ * @param value - The value
1207
+ */
1208
+ declare function timestampDomainViolation(value: unknown): string | null;
752
1209
  /**
753
- * Infers the insert input type using inline key remapping.
754
- * - Required non-auto fields are required keys
755
- * - Optional/defaulted non-auto fields are optional keys
756
- * - Auto fields are excluded entirely
1210
+ * The path of the first container nested deeper than `maxDepth`, or null.
1211
+ *
1212
+ * @param value - Any value
1213
+ * @param maxDepth - The deepest allowed nesting of arrays and objects
757
1214
  */
758
- type InferInsertInput<Fields extends Record<string, FieldBuilder<any, any, any>>> = {
759
- [K in keyof Fields as Fields[K] extends FieldBuilder<any, any, true> ? never : Fields[K] extends FieldBuilder<any, true, false> ? K : never]: InferFieldType<Fields[K]>;
760
- } & {
761
- [K in keyof Fields as Fields[K] extends FieldBuilder<any, any, true> ? never : Fields[K] extends FieldBuilder<any, true, false> ? never : K]?: InferFieldType<Fields[K]>;
762
- };
1215
+ declare function valueDepthViolation(value: unknown, maxDepth?: number): string | null;
763
1216
  /**
764
- * Infers the update input type using inline key remapping.
765
- * All non-auto fields are optional (partial update semantics).
1217
+ * The path of the first own `__proto__` key in a value, or null. JSON can carry such a
1218
+ * key, but most JavaScript code (including object spreads into a fresh `{}` with
1219
+ * assignment) turns it into a prototype and loses it, so it is refused everywhere.
1220
+ *
1221
+ * @param value - Any value
766
1222
  */
767
- type InferUpdateInput<Fields extends Record<string, FieldBuilder<any, any, any>>> = {
768
- [K in keyof Fields as Fields[K] extends FieldBuilder<any, any, true> ? never : K]?: InferFieldType<Fields[K]>;
769
- };
1223
+ declare function protoKeyViolation(value: unknown): string | null;
1224
+ /**
1225
+ * Why a field value, as an OPERATION carries it (op-data form: richtext as
1226
+ * `{ $koraBytes }` or a string, secrets in their at-rest string form), is outside the
1227
+ * field's domain, or null when it is inside. The server checks every uploaded and
1228
+ * route-written operation's data with it, so every server store and every peer holds
1229
+ * the same values (RT-86, RT-87). `null` is always accepted (a cleared field).
1230
+ *
1231
+ * @param descriptor - The field's descriptor in the schema that reads the operation
1232
+ * @param value - The value from `data`
1233
+ */
1234
+ declare function operationValueViolation(descriptor: FieldDescriptor, value: unknown): string | null;
1235
+ /**
1236
+ * Why a json or object field value is reserved by the wire, or null: an object whose
1237
+ * only member is `__kora_bytes__`.
1238
+ *
1239
+ * @param value - A field value
1240
+ */
1241
+ declare function reservedWireShapeViolation(value: unknown): string | null;
1242
+ /**
1243
+ * Serialized size of an operation in bytes (UTF-8 of its JSON), the measure the server
1244
+ * enforces `maxOperationBytes` with and the local write path checks before a write is
1245
+ * accepted (RT-86).
1246
+ *
1247
+ * @param op - The operation
1248
+ */
1249
+ declare function measureOperationBytes(op: Operation): number;
770
1250
 
771
1251
  /**
772
1252
  * Create an empty version vector.
@@ -837,6 +1317,45 @@ declare function isCollectionSyncScoped(schema: SchemaDefinition, collectionName
837
1317
  */
838
1318
  declare function collectSchemaScopeValueKeys(schema: SchemaDefinition): string[];
839
1319
 
1320
+ /**
1321
+ * Reserved key in a server scope grant that carries VERIFIED claim values
1322
+ * (for example `{ userId: <jwt sub>, orgId: <from your user store> }`).
1323
+ *
1324
+ * An auth provider usually cannot know the sync schema, so instead of listing
1325
+ * every collection it returns `{ $claims: { userId } }` and the sync server binds
1326
+ * each schema-scoped collection from those values when the session starts. A
1327
+ * collection whose scope bindings cannot all be resolved from the claims is
1328
+ * denied (fail closed), never widened to "everything".
1329
+ *
1330
+ * Collection names must start with a letter, so `$claims` can never collide with
1331
+ * a real collection. Any consumer that does not expand it sees a key that matches
1332
+ * no collection, which is also fail-closed.
1333
+ */
1334
+ declare const SCOPE_CLAIMS_KEY: "$claims";
1335
+ /**
1336
+ * Build a server scope grant from verified claim values, optionally combined
1337
+ * with explicit per-collection grants. Explicit entries win over claim-derived
1338
+ * bindings for the same collection.
1339
+ *
1340
+ * @param claims - Verified scope values, keyed by the scope binding names your
1341
+ * schema uses (`scope: ['userId']` binds the `userId` value)
1342
+ * @param explicit - Optional explicit per-collection predicates
1343
+ * @returns A scope grant suitable for `AuthContext.scopes`
1344
+ *
1345
+ * @example
1346
+ * ```typescript
1347
+ * return { userId: payload.sub, scopes: claimScopes({ userId: payload.sub, orgId }) }
1348
+ * ```
1349
+ */
1350
+ declare function claimScopes(claims: Record<string, unknown>, explicit?: ScopeMap): ScopeMap;
1351
+ /**
1352
+ * Read the verified claim values carried by a scope grant, if any.
1353
+ *
1354
+ * @param grant - A scope grant (possibly containing {@link SCOPE_CLAIMS_KEY})
1355
+ * @returns The claim values, or null when the grant has none
1356
+ */
1357
+ declare function getScopeClaims(grant: ScopeMap | undefined): Record<string, unknown> | null;
1358
+
840
1359
  /**
841
1360
  * Collect every unique scope field name declared across all collections.
842
1361
  *
@@ -882,28 +1401,99 @@ declare function formatSequenceValue(template: string, counter: number, nodeId:
882
1401
  */
883
1402
  declare function defaultSequenceFormat(name: string): string;
884
1403
 
885
- /**
886
- * Transforms operations created under an older schema version so they are valid
887
- * after a schema upgrade. Used during sync when client and server schema versions differ
888
- * but remain within the server's supported range.
889
- */
890
- interface OperationTransform {
891
- /** Schema version the operation was authored against */
892
- readonly fromVersion: number;
893
- /** Target schema version after transformation */
894
- readonly toVersion: number;
895
- /**
896
- * Transform a single operation. Return `null` to drop operations that cannot be migrated.
897
- */
898
- transform(operation: Operation): Operation | null;
899
- }
900
-
901
1404
  /**
902
1405
  * Apply registered transforms until the operation matches `targetSchemaVersion`.
903
1406
  * Returns null when a transform drops the operation or no path exists.
904
1407
  */
905
1408
  declare function applyOperationTransforms(operation: Operation, targetSchemaVersion: number, transforms: readonly OperationTransform[]): Operation | null;
906
1409
 
1410
+ /**
1411
+ * Thrown when a schema transform breaks its contract: it changed an operation's
1412
+ * identity (id, node, type, collection, record or timestamp), or produced a body with
1413
+ * no canonical JSON form.
1414
+ */
1415
+ declare class OperationTransformError extends KoraError {
1416
+ constructor(message: string, context: Record<string, unknown>);
1417
+ }
1418
+ /**
1419
+ * The operation as a schema reads it: its schema view (transforms at fold time).
1420
+ *
1421
+ * Operations are immutable and content-addressed, so a schema transform never rewrites
1422
+ * a stored operation. Every replica (server stores and devices alike) stores and sends
1423
+ * the operation exactly as its author wrote it, and applies the transform chain only
1424
+ * when it judges or folds the operation, through this one function:
1425
+ * - the server authorizes and validates the view, and its stores fold the view;
1426
+ * - a device folds the view; an operation with no view (no transform path, e.g. one
1427
+ * authored under a NEWER schema than the device's) is kept aside and replayed after
1428
+ * the device upgrades.
1429
+ *
1430
+ * Contract, enforced here:
1431
+ * - transforms are PURE and DETERMINISTIC: the same operation and transforms give the
1432
+ * same view on every replica, at every time (no clock, randomness or I/O). Every
1433
+ * replica must register the same transforms for the schema versions its log holds;
1434
+ * the fold plan fingerprint includes them, so changing a transform re-folds;
1435
+ * - a transform rewrites `data`, `previousData`, `atomicOps` and `schemaVersion` only. A
1436
+ * view whose id, node, type, collection, record or timestamp differs is refused
1437
+ * ({@link OperationTransformError});
1438
+ * - the view's body is canonical (core canonical-body), like any written body.
1439
+ *
1440
+ * Returned unchanged: an operation already at `targetSchemaVersion`, any operation
1441
+ * when no transform is registered (it folds as written), and an encryption envelope the
1442
+ * caller cannot read (`encrypted` set: the server folds its cleartext header as is;
1443
+ * devices transform after decryption).
1444
+ *
1445
+ * @param op - The operation as stored
1446
+ * @param targetSchemaVersion - The schema version of the replica judging or folding it
1447
+ * @param transforms - The registered transforms
1448
+ * @returns The view, or null when a transform drops the operation or no path exists
1449
+ * @throws {OperationTransformError} When a transform breaks the contract above
1450
+ */
1451
+ declare function operationSchemaView(op: Operation, targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined): Operation | null;
1452
+ /**
1453
+ * Fingerprint of a transform chain for a target schema version: '' when no transform
1454
+ * is registered (operations fold as written, whatever the target), otherwise the
1455
+ * target version and each transform's versions and source text. Part of the fold plan
1456
+ * fingerprint, so a replica re-folds when its views change.
1457
+ *
1458
+ * @param targetSchemaVersion - The replica's schema version
1459
+ * @param transforms - The registered transforms
1460
+ */
1461
+ declare function operationTransformsFingerprint(targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined): string;
1462
+ /**
1463
+ * Thrown when stored operations have a schema version the registered transforms cannot
1464
+ * read (RT-103): starting would fold those operations as absent and silently erase them
1465
+ * from every record they belong to.
1466
+ */
1467
+ declare class OperationTransformCoverageError extends KoraError {
1468
+ readonly versions: readonly number[];
1469
+ readonly targetSchemaVersion: number;
1470
+ constructor(message: string, versions: readonly number[], targetSchemaVersion: number, context?: Record<string, unknown>);
1471
+ }
1472
+ /**
1473
+ * The stored schema versions the transforms have no path from (RT-103). Mirrors the
1474
+ * path {@link operationSchemaView} follows (`fromVersion` to `toVersion`, at most 32
1475
+ * steps). An empty transform list reads everything (operations fold as written).
1476
+ *
1477
+ * @param storedVersions - Distinct schema versions of the stored operations
1478
+ * @param targetSchemaVersion - The replica's schema version
1479
+ * @param transforms - The registered transforms
1480
+ * @returns Each version with no path and the version whose transform is missing, ascending
1481
+ */
1482
+ declare function missingTransformPaths(storedVersions: Iterable<number>, targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined): Array<{
1483
+ version: number;
1484
+ missingFrom: number;
1485
+ }>;
1486
+ /**
1487
+ * Refuse to start a replica whose transforms cannot read operations it stores (RT-103).
1488
+ *
1489
+ * @param storedVersions - Distinct schema versions of the stored operations
1490
+ * @param targetSchemaVersion - The replica's schema version
1491
+ * @param transforms - The registered transforms
1492
+ * @param replica - Names the replica in the message ("server store", "local database")
1493
+ * @throws {OperationTransformCoverageError} When a stored version has no path
1494
+ */
1495
+ declare function assertOperationTransformCoverage(storedVersions: Iterable<number>, targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined, replica: string): void;
1496
+
907
1497
  /**
908
1498
  * Convert migration steps to SQL statements.
909
1499
  *
@@ -933,7 +1523,7 @@ declare function migrationStepsToSQL(steps: readonly MigrationStep[]): string[];
933
1523
  * .addIndex('todos', 'priority')
934
1524
  *
935
1525
  * const rollbackSQL = rollbackStepsToSQL(migration)
936
- * // ['DROP INDEX IF EXISTS idx_todos_priority',
1526
+ * // ['DROP INDEX IF EXISTS "idx_5_todos_priority"',
937
1527
  * // 'ALTER TABLE "todos" DROP COLUMN "priority"']
938
1528
  * ```
939
1529
  */
@@ -1125,4 +1715,4 @@ interface ProtoOutput {
1125
1715
  */
1126
1716
  declare function generateProtoDefinitions(schema: SchemaDefinition): ProtoOutput;
1127
1717
 
1128
- export { APPLY_FAILURE_CODES, APPLY_RESULTS, AppNotReadyError, type ApplyFailureReason, type ApplyResult, ArrayFieldBuilder, AtomicOp, type AtomicOpSentinel, AtomicOpType, BlobRef, type BlobRefMetadata, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, EnumFieldBuilder, FieldBuilder, FieldKind, type FieldKindToType, type InferFieldType, type InferInsertInput, type InferRecord, type InferUpdateInput, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, MergeConflictError, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, Operation, OperationError, type OperationLog, type OperationTransform, OperationType, type ProtoOutput, RandomSource, RelationDefinition, type RelationInput, RemoteClockDriftError, type ReplayOperation, type ReversibleMigration, SchemaDefinition, type SchemaInput, SchemaValidationError, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, advanceVector, applyAtomicOp, applyOperationTransforms, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createReversibleMigration, createVersionVector, decodeBytesFromOpData, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deserializeVector, dominates, encodeBytesForOpData, encryptSecret, extractScopeValuesFromClaims, extractTimestamp, formatSequenceValue, generateFullDDL, generateProtoDefinitions, generateRollbackSteps, generateSQL, generateUUIDv7, getCollectionScopeBindings, getKoraErrorFix, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAtomicOp, isBlobRef, isCollectionSyncScoped, isKoraBytesValue, isLegacyNumericByteObject, isValidUUIDv7, mergeVectors, migrationStepsToSQL, op, quoteIdent, replayOperationsForRecord, resolveAtomicOp, revealSecret, rollbackStepsToSQL, serializeVector, toAtomicOp, transformSecretFieldsForWrite, validateRecord, validateTransition, vectorsEqual, verifySecret, verifySecretValue };
1718
+ export { APPLY_FAILURE_CODES, APPLY_RESULTS, type AdaptedFoldState, AppNotReadyError, type ApplyFailureReason, type ApplyResult, AtomicOp, BlobRef, type BlobRefMetadata, type CanonicalizableBody, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, DEFAULT_MAX_OPERATION_BYTES, FOLD_RECORD_TRACE_FIELD, FieldBuilder, FieldDescriptor, FoldConfigurationError, FoldFieldVersions, FoldOptions, FoldRecordResult, type FoldSnapshotInput, type FoldSnapshotOptions, FoldState, FoldStateError, FoldTrace, HLCTimestamp, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, type LegacyBody, MAX_VALUE_DEPTH, MergeConflictError, MergeOpResult, MergeTrace, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, NonCanonicalValueError, Operation, OperationError, OperationInput, type OperationLog, OperationTooLargeError, OperationTransform, OperationTransformCoverageError, OperationTransformError, OperationType, type ProtoOutput, RESERVED_NODE_ID_PREFIX, RandomSource, type RecordFieldVersions, type RelationInput, RemoteClockDriftError, type ReplayOperation, type ReversibleMigration, SCOPE_CLAIMS_KEY, SERVER_NODE_ID_PREFIX, STORED_TEXT_ESCAPE, SchemaDefinition, type SchemaInput, SchemaValidationError, ScopeMap, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TIMESTAMP_MAX_MS, TIMESTAMP_MIN_MS, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, type VersionedReplayOperation, adaptFoldState, advanceVector, applyOperationTransforms, assertOperationTransformCoverage, assertResolvedFieldValue, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, canonicalValue, canonicalizeLegacyOperation, canonicalizeOperationBody, canonicalizeProvenLegacyClear, claimScopes, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createFoldState, createReversibleMigration, createSnapshotState, createVersionVector, decodeBytesFromOpData, decodeStoredJsonValue, decodeStoredText, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deriveSideEffectOpId, deserializeFoldState, deserializeVector, dominates, encodeBytesForOpData, encodeStoredJsonValue, encodeStoredText, encryptSecret, expandFieldVersionedOperations, extractScopeValuesFromClaims, extractTimestamp, foldPlanFingerprint, foldPlanFingerprints, foldRecord, formatSequenceValue, generateProtoDefinitions, generateRollbackSteps, generateUUIDv7, getCollectionScopeBindings, getFoldFieldVersionStrings, getFoldFieldVersions, getKoraErrorFix, getScopeClaims, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAuthoritativeNodeId, isBlobRef, isCollectionSyncScoped, isFoldStateLive, isKoraBytesValue, isLegacyNumericByteObject, isReservedNodeId, isServerNodeId, isValidUUIDv7, joinStates, materialize, measureOperationBytes, mergeArraySet, mergeOp, mergeVectors, migrationStepsToSQL, mismatchedFoldFields, missingTransformPaths, needsStoredTextEncoding, operationSchemaView, operationTransformsFingerprint, operationValueViolation, protoKeyViolation, quoteIdent, replayFieldVersionsForRecord, replayOperationsForRecord, reservedWireShapeViolation, revealSecret, rollbackStepsToSQL, serializeFoldState, serializeVector, timestampDomainViolation, toMergeTrace, transformSecretFieldsForWrite, validateRecord, validateTransition, valueDepthViolation, vectorsEqual, verifySecret, verifySecretValue };