@korajs/server 0.0.0-canary-20261008174129

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. Obed Ehoneah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @korajs/server
2
+
3
+ The self-hosted sync server for Kora.js: stores every operation, folds records exactly as devices
4
+ do, enforces scopes, constraints and your validators, and relays changes between devices over
5
+ WebSocket or HTTP long-polling. SQLite and Postgres stores are built in.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @korajs/server@beta
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ <!-- docs-check: standalone -->
16
+ ```typescript
17
+ import { createProductionServer, createSqliteServerStore } from '@korajs/server'
18
+ import { defineSchema, t } from 'korajs'
19
+
20
+ const schema = defineSchema({ version: 1, collections: { todos: { fields: { title: t.string() } } } })
21
+
22
+ const store = createSqliteServerStore({ filename: './kora-server.db' })
23
+ await store.setSchema(schema)
24
+
25
+ const server = createProductionServer({
26
+ store,
27
+ staticDir: './dist', // the built app, served with an offline app shell
28
+ syncPath: '/kora-sync',
29
+ syncOptions: { schemaVersion: schema.version },
30
+ operationalAuth: { adminToken: process.env.KORA_ADMIN_TOKEN },
31
+ })
32
+
33
+ console.log(`Listening on ${await server.start()}`) // PORT, default 3001
34
+ ```
35
+
36
+ For production use Postgres (`await createPostgresServerStore({ connectionString })`), an auth
37
+ provider (`createKoraAuthServer().auth` from `@korajs/auth/server`, or your own
38
+ `TokenAuthProvider`), and the operational tokens.
39
+
40
+ <!-- docs-check: standalone -->
41
+ ```typescript
42
+ import { createPostgresServerStore, createProductionServer, TokenAuthProvider } from '@korajs/server'
43
+
44
+ declare function verifyToken(token: string): Promise<{ id: string } | null>
45
+
46
+ const server = createProductionServer({
47
+ store: await createPostgresServerStore({ connectionString: process.env.DATABASE_URL ?? '' }),
48
+ syncOptions: {
49
+ auth: new TokenAuthProvider({
50
+ validate: async (token) => {
51
+ const user = await verifyToken(token)
52
+ // The grant decides what this session may read and write.
53
+ return user ? { userId: user.id, scopes: { todos: { userId: user.id } } } : null
54
+ },
55
+ }),
56
+ },
57
+ })
58
+ ```
59
+
60
+ ## Stores
61
+
62
+ | Store | Use |
63
+ |-------|-----|
64
+ | `createSqliteServerStore({ filename })` | One server process |
65
+ | `await createPostgresServerStore({ connectionString })` | Production, several instances |
66
+ | `new MemoryServerStore()` | Tests |
67
+
68
+ `store.setSchema(schema)` creates one table per collection and enables scope, constraint and
69
+ relation checks; `queryCollection`, `findRecord` and `countCollection` read them. Server-side
70
+ writes go through `server.kora.apply(...)`, the same pipeline as sync.
71
+
72
+ ## Testing
73
+
74
+ The store parity suite runs against Memory and SQLite, and against Postgres when `DATABASE_URL` is
75
+ set:
76
+
77
+ ```bash
78
+ DATABASE_URL="postgres://user:pass@localhost:5432/kora_test" pnpm --filter @korajs/server test -- tests/integration/server-store-parity.test.ts
79
+ ```
80
+
81
+ ## Documentation
82
+
83
+ [Production Server](https://korajs.dev/guide/production-server),
84
+ [Server-side Validation](https://korajs.dev/guide/server-side-validation) and the
85
+ [Server API reference](https://korajs.dev/api/server).
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,188 @@
1
+ // src/store/server-store.ts
2
+ import { KoraError } from "@korajs/core";
3
+ var SEQUENCE_CONFLICT_CODE = "SEQUENCE_CONFLICT";
4
+ var SequenceConflictError = class extends KoraError {
5
+ constructor(operation, existingOperationId) {
6
+ super(
7
+ `Operation "${operation.id}" claims sequence ${String(operation.sequenceNumber)} of node "${operation.nodeId}", which operation "${existingOperationId}" already holds. A node may never reuse a sequence number for different content.`,
8
+ SEQUENCE_CONFLICT_CODE,
9
+ {
10
+ operationId: operation.id,
11
+ nodeId: operation.nodeId,
12
+ sequenceNumber: operation.sequenceNumber,
13
+ existingOperationId
14
+ }
15
+ );
16
+ this.operation = operation;
17
+ this.existingOperationId = existingOperationId;
18
+ this.name = "SequenceConflictError";
19
+ }
20
+ operation;
21
+ existingOperationId;
22
+ };
23
+ var UNSTORABLE_VALUE_CODE = "UNSTORABLE_VALUE";
24
+ var UnstorableValueError = class extends KoraError {
25
+ constructor(operation, detail) {
26
+ super(
27
+ `Operation "${operation.id}" on ${operation.collection}/${operation.recordId} holds a value this server's database cannot store (${detail}). It is refused; nothing was written.`,
28
+ UNSTORABLE_VALUE_CODE,
29
+ { operationId: operation.id, collection: operation.collection, recordId: operation.recordId }
30
+ );
31
+ this.operation = operation;
32
+ this.name = "UnstorableValueError";
33
+ }
34
+ operation;
35
+ };
36
+ var SEQUENCE_ENFORCEMENT_EPOCH_KEY = "sequence_enforcement_epoch";
37
+ var SEQUENCE_PAIRS_BACKFILLED_KEY = "sequence_pairs_backfilled";
38
+ var NODE_SEQ_UNIQUE_INDEX = "idx_node_seq_unique_sole";
39
+ var SUPERSEDED_NODE_SEQ_UNIQUE_INDEXES = [
40
+ "idx_node_seq_unique",
41
+ "idx_node_seq_unique_after_epoch"
42
+ ];
43
+ function judgeSequenceHolders(op, holders, epoch, writer = {}) {
44
+ const others = holders.filter((holder) => holder.id !== op.id);
45
+ if (others.length === 0) return { verdict: "free" };
46
+ const enforced = others.find((holder) => holder.deliverySequence > epoch);
47
+ const holderIds = others.map((holder) => holder.id);
48
+ const list = holderIds.map((id) => `"${id}"`).join(", ");
49
+ if (enforced) {
50
+ if (writer.legacySequenceWriter !== true) {
51
+ return { verdict: "conflict", holderId: enforced.id };
52
+ }
53
+ console.warn(
54
+ `[kora] Operation "${op.id}" shares sequence ${String(op.sequenceNumber)} of node "${op.nodeId}" with operation(s) ${list}. The client does not reserve sequence numbers (no sequenceReservation capability: Kora <= beta.12), so this is a legacy duplicate pair: both are stored and delivered. Upgrade the client.`
55
+ );
56
+ return { verdict: "legacy", holderIds, legacyWriter: true };
57
+ }
58
+ console.warn(
59
+ `[kora] Operation "${op.id}" shares sequence ${String(op.sequenceNumber)} of node "${op.nodeId}" with operation(s) ${list} stored before sequence enforcement (a duplicate written by Kora <= beta.12). Accepted and stored, as that release did.`
60
+ );
61
+ return { verdict: "legacy", holderIds, legacyWriter: false };
62
+ }
63
+ function reportLegacyPair(op, decision, options) {
64
+ if (decision.verdict !== "legacy" || !options?.onLegacySequencePair) return;
65
+ options.onLegacySequencePair({
66
+ operationId: op.id,
67
+ nodeId: op.nodeId,
68
+ sequenceNumber: op.sequenceNumber,
69
+ holderIds: decision.holderIds,
70
+ legacyWriter: decision.legacyWriter
71
+ });
72
+ }
73
+ var MAX_RESOLUTION_MESSAGE_LENGTH = 1024;
74
+ var RELEASED_NODE_OWNER = "";
75
+ var MAX_SCOPE_SNAPSHOT_STRING_LENGTH = 512;
76
+
77
+ // src/apply/ingest-validation.ts
78
+ import { KoraError as KoraError2 } from "@korajs/core";
79
+
80
+ // src/session/operation-validation.ts
81
+ import { MAX_LOGICAL } from "@korajs/core";
82
+ var SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
83
+ function isOperationTimestampValid(op, now = Date.now()) {
84
+ const { wallTime, logical } = op.timestamp;
85
+ if (!Number.isInteger(wallTime) || wallTime < 0) {
86
+ return false;
87
+ }
88
+ if (!Number.isInteger(logical) || logical < 0 || logical > MAX_LOGICAL) {
89
+ return false;
90
+ }
91
+ return wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
92
+ }
93
+
94
+ // src/apply/ingest-validation.ts
95
+ var INVALID_TIMESTAMP_CODE = "INVALID_TIMESTAMP";
96
+ var INVALID_SEQUENCE_NUMBER_CODE = "INVALID_SEQUENCE_NUMBER";
97
+ var INVALID_IDENTIFIER_CODE = "INVALID_IDENTIFIER";
98
+ var UNSTORABLE_IDENTIFIER = (
99
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: U+0000 is exactly what identifiers may not hold (RT-65)
100
+ /\u0000|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/
101
+ );
102
+ function isStorableIdentifier(value) {
103
+ return typeof value === "string" && !UNSTORABLE_IDENTIFIER.test(value);
104
+ }
105
+ function isValidSequenceNumber(sequenceNumber) {
106
+ return typeof sequenceNumber === "number" && Number.isSafeInteger(sequenceNumber) && sequenceNumber >= 1;
107
+ }
108
+ function validateIngestedOperation(op, now = Date.now()) {
109
+ const identifiers = [
110
+ op.id,
111
+ op.nodeId,
112
+ op.collection,
113
+ op.recordId,
114
+ op.timestamp?.nodeId,
115
+ ...Array.isArray(op.causalDeps) ? op.causalDeps : []
116
+ ];
117
+ if (!identifiers.every(isStorableIdentifier)) {
118
+ return {
119
+ valid: false,
120
+ code: INVALID_IDENTIFIER_CODE,
121
+ message: `Operation "${String(op.id)}" has an identifier (id, node id, collection, record id or causal dependency) that is not a string or holds U+0000 or an unpaired UTF-16 surrogate. Identifiers must be well-formed strings.`
122
+ };
123
+ }
124
+ if (!isValidSequenceNumber(op.sequenceNumber)) {
125
+ return {
126
+ valid: false,
127
+ code: INVALID_SEQUENCE_NUMBER_CODE,
128
+ message: `Operation "${op.id}" has sequence number ${String(op.sequenceNumber)}; sequence numbers are positive integers no larger than ${String(Number.MAX_SAFE_INTEGER)}.`
129
+ };
130
+ }
131
+ if (typeof op.timestamp !== "object" || op.timestamp === null || !isOperationTimestampValid(op, now)) {
132
+ return {
133
+ valid: false,
134
+ code: INVALID_TIMESTAMP_CODE,
135
+ message: `Operation "${op.id}" has an invalid timestamp or one more than ${String(SERVER_MAX_TIMESTAMP_FUTURE_MS)} ms ahead of server time. Check the clock of the device or server that wrote it.`
136
+ };
137
+ }
138
+ return { valid: true };
139
+ }
140
+ var BackupValidationError = class extends KoraError2 {
141
+ constructor(invalidOperations, totalOperations) {
142
+ super(
143
+ `Backup rejected: ${String(invalidOperations.length)} of ${String(totalOperations)} operations failed ingest validation (first: "${invalidOperations[0]?.operationId ?? ""}", ${invalidOperations[0]?.code ?? ""}). A far-future timestamp usually means the exporting server's clock was wrong; fix the clock and re-export, or repair the operations before restoring.`,
144
+ "BACKUP_INVALID_OPERATION",
145
+ {
146
+ invalidCount: invalidOperations.length,
147
+ totalOperations,
148
+ sample: invalidOperations.slice(0, 10)
149
+ }
150
+ );
151
+ this.invalidOperations = invalidOperations;
152
+ this.totalOperations = totalOperations;
153
+ this.name = "BackupValidationError";
154
+ }
155
+ invalidOperations;
156
+ totalOperations;
157
+ };
158
+ function assertBackupOperationsIngestible(operations, now = Date.now()) {
159
+ const invalid = [];
160
+ for (const op of operations) {
161
+ const result = validateIngestedOperation(op, now);
162
+ if (!result.valid) invalid.push({ operationId: String(op.id), code: result.code });
163
+ }
164
+ if (invalid.length > 0) throw new BackupValidationError(invalid, operations.length);
165
+ }
166
+
167
+ export {
168
+ SEQUENCE_CONFLICT_CODE,
169
+ SequenceConflictError,
170
+ UNSTORABLE_VALUE_CODE,
171
+ UnstorableValueError,
172
+ SEQUENCE_ENFORCEMENT_EPOCH_KEY,
173
+ SEQUENCE_PAIRS_BACKFILLED_KEY,
174
+ NODE_SEQ_UNIQUE_INDEX,
175
+ SUPERSEDED_NODE_SEQ_UNIQUE_INDEXES,
176
+ judgeSequenceHolders,
177
+ reportLegacyPair,
178
+ MAX_RESOLUTION_MESSAGE_LENGTH,
179
+ RELEASED_NODE_OWNER,
180
+ MAX_SCOPE_SNAPSHOT_STRING_LENGTH,
181
+ isOperationTimestampValid,
182
+ INVALID_IDENTIFIER_CODE,
183
+ isStorableIdentifier,
184
+ validateIngestedOperation,
185
+ BackupValidationError,
186
+ assertBackupOperationsIngestible
187
+ };
188
+ //# sourceMappingURL=chunk-UKWG5T6J.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/server-store.ts","../src/apply/ingest-validation.ts","../src/session/operation-validation.ts"],"sourcesContent":["import type {\n\tFoldState,\n\tHLCTimestamp,\n\tHybridLogicalClock,\n\tOperation,\n\tRecordFieldVersions,\n\tSchemaDefinition,\n\tVersionVector,\n} from '@korajs/core'\nimport { KoraError } from '@korajs/core'\nimport type { OperationTransform } from '@korajs/core'\nimport type { ApplyResult, SyncStore } from '@korajs/sync'\nimport type { UplinkAuthorizationResult } from '../scopes/server-scope-filter'\n\n/** Rejection code for a second, different operation under an existing (node, sequence). */\nexport const SEQUENCE_CONFLICT_CODE = 'SEQUENCE_CONFLICT'\n\n/**\n * Thrown by a store when an operation claims a `(nodeId, sequenceNumber)` that an\n * operation with a DIFFERENT id already holds (W3 step 4). A node's sequence numbers\n * identify its writes: accepting a second operation under the same number would make\n * one of them invisible to every version-vector delta and to the client's contiguous\n * acknowledged prefix. Nothing was written. Not retriable: the same bytes always fail.\n */\nexport class SequenceConflictError extends KoraError {\n\tconstructor(\n\t\treadonly operation: Pick<Operation, 'id' | 'nodeId' | 'sequenceNumber'>,\n\t\treadonly existingOperationId: string,\n\t) {\n\t\tsuper(\n\t\t\t`Operation \"${operation.id}\" claims sequence ${String(operation.sequenceNumber)} of node \"${operation.nodeId}\", which operation \"${existingOperationId}\" already holds. A node may never reuse a sequence number for different content.`,\n\t\t\tSEQUENCE_CONFLICT_CODE,\n\t\t\t{\n\t\t\t\toperationId: operation.id,\n\t\t\t\tnodeId: operation.nodeId,\n\t\t\t\tsequenceNumber: operation.sequenceNumber,\n\t\t\t\texistingOperationId,\n\t\t\t},\n\t\t)\n\t\tthis.name = 'SequenceConflictError'\n\t}\n}\n\n/** Rejection code for an operation holding a value the store cannot represent. */\nexport const UNSTORABLE_VALUE_CODE = 'UNSTORABLE_VALUE'\n\n/**\n * Thrown by a store when an operation carries a value the database cannot represent\n * (RT-65). Nothing was written. Not retriable: the same bytes always fail, so the\n * session refuses the operation terminally instead of failing the connection (which\n * made the device re-send it forever and blocked every later write of that device).\n */\nexport class UnstorableValueError extends KoraError {\n\tconstructor(\n\t\treadonly operation: Pick<Operation, 'id' | 'collection' | 'recordId'>,\n\t\tdetail: string,\n\t) {\n\t\tsuper(\n\t\t\t`Operation \"${operation.id}\" on ${operation.collection}/${operation.recordId} holds a value this server's database cannot store (${detail}). It is refused; nothing was written.`,\n\t\t\tUNSTORABLE_VALUE_CODE,\n\t\t\t{ operationId: operation.id, collection: operation.collection, recordId: operation.recordId },\n\t\t)\n\t\tthis.name = 'UnstorableValueError'\n\t}\n}\n\n/**\n * `kora_server_meta` key holding the sequence-enforcement epoch: the highest delivery\n * sequence in the log when this release first opened the store. Operations stored at\n * or below it were accepted by a release that allowed a node to reuse a sequence\n * number (beta.12 STORE-1/2); a newly uploaded operation that shares a (node,\n * sequence) only with such legacy operations is accepted as before (the client's\n * sequence repair may keep the OTHER op of a legacy duplicate pair at that number).\n * Above it, a writer that reserves its sequence numbers (see\n * {@link ApplyRemoteOptions.legacySequenceWriter}) is refused a held sequence.\n */\nexport const SEQUENCE_ENFORCEMENT_EPOCH_KEY = 'sequence_enforcement_epoch'\n\n/**\n * `kora_server_meta` key set once the legacy pairs already in the log were indexed in\n * `sequence_pairs` (RT-48); appends maintain the index from then on.\n */\nexport const SEQUENCE_PAIRS_BACKFILLED_KEY = 'sequence_pairs_backfilled'\n\n/**\n * Name of the partial unique index over (node_id, sequence_number) of the rows that\n * were the sole holder of their sequence when stored (`seq_unique = 1`, RT-37).\n *\n * Design: a row is flagged when nothing else held its (node, sequence) at insert, and\n * a row stored as the second of a pair (a legacy duplicate: a pre-epoch holder, or an\n * upload from a client without the `sequenceReservation` capability) is not. So the\n * index can never be violated by an accepted legacy pair, yet two concurrent sole\n * inserts of one (node, sequence) (the race between server instances it backs) still\n * collide. Rows written by a release without the column (older instances during a\n * rolling upgrade, or history) default to 0 and sit outside it; the append check, run\n * under the store's write lock, judges them as holders.\n */\nexport const NODE_SEQ_UNIQUE_INDEX = 'idx_node_seq_unique_sole'\n\n/** Earlier unique indexes over (node_id, sequence_number), dropped at startup. */\nexport const SUPERSEDED_NODE_SEQ_UNIQUE_INDEXES = [\n\t'idx_node_seq_unique',\n\t'idx_node_seq_unique_after_epoch',\n] as const\n\n/** Outcome of {@link judgeSequenceHolders}. */\nexport type SequenceHolderVerdict =\n\t/** No other operation holds the sequence: stored as its sole holder (indexed). */\n\t| { verdict: 'free' }\n\t/** Shares the sequence with `holderIds`, accepted: stored as a legacy pair. */\n\t| { verdict: 'legacy'; holderIds: string[]; legacyWriter: boolean }\n\t/** Refused: `holderId` holds the sequence under enforcement. */\n\t| { verdict: 'conflict'; holderId: string }\n\n/**\n * Decide an append against the operations already holding its (node, sequence):\n * `'conflict'` when any of them was stored after the enforcement epoch and the writer\n * reserves its sequence numbers; `'legacy'` (accepted, with a warning) when all of\n * them predate the epoch, or when the writer is a legacy client (RT-37: Kora <=\n * beta.12 could give two concurrent transactions one number, and refusing the second\n * would drop a write the user made); `'free'` when there are none. Holders with the\n * operation's own id are the caller's duplicate case.\n */\nexport function judgeSequenceHolders(\n\top: Pick<Operation, 'id' | 'nodeId' | 'sequenceNumber'>,\n\tholders: ReadonlyArray<{ id: string; deliverySequence: number }>,\n\tepoch: number,\n\twriter: { legacySequenceWriter?: boolean } = {},\n): SequenceHolderVerdict {\n\tconst others = holders.filter((holder) => holder.id !== op.id)\n\tif (others.length === 0) return { verdict: 'free' }\n\tconst enforced = others.find((holder) => holder.deliverySequence > epoch)\n\tconst holderIds = others.map((holder) => holder.id)\n\tconst list = holderIds.map((id) => `\"${id}\"`).join(', ')\n\tif (enforced) {\n\t\tif (writer.legacySequenceWriter !== true) {\n\t\t\treturn { verdict: 'conflict', holderId: enforced.id }\n\t\t}\n\t\tconsole.warn(\n\t\t\t`[kora] Operation \"${op.id}\" shares sequence ${String(op.sequenceNumber)} of node \"${op.nodeId}\" with operation(s) ${list}. The client does not reserve sequence numbers (no sequenceReservation capability: Kora <= beta.12), so this is a legacy duplicate pair: both are stored and delivered. Upgrade the client.`,\n\t\t)\n\t\treturn { verdict: 'legacy', holderIds, legacyWriter: true }\n\t}\n\tconsole.warn(\n\t\t`[kora] Operation \"${op.id}\" shares sequence ${String(op.sequenceNumber)} of node \"${op.nodeId}\" with operation(s) ${list} stored before sequence enforcement (a duplicate written by Kora <= beta.12). Accepted and stored, as that release did.`,\n\t)\n\treturn { verdict: 'legacy', holderIds, legacyWriter: false }\n}\n\n/** A stored legacy pair, reported to {@link ApplyRemoteOptions.onLegacySequencePair}. */\nexport interface LegacySequencePair {\n\toperationId: string\n\tnodeId: string\n\tsequenceNumber: number\n\t/** The operations that already held the sequence. */\n\tholderIds: string[]\n\t/** True when accepted because the writer is a legacy client (not a pre-epoch holder). */\n\tlegacyWriter: boolean\n}\n\n/**\n * Tell the caller of an append that it committed a legacy pair (see\n * {@link ApplyRemoteOptions.onLegacySequencePair}). Called by the built-in stores\n * after the write committed.\n */\nexport function reportLegacyPair(\n\top: Pick<Operation, 'id' | 'nodeId' | 'sequenceNumber'>,\n\tdecision: SequenceHolderVerdict,\n\toptions: Pick<ApplyRemoteOptions, 'onLegacySequencePair'> | undefined,\n): void {\n\tif (decision.verdict !== 'legacy' || !options?.onLegacySequencePair) return\n\toptions.onLegacySequencePair({\n\t\toperationId: op.id,\n\t\tnodeId: op.nodeId,\n\t\tsequenceNumber: op.sequenceNumber,\n\t\tholderIds: decision.holderIds,\n\t\tlegacyWriter: decision.legacyWriter,\n\t})\n}\n\n/**\n * How the server resolved an uploaded operation it did NOT store under the submitted\n * (node, sequence) (RT-43, RT-47):\n * - `'ignored'`: the validator answered `ignore` (handled out of band, nothing stored);\n * - `'refused'`: a terminal (non-retriable) rejection; the same id is answered with the\n * original rejection forever after;\n * - `'stored-elsewhere'`: the id is stored under another sequence of the same node (a\n * client sequence repair renumbered it), acknowledged as a duplicate.\n * A retriable rejection, and a `SEQUENCE_CONFLICT` (the client renumbers and resubmits\n * under the same id), are never resolutions.\n */\nexport type OperationResolutionOutcome = 'ignored' | 'refused' | 'stored-elsewhere'\n\n/** A durable record of how an uploaded operation was resolved without being stored. */\nexport interface OperationResolution {\n\toperationId: string\n\tnodeId: string\n\tsequenceNumber: number\n\toutcome: OperationResolutionOutcome\n\t/** Rejection code, for `'refused'`. */\n\tcode: string | null\n\t/** Rejection message, for `'refused'`. */\n\tmessage: string | null\n}\n\n/** Longest rejection message kept in an {@link OperationResolution}. */\nexport const MAX_RESOLUTION_MESSAGE_LENGTH = 1024\n\n/** Where a stored operation sits in its node's sequence space. */\nexport interface StoredOperationKey {\n\tnodeId: string\n\tsequenceNumber: number\n}\n\n/**\n * Owner recorded for a node id an admin released (see `ServerStore.releaseNodeClaim`):\n * the next principal to claim the node takes it over. Never a valid principal id.\n */\nexport const RELEASED_NODE_OWNER = ''\n\n/**\n * A materialized record reconstructed from the operation log\n * or read from a materialized collection table.\n */\nexport interface MaterializedRecord {\n\tid: string\n\t[key: string]: unknown\n}\n\n/**\n * A conditional, atomic multi-operation apply. The store reads the target under a\n * cross-instance lock, lets the caller decide admission and build the operations\n * against that locked state, and applies them in one transaction, or applies\n * nothing. `admit` and `buildOperations` run inside the locked transaction so the\n * predicate check and any atomic-op resolution see the authoritative current value\n * even when multiple server instances race on the same target.\n */\nexport interface ConditionalApplyInput {\n\t/** Record the admission lock and predicate are keyed on. */\n\ttarget: { collection: string; id: string }\n\t/** Decides admission against the target's locked current state. */\n\tadmit: (current: MaterializedRecord | null) => boolean\n\t/**\n\t * Builds the operations to apply, given the locked current state and a clock the\n\t * store has already advanced past the target record's latest committed operation.\n\t * Stamping the built operations with this clock guarantees they sort strictly\n\t * after every prior write to the target, so last-write-wins materialization\n\t * reflects the serialized commit order even when two instances commit in the same\n\t * millisecond. Without it, a same-millisecond tie could let materialization pick\n\t * an earlier resolved value and undercount an atomic counter, admitting past a cap.\n\t */\n\tbuildOperations: (\n\t\tcurrent: MaterializedRecord | null,\n\t\tcontext: ConditionalApplyContext,\n\t) => Promise<Operation[]>\n\t/**\n\t * When set, a record whose prior existence proves this set already committed.\n\t * Checked under the same lock as admission, so a retry is idempotent even across\n\t * instances: it returns `idempotent: true` without re-running the operations.\n\t */\n\tidempotencyKey?: { collection: string; id: string }\n}\n\n/** Context passed to {@link ConditionalApplyInput.buildOperations}. */\nexport interface ConditionalApplyContext {\n\t/**\n\t * A clock the store has advanced past the target record's latest committed\n\t * operation. Operations built here must be stamped with it (route context threads\n\t * it into operation creation) so they sort strictly after every prior write.\n\t */\n\tclock: HybridLogicalClock\n\t/**\n\t * Read a record as stored (including a soft-deleted one) inside the store's\n\t * conditional-apply transaction, for authorizing the built operations against the\n\t * same state they commit over. Optional; callers fall back to a plain read.\n\t */\n\treadStoredRow?: (collection: string, id: string) => Promise<MaterializedRecord | null>\n}\n\n/** Result of {@link ServerStore.applyConditional}. */\nexport interface ConditionalApplyResult {\n\t/** True when the predicate held (or the set was already committed). */\n\tadmitted: boolean\n\t/** True when the idempotency key already existed and nothing was re-applied. */\n\tidempotent: boolean\n\t/** The operations that were applied (empty when not admitted or idempotent). */\n\tapplied: Operation[]\n}\n\n/**\n * A stored operation paired with its server-assigned delivery sequence. The\n * delivery sequence is monotonic in commit order, so a batch of these can be\n * chained (each batch's base links to the previous batch's max) to give the\n * client a gap-free, scope-agnostic recovery stream.\n */\nexport interface DeliveredOperation {\n\toperation: Operation\n\tdeliverySequence: number\n\t/**\n\t * The record's scope-relevant values around this operation, captured from the\n\t * server's own rows when it was applied (RT-14, RT-15). Absent (or null) for\n\t * operations stored before snapshots existed that could not be backfilled.\n\t */\n\tscopeSnapshot?: OperationScopeSnapshot | null\n}\n\n/**\n * Scope-relevant field values of a record, captured by the store from its own rows\n * when an operation is applied (never from the writer's `previousData`). Download\n * visibility of a historical operation is judged on `post`, so a later ownership\n * transfer does not disclose the record's earlier history to the new owner (RT-14),\n * and a scope exit is judged from `pre` to `post` (RT-15).\n *\n * Only scalar fields (string, number, boolean, enum, timestamp) and `id` are kept:\n * scope predicates compare with `Object.is`, so other kinds can never match. A\n * string longer than {@link MAX_SCOPE_SNAPSHOT_STRING_LENGTH} is left out (fails\n * closed for a scope on such a field) so a snapshot never copies large text.\n */\nexport interface OperationScopeSnapshot {\n\t/** Values before the operation; null when the record did not exist or was deleted. */\n\tpre: Record<string, unknown> | null\n\t/** Values after it; a delete keeps the record's last values; null when unknown. */\n\tpost: Record<string, unknown> | null\n}\n\n/** Longest string value copied into an {@link OperationScopeSnapshot}. */\nexport const MAX_SCOPE_SNAPSHOT_STRING_LENGTH = 512\n\n/**\n * Options for querying a materialized collection table.\n */\nexport interface CollectionQueryOptions {\n\t/** Exact-match filters on field values */\n\twhere?: Record<string, unknown>\n\t/** Field name to order results by */\n\torderBy?: string\n\t/** Sort direction (default: 'asc') */\n\torderDirection?: 'asc' | 'desc'\n\t/** Maximum number of records to return */\n\tlimit?: number\n\t/** Number of records to skip (for pagination) */\n\toffset?: number\n\t/** Include soft-deleted records (default: false) */\n\tincludeDeleted?: boolean\n}\n\n/**\n * Options for {@link ServerStore.applyRemoteOperation}.\n */\nexport interface ApplyRemoteOptions {\n\t/**\n\t * Authorization re-check for an operation submitted by an untrusted writer.\n\t *\n\t * The store calls it inside its apply critical section (the SQLite write\n\t * transaction, the in-memory store's synchronous apply, or a Postgres\n\t * transaction holding the per-record advisory lock shared with conditional\n\t * applies) with the record as stored at that moment, including a soft-deleted\n\t * one, or null when none exists. Returning a refusal aborts the apply and the\n\t * store throws {@link UplinkAuthorizationError}; nothing is written.\n\t *\n\t * This closes the window between a caller's own pre-check and the write, where an\n\t * ownership change or a same-id insert committed by another writer (or another\n\t * server instance) could otherwise slip in.\n\t */\n\tauthorize?: (storedRow: MaterializedRecord | null) => UplinkAuthorizationResult\n\t/**\n\t * The writer does not reserve its sequence numbers inside the write transaction\n\t * (a client that did not advertise the `sequenceReservation` handshake capability,\n\t * Kora <= beta.12; RT-37). A different operation already holding the\n\t * `(nodeId, sequenceNumber)` then does not refuse this one: both are stored as a\n\t * legacy pair (each keeps its own delivery sequence, so both are delivered) instead\n\t * of throwing {@link SequenceConflictError}. Default false: enforce.\n\t */\n\tlegacySequenceWriter?: boolean\n\t/**\n\t * Called once the operation was committed as the second holder of its sequence (a\n\t * legacy pair), for logging and diagnostics. Never called for a refused write.\n\t */\n\tonLegacySequencePair?: (pair: LegacySequencePair) => void\n}\n\n/** Options of {@link ServerStore.setSchema}. */\nexport interface ServerSchemaOptions {\n\t/** Schema transforms the store folds with (transforms at fold time, RT-84). */\n\toperationTransforms?: readonly OperationTransform[]\n}\n\n/** One stored end-to-end key record (opaque JSON) and its owner, as backups carry it. */\nexport interface EncryptionKeyRecordRow {\n\towner: string\n\tkeyring: string\n\trevision: number\n\t/** The record's JSON, exactly as stored. */\n\trecord: string\n}\n\n/**\n * Server-side store interface. Extends SyncStore with lifecycle,\n * introspection, and materialization methods needed by the sync server.\n */\nexport interface ServerStore extends SyncStore {\n\t/**\n\t * Apply an operation to the log and materialized state. With\n\t * `options.authorize`, the authorization is re-checked atomically with the write\n\t * (see {@link ApplyRemoteOptions}). Stores written before this option existed\n\t * may ignore it; callers always pre-check as well.\n\t *\n\t * Built-in stores throw {@link SequenceConflictError} when a different operation\n\t * already holds the operation's `(nodeId, sequenceNumber)` under enforcement\n\t * (unless `options.legacySequenceWriter`, see {@link judgeSequenceHolders}), and return\n\t * `'duplicate'` (writing nothing) for an operation id already stored, decided\n\t * atomically with the write.\n\t */\n\tapplyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>\n\n\t/**\n\t * The version vector as committed in the shared database, read fresh (SRV-4). A\n\t * store shared by several server instances must implement it: its synchronous\n\t * {@link SyncStore.getVersionVector} can only reflect this instance's own writes.\n\t * Optional; callers fall back to `getVersionVector()`.\n\t */\n\treadVersionVector?(): Promise<VersionVector>\n\n\t/**\n\t * Several records of one collection by id, as stored (soft-deleted ones included),\n\t * in one read: `id = ANY(...)` on Postgres, chunked `IN (...)` on SQLite (LMS #11).\n\t * Ids with no row are absent from the result. Optional; the delivery stream falls\n\t * back to one lookup per operation without it.\n\t */\n\tfindRecordsByIds?(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>\n\n\t/**\n\t * Which of `ids` the operation log already holds, with the node and sequence each is\n\t * stored under, in one read per batch (chunked `IN (...)`). The sync session asks before any other per-operation check, so a\n\t * device re-uploading history the server already stores (the one-time upgrade\n\t * re-upload, or an op the client's sequence repair renumbered under its original\n\t * id) is acknowledged as a duplicate instead of being re-judged by today's\n\t * authorization and validators. Optional; sessions fall back to a per-operation\n\t * (node, sequence) lookup without it.\n\t */\n\tfindStoredOperations?(ids: string[]): Promise<Map<string, StoredOperationKey>>\n\n\t/**\n\t * Durably record how an uploaded operation was resolved without being stored under\n\t * its sequence (RT-43, RT-47; see {@link OperationResolution}). Idempotent per\n\t * operation id: the first resolution wins. The session awaits it BEFORE answering the\n\t * client, so a client never acts on a resolution the server could forget. Optional\n\t * for custom stores: without it an ignored tail op may be re-validated at reconnect\n\t * and a refused id re-judged on resubmission.\n\t */\n\trecordOperationResolution?(resolution: OperationResolution): Promise<void>\n\n\t/**\n\t * The resolutions recorded for `ids` under `nodeId` (ids resolved under another node\n\t * are absent: the answer never crosses devices or tenants).\n\t */\n\tfindOperationResolutions?(\n\t\tnodeId: string,\n\t\tids: string[],\n\t): Promise<Map<string, OperationResolution>>\n\n\t/** The highest sequence number of `nodeId` with a recorded resolution (0 when none). */\n\tgetResolvedThrough?(nodeId: string): Promise<number>\n\n\t/**\n\t * Forget the resolution recorded for `operationId` under `nodeId` (no-op when none).\n\t * The session calls it for a stale `stored-elsewhere` record: one whose id the batch\n\t * lookup did not find stored (RT-51). The operation is then judged normally, and its\n\t * real outcome is recorded in place of the stale one. Optional for custom stores:\n\t * without it the stale record is ignored but kept.\n\t */\n\tdeleteOperationResolution?(nodeId: string, operationId: string): Promise<void>\n\n\t/**\n\t * Every operation of `nodeId` at or below `throughSequence` that shares its sequence\n\t * with another stored operation (a legacy pair, RT-37), ordered by sequence then\n\t * delivery. A version-vector client reporting `throughSequence` for the node may hold\n\t * only one of a pair, and a range read above its entry can never return the other\n\t * (RT-48). Optional; without it such clients miss the second op of a pair.\n\t */\n\tgetSequencePairOperations?(nodeId: string, throughSequence: number): Promise<Operation[]>\n\n\t/**\n\t * The distinct node ids of the operations with `deliverySequence >\n\t * afterDeliverySequence`. Optional; lets a handshake stop judging which nodes a\n\t * scoped client will hear from as soon as every candidate was found visible.\n\t */\n\tgetNodeIdsAfterDelivery?(afterDeliverySequence: number): Promise<string[]>\n\n\t/**\n\t * Bind a client node id to the authenticated principal that first used it.\n\t * Returns true when the node id is already owned by `userId`, was released by an\n\t * admin (see {@link releaseNodeClaim}; the caller takes it over), or is unclaimed\n\t * AND has no operation history. Returns false when another principal owns it, or\n\t * when it is unclaimed but already has operations in the log: history written\n\t * before node claims existed has no recorded writer, so nobody may adopt it\n\t * until an admin releases it (RT-5). Called at handshake when auth is configured,\n\t * so one user cannot upload operations under another user's device id. Every\n\t * write after this check is preceded by a claim, so going forward the claim row\n\t * records the writer of a node's history. Must be atomic per node id. Optional\n\t * for custom stores; the built-in stores persist the claim.\n\t */\n\tclaimNode?(nodeId: string, userId: string): Promise<boolean>\n\t/**\n\t * Atomically claim a node id that has no real owner for `userId` (F1): a node\n\t * with operation history but no claim (written before node claims existed, by a\n\t * beta.12 or older server) or one an admin released (owner `''`). A node another\n\t * principal owns is never taken. One statement, so there is no gap between a\n\t * release and a claim another claimant could slip into. Returns true when\n\t * `userId` owns the node afterwards. The server calls it only for a node whose id\n\t * equals the device id the auth provider verified for that user. Optional for\n\t * custom stores; without it such nodes stay refused until an admin binds them.\n\t */\n\tclaimUnownedNode?(nodeId: string, userId: string): Promise<boolean>\n\t/**\n\t * Admin release of a node id (RT-5): the next principal to claim it takes it\n\t * over, even when the node has operation history. Returns true when the node had\n\t * a claim or history to release, false when it was unknown.\n\t */\n\treleaseNodeClaim?(nodeId: string): Promise<boolean>\n\t/**\n\t * The owner a node id is currently claimed by, or null when unclaimed (RT-21).\n\t * Optional; anonymous provisional claims need it together with\n\t * {@link replaceNodeClaim}.\n\t */\n\tgetNodeClaimOwner?(nodeId: string): Promise<string | null>\n\t/**\n\t * Atomically replace the owner of a node claim, only if it is still\n\t * `expectedOwner` (compare-and-set, RT-21). Returns true when replaced. Used to\n\t * confirm or re-issue an anonymous device's provisional claim; never creates one.\n\t */\n\treplaceNodeClaim?(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>\n\t/**\n\t * The wrapped encryption key record (JSON) of one owner's keyring, or null (ENC-1).\n\t * The record holds only salt, KDF parameters and wrapped keys: never a usable key.\n\t * Optional for custom stores; without it the server answers key requests with\n\t * `unsupported` (it never keeps key records in memory only, which would fork a\n\t * user's keys after a restart).\n\t */\n\tgetEncryptionKeyRecord?(owner: string, keyring: string): Promise<string | null>\n\t/**\n\t * Write a key record with compare-and-set: store `record` at `revision` only when the\n\t * stored revision is `expectedRevision` (0: no record yet). Returns false when\n\t * another write won. Must be atomic per (owner, keyring) across server instances.\n\t */\n\tputEncryptionKeyRecord?(\n\t\towner: string,\n\t\tkeyring: string,\n\t\trecord: string,\n\t\trevision: number,\n\t\texpectedRevision: number,\n\t): Promise<boolean>\n\t/**\n\t * Stored key records: every one (for `exportBackup`; RT-104: a server restored from\n\t * its backup must still hold the records its encrypted history needs), or one owner's\n\t * (the key service tells another keyring's history from a lost record with it).\n\t * Optional.\n\t */\n\tlistEncryptionKeyRecords?(owner?: string): Promise<EncryptionKeyRecordRow[]>\n\t/**\n\t * Key ids named by stored encrypted operations (their envelope's `keyId`), a sample\n\t * of at most `limit` distinct ids (RT-104). `nodeOwner` restricts it to operations of\n\t * nodes claimed by that principal (see {@link claimNode}); null means every node (a\n\t * server without authentication, whose clients share one keyring). The key service\n\t * reports them when an owner has no key record, so a new device can tell a lost\n\t * record (encrypted history exists) from a first one. Optional; without it a new\n\t * device cannot tell, and a fork it creates is merged later by a device holding the\n\t * old ring.\n\t */\n\tgetEncryptedKeyIds?(nodeOwner: string | null, limit: number): Promise<string[]>\n\t/**\n\t * Record that `owner` holds the bytes behind a blob content hash (it pushed them,\n\t * proving possession) (RT-11). Idempotent. Optional; without it the sync server\n\t * keeps ownership in memory (lost on restart, not shared between instances).\n\t */\n\trecordBlobOwner?(hash: string, owner: string): Promise<void>\n\t/** The owners recorded for each hash (an empty list for unowned hashes). */\n\tgetBlobOwners?(hashes: string[]): Promise<Map<string, string[]>>\n\t/**\n\t * Atomically make `owner` the first owner of a hash nobody owns yet. Returns true\n\t * when `owner` owns the hash afterwards (newly, or already), false when someone\n\t * else does. Must be atomic per hash across server instances.\n\t */\n\tclaimBlobIfUnowned?(hash: string, owner: string): Promise<boolean>\n\t/**\n\t * The scope snapshots captured when the given operations were applied (RT-14).\n\t * Operations without one are absent from the result.\n\t */\n\tgetOperationScopeSnapshots?(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>\n\t/**\n\t * The greatest HLC timestamp among every stored operation of one record (its\n\t * newest field write), or null when the record has no operations (RT-19). A\n\t * scope-entry operation is stamped with it so it never overrides newer client data.\n\t */\n\tgetRecordLatestTimestamp?(collection: string, recordId: string): Promise<HLCTimestamp | null>\n\t/**\n\t * Per-field versions of one live record, folded from its stored operations exactly\n\t * as the materialization folds its values (RT-27): each field's version is the HLC\n\t * of the write that produced its current value, plus the record's first and newest\n\t * operation. Null when the record is deleted or has no operations. A scope-entry\n\t * operation carries them so a receiver resolves every field on its own.\n\t */\n\tgetRecordFieldVersions?(collection: string, recordId: string): Promise<RecordFieldVersions | null>\n\t/**\n\t * The record's fold state (W7): the per-field CRDT state its row is projected from.\n\t * Null when the record has no operations or its collection is not materialized. A\n\t * scope-entry operation carries it (filtered to the fields the receiver may see) so\n\t * the receiver merges richtext, counter and resolver fields exactly (RT-29).\n\t */\n\tgetRecordFoldState?(collection: string, recordId: string): Promise<FoldState | null>\n\t/** Every stored operation of one record, in delivery (commit) order. */\n\tgetRecordOperations?(collection: string, recordId: string): Promise<Operation[]>\n\t/**\n\t * The record's row as it would be after merging `op` into its fold state, with\n\t * nothing written: the candidate a Tier-2 constraint check judges at ingest. Null\n\t * when the record would not be live (deleted, or never inserted).\n\t */\n\tpreviewOperation?(op: Operation): Promise<MaterializedRecord | null>\n\t/**\n\t * Node ids whose operations win `merge('server-authoritative')` fields in the fold:\n\t * this store's own node id (every server-originated operation is authored by it)\n\t * plus any configured extras. The sync server advertises them in the handshake so\n\t * clients fold with the same authority.\n\t */\n\tgetAuthoritativeNodeIds?(): string[]\n\n\t/** Explicit authoritative ids the deployment revoked (RT-81); advertised at handshake. */\n\tgetRevokedAuthoritativeNodeIds?(): string[]\n\n\t/**\n\t * Every explicit id the deployment ever held authoritative, revoked ones included\n\t * (RT-81). The session never accepts one as a device node id.\n\t */\n\tgetEverAuthoritativeNodeIds?(): string[]\n\t/**\n\t * Keyed id of a server-derived operation (cascade, set-null, constraint correction):\n\t * deterministic across every instance of the deployment, unpredictable to clients\n\t * (RT-64). Built-in stores key it with a persisted deployment secret. A store\n\t * without it falls back to the unkeyed `deriveSideEffectOpId`.\n\t */\n\tderiveServerOperationId?(\n\t\tparentOpId: string,\n\t\truleId: string,\n\t\ttargetRecordId: string,\n\t): Promise<string>\n\t/** Close the store and release resources */\n\tclose(): Promise<void>\n\n\t/** Get the total number of stored operations */\n\tgetOperationCount(): Promise<number>\n\n\t/**\n\t * The highest delivery sequence currently visible in the store (0 when empty).\n\t * Used to initialize a fresh client's watermark baseline and for diagnostics.\n\t */\n\tgetMaxDeliverySequence(): Promise<number>\n\n\t/**\n\t * Operations with `deliverySequence > afterDeliverySequence`, ordered ascending\n\t * by delivery sequence, at most `limit` of them. This is the substrate for the\n\t * gap-free server->client stream: the caller (a client session) applies its own\n\t * scope filter and chains the results into batches, advancing the client's\n\t * durable watermark as batches are acknowledged. Because delivery sequence is\n\t * assigned in commit order, a lower sequence is always visible before any higher\n\t * one, so this scan can never skip an operation that later appears below the\n\t * cursor.\n\t */\n\tgetOperationsAfterDelivery(\n\t\tafterDeliverySequence: number,\n\t\tlimit: number,\n\t): Promise<DeliveredOperation[]>\n\n\t/**\n\t * Conditionally apply a set of operations atomically, serialized across server\n\t * instances on the target record. Optional: stores that can only run in a single\n\t * process (or do not back a shared database) may omit it, and callers fall back\n\t * to per-instance serialization. The Postgres store implements it with a\n\t * transaction-scoped advisory lock so concurrent admissions cannot both pass a\n\t * cap check (`applyConditional` returns `admitted: false` for the loser).\n\t */\n\tapplyConditional?(input: ConditionalApplyInput): Promise<ConditionalApplyResult>\n\n\t/**\n\t * Atomically reserve the next sequence number for a server-originated operation\n\t * on this store's own node. Stores that serve conditional applies concurrently\n\t * (the Postgres store, which does not serialize behind a single mutation tail)\n\t * must implement this so two in-flight server operations cannot be handed the\n\t * same sequence number, which would let one shadow the other during version-vector\n\t * delta sync. Stores whose server writes are fully serialized may omit it, and\n\t * callers fall back to reading the version vector.\n\t */\n\treserveSequenceNumber?(): number\n\n\t/**\n\t * Set the schema for materialized collection tables.\n\t * Creates collection tables and indexes based on the schema definition.\n\t * If operations already exist in the store, backfills the materialized\n\t * tables from the operation log.\n\t *\n\t * @param schema - The schema definition describing all collections\n\t * @param options - `operationTransforms`: the schema transforms the store folds with\n\t * (transforms at fold time, RT-84). Pass the same list as the sync server's\n\t * `operationTransforms`, so the startup re-materialization folds with them once.\n\t */\n\tsetSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>\n\n\t/** Schema used for materialized tables and server-side validation, if set. */\n\tgetSchema(): SchemaDefinition | null\n\n\t/**\n\t * Set the schema transforms the store folds with (RT-84). Operations are stored\n\t * exactly as uploaded; the fold merges each operation as\n\t * `operationSchemaView(op, schema.version, transforms)` reads it. Transforms are part\n\t * of the fold plan fingerprint: a change re-folds every record (once). The sync\n\t * server calls this with its `operationTransforms` at construction.\n\t */\n\tsetOperationTransforms?(transforms: readonly OperationTransform[]): Promise<void>\n\n\t/** The schema transforms the store folds with (empty when none). */\n\tgetOperationTransforms?(): readonly OperationTransform[]\n\n\t/**\n\t * Get all records from a materialized collection.\n\t * When schema is set, reads directly from the collection table (O(1) indexed).\n\t * When schema is not set, falls back to replaying the operation log.\n\t * Deleted records are excluded.\n\t *\n\t * @param collection - The collection name to query\n\t * @returns Array of records with their current state\n\t */\n\tmaterializeCollection(collection: string): Promise<MaterializedRecord[]>\n\n\t/**\n\t * Query records from a materialized collection with filtering, ordering,\n\t * and pagination. Requires schema to be set via setSchema().\n\t *\n\t * @param collection - The collection name to query\n\t * @param options - Query options (where, orderBy, limit, offset)\n\t * @returns Array of matching records\n\t */\n\tqueryCollection(\n\t\tcollection: string,\n\t\toptions?: CollectionQueryOptions,\n\t): Promise<MaterializedRecord[]>\n\n\t/**\n\t * Find a single record by ID from a materialized collection.\n\t * Requires schema to be set via setSchema().\n\t *\n\t * @param collection - The collection name\n\t * @param id - The record ID\n\t * @returns The record or null if not found (or deleted)\n\t */\n\tfindRecord(collection: string, id: string): Promise<MaterializedRecord | null>\n\n\t/**\n\t * Count records in a materialized collection, optionally filtered.\n\t * Requires schema to be set via setSchema().\n\t *\n\t * @param collection - The collection name\n\t * @param where - Optional exact-match filters\n\t * @returns Number of matching records\n\t */\n\tcountCollection(collection: string, where?: Record<string, unknown>): Promise<number>\n\n\t/**\n\t * Export all data as a portable backup binary.\n\t * Ships operations, version vector, and metadata in a self-describing format\n\t * with SHA-256 checksum.\n\t */\n\texportBackup(): Promise<Uint8Array>\n\n\t/**\n\t * Restore data from a portable backup binary.\n\t * Operations are applied through applyRemoteOperation for safe merge.\n\t *\n\t * @param data - Backup binary\n\t * @param merge - If true, merge with existing operations; if false, replace all\n\t */\n\timportBackup(\n\t\tdata: Uint8Array,\n\t\tmerge?: boolean,\n\t): Promise<{ operationsRestored: number; success: boolean }>\n}\n","import type { Operation } from '@korajs/core'\nimport { KoraError } from '@korajs/core'\nimport {\n\tSERVER_MAX_TIMESTAMP_FUTURE_MS,\n\tisOperationTimestampValid,\n} from '../session/operation-validation'\n\n/** Rejection code for an operation stamped too far ahead of server time (SYNC-7). */\nexport const INVALID_TIMESTAMP_CODE = 'INVALID_TIMESTAMP'\n/** Rejection code for a sequence number that is not a safe positive integer (SRV-4). */\nexport const INVALID_SEQUENCE_NUMBER_CODE = 'INVALID_SEQUENCE_NUMBER'\n\n/**\n * Rejection code for an identifier (operation id, node id, collection, record id,\n * causal dependency) holding U+0000 or a lone UTF-16 surrogate (RT-65).\n */\nexport const INVALID_IDENTIFIER_CODE = 'INVALID_IDENTIFIER'\n\n/** Result of {@link validateIngestedOperation}. */\nexport type IngestValidationResult =\n\t| { valid: true }\n\t| {\n\t\t\tvalid: false\n\t\t\tcode:\n\t\t\t\t| typeof INVALID_TIMESTAMP_CODE\n\t\t\t\t| typeof INVALID_SEQUENCE_NUMBER_CODE\n\t\t\t\t| typeof INVALID_IDENTIFIER_CODE\n\t\t\tmessage: string\n\t }\n\n// NUL, or a high surrogate not followed by a low one, or a low surrogate not preceded by a high one.\nconst UNSTORABLE_IDENTIFIER =\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: U+0000 is exactly what identifiers may not hold (RT-65)\n\t/\\u0000|[\\ud800-\\udbff](?![\\udc00-\\udfff])|(?<![\\ud800-\\udbff])[\\udc00-\\udfff]/\n\n/**\n * True for a string every store can hold as an identifier: no U+0000 (Postgres TEXT\n * refuses it) and no lone surrogate (UTF-8 cannot encode it). Data values may hold\n * both (they are encoded losslessly); identifiers are keys, compared and indexed raw.\n */\nexport function isStorableIdentifier(value: unknown): value is string {\n\treturn typeof value === 'string' && !UNSTORABLE_IDENTIFIER.test(value)\n}\n\n/**\n * True when `sequenceNumber` can identify a node's write: a positive integer no larger\n * than `Number.MAX_SAFE_INTEGER` (beyond it, two numbers collapse into one double).\n */\nexport function isValidSequenceNumber(sequenceNumber: unknown): sequenceNumber is number {\n\treturn (\n\t\ttypeof sequenceNumber === 'number' &&\n\t\tNumber.isSafeInteger(sequenceNumber) &&\n\t\tsequenceNumber >= 1\n\t)\n}\n\n/**\n * The checks every path that brings an operation INTO the server's log runs (sync\n * ingest, route `kora.apply`, `applyLocalOperation`, backup import): identifiers are\n * well-formed strings (RT-65), the HLC\n * timestamp is well formed and not more than {@link SERVER_MAX_TIMESTAMP_FUTURE_MS}\n * ahead of the server's clock, and the sequence number is a safe positive integer.\n *\n * A far-future operation that got in would make every receiver's HLC jump (or refuse\n * it), and every later local edit would lose to it until real time caught up (SYNC-7),\n * so it is stopped at the server, where time is trusted.\n *\n * @param op - The operation entering the log\n * @param now - The server's current time in ms (injectable for tests)\n */\nexport function validateIngestedOperation(\n\top: Operation,\n\tnow: number = Date.now(),\n): IngestValidationResult {\n\tconst identifiers: unknown[] = [\n\t\top.id,\n\t\top.nodeId,\n\t\top.collection,\n\t\top.recordId,\n\t\top.timestamp?.nodeId,\n\t\t...(Array.isArray(op.causalDeps) ? op.causalDeps : []),\n\t]\n\tif (!identifiers.every(isStorableIdentifier)) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tcode: INVALID_IDENTIFIER_CODE,\n\t\t\tmessage: `Operation \"${String(op.id)}\" has an identifier (id, node id, collection, record id or causal dependency) that is not a string or holds U+0000 or an unpaired UTF-16 surrogate. Identifiers must be well-formed strings.`,\n\t\t}\n\t}\n\tif (!isValidSequenceNumber(op.sequenceNumber)) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tcode: INVALID_SEQUENCE_NUMBER_CODE,\n\t\t\tmessage: `Operation \"${op.id}\" has sequence number ${String(op.sequenceNumber)}; sequence numbers are positive integers no larger than ${String(Number.MAX_SAFE_INTEGER)}.`,\n\t\t}\n\t}\n\tif (\n\t\ttypeof op.timestamp !== 'object' ||\n\t\top.timestamp === null ||\n\t\t!isOperationTimestampValid(op, now)\n\t) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tcode: INVALID_TIMESTAMP_CODE,\n\t\t\tmessage: `Operation \"${op.id}\" has an invalid timestamp or one more than ${String(SERVER_MAX_TIMESTAMP_FUTURE_MS)} ms ahead of server time. Check the clock of the device or server that wrote it.`,\n\t\t}\n\t}\n\treturn { valid: true }\n}\n\n/**\n * Thrown by a backup import whose operations fail {@link validateIngestedOperation}.\n * Nothing is restored: a backup is all-or-nothing, so a far-future (or malformed)\n * operation is reported instead of silently dropped or silently restored.\n */\nexport class BackupValidationError extends KoraError {\n\tconstructor(\n\t\treadonly invalidOperations: Array<{ operationId: string; code: string }>,\n\t\treadonly totalOperations: number,\n\t) {\n\t\tsuper(\n\t\t\t`Backup rejected: ${String(invalidOperations.length)} of ${String(totalOperations)} operations failed ingest validation (first: \"${invalidOperations[0]?.operationId ?? ''}\", ${invalidOperations[0]?.code ?? ''}). A far-future timestamp usually means the exporting server's clock was wrong; fix the clock and re-export, or repair the operations before restoring.`,\n\t\t\t'BACKUP_INVALID_OPERATION',\n\t\t\t{\n\t\t\t\tinvalidCount: invalidOperations.length,\n\t\t\t\ttotalOperations,\n\t\t\t\tsample: invalidOperations.slice(0, 10),\n\t\t\t},\n\t\t)\n\t\tthis.name = 'BackupValidationError'\n\t}\n}\n\n/**\n * Validate every operation of a backup against server time before anything is\n * restored. Throws {@link BackupValidationError} listing the offenders.\n */\nexport function assertBackupOperationsIngestible(\n\toperations: Operation[],\n\tnow: number = Date.now(),\n): void {\n\tconst invalid: Array<{ operationId: string; code: string }> = []\n\tfor (const op of operations) {\n\t\tconst result = validateIngestedOperation(op, now)\n\t\tif (!result.valid) invalid.push({ operationId: String(op.id), code: result.code })\n\t}\n\tif (invalid.length > 0) throw new BackupValidationError(invalid, operations.length)\n}\n","import { MAX_LOGICAL } from '@korajs/core'\nimport type { Operation } from '@korajs/core'\n\n/** Max allowed future skew for operation timestamps at server ingest (1 minute). */\nexport const SERVER_MAX_TIMESTAMP_FUTURE_MS = 60_000\n\n/**\n * Returns false when an operation's HLC timestamp is structurally invalid or\n * unreasonably far in the future.\n *\n * Structural checks matter because operations are content-addressed and\n * immutable: a timestamp with a non-integer/negative field or a logical\n * counter past the 5-digit serialization cap would poison the shared log\n * forever (its serialized form no longer sorts like HybridLogicalClock.compare),\n * so it must never be accepted at ingest.\n */\nexport function isOperationTimestampValid(op: Operation, now: number = Date.now()): boolean {\n\tconst { wallTime, logical } = op.timestamp\n\tif (!Number.isInteger(wallTime) || wallTime < 0) {\n\t\treturn false\n\t}\n\tif (!Number.isInteger(logical) || logical < 0 || logical > MAX_LOGICAL) {\n\t\treturn false\n\t}\n\treturn wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS\n}\n"],"mappings":"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","names":["KoraError","KoraError"]}