@korajs/core 0.6.0 → 1.0.0-beta.1
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 +21 -0
- package/dist/bindings/index.d.cts +6 -4
- package/dist/bindings/index.d.ts +6 -4
- package/dist/{build-scope-map-B1CcncZN.d.cts → build-scope-map-DtTr9C7A.d.cts} +1 -1
- package/dist/{build-scope-map-BmgAT82h.d.ts → build-scope-map-DvOHbYjn.d.ts} +1 -1
- package/dist/{chunk-3VIOZT7D.js → chunk-5IICSH6H.js} +150 -7
- package/dist/chunk-5IICSH6H.js.map +1 -0
- package/dist/{events-D-FWfFg8.d.cts → events-CqDkr1LN.d.cts} +114 -3
- package/dist/{events-D-FWfFg8.d.ts → events-CqDkr1LN.d.ts} +114 -3
- package/dist/index.cjs +802 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +209 -6
- package/dist/index.d.ts +209 -6
- package/dist/index.js +639 -5
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +142 -17
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/operation-BJtu18Fi.d.ts +164 -0
- package/dist/operation-CFoY-ZmB.d.cts +164 -0
- package/package.json +2 -1
- package/dist/chunk-3VIOZT7D.js.map +0 -1
- package/dist/operation-3-ZJJf3w.d.cts +0 -94
- package/dist/operation-CygOwIjZ.d.ts +0 -94
package/dist/index.cjs
CHANGED
|
@@ -30,33 +30,44 @@ __export(index_exports, {
|
|
|
30
30
|
EnumFieldBuilder: () => EnumFieldBuilder,
|
|
31
31
|
FieldBuilder: () => FieldBuilder,
|
|
32
32
|
HybridLogicalClock: () => HybridLogicalClock,
|
|
33
|
+
InvalidTimestampError: () => InvalidTimestampError,
|
|
33
34
|
KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
|
|
34
35
|
KoraError: () => KoraError,
|
|
36
|
+
MAX_LOGICAL: () => MAX_LOGICAL,
|
|
35
37
|
MERGE_STRATEGIES: () => MERGE_STRATEGIES,
|
|
36
38
|
MergeConflictError: () => MergeConflictError,
|
|
37
39
|
MigrationBuilder: () => MigrationBuilder,
|
|
38
40
|
MigrationRollbackError: () => MigrationRollbackError,
|
|
41
|
+
MissingSecretKeyError: () => MissingSecretKeyError,
|
|
39
42
|
OperationError: () => OperationError,
|
|
43
|
+
RemoteClockDriftError: () => RemoteClockDriftError,
|
|
40
44
|
RollbackBuilder: () => RollbackBuilder,
|
|
41
45
|
SchemaValidationError: () => SchemaValidationError,
|
|
42
46
|
StorageError: () => StorageError,
|
|
43
47
|
SyncError: () => SyncError,
|
|
44
48
|
advanceVector: () => advanceVector,
|
|
45
49
|
applyOperationTransforms: () => applyOperationTransforms,
|
|
50
|
+
base64ToBytes: () => base64ToBytes,
|
|
46
51
|
buildScopeMap: () => buildScopeMap,
|
|
47
52
|
buildStateMachineConstraints: () => buildStateMachineConstraints,
|
|
53
|
+
bytesToBase64: () => bytesToBase64,
|
|
48
54
|
canAutoRollback: () => canAutoRollback,
|
|
49
55
|
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
50
56
|
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
51
57
|
computeDelta: () => computeDelta,
|
|
58
|
+
createBlobRef: () => createBlobRef,
|
|
52
59
|
createOperation: () => createOperation,
|
|
53
60
|
createReversibleMigration: () => createReversibleMigration,
|
|
54
61
|
createVersionVector: () => createVersionVector,
|
|
62
|
+
decodeBytesFromOpData: () => decodeBytesFromOpData,
|
|
63
|
+
decryptSecret: () => decryptSecret,
|
|
55
64
|
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
56
65
|
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
57
66
|
defineSchema: () => defineSchema,
|
|
58
67
|
deserializeVector: () => deserializeVector,
|
|
59
68
|
dominates: () => dominates,
|
|
69
|
+
encodeBytesForOpData: () => encodeBytesForOpData,
|
|
70
|
+
encryptSecret: () => encryptSecret,
|
|
60
71
|
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
61
72
|
extractTimestamp: () => extractTimestamp,
|
|
62
73
|
formatSequenceValue: () => formatSequenceValue,
|
|
@@ -69,9 +80,14 @@ __export(index_exports, {
|
|
|
69
80
|
getKoraErrorFix: () => getKoraErrorFix,
|
|
70
81
|
getTransitionMap: () => getTransitionMap,
|
|
71
82
|
hasSchemaSyncRules: () => hasSchemaSyncRules,
|
|
83
|
+
hashBlob: () => hashBlob,
|
|
84
|
+
hashSecret: () => hashSecret,
|
|
72
85
|
isApplyFailure: () => isApplyFailure,
|
|
73
86
|
isAtomicOp: () => isAtomicOp,
|
|
87
|
+
isBlobRef: () => isBlobRef,
|
|
74
88
|
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
89
|
+
isKoraBytesValue: () => isKoraBytesValue,
|
|
90
|
+
isLegacyNumericByteObject: () => isLegacyNumericByteObject,
|
|
75
91
|
isValidOperation: () => isValidOperation,
|
|
76
92
|
isValidUUIDv7: () => isValidUUIDv7,
|
|
77
93
|
mergeVectors: () => mergeVectors,
|
|
@@ -79,14 +95,18 @@ __export(index_exports, {
|
|
|
79
95
|
migrationStepsToSQL: () => migrationStepsToSQL,
|
|
80
96
|
op: () => op,
|
|
81
97
|
resolveAtomicOp: () => resolveAtomicOp,
|
|
98
|
+
revealSecret: () => revealSecret,
|
|
82
99
|
rollbackStepsToSQL: () => rollbackStepsToSQL,
|
|
83
100
|
serializeVector: () => serializeVector,
|
|
84
101
|
t: () => t,
|
|
85
102
|
toAtomicOp: () => toAtomicOp,
|
|
103
|
+
transformSecretFieldsForWrite: () => transformSecretFieldsForWrite,
|
|
86
104
|
validateRecord: () => validateRecord,
|
|
87
105
|
validateTransition: () => validateTransition,
|
|
88
106
|
vectorsEqual: () => vectorsEqual,
|
|
89
|
-
verifyOperationIntegrity: () => verifyOperationIntegrity
|
|
107
|
+
verifyOperationIntegrity: () => verifyOperationIntegrity,
|
|
108
|
+
verifySecret: () => verifySecret,
|
|
109
|
+
verifySecretValue: () => verifySecretValue
|
|
90
110
|
});
|
|
91
111
|
module.exports = __toCommonJS(index_exports);
|
|
92
112
|
|
|
@@ -108,6 +128,7 @@ var KORA_ERROR_FIX_SUGGESTIONS = {
|
|
|
108
128
|
SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
|
|
109
129
|
STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
|
|
110
130
|
CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
|
|
131
|
+
INVALID_TIMESTAMP_FIELDS: "HLC timestamps need non-negative integer wallTime (< 10^15) and logical (<= 99999). Generate timestamps through HybridLogicalClock instead of building them by hand.",
|
|
111
132
|
PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
|
|
112
133
|
MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
|
|
113
134
|
INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
|
|
@@ -186,6 +207,31 @@ var AppNotReadyError = class extends KoraError {
|
|
|
186
207
|
this.name = "AppNotReadyError";
|
|
187
208
|
}
|
|
188
209
|
};
|
|
210
|
+
var RemoteClockDriftError = class extends KoraError {
|
|
211
|
+
constructor(remoteWallTime, localReferenceTime) {
|
|
212
|
+
const aheadSeconds = Math.round((remoteWallTime - localReferenceTime) / 1e3);
|
|
213
|
+
super(
|
|
214
|
+
`Rejected remote timestamp ${aheadSeconds}s ahead of local reference time. The sending device's clock is set too far in the future.`,
|
|
215
|
+
"REMOTE_CLOCK_DRIFT",
|
|
216
|
+
{ remoteWallTime, localReferenceTime, aheadSeconds }
|
|
217
|
+
);
|
|
218
|
+
this.remoteWallTime = remoteWallTime;
|
|
219
|
+
this.localReferenceTime = localReferenceTime;
|
|
220
|
+
this.name = "RemoteClockDriftError";
|
|
221
|
+
}
|
|
222
|
+
remoteWallTime;
|
|
223
|
+
localReferenceTime;
|
|
224
|
+
};
|
|
225
|
+
var InvalidTimestampError = class extends KoraError {
|
|
226
|
+
constructor(message, wallTime, logical) {
|
|
227
|
+
super(message, "INVALID_TIMESTAMP_FIELDS", { wallTime, logical });
|
|
228
|
+
this.wallTime = wallTime;
|
|
229
|
+
this.logical = logical;
|
|
230
|
+
this.name = "InvalidTimestampError";
|
|
231
|
+
}
|
|
232
|
+
wallTime;
|
|
233
|
+
logical;
|
|
234
|
+
};
|
|
189
235
|
var ClockDriftError = class extends KoraError {
|
|
190
236
|
constructor(currentHlcTime, physicalTime) {
|
|
191
237
|
const driftSeconds = Math.round((currentHlcTime - physicalTime) / 1e3);
|
|
@@ -204,24 +250,51 @@ var ClockDriftError = class extends KoraError {
|
|
|
204
250
|
|
|
205
251
|
// src/clock/hlc.ts
|
|
206
252
|
var systemTimeSource = { now: () => Date.now() };
|
|
253
|
+
var MAX_LOGICAL = 99999;
|
|
254
|
+
var LOGICAL_MODULUS = MAX_LOGICAL + 1;
|
|
255
|
+
var WALL_TIME_LIMIT = 10 ** 15;
|
|
207
256
|
var DRIFT_WARN_MS = 6e4;
|
|
208
257
|
var DRIFT_ERROR_MS = 5 * 6e4;
|
|
209
|
-
var
|
|
210
|
-
|
|
258
|
+
var MAX_REMOTE_FUTURE_MS = 5 * 6e4;
|
|
259
|
+
var HybridLogicalClock = class _HybridLogicalClock {
|
|
260
|
+
constructor(nodeId, timeSource = systemTimeSource, onDriftWarning, onDriftError) {
|
|
211
261
|
this.nodeId = nodeId;
|
|
212
262
|
this.timeSource = timeSource;
|
|
213
263
|
this.onDriftWarning = onDriftWarning;
|
|
264
|
+
this.onDriftError = onDriftError;
|
|
214
265
|
}
|
|
215
266
|
nodeId;
|
|
216
267
|
timeSource;
|
|
217
268
|
onDriftWarning;
|
|
269
|
+
onDriftError;
|
|
218
270
|
wallTime = 0;
|
|
219
271
|
logical = 0;
|
|
272
|
+
referenceOffsetMs = null;
|
|
273
|
+
/**
|
|
274
|
+
* Records the known offset between an external time reference (usually the
|
|
275
|
+
* sync server, learned at handshake) and this device's physical clock:
|
|
276
|
+
* `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote
|
|
277
|
+
* timestamp validation are performed against reference-corrected time, so a
|
|
278
|
+
* device with a wrong local clock still validates remote timestamps correctly.
|
|
279
|
+
*/
|
|
280
|
+
setReferenceOffset(offsetMs) {
|
|
281
|
+
this.referenceOffsetMs = offsetMs;
|
|
282
|
+
}
|
|
283
|
+
getReferenceOffset() {
|
|
284
|
+
return this.referenceOffsetMs;
|
|
285
|
+
}
|
|
286
|
+
/** Physical time corrected by the known reference offset, when available. */
|
|
287
|
+
effectiveTime(physicalTime) {
|
|
288
|
+
return physicalTime + (this.referenceOffsetMs ?? 0);
|
|
289
|
+
}
|
|
220
290
|
/**
|
|
221
291
|
* Generate a new timestamp for a local event.
|
|
222
292
|
* Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
|
|
223
293
|
*
|
|
224
|
-
*
|
|
294
|
+
* Never throws and never blocks a local write: if the physical clock has
|
|
295
|
+
* fallen behind the HLC (e.g. the user corrected a fast clock), the HLC
|
|
296
|
+
* freezes wallTime and advances the logical counter, and drift is reported
|
|
297
|
+
* through the onDriftWarning / onDriftError callbacks instead.
|
|
225
298
|
*/
|
|
226
299
|
now() {
|
|
227
300
|
const physicalTime = this.timeSource.now();
|
|
@@ -232,17 +305,40 @@ var HybridLogicalClock = class {
|
|
|
232
305
|
} else {
|
|
233
306
|
this.logical++;
|
|
234
307
|
}
|
|
308
|
+
this.carryLogicalOverflow();
|
|
235
309
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
236
310
|
}
|
|
237
311
|
/**
|
|
238
312
|
* Update clock on receiving a remote timestamp.
|
|
239
313
|
* Merges the remote clock state with the local state to maintain causal ordering.
|
|
240
314
|
*
|
|
241
|
-
* @throws {
|
|
315
|
+
* @throws {InvalidTimestampError} If the remote timestamp has non-integer or
|
|
316
|
+
* negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked
|
|
317
|
+
* BEFORE any state change, so malformed input cannot corrupt this clock.
|
|
318
|
+
* @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes
|
|
319
|
+
* ahead of reference-corrected local time. Validation happens BEFORE any state
|
|
320
|
+
* is adopted, so a rejected timestamp cannot poison this clock. When no
|
|
321
|
+
* reference offset is known and this clock is uninitialized (cold start with
|
|
322
|
+
* a possibly-wrong local clock), validation is skipped, matching the previous
|
|
323
|
+
* cold-start behavior but now without a corrupting failure mode afterward.
|
|
242
324
|
*/
|
|
243
325
|
receive(remote) {
|
|
244
326
|
const physicalTime = this.timeSource.now();
|
|
245
327
|
const wasColdStart = this.wallTime === 0;
|
|
328
|
+
if (!Number.isInteger(remote.wallTime) || remote.wallTime < 0 || !Number.isInteger(remote.logical) || remote.logical < 0 || remote.logical > MAX_LOGICAL) {
|
|
329
|
+
throw new InvalidTimestampError(
|
|
330
|
+
`Rejected remote HLC timestamp with invalid fields (wallTime=${remote.wallTime}, logical=${remote.logical}). wallTime and logical must be non-negative integers and logical must not exceed ${MAX_LOGICAL}.`,
|
|
331
|
+
remote.wallTime,
|
|
332
|
+
remote.logical
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const canValidate = this.referenceOffsetMs !== null || !wasColdStart;
|
|
336
|
+
if (canValidate) {
|
|
337
|
+
const reference = this.effectiveTime(physicalTime);
|
|
338
|
+
if (remote.wallTime > reference + MAX_REMOTE_FUTURE_MS) {
|
|
339
|
+
throw new RemoteClockDriftError(remote.wallTime, reference);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
246
342
|
if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
|
|
247
343
|
this.wallTime = physicalTime;
|
|
248
344
|
this.logical = 0;
|
|
@@ -254,11 +350,54 @@ var HybridLogicalClock = class {
|
|
|
254
350
|
} else {
|
|
255
351
|
this.logical++;
|
|
256
352
|
}
|
|
353
|
+
this.carryLogicalOverflow();
|
|
257
354
|
if (!wasColdStart) {
|
|
258
355
|
this.checkDrift(physicalTime);
|
|
259
356
|
}
|
|
260
357
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
261
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Advance this clock to at least the given timestamp.
|
|
361
|
+
*
|
|
362
|
+
* Used after a timestamp rebase rewrites unsynced operations: future `now()`
|
|
363
|
+
* timestamps must sort after every rebased operation, otherwise a write issued
|
|
364
|
+
* immediately after the rebase could interleave with (or precede) rebased ops
|
|
365
|
+
* and break the log's total order. Never moves the clock backward — a
|
|
366
|
+
* timestamp at or before the current state is a no-op, preserving the
|
|
367
|
+
* monotonicity guarantee of `now()`.
|
|
368
|
+
*
|
|
369
|
+
* Inputs with logical > MAX_LOGICAL are normalized deterministically by
|
|
370
|
+
* carrying the excess into wallTime, so the adopted state always leaves room
|
|
371
|
+
* for the next increment inside the serializable range.
|
|
372
|
+
*/
|
|
373
|
+
advanceTo(ts) {
|
|
374
|
+
const normalized = {
|
|
375
|
+
wallTime: ts.wallTime + Math.floor(ts.logical / LOGICAL_MODULUS),
|
|
376
|
+
logical: ts.logical % LOGICAL_MODULUS,
|
|
377
|
+
nodeId: ts.nodeId
|
|
378
|
+
};
|
|
379
|
+
const current = {
|
|
380
|
+
wallTime: this.wallTime,
|
|
381
|
+
logical: this.logical,
|
|
382
|
+
nodeId: this.nodeId
|
|
383
|
+
};
|
|
384
|
+
if (_HybridLogicalClock.compare(normalized, current) > 0) {
|
|
385
|
+
this.wallTime = normalized.wallTime;
|
|
386
|
+
this.logical = normalized.logical;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Carry the logical counter into wallTime when an increment pushed it past
|
|
391
|
+
* MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater
|
|
392
|
+
* than everything issued before (monotonicity) while keeping the logical
|
|
393
|
+
* counter inside the 5-digit slot the serialized form depends on.
|
|
394
|
+
*/
|
|
395
|
+
carryLogicalOverflow() {
|
|
396
|
+
if (this.logical > MAX_LOGICAL) {
|
|
397
|
+
this.wallTime += 1;
|
|
398
|
+
this.logical = 0;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
262
401
|
/**
|
|
263
402
|
* Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
|
|
264
403
|
* Total order: wallTime first, then logical, then nodeId (lexicographic).
|
|
@@ -273,8 +412,23 @@ var HybridLogicalClock = class {
|
|
|
273
412
|
/**
|
|
274
413
|
* Serialize an HLC timestamp to a string that sorts lexicographically.
|
|
275
414
|
* Format: zero-padded wallTime:logical:nodeId
|
|
415
|
+
*
|
|
416
|
+
* @throws {InvalidTimestampError} If wallTime does not fit 15 digits or
|
|
417
|
+
* logical does not fit 5 digits (or either is negative/non-integer). Such a
|
|
418
|
+
* value would overflow its zero-padded slot and the serialized string would
|
|
419
|
+
* no longer sort in the same order as {@link HybridLogicalClock.compare} —
|
|
420
|
+
* silently corrupting every LWW comparison on stored `_version` columns.
|
|
421
|
+
* Internal clocks can no longer produce such values (the logical counter
|
|
422
|
+
* carries into wallTime); this guards against hand-built timestamps.
|
|
276
423
|
*/
|
|
277
424
|
static serialize(ts) {
|
|
425
|
+
if (!Number.isInteger(ts.wallTime) || ts.wallTime < 0 || ts.wallTime >= WALL_TIME_LIMIT || !Number.isInteger(ts.logical) || ts.logical < 0 || ts.logical > MAX_LOGICAL) {
|
|
426
|
+
throw new InvalidTimestampError(
|
|
427
|
+
`Cannot serialize HLC timestamp (wallTime=${ts.wallTime}, logical=${ts.logical}): wallTime must be an integer in [0, 10^15) and logical an integer in [0, ${MAX_LOGICAL}]. Values outside these ranges overflow the zero-padded serialized form, which must sort lexicographically identically to HybridLogicalClock.compare.`,
|
|
428
|
+
ts.wallTime,
|
|
429
|
+
ts.logical
|
|
430
|
+
);
|
|
431
|
+
}
|
|
278
432
|
const wall = ts.wallTime.toString().padStart(15, "0");
|
|
279
433
|
const log = ts.logical.toString().padStart(5, "0");
|
|
280
434
|
return `${wall}:${log}:${ts.nodeId}`;
|
|
@@ -294,10 +448,16 @@ var HybridLogicalClock = class {
|
|
|
294
448
|
nodeId: parts.slice(2).join(":")
|
|
295
449
|
};
|
|
296
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Reports drift between the HLC and (reference-corrected) physical time.
|
|
453
|
+
* Reporting only: local timestamp generation is never blocked, because the
|
|
454
|
+
* user's data always outranks the quality of its timestamps.
|
|
455
|
+
*/
|
|
297
456
|
checkDrift(physicalTime) {
|
|
298
|
-
const drift = this.wallTime - physicalTime;
|
|
457
|
+
const drift = this.wallTime - this.effectiveTime(physicalTime);
|
|
299
458
|
if (drift > DRIFT_ERROR_MS) {
|
|
300
|
-
|
|
459
|
+
this.onDriftError?.(drift);
|
|
460
|
+
return;
|
|
301
461
|
}
|
|
302
462
|
if (drift > DRIFT_WARN_MS) {
|
|
303
463
|
this.onDriftWarning?.(drift);
|
|
@@ -385,6 +545,226 @@ function formatUUID(bytes) {
|
|
|
385
545
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
386
546
|
}
|
|
387
547
|
|
|
548
|
+
// src/blob/blob-ref.ts
|
|
549
|
+
async function hashBlob(bytes) {
|
|
550
|
+
const view = new Uint8Array(bytes.byteLength);
|
|
551
|
+
view.set(bytes);
|
|
552
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", view);
|
|
553
|
+
const out = new Uint8Array(digest);
|
|
554
|
+
return Array.from(out, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
555
|
+
}
|
|
556
|
+
async function createBlobRef(bytes, metadata = {}) {
|
|
557
|
+
const hash = await hashBlob(bytes);
|
|
558
|
+
const ref = { hash, size: bytes.byteLength };
|
|
559
|
+
if (metadata.mimeType !== void 0) {
|
|
560
|
+
ref.mimeType = metadata.mimeType;
|
|
561
|
+
}
|
|
562
|
+
if (metadata.filename !== void 0) {
|
|
563
|
+
ref.filename = metadata.filename;
|
|
564
|
+
}
|
|
565
|
+
return ref;
|
|
566
|
+
}
|
|
567
|
+
function isBlobRef(value) {
|
|
568
|
+
if (typeof value !== "object" || value === null) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
const ref = value;
|
|
572
|
+
if (typeof ref.hash !== "string" || !/^[0-9a-f]{64}$/.test(ref.hash)) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
if (typeof ref.size !== "number" || !Number.isInteger(ref.size) || ref.size < 0) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
if (ref.mimeType !== void 0 && typeof ref.mimeType !== "string") {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (ref.filename !== void 0 && typeof ref.filename !== "string") {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (ref.manifestHash !== void 0 && (typeof ref.manifestHash !== "string" || !/^[0-9a-f]{64}$/.test(ref.manifestHash))) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/secret/secret-crypto.ts
|
|
591
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
592
|
+
var SALT_BYTES = 16;
|
|
593
|
+
var IV_BYTES = 12;
|
|
594
|
+
var HASH_BYTES = 32;
|
|
595
|
+
function toHex(bytes) {
|
|
596
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
597
|
+
}
|
|
598
|
+
function fromHex(hex) {
|
|
599
|
+
if (hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
|
|
600
|
+
throw new Error("Invalid hex string");
|
|
601
|
+
}
|
|
602
|
+
const out = new Uint8Array(hex.length / 2);
|
|
603
|
+
for (let i = 0; i < out.length; i++) {
|
|
604
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
function randomBytes(n) {
|
|
609
|
+
const out = new Uint8Array(n);
|
|
610
|
+
globalThis.crypto.getRandomValues(out);
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
function timingSafeEqual(a, b) {
|
|
614
|
+
if (a.length !== b.length) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
let diff = 0;
|
|
618
|
+
for (let i = 0; i < a.length; i++) {
|
|
619
|
+
diff |= a[i] ^ b[i];
|
|
620
|
+
}
|
|
621
|
+
return diff === 0;
|
|
622
|
+
}
|
|
623
|
+
async function deriveAesKey(secretKey, salt) {
|
|
624
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
625
|
+
"raw",
|
|
626
|
+
new TextEncoder().encode(secretKey),
|
|
627
|
+
"PBKDF2",
|
|
628
|
+
false,
|
|
629
|
+
["deriveKey"]
|
|
630
|
+
);
|
|
631
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
632
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
633
|
+
baseKey,
|
|
634
|
+
{ name: "AES-GCM", length: 256 },
|
|
635
|
+
false,
|
|
636
|
+
["encrypt", "decrypt"]
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
async function encryptSecret(plaintext, secretKey) {
|
|
640
|
+
const salt = randomBytes(SALT_BYTES);
|
|
641
|
+
const iv = randomBytes(IV_BYTES);
|
|
642
|
+
const key = await deriveAesKey(secretKey, salt);
|
|
643
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
644
|
+
{ name: "AES-GCM", iv },
|
|
645
|
+
key,
|
|
646
|
+
new TextEncoder().encode(plaintext)
|
|
647
|
+
);
|
|
648
|
+
return `v1.${toHex(salt)}.${toHex(iv)}.${toHex(new Uint8Array(ciphertext))}`;
|
|
649
|
+
}
|
|
650
|
+
async function decryptSecret(encrypted, secretKey) {
|
|
651
|
+
const parts = encrypted.split(".");
|
|
652
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
653
|
+
throw new Error("Invalid encrypted secret format");
|
|
654
|
+
}
|
|
655
|
+
const salt = fromHex(parts[1]);
|
|
656
|
+
const iv = fromHex(parts[2]);
|
|
657
|
+
const ciphertext = fromHex(parts[3]);
|
|
658
|
+
const key = await deriveAesKey(secretKey, salt);
|
|
659
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
660
|
+
{ name: "AES-GCM", iv },
|
|
661
|
+
key,
|
|
662
|
+
ciphertext
|
|
663
|
+
);
|
|
664
|
+
return new TextDecoder().decode(plaintext);
|
|
665
|
+
}
|
|
666
|
+
async function hashSecret(plaintext) {
|
|
667
|
+
const salt = randomBytes(SALT_BYTES);
|
|
668
|
+
const hash = await pbkdf2(plaintext, salt);
|
|
669
|
+
return `v1.${toHex(salt)}.${toHex(hash)}`;
|
|
670
|
+
}
|
|
671
|
+
async function verifySecret(plaintext, stored) {
|
|
672
|
+
const parts = stored.split(".");
|
|
673
|
+
if (parts.length !== 3 || parts[0] !== "v1") {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
let salt;
|
|
677
|
+
let expected;
|
|
678
|
+
try {
|
|
679
|
+
salt = fromHex(parts[1]);
|
|
680
|
+
expected = fromHex(parts[2]);
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
const actual = await pbkdf2(plaintext, salt);
|
|
685
|
+
return timingSafeEqual(actual, expected);
|
|
686
|
+
}
|
|
687
|
+
async function pbkdf2(plaintext, salt) {
|
|
688
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
689
|
+
"raw",
|
|
690
|
+
new TextEncoder().encode(plaintext),
|
|
691
|
+
"PBKDF2",
|
|
692
|
+
false,
|
|
693
|
+
["deriveBits"]
|
|
694
|
+
);
|
|
695
|
+
const bits = await globalThis.crypto.subtle.deriveBits(
|
|
696
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
697
|
+
baseKey,
|
|
698
|
+
HASH_BYTES * 8
|
|
699
|
+
);
|
|
700
|
+
return new Uint8Array(bits);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/secret/secret-transform.ts
|
|
704
|
+
var MissingSecretKeyError = class extends Error {
|
|
705
|
+
constructor(field) {
|
|
706
|
+
super(
|
|
707
|
+
`Secret field "${field}" is encrypted but no encryption key is configured. Provide sync.encryption.key (or a key provider) to createApp.`
|
|
708
|
+
);
|
|
709
|
+
this.field = field;
|
|
710
|
+
this.name = "MissingSecretKeyError";
|
|
711
|
+
}
|
|
712
|
+
field;
|
|
713
|
+
};
|
|
714
|
+
async function resolveKey(provider) {
|
|
715
|
+
if (!provider) {
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
return provider();
|
|
719
|
+
}
|
|
720
|
+
async function transformSecretFieldsForWrite(data, collectionDef, keyProvider) {
|
|
721
|
+
const hasSecret = Object.values(collectionDef.fields).some((f) => f.kind === "secret");
|
|
722
|
+
if (!hasSecret) {
|
|
723
|
+
return data;
|
|
724
|
+
}
|
|
725
|
+
const result = { ...data };
|
|
726
|
+
let key;
|
|
727
|
+
let keyResolved = false;
|
|
728
|
+
for (const [field, value] of Object.entries(data)) {
|
|
729
|
+
const descriptor = collectionDef.fields[field];
|
|
730
|
+
if (descriptor?.kind !== "secret") {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (typeof value !== "string") {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const mode = descriptor.secretMode ?? "encrypted";
|
|
737
|
+
if (mode === "hashed") {
|
|
738
|
+
result[field] = await hashSecret(value);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (!keyResolved) {
|
|
742
|
+
key = await resolveKey(keyProvider);
|
|
743
|
+
keyResolved = true;
|
|
744
|
+
}
|
|
745
|
+
if (key === void 0) {
|
|
746
|
+
throw new MissingSecretKeyError(field);
|
|
747
|
+
}
|
|
748
|
+
result[field] = await encryptSecret(value, key);
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
async function revealSecret(storedValue, mode, keyProvider, field = "secret") {
|
|
753
|
+
if (mode === "hashed") {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Secret field "${field}" is hashed (one-way) and cannot be revealed. Use verifySecretValue to check a candidate.`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const key = await resolveKey(keyProvider);
|
|
759
|
+
if (key === void 0) {
|
|
760
|
+
throw new MissingSecretKeyError(field);
|
|
761
|
+
}
|
|
762
|
+
return decryptSecret(storedValue, key);
|
|
763
|
+
}
|
|
764
|
+
async function verifySecretValue(candidate, storedValue) {
|
|
765
|
+
return verifySecret(candidate, storedValue);
|
|
766
|
+
}
|
|
767
|
+
|
|
388
768
|
// src/operations/apply-result.ts
|
|
389
769
|
var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
|
|
390
770
|
var APPLY_FAILURE_CODES = {
|
|
@@ -680,6 +1060,106 @@ function toAtomicOp(sentinel) {
|
|
|
680
1060
|
return { type: sentinel.type, value: sentinel.value };
|
|
681
1061
|
}
|
|
682
1062
|
|
|
1063
|
+
// src/operations/op-data-binary.ts
|
|
1064
|
+
function isKoraBytesValue(value) {
|
|
1065
|
+
if (typeof value !== "object" || value === null) {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
const record = value;
|
|
1069
|
+
return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
|
|
1070
|
+
}
|
|
1071
|
+
function isLegacyNumericByteObject(value) {
|
|
1072
|
+
if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
const record = value;
|
|
1076
|
+
const keys = Object.keys(record);
|
|
1077
|
+
if (keys.length === 0) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1081
|
+
const byte = record[String(i)];
|
|
1082
|
+
if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1089
|
+
var BASE64_REVERSE = new Map(
|
|
1090
|
+
Array.from(BASE64_ALPHABET, (char, index) => [char, index])
|
|
1091
|
+
);
|
|
1092
|
+
function bytesToBase64(bytes) {
|
|
1093
|
+
let out = "";
|
|
1094
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
1095
|
+
const b0 = bytes[i] ?? 0;
|
|
1096
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
1097
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
1098
|
+
const triple = b0 << 16 | b1 << 8 | b2;
|
|
1099
|
+
out += BASE64_ALPHABET[triple >> 18 & 63] ?? "";
|
|
1100
|
+
out += BASE64_ALPHABET[triple >> 12 & 63] ?? "";
|
|
1101
|
+
out += i + 1 < bytes.length ? BASE64_ALPHABET[triple >> 6 & 63] ?? "" : "=";
|
|
1102
|
+
out += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] ?? "" : "=";
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
function base64ToBytes(base64) {
|
|
1107
|
+
const cleaned = base64.replace(/=+$/, "");
|
|
1108
|
+
const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
|
|
1109
|
+
let buffer = 0;
|
|
1110
|
+
let bits = 0;
|
|
1111
|
+
let index = 0;
|
|
1112
|
+
for (const char of cleaned) {
|
|
1113
|
+
const value = BASE64_REVERSE.get(char);
|
|
1114
|
+
if (value === void 0) {
|
|
1115
|
+
throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
|
|
1116
|
+
char
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
buffer = buffer << 6 | value;
|
|
1120
|
+
bits += 6;
|
|
1121
|
+
if (bits >= 8) {
|
|
1122
|
+
bits -= 8;
|
|
1123
|
+
out[index] = buffer >> bits & 255;
|
|
1124
|
+
index += 1;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return out;
|
|
1128
|
+
}
|
|
1129
|
+
function encodeBytesForOpData(value) {
|
|
1130
|
+
if (typeof value === "string") {
|
|
1131
|
+
return value;
|
|
1132
|
+
}
|
|
1133
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
1134
|
+
return { $koraBytes: bytesToBase64(bytes) };
|
|
1135
|
+
}
|
|
1136
|
+
function decodeBytesFromOpData(value) {
|
|
1137
|
+
if (typeof value === "string") {
|
|
1138
|
+
return value;
|
|
1139
|
+
}
|
|
1140
|
+
if (value instanceof Uint8Array) {
|
|
1141
|
+
return value;
|
|
1142
|
+
}
|
|
1143
|
+
if (value instanceof ArrayBuffer) {
|
|
1144
|
+
return new Uint8Array(value);
|
|
1145
|
+
}
|
|
1146
|
+
if (isKoraBytesValue(value)) {
|
|
1147
|
+
return base64ToBytes(value.$koraBytes);
|
|
1148
|
+
}
|
|
1149
|
+
if (isLegacyNumericByteObject(value)) {
|
|
1150
|
+
const keys = Object.keys(value);
|
|
1151
|
+
const bytes = new Uint8Array(keys.length);
|
|
1152
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1153
|
+
bytes[i] = value[String(i)] ?? 0;
|
|
1154
|
+
}
|
|
1155
|
+
return bytes;
|
|
1156
|
+
}
|
|
1157
|
+
throw new OperationError(
|
|
1158
|
+
"Binary op-data value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, or legacy numeric-key byte object.",
|
|
1159
|
+
{ receivedType: typeof value }
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
683
1163
|
// src/scopes/sync-scope-bindings.ts
|
|
684
1164
|
function hasSchemaSyncRules(schema) {
|
|
685
1165
|
return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
|
|
@@ -1109,6 +1589,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
1109
1589
|
columns.push("_created_at INTEGER NOT NULL");
|
|
1110
1590
|
columns.push("_updated_at INTEGER NOT NULL");
|
|
1111
1591
|
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
1592
|
+
columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
|
|
1112
1593
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
1113
1594
|
statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
1114
1595
|
${columns.join(",\n ")}
|
|
@@ -1121,6 +1602,10 @@ ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
|
|
|
1121
1602
|
statements.push(
|
|
1122
1603
|
`--kora:safe-alter
|
|
1123
1604
|
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1605
|
+
);
|
|
1606
|
+
statements.push(
|
|
1607
|
+
`--kora:safe-alter
|
|
1608
|
+
ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
1124
1609
|
);
|
|
1125
1610
|
for (const indexField of collection.indexes) {
|
|
1126
1611
|
statements.push(
|
|
@@ -1148,6 +1633,9 @@ ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
|
1148
1633
|
schema_version INTEGER NOT NULL
|
|
1149
1634
|
)`
|
|
1150
1635
|
);
|
|
1636
|
+
statements.push(
|
|
1637
|
+
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
|
|
1638
|
+
);
|
|
1151
1639
|
return statements;
|
|
1152
1640
|
}
|
|
1153
1641
|
function generateFullDDL(schema) {
|
|
@@ -1208,6 +1696,17 @@ function mapFieldType(descriptor) {
|
|
|
1208
1696
|
case "array":
|
|
1209
1697
|
return "TEXT";
|
|
1210
1698
|
// JSON-serialized
|
|
1699
|
+
case "object":
|
|
1700
|
+
return "TEXT";
|
|
1701
|
+
// JSON-serialized structured object
|
|
1702
|
+
case "json":
|
|
1703
|
+
return "TEXT";
|
|
1704
|
+
// JSON-serialized dynamic-key value
|
|
1705
|
+
case "blob":
|
|
1706
|
+
return "TEXT";
|
|
1707
|
+
// JSON-serialized content-addressed BlobRef (bytes stored out of band)
|
|
1708
|
+
case "secret":
|
|
1709
|
+
return "TEXT";
|
|
1211
1710
|
case "richtext":
|
|
1212
1711
|
return "BLOB";
|
|
1213
1712
|
}
|
|
@@ -1441,6 +1940,160 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1441
1940
|
};
|
|
1442
1941
|
}
|
|
1443
1942
|
};
|
|
1943
|
+
var ObjectFieldBuilder = class _ObjectFieldBuilder extends FieldBuilder {
|
|
1944
|
+
_fields;
|
|
1945
|
+
constructor(fields, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
1946
|
+
super("object", required, defaultValue, auto, mergeStrategy);
|
|
1947
|
+
this._fields = fields;
|
|
1948
|
+
}
|
|
1949
|
+
optional() {
|
|
1950
|
+
return new _ObjectFieldBuilder(
|
|
1951
|
+
this._fields,
|
|
1952
|
+
false,
|
|
1953
|
+
this._defaultValue,
|
|
1954
|
+
this._auto,
|
|
1955
|
+
this._mergeStrategy
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
default(value) {
|
|
1959
|
+
return new _ObjectFieldBuilder(this._fields, false, value, this._auto, this._mergeStrategy);
|
|
1960
|
+
}
|
|
1961
|
+
auto() {
|
|
1962
|
+
return new _ObjectFieldBuilder(this._fields, false, void 0, true, this._mergeStrategy);
|
|
1963
|
+
}
|
|
1964
|
+
merge(strategy) {
|
|
1965
|
+
return new _ObjectFieldBuilder(
|
|
1966
|
+
this._fields,
|
|
1967
|
+
this._required,
|
|
1968
|
+
this._defaultValue,
|
|
1969
|
+
this._auto,
|
|
1970
|
+
strategy
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
_build() {
|
|
1974
|
+
const nestedFields = {};
|
|
1975
|
+
for (const [key, builder] of Object.entries(this._fields)) {
|
|
1976
|
+
nestedFields[key] = builder._build();
|
|
1977
|
+
}
|
|
1978
|
+
return {
|
|
1979
|
+
kind: "object",
|
|
1980
|
+
required: this._required,
|
|
1981
|
+
defaultValue: this._defaultValue,
|
|
1982
|
+
auto: this._auto,
|
|
1983
|
+
enumValues: null,
|
|
1984
|
+
itemKind: null,
|
|
1985
|
+
mergeStrategy: this._mergeStrategy,
|
|
1986
|
+
transitions: null,
|
|
1987
|
+
nestedFields
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
var JsonFieldBuilder = class _JsonFieldBuilder extends FieldBuilder {
|
|
1992
|
+
optional() {
|
|
1993
|
+
return new _JsonFieldBuilder(
|
|
1994
|
+
"json",
|
|
1995
|
+
false,
|
|
1996
|
+
this._defaultValue,
|
|
1997
|
+
this._auto,
|
|
1998
|
+
this._mergeStrategy
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
default(value) {
|
|
2002
|
+
return new _JsonFieldBuilder(
|
|
2003
|
+
"json",
|
|
2004
|
+
false,
|
|
2005
|
+
value,
|
|
2006
|
+
this._auto,
|
|
2007
|
+
this._mergeStrategy
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
auto() {
|
|
2011
|
+
return new _JsonFieldBuilder("json", false, void 0, true, this._mergeStrategy);
|
|
2012
|
+
}
|
|
2013
|
+
merge(strategy) {
|
|
2014
|
+
return new _JsonFieldBuilder(
|
|
2015
|
+
"json",
|
|
2016
|
+
this._required,
|
|
2017
|
+
this._defaultValue,
|
|
2018
|
+
this._auto,
|
|
2019
|
+
strategy
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
_build() {
|
|
2023
|
+
return {
|
|
2024
|
+
kind: "json",
|
|
2025
|
+
required: this._required,
|
|
2026
|
+
defaultValue: this._defaultValue,
|
|
2027
|
+
auto: this._auto,
|
|
2028
|
+
enumValues: null,
|
|
2029
|
+
itemKind: null,
|
|
2030
|
+
mergeStrategy: this._mergeStrategy,
|
|
2031
|
+
transitions: null,
|
|
2032
|
+
nestedFields: null
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
2037
|
+
_secretMode;
|
|
2038
|
+
constructor(secretMode = "encrypted", required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
2039
|
+
super("secret", required, defaultValue, auto, mergeStrategy);
|
|
2040
|
+
this._secretMode = secretMode;
|
|
2041
|
+
}
|
|
2042
|
+
/** Store this secret as a one-way salted hash (passwords: verify, never read back). */
|
|
2043
|
+
hashed() {
|
|
2044
|
+
return new _SecretFieldBuilder(
|
|
2045
|
+
"hashed",
|
|
2046
|
+
this._required,
|
|
2047
|
+
this._defaultValue,
|
|
2048
|
+
this._auto,
|
|
2049
|
+
this._mergeStrategy
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
/** Store this secret as reversible ciphertext (tokens/keys: decrypt to use). */
|
|
2053
|
+
encrypted() {
|
|
2054
|
+
return new _SecretFieldBuilder(
|
|
2055
|
+
"encrypted",
|
|
2056
|
+
this._required,
|
|
2057
|
+
this._defaultValue,
|
|
2058
|
+
this._auto,
|
|
2059
|
+
this._mergeStrategy
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
optional() {
|
|
2063
|
+
return new _SecretFieldBuilder(
|
|
2064
|
+
this._secretMode,
|
|
2065
|
+
false,
|
|
2066
|
+
this._defaultValue,
|
|
2067
|
+
this._auto,
|
|
2068
|
+
this._mergeStrategy
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
auto() {
|
|
2072
|
+
return new _SecretFieldBuilder(this._secretMode, false, void 0, true, this._mergeStrategy);
|
|
2073
|
+
}
|
|
2074
|
+
merge(strategy) {
|
|
2075
|
+
return new _SecretFieldBuilder(
|
|
2076
|
+
this._secretMode,
|
|
2077
|
+
this._required,
|
|
2078
|
+
this._defaultValue,
|
|
2079
|
+
this._auto,
|
|
2080
|
+
strategy
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
_build() {
|
|
2084
|
+
return {
|
|
2085
|
+
kind: "secret",
|
|
2086
|
+
required: this._required,
|
|
2087
|
+
defaultValue: this._defaultValue,
|
|
2088
|
+
auto: this._auto,
|
|
2089
|
+
enumValues: null,
|
|
2090
|
+
itemKind: null,
|
|
2091
|
+
mergeStrategy: this._mergeStrategy,
|
|
2092
|
+
transitions: null,
|
|
2093
|
+
secretMode: this._secretMode
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
1444
2097
|
var t = {
|
|
1445
2098
|
string() {
|
|
1446
2099
|
return new FieldBuilder("string", true, void 0, false);
|
|
@@ -1462,6 +2115,18 @@ var t = {
|
|
|
1462
2115
|
},
|
|
1463
2116
|
array(itemBuilder) {
|
|
1464
2117
|
return new ArrayFieldBuilder(itemBuilder, true, void 0, false);
|
|
2118
|
+
},
|
|
2119
|
+
object(fields) {
|
|
2120
|
+
return new ObjectFieldBuilder(fields, true, void 0, false);
|
|
2121
|
+
},
|
|
2122
|
+
json() {
|
|
2123
|
+
return new JsonFieldBuilder("json", true, void 0, false);
|
|
2124
|
+
},
|
|
2125
|
+
blob() {
|
|
2126
|
+
return new FieldBuilder("blob", true, void 0, false);
|
|
2127
|
+
},
|
|
2128
|
+
secret() {
|
|
2129
|
+
return new SecretFieldBuilder("encrypted", true, void 0, false);
|
|
1465
2130
|
}
|
|
1466
2131
|
};
|
|
1467
2132
|
|
|
@@ -1613,9 +2278,9 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1613
2278
|
break;
|
|
1614
2279
|
}
|
|
1615
2280
|
case "richtext": {
|
|
1616
|
-
if (!(value instanceof Uint8Array) && typeof value !== "string") {
|
|
2281
|
+
if (!(value instanceof Uint8Array) && !(value instanceof ArrayBuffer) && typeof value !== "string") {
|
|
1617
2282
|
throw new SchemaValidationError(
|
|
1618
|
-
`Field "${fieldName}" in collection "${collection}" must be a Uint8Array or string for richtext, got ${typeof value}`,
|
|
2283
|
+
`Field "${fieldName}" in collection "${collection}" must be a Uint8Array, ArrayBuffer, or string for richtext, got ${typeof value}`,
|
|
1619
2284
|
{
|
|
1620
2285
|
collection,
|
|
1621
2286
|
field: fieldName,
|
|
@@ -1626,8 +2291,99 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1626
2291
|
}
|
|
1627
2292
|
break;
|
|
1628
2293
|
}
|
|
2294
|
+
case "object": {
|
|
2295
|
+
if (!isPlainObject(value)) {
|
|
2296
|
+
throw new SchemaValidationError(
|
|
2297
|
+
`Field "${fieldName}" in collection "${collection}" must be a plain object, got ${describeType(value)}`,
|
|
2298
|
+
{
|
|
2299
|
+
collection,
|
|
2300
|
+
field: fieldName,
|
|
2301
|
+
expectedType: "object",
|
|
2302
|
+
receivedType: describeType(value)
|
|
2303
|
+
}
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
if (descriptor.nestedFields) {
|
|
2307
|
+
for (const [nestedName, nestedDescriptor] of Object.entries(descriptor.nestedFields)) {
|
|
2308
|
+
const nestedValue = value[nestedName];
|
|
2309
|
+
if (nestedValue !== void 0 && nestedValue !== null) {
|
|
2310
|
+
validateFieldValue(
|
|
2311
|
+
collection,
|
|
2312
|
+
`${fieldName}.${nestedName}`,
|
|
2313
|
+
nestedDescriptor,
|
|
2314
|
+
nestedValue
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
break;
|
|
2320
|
+
}
|
|
2321
|
+
case "json": {
|
|
2322
|
+
if (!isJsonSerializable(value)) {
|
|
2323
|
+
throw new SchemaValidationError(
|
|
2324
|
+
`Field "${fieldName}" in collection "${collection}" must be JSON-serializable, got ${describeType(value)}`,
|
|
2325
|
+
{ collection, field: fieldName, expectedType: "json", receivedType: describeType(value) }
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
case "blob": {
|
|
2331
|
+
if (!isBlobRef(value)) {
|
|
2332
|
+
throw new SchemaValidationError(
|
|
2333
|
+
`Field "${fieldName}" in collection "${collection}" must be a BlobRef (from the blob store), got ${describeType(value)}`,
|
|
2334
|
+
{ collection, field: fieldName, expectedType: "blob", receivedType: describeType(value) }
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
2339
|
+
case "secret": {
|
|
2340
|
+
if (typeof value !== "string") {
|
|
2341
|
+
throw new SchemaValidationError(
|
|
2342
|
+
`Field "${fieldName}" in collection "${collection}" must be a string, got ${describeType(value)}`,
|
|
2343
|
+
{
|
|
2344
|
+
collection,
|
|
2345
|
+
field: fieldName,
|
|
2346
|
+
expectedType: "secret",
|
|
2347
|
+
receivedType: describeType(value)
|
|
2348
|
+
}
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
1629
2353
|
}
|
|
1630
2354
|
}
|
|
2355
|
+
function isPlainObject(value) {
|
|
2356
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2357
|
+
return false;
|
|
2358
|
+
}
|
|
2359
|
+
const proto = Object.getPrototypeOf(value);
|
|
2360
|
+
return proto === null || proto === Object.prototype;
|
|
2361
|
+
}
|
|
2362
|
+
function isJsonSerializable(value) {
|
|
2363
|
+
if (value === null) {
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2366
|
+
const type = typeof value;
|
|
2367
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
2368
|
+
return Number.isFinite(value) || type !== "number";
|
|
2369
|
+
}
|
|
2370
|
+
if (Array.isArray(value)) {
|
|
2371
|
+
return value.every(isJsonSerializable);
|
|
2372
|
+
}
|
|
2373
|
+
if (type === "object") {
|
|
2374
|
+
return Object.values(value).every(isJsonSerializable);
|
|
2375
|
+
}
|
|
2376
|
+
return false;
|
|
2377
|
+
}
|
|
2378
|
+
function describeType(value) {
|
|
2379
|
+
if (value === null) {
|
|
2380
|
+
return "null";
|
|
2381
|
+
}
|
|
2382
|
+
if (Array.isArray(value)) {
|
|
2383
|
+
return "array";
|
|
2384
|
+
}
|
|
2385
|
+
return typeof value;
|
|
2386
|
+
}
|
|
1631
2387
|
function jsTypeForKind(kind) {
|
|
1632
2388
|
switch (kind) {
|
|
1633
2389
|
case "string":
|
|
@@ -2225,6 +2981,14 @@ function mapFieldType2(descriptor) {
|
|
|
2225
2981
|
return "INTEGER";
|
|
2226
2982
|
case "array":
|
|
2227
2983
|
return "TEXT";
|
|
2984
|
+
case "object":
|
|
2985
|
+
return "TEXT";
|
|
2986
|
+
case "json":
|
|
2987
|
+
return "TEXT";
|
|
2988
|
+
case "blob":
|
|
2989
|
+
return "TEXT";
|
|
2990
|
+
case "secret":
|
|
2991
|
+
return "TEXT";
|
|
2228
2992
|
case "richtext":
|
|
2229
2993
|
return "BLOB";
|
|
2230
2994
|
}
|
|
@@ -2243,7 +3007,14 @@ var SCALAR_TYPE_MAP = {
|
|
|
2243
3007
|
number: "double",
|
|
2244
3008
|
boolean: "bool",
|
|
2245
3009
|
timestamp: "int64",
|
|
2246
|
-
richtext: "bytes"
|
|
3010
|
+
richtext: "bytes",
|
|
3011
|
+
// object/json values travel as JSON-serialized strings on the proto wire.
|
|
3012
|
+
object: "string",
|
|
3013
|
+
json: "string",
|
|
3014
|
+
// A blob field carries a JSON-serialized content-addressed reference, not bytes.
|
|
3015
|
+
blob: "string",
|
|
3016
|
+
// A secret field's stored value is a JSON string (ciphertext or hash).
|
|
3017
|
+
secret: "string"
|
|
2247
3018
|
};
|
|
2248
3019
|
var ARRAY_ITEM_TYPE_MAP = {
|
|
2249
3020
|
string: "string",
|
|
@@ -2479,33 +3250,44 @@ function generateProtoDefinitions(schema) {
|
|
|
2479
3250
|
EnumFieldBuilder,
|
|
2480
3251
|
FieldBuilder,
|
|
2481
3252
|
HybridLogicalClock,
|
|
3253
|
+
InvalidTimestampError,
|
|
2482
3254
|
KORA_ERROR_FIX_SUGGESTIONS,
|
|
2483
3255
|
KoraError,
|
|
3256
|
+
MAX_LOGICAL,
|
|
2484
3257
|
MERGE_STRATEGIES,
|
|
2485
3258
|
MergeConflictError,
|
|
2486
3259
|
MigrationBuilder,
|
|
2487
3260
|
MigrationRollbackError,
|
|
3261
|
+
MissingSecretKeyError,
|
|
2488
3262
|
OperationError,
|
|
3263
|
+
RemoteClockDriftError,
|
|
2489
3264
|
RollbackBuilder,
|
|
2490
3265
|
SchemaValidationError,
|
|
2491
3266
|
StorageError,
|
|
2492
3267
|
SyncError,
|
|
2493
3268
|
advanceVector,
|
|
2494
3269
|
applyOperationTransforms,
|
|
3270
|
+
base64ToBytes,
|
|
2495
3271
|
buildScopeMap,
|
|
2496
3272
|
buildStateMachineConstraints,
|
|
3273
|
+
bytesToBase64,
|
|
2497
3274
|
canAutoRollback,
|
|
2498
3275
|
collectSchemaScopeFields,
|
|
2499
3276
|
collectSchemaScopeValueKeys,
|
|
2500
3277
|
computeDelta,
|
|
3278
|
+
createBlobRef,
|
|
2501
3279
|
createOperation,
|
|
2502
3280
|
createReversibleMigration,
|
|
2503
3281
|
createVersionVector,
|
|
3282
|
+
decodeBytesFromOpData,
|
|
3283
|
+
decryptSecret,
|
|
2504
3284
|
defaultApplyFailureReason,
|
|
2505
3285
|
defaultSequenceFormat,
|
|
2506
3286
|
defineSchema,
|
|
2507
3287
|
deserializeVector,
|
|
2508
3288
|
dominates,
|
|
3289
|
+
encodeBytesForOpData,
|
|
3290
|
+
encryptSecret,
|
|
2509
3291
|
extractScopeValuesFromClaims,
|
|
2510
3292
|
extractTimestamp,
|
|
2511
3293
|
formatSequenceValue,
|
|
@@ -2518,9 +3300,14 @@ function generateProtoDefinitions(schema) {
|
|
|
2518
3300
|
getKoraErrorFix,
|
|
2519
3301
|
getTransitionMap,
|
|
2520
3302
|
hasSchemaSyncRules,
|
|
3303
|
+
hashBlob,
|
|
3304
|
+
hashSecret,
|
|
2521
3305
|
isApplyFailure,
|
|
2522
3306
|
isAtomicOp,
|
|
3307
|
+
isBlobRef,
|
|
2523
3308
|
isCollectionSyncScoped,
|
|
3309
|
+
isKoraBytesValue,
|
|
3310
|
+
isLegacyNumericByteObject,
|
|
2524
3311
|
isValidOperation,
|
|
2525
3312
|
isValidUUIDv7,
|
|
2526
3313
|
mergeVectors,
|
|
@@ -2528,13 +3315,17 @@ function generateProtoDefinitions(schema) {
|
|
|
2528
3315
|
migrationStepsToSQL,
|
|
2529
3316
|
op,
|
|
2530
3317
|
resolveAtomicOp,
|
|
3318
|
+
revealSecret,
|
|
2531
3319
|
rollbackStepsToSQL,
|
|
2532
3320
|
serializeVector,
|
|
2533
3321
|
t,
|
|
2534
3322
|
toAtomicOp,
|
|
3323
|
+
transformSecretFieldsForWrite,
|
|
2535
3324
|
validateRecord,
|
|
2536
3325
|
validateTransition,
|
|
2537
3326
|
vectorsEqual,
|
|
2538
|
-
verifyOperationIntegrity
|
|
3327
|
+
verifyOperationIntegrity,
|
|
3328
|
+
verifySecret,
|
|
3329
|
+
verifySecretValue
|
|
2539
3330
|
});
|
|
2540
3331
|
//# sourceMappingURL=index.cjs.map
|