@korajs/server 1.0.0-beta.13 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-IRRXJ7YN.js → chunk-UKWG5T6J.js} +1 -1
- package/dist/chunk-UKWG5T6J.js.map +1 -0
- package/dist/index.cjs +1203 -379
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +416 -14
- package/dist/index.d.ts +416 -14
- package/dist/index.js +1001 -181
- package/dist/index.js.map +1 -1
- package/dist/{server-backup-WBIYHWYQ.js → server-backup-KITNPMZJ.js} +2 -2
- package/package.json +5 -4
- package/dist/chunk-IRRXJ7YN.js.map +0 -1
- /package/dist/{server-backup-WBIYHWYQ.js.map → server-backup-KITNPMZJ.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -71,14 +71,14 @@ function reportLegacyPair(op, decision, options) {
|
|
|
71
71
|
legacyWriter: decision.legacyWriter
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
var
|
|
74
|
+
var import_core7, SEQUENCE_CONFLICT_CODE, SequenceConflictError, UNSTORABLE_VALUE_CODE, UnstorableValueError, SEQUENCE_ENFORCEMENT_EPOCH_KEY, SEQUENCE_PAIRS_BACKFILLED_KEY, NODE_SEQ_UNIQUE_INDEX, SUPERSEDED_NODE_SEQ_UNIQUE_INDEXES, MAX_RESOLUTION_MESSAGE_LENGTH, RELEASED_NODE_OWNER, MAX_SCOPE_SNAPSHOT_STRING_LENGTH;
|
|
75
75
|
var init_server_store = __esm({
|
|
76
76
|
"src/store/server-store.ts"() {
|
|
77
77
|
"use strict";
|
|
78
78
|
init_cjs_shims();
|
|
79
|
-
|
|
79
|
+
import_core7 = require("@korajs/core");
|
|
80
80
|
SEQUENCE_CONFLICT_CODE = "SEQUENCE_CONFLICT";
|
|
81
|
-
SequenceConflictError = class extends
|
|
81
|
+
SequenceConflictError = class extends import_core7.KoraError {
|
|
82
82
|
constructor(operation, existingOperationId) {
|
|
83
83
|
super(
|
|
84
84
|
`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.`,
|
|
@@ -98,7 +98,7 @@ var init_server_store = __esm({
|
|
|
98
98
|
existingOperationId;
|
|
99
99
|
};
|
|
100
100
|
UNSTORABLE_VALUE_CODE = "UNSTORABLE_VALUE";
|
|
101
|
-
UnstorableValueError = class extends
|
|
101
|
+
UnstorableValueError = class extends import_core7.KoraError {
|
|
102
102
|
constructor(operation, detail) {
|
|
103
103
|
super(
|
|
104
104
|
`Operation "${operation.id}" on ${operation.collection}/${operation.recordId} holds a value this server's database cannot store (${detail}). It is refused; nothing was written.`,
|
|
@@ -129,17 +129,17 @@ function isOperationTimestampValid(op, now = Date.now()) {
|
|
|
129
129
|
if (!Number.isInteger(wallTime) || wallTime < 0) {
|
|
130
130
|
return false;
|
|
131
131
|
}
|
|
132
|
-
if (!Number.isInteger(logical) || logical < 0 || logical >
|
|
132
|
+
if (!Number.isInteger(logical) || logical < 0 || logical > import_core8.MAX_LOGICAL) {
|
|
133
133
|
return false;
|
|
134
134
|
}
|
|
135
135
|
return wallTime <= now + SERVER_MAX_TIMESTAMP_FUTURE_MS;
|
|
136
136
|
}
|
|
137
|
-
var
|
|
137
|
+
var import_core8, SERVER_MAX_TIMESTAMP_FUTURE_MS;
|
|
138
138
|
var init_operation_validation = __esm({
|
|
139
139
|
"src/session/operation-validation.ts"() {
|
|
140
140
|
"use strict";
|
|
141
141
|
init_cjs_shims();
|
|
142
|
-
|
|
142
|
+
import_core8 = require("@korajs/core");
|
|
143
143
|
SERVER_MAX_TIMESTAMP_FUTURE_MS = 6e4;
|
|
144
144
|
}
|
|
145
145
|
});
|
|
@@ -191,19 +191,19 @@ function assertBackupOperationsIngestible(operations2, now = Date.now()) {
|
|
|
191
191
|
}
|
|
192
192
|
if (invalid.length > 0) throw new BackupValidationError(invalid, operations2.length);
|
|
193
193
|
}
|
|
194
|
-
var
|
|
194
|
+
var import_core9, INVALID_TIMESTAMP_CODE, INVALID_SEQUENCE_NUMBER_CODE, INVALID_IDENTIFIER_CODE, UNSTORABLE_IDENTIFIER, BackupValidationError;
|
|
195
195
|
var init_ingest_validation = __esm({
|
|
196
196
|
"src/apply/ingest-validation.ts"() {
|
|
197
197
|
"use strict";
|
|
198
198
|
init_cjs_shims();
|
|
199
|
-
|
|
199
|
+
import_core9 = require("@korajs/core");
|
|
200
200
|
init_operation_validation();
|
|
201
201
|
INVALID_TIMESTAMP_CODE = "INVALID_TIMESTAMP";
|
|
202
202
|
INVALID_SEQUENCE_NUMBER_CODE = "INVALID_SEQUENCE_NUMBER";
|
|
203
203
|
INVALID_IDENTIFIER_CODE = "INVALID_IDENTIFIER";
|
|
204
204
|
UNSTORABLE_IDENTIFIER = // biome-ignore lint/suspicious/noControlCharactersInRegex: U+0000 is exactly what identifiers may not hold (RT-65)
|
|
205
205
|
/\u0000|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
|
|
206
|
-
BackupValidationError = class extends
|
|
206
|
+
BackupValidationError = class extends import_core9.KoraError {
|
|
207
207
|
constructor(invalidOperations, totalOperations) {
|
|
208
208
|
super(
|
|
209
209
|
`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.`,
|
|
@@ -350,7 +350,7 @@ function parseServerBackup(data) {
|
|
|
350
350
|
function parseKeyRecords(value) {
|
|
351
351
|
if (value === null || value === void 0) return [];
|
|
352
352
|
const refuse = (reason) => {
|
|
353
|
-
throw new
|
|
353
|
+
throw new import_core10.KoraError(
|
|
354
354
|
`Backup rejected: its encryption key records are malformed (${reason}).`,
|
|
355
355
|
"BACKUP_INVALID_KEY_RECORD",
|
|
356
356
|
{ reason }
|
|
@@ -464,12 +464,12 @@ async function mergeBackupOperations(operations2, apply) {
|
|
|
464
464
|
}
|
|
465
465
|
return { operationsRestored: restored, success: conflicts.length === 0 };
|
|
466
466
|
}
|
|
467
|
-
var
|
|
467
|
+
var import_core10, import_sync, BACKUP_VERSION, KEY_RESTORE_ATTEMPTS;
|
|
468
468
|
var init_server_backup = __esm({
|
|
469
469
|
"src/store/server-backup.ts"() {
|
|
470
470
|
"use strict";
|
|
471
471
|
init_cjs_shims();
|
|
472
|
-
|
|
472
|
+
import_core10 = require("@korajs/core");
|
|
473
473
|
import_sync = require("@korajs/sync");
|
|
474
474
|
init_ingest_validation();
|
|
475
475
|
init_server_store();
|
|
@@ -485,6 +485,7 @@ __export(index_exports, {
|
|
|
485
485
|
ClientSession: () => ClientSession,
|
|
486
486
|
DEFAULT_MAX_SCOPE_PREDICATE_VALUES: () => DEFAULT_MAX_SCOPE_PREDICATE_VALUES,
|
|
487
487
|
HttpServerTransport: () => HttpServerTransport,
|
|
488
|
+
InvalidDiagnosticOptionsError: () => InvalidDiagnosticOptionsError,
|
|
488
489
|
InvalidScopePredicateError: () => InvalidScopePredicateError,
|
|
489
490
|
KoraAuthProvider: () => KoraAuthProvider,
|
|
490
491
|
KoraSyncServer: () => KoraSyncServer,
|
|
@@ -493,7 +494,7 @@ __export(index_exports, {
|
|
|
493
494
|
NoAuthProvider: () => NoAuthProvider,
|
|
494
495
|
PostgresServerStore: () => PostgresServerStore,
|
|
495
496
|
RETRIABLE_REJECTION_CODES: () => RETRIABLE_REJECTION_CODES,
|
|
496
|
-
SCOPE_CLAIMS_KEY: () =>
|
|
497
|
+
SCOPE_CLAIMS_KEY: () => import_core38.SCOPE_CLAIMS_KEY,
|
|
497
498
|
SERVER_NODE_PREFIX: () => SERVER_NODE_PREFIX,
|
|
498
499
|
ScopePredicateLimitError: () => ScopePredicateLimitError,
|
|
499
500
|
ScopeRequiredError: () => ScopeRequiredError,
|
|
@@ -504,7 +505,7 @@ __export(index_exports, {
|
|
|
504
505
|
UplinkAuthorizationError: () => UplinkAuthorizationError,
|
|
505
506
|
WsServerTransport: () => WsServerTransport,
|
|
506
507
|
authorizeUplinkWrite: () => authorizeUplinkWrite,
|
|
507
|
-
claimScopes: () =>
|
|
508
|
+
claimScopes: () => import_core38.claimScopes,
|
|
508
509
|
createDefaultLogger: () => createDefaultLogger,
|
|
509
510
|
createJsonLogger: () => createJsonLogger,
|
|
510
511
|
createKoraServer: () => createKoraServer,
|
|
@@ -513,6 +514,7 @@ __export(index_exports, {
|
|
|
513
514
|
createProductionServer: () => createProductionServer,
|
|
514
515
|
createSilentLogger: () => createSilentLogger,
|
|
515
516
|
createSqliteServerStore: () => createSqliteServerStore,
|
|
517
|
+
findJsonStringValues: () => findJsonStringValues,
|
|
516
518
|
isRetriableRejection: () => isRetriableRejection,
|
|
517
519
|
isServerNodeId: () => isServerNodeId,
|
|
518
520
|
normalizeScopeMap: () => normalizeScopeMap,
|
|
@@ -523,6 +525,72 @@ __export(index_exports, {
|
|
|
523
525
|
module.exports = __toCommonJS(index_exports);
|
|
524
526
|
init_cjs_shims();
|
|
525
527
|
|
|
528
|
+
// src/diagnostics/json-string-values.ts
|
|
529
|
+
init_cjs_shims();
|
|
530
|
+
var import_core = require("@korajs/core");
|
|
531
|
+
var InvalidDiagnosticOptionsError = class extends import_core.KoraError {
|
|
532
|
+
constructor(option, value) {
|
|
533
|
+
super(
|
|
534
|
+
`findJsonStringValues: ${option} must be a ${option === "pageSize" ? "positive" : "non-negative"} integer, got ${String(value)}.`,
|
|
535
|
+
"INVALID_DIAGNOSTIC_OPTIONS",
|
|
536
|
+
{ option, value }
|
|
537
|
+
);
|
|
538
|
+
this.name = "InvalidDiagnosticOptionsError";
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
async function findJsonStringValues(store, options = {}) {
|
|
542
|
+
const schema = store.getSchema();
|
|
543
|
+
if (!schema) return [];
|
|
544
|
+
const sampleSize = options.sampleSize ?? 10;
|
|
545
|
+
const pageSize = options.pageSize ?? 1e3;
|
|
546
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
547
|
+
throw new InvalidDiagnosticOptionsError("pageSize", pageSize);
|
|
548
|
+
}
|
|
549
|
+
if (!Number.isInteger(sampleSize) || sampleSize < 0) {
|
|
550
|
+
throw new InvalidDiagnosticOptionsError("sampleSize", sampleSize);
|
|
551
|
+
}
|
|
552
|
+
const reports = [];
|
|
553
|
+
for (const [collection, definition] of Object.entries(schema.collections)) {
|
|
554
|
+
const fields = Object.entries(definition.fields).filter(([, descriptor]) => descriptor.kind === "json" || descriptor.kind === "object").map(([name]) => name);
|
|
555
|
+
if (fields.length === 0) continue;
|
|
556
|
+
const found = /* @__PURE__ */ new Map();
|
|
557
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
558
|
+
const rows = await store.queryCollection(collection, {
|
|
559
|
+
orderBy: "id",
|
|
560
|
+
limit: pageSize,
|
|
561
|
+
offset
|
|
562
|
+
});
|
|
563
|
+
for (const row of rows) {
|
|
564
|
+
for (const field of fields) {
|
|
565
|
+
if (!isEncodedJson(row[field])) continue;
|
|
566
|
+
let report = found.get(field);
|
|
567
|
+
if (!report) {
|
|
568
|
+
report = { collection, field, count: 0, sampleIds: [] };
|
|
569
|
+
found.set(field, report);
|
|
570
|
+
}
|
|
571
|
+
report.count += 1;
|
|
572
|
+
if (report.sampleIds.length < sampleSize) report.sampleIds.push(String(row.id));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (rows.length < pageSize) break;
|
|
576
|
+
}
|
|
577
|
+
reports.push(...found.values());
|
|
578
|
+
}
|
|
579
|
+
return reports;
|
|
580
|
+
}
|
|
581
|
+
function isEncodedJson(value) {
|
|
582
|
+
if (typeof value !== "string") return false;
|
|
583
|
+
const trimmed = value.trim();
|
|
584
|
+
const first = trimmed[0];
|
|
585
|
+
if (first !== "{" && first !== "[" && first !== '"') return false;
|
|
586
|
+
try {
|
|
587
|
+
JSON.parse(trimmed);
|
|
588
|
+
return true;
|
|
589
|
+
} catch {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
526
594
|
// src/logging/structured-logger.ts
|
|
527
595
|
init_cjs_shims();
|
|
528
596
|
function defaultJsonSerializer(entry) {
|
|
@@ -589,17 +657,17 @@ function isRetriableRejection(code) {
|
|
|
589
657
|
|
|
590
658
|
// src/store/memory-server-store.ts
|
|
591
659
|
init_cjs_shims();
|
|
592
|
-
var
|
|
660
|
+
var import_core11 = require("@korajs/core");
|
|
593
661
|
|
|
594
662
|
// src/scopes/server-scope-filter.ts
|
|
595
663
|
init_cjs_shims();
|
|
596
|
-
var
|
|
664
|
+
var import_core3 = require("@korajs/core");
|
|
597
665
|
var import_internal = require("@korajs/sync/internal");
|
|
598
666
|
|
|
599
667
|
// src/scopes/scope-predicate-errors.ts
|
|
600
668
|
init_cjs_shims();
|
|
601
|
-
var
|
|
602
|
-
var InvalidScopePredicateError = class extends
|
|
669
|
+
var import_core2 = require("@korajs/core");
|
|
670
|
+
var InvalidScopePredicateError = class extends import_core2.KoraError {
|
|
603
671
|
constructor(collection, field) {
|
|
604
672
|
super(
|
|
605
673
|
`Sync scope predicate for "${collection}.${field}" is undefined or null. A missing value would match every record without "${field}", so the grant is refused.`,
|
|
@@ -617,7 +685,7 @@ var InvalidScopePredicateError = class extends import_core.KoraError {
|
|
|
617
685
|
collection;
|
|
618
686
|
field;
|
|
619
687
|
};
|
|
620
|
-
var ScopePredicateLimitError = class extends
|
|
688
|
+
var ScopePredicateLimitError = class extends import_core2.KoraError {
|
|
621
689
|
constructor(message, context) {
|
|
622
690
|
super(message, "SCOPE_PREDICATE_LIMIT", context);
|
|
623
691
|
this.name = "ScopePredicateLimitError";
|
|
@@ -656,14 +724,18 @@ function normalizeScopeMap(scopes, maxValues = DEFAULT_MAX_SCOPE_PREDICATE_VALUE
|
|
|
656
724
|
collection,
|
|
657
725
|
field
|
|
658
726
|
});
|
|
659
|
-
const unique = [...new Map(values.map((value) => [stableValueKey(value), value])).
|
|
727
|
+
const unique = [...new Map(values.map((value) => [stableValueKey(value), value])).entries()];
|
|
660
728
|
if (unique.length > maxValues)
|
|
661
729
|
throw new ScopePredicateLimitError(
|
|
662
730
|
`Scope predicate for ${collection}.${field} exceeds the ${maxValues}-value limit`,
|
|
663
731
|
{ collection, field, maxValues }
|
|
664
732
|
);
|
|
665
733
|
predicate[field] = {
|
|
666
|
-
|
|
734
|
+
// Frozen: matchers may cache a set of a large list (F17), which is only
|
|
735
|
+
// sound for a list that cannot change.
|
|
736
|
+
$in: Object.freeze(
|
|
737
|
+
unique.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, value]) => value)
|
|
738
|
+
)
|
|
667
739
|
};
|
|
668
740
|
} else predicate[field] = expected;
|
|
669
741
|
}
|
|
@@ -734,7 +806,7 @@ function missingScopeFields(op, scopes) {
|
|
|
734
806
|
const snapshot = (0, import_internal.buildScopeSnapshot)(op);
|
|
735
807
|
return Object.keys(collectionScope).filter((field) => !(field in snapshot));
|
|
736
808
|
}
|
|
737
|
-
var UplinkAuthorizationError = class extends
|
|
809
|
+
var UplinkAuthorizationError = class extends import_core3.KoraError {
|
|
738
810
|
constructor(rejectionCode, message, context) {
|
|
739
811
|
super(message, rejectionCode, context);
|
|
740
812
|
this.rejectionCode = rejectionCode;
|
|
@@ -816,7 +888,7 @@ function authorizeRecordWrite(collection, recordId, storedRow, scopes) {
|
|
|
816
888
|
|
|
817
889
|
// src/store/materialization.ts
|
|
818
890
|
init_cjs_shims();
|
|
819
|
-
var
|
|
891
|
+
var import_core4 = require("@korajs/core");
|
|
820
892
|
var import_internal2 = require("@korajs/core/internal");
|
|
821
893
|
function fieldTypeToSql(descriptor, dialect) {
|
|
822
894
|
switch (descriptor.kind) {
|
|
@@ -849,7 +921,7 @@ function generateCollectionDDL(name, collection, dialect) {
|
|
|
849
921
|
const columns = ["id TEXT PRIMARY KEY NOT NULL"];
|
|
850
922
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
851
923
|
const sqlType = fieldTypeToSql(descriptor, dialect);
|
|
852
|
-
let colDef = `${(0,
|
|
924
|
+
let colDef = `${(0, import_core4.quoteIdent)(fieldName)} ${sqlType}`;
|
|
853
925
|
if (descriptor.defaultValue !== void 0) {
|
|
854
926
|
colDef += ` DEFAULT ${(0, import_internal2.sqlDefaultLiteral)(descriptor.defaultValue)}`;
|
|
855
927
|
}
|
|
@@ -859,25 +931,25 @@ function generateCollectionDDL(name, collection, dialect) {
|
|
|
859
931
|
columns.push(`_created_at ${tsType} NOT NULL DEFAULT 0`);
|
|
860
932
|
columns.push(`_updated_at ${tsType} NOT NULL DEFAULT 0`);
|
|
861
933
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
862
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${(0,
|
|
934
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${(0, import_core4.quoteIdent)(name)} (
|
|
863
935
|
${columns.join(",\n ")}
|
|
864
936
|
)`);
|
|
865
937
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
866
938
|
const sqlType = fieldTypeToSql(descriptor, dialect);
|
|
867
|
-
let colDef = `${(0,
|
|
939
|
+
let colDef = `${(0, import_core4.quoteIdent)(fieldName)} ${sqlType}`;
|
|
868
940
|
if (descriptor.defaultValue !== void 0) {
|
|
869
941
|
colDef += ` DEFAULT ${(0, import_internal2.sqlDefaultLiteral)(descriptor.defaultValue)}`;
|
|
870
942
|
}
|
|
871
943
|
statements.push(`--kora:safe-alter
|
|
872
|
-
ALTER TABLE ${(0,
|
|
944
|
+
ALTER TABLE ${(0, import_core4.quoteIdent)(name)} ADD COLUMN ${colDef}`);
|
|
873
945
|
}
|
|
874
946
|
for (const indexField of collection.indexes) {
|
|
875
947
|
statements.push(
|
|
876
|
-
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${(0,
|
|
948
|
+
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${(0, import_core4.quoteIdent)(name)} (${(0, import_core4.quoteIdent)(indexField)})`
|
|
877
949
|
);
|
|
878
950
|
}
|
|
879
951
|
statements.push(
|
|
880
|
-
`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${(0,
|
|
952
|
+
`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${(0, import_core4.quoteIdent)(name)} (_deleted)`
|
|
881
953
|
);
|
|
882
954
|
return statements;
|
|
883
955
|
}
|
|
@@ -901,7 +973,7 @@ function base64ToBytes(base64) {
|
|
|
901
973
|
for (const char of cleaned) {
|
|
902
974
|
const value = BASE64_REVERSE.get(char);
|
|
903
975
|
if (value === void 0) {
|
|
904
|
-
throw new
|
|
976
|
+
throw new import_core4.StorageError(`Invalid base64 character "${char}" in tagged richtext value`, {
|
|
905
977
|
char
|
|
906
978
|
});
|
|
907
979
|
}
|
|
@@ -996,12 +1068,12 @@ function validateFieldName(collectionName, fieldName, schema) {
|
|
|
996
1068
|
|
|
997
1069
|
// src/store/record-fold.ts
|
|
998
1070
|
init_cjs_shims();
|
|
999
|
-
var
|
|
1071
|
+
var import_core6 = require("@korajs/core");
|
|
1000
1072
|
var import_merge = require("@korajs/merge");
|
|
1001
1073
|
|
|
1002
1074
|
// src/store/server-identity.ts
|
|
1003
1075
|
init_cjs_shims();
|
|
1004
|
-
var
|
|
1076
|
+
var import_core5 = require("@korajs/core");
|
|
1005
1077
|
var SERVER_NODE_PREFIX = "kora:server:";
|
|
1006
1078
|
var RESERVED_NODE_PREFIX = "kora:";
|
|
1007
1079
|
var SERVER_DEPLOYMENT_ID_KEY = "server_deployment_id";
|
|
@@ -1014,7 +1086,7 @@ var SERVER_REVOKED_AUTHORITY_KEY = "server_revoked_authoritative_node_ids";
|
|
|
1014
1086
|
var SERVER_LEGACY_SCAN_KEY = "server_legacy_authority_scan_v1";
|
|
1015
1087
|
var INSTANCE_ID_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
1016
1088
|
var SERVER_DERIVED_DOMAIN = "kora/server-derived/v1";
|
|
1017
|
-
var ServerIdentityError = class extends
|
|
1089
|
+
var ServerIdentityError = class extends import_core5.KoraError {
|
|
1018
1090
|
constructor(message, context) {
|
|
1019
1091
|
super(message, "INVALID_SERVER_IDENTITY", context);
|
|
1020
1092
|
this.name = "ServerIdentityError";
|
|
@@ -1120,7 +1192,7 @@ function isServerNodeId(nodeId) {
|
|
|
1120
1192
|
return nodeId.startsWith(SERVER_NODE_PREFIX);
|
|
1121
1193
|
}
|
|
1122
1194
|
function generateDeploymentId() {
|
|
1123
|
-
return (0,
|
|
1195
|
+
return (0, import_core5.generateUUIDv7)();
|
|
1124
1196
|
}
|
|
1125
1197
|
function generateDerivationSecret() {
|
|
1126
1198
|
const bytes = new Uint8Array(32);
|
|
@@ -1280,19 +1352,19 @@ function serverFoldOptions(explicitAuthorities, transforms = []) {
|
|
|
1280
1352
|
function serverOperationView(store, op) {
|
|
1281
1353
|
const schema = store.getSchema();
|
|
1282
1354
|
if (schema === null) return op;
|
|
1283
|
-
return (0,
|
|
1355
|
+
return (0, import_core6.operationSchemaView)(op, schema.version, store.getOperationTransforms?.());
|
|
1284
1356
|
}
|
|
1285
1357
|
function serverFoldPlanFingerprint(schema, explicitAuthorities, transforms = []) {
|
|
1286
1358
|
const explicit = normalizeLegacyAuthorities(explicitAuthorities);
|
|
1287
|
-
return `${(0,
|
|
1359
|
+
return `${(0, import_core6.foldPlanFingerprint)(schema, transforms)}|auth:${SERVER_NODE_PREFIX}*${explicit.length > 0 ? `,${explicit.join(",")}` : ""}`;
|
|
1288
1360
|
}
|
|
1289
1361
|
function parseStoredFoldState(json, collection, recordId) {
|
|
1290
1362
|
if (typeof json !== "string" || json.length === 0) return null;
|
|
1291
1363
|
try {
|
|
1292
|
-
const state = (0,
|
|
1364
|
+
const state = (0, import_core6.deserializeFoldState)(json);
|
|
1293
1365
|
return state.c === collection && state.r === recordId ? state : null;
|
|
1294
1366
|
} catch (error) {
|
|
1295
|
-
if (error instanceof
|
|
1367
|
+
if (error instanceof import_core6.FoldStateError) return null;
|
|
1296
1368
|
throw error;
|
|
1297
1369
|
}
|
|
1298
1370
|
}
|
|
@@ -1301,23 +1373,23 @@ function mergeIntoFoldState(state, ops, schema, options) {
|
|
|
1301
1373
|
const first = ops[0];
|
|
1302
1374
|
if (first === void 0) return state;
|
|
1303
1375
|
try {
|
|
1304
|
-
let next = state ?? (0,
|
|
1305
|
-
for (const op of ops) next = (0,
|
|
1376
|
+
let next = state ?? (0, import_core6.createFoldState)(first.collection, first.recordId);
|
|
1377
|
+
for (const op of ops) next = (0, import_core6.mergeOp)(next, op, schema, options).state;
|
|
1306
1378
|
return next;
|
|
1307
1379
|
} catch (error) {
|
|
1308
|
-
if (error instanceof
|
|
1380
|
+
if (error instanceof import_core6.FoldStateError) return REFOLD_REQUIRED;
|
|
1309
1381
|
throw error;
|
|
1310
1382
|
}
|
|
1311
1383
|
}
|
|
1312
1384
|
function refoldRecord(ops, schema, options) {
|
|
1313
|
-
return (0,
|
|
1385
|
+
return (0, import_core6.foldRecord)(ops, schema, options).state;
|
|
1314
1386
|
}
|
|
1315
1387
|
function projectFoldState(state, options) {
|
|
1316
1388
|
if (state.cr === null) return null;
|
|
1317
|
-
const live = (0,
|
|
1318
|
-
const values = (0,
|
|
1319
|
-
const createdAt =
|
|
1320
|
-
const updatedAt = state.u ?
|
|
1389
|
+
const live = (0, import_core6.isFoldStateLive)(state);
|
|
1390
|
+
const values = (0, import_core6.materialize)(live ? state : { ...state, d: null }, options) ?? {};
|
|
1391
|
+
const createdAt = import_core6.HybridLogicalClock.deserialize(state.cr.t).wallTime;
|
|
1392
|
+
const updatedAt = state.u ? import_core6.HybridLogicalClock.deserialize(state.u.t).wallTime : createdAt;
|
|
1321
1393
|
return { values, createdAt, updatedAt, deleted: !live };
|
|
1322
1394
|
}
|
|
1323
1395
|
function materializedFieldValue(values, field, descriptor) {
|
|
@@ -1326,7 +1398,7 @@ function materializedFieldValue(values, field, descriptor) {
|
|
|
1326
1398
|
}
|
|
1327
1399
|
function foldFieldVersions(state) {
|
|
1328
1400
|
if (!state) return null;
|
|
1329
|
-
return (0,
|
|
1401
|
+
return (0, import_core6.getFoldFieldVersions)(state);
|
|
1330
1402
|
}
|
|
1331
1403
|
function filterFoldStateFields(state, fields) {
|
|
1332
1404
|
const keep = new Set(fields);
|
|
@@ -1337,7 +1409,7 @@ function filterFoldStateFields(state, fields) {
|
|
|
1337
1409
|
return { ...state, f: filtered };
|
|
1338
1410
|
}
|
|
1339
1411
|
function serializeServerFoldState(state) {
|
|
1340
|
-
return (0,
|
|
1412
|
+
return (0, import_core6.serializeFoldState)(state);
|
|
1341
1413
|
}
|
|
1342
1414
|
|
|
1343
1415
|
// src/store/scope-snapshot.ts
|
|
@@ -1516,7 +1588,7 @@ var MemoryServerStore = class {
|
|
|
1516
1588
|
}
|
|
1517
1589
|
async setSchema(schema, options = {}) {
|
|
1518
1590
|
this.assertOpen();
|
|
1519
|
-
(0,
|
|
1591
|
+
(0, import_core11.assertOperationTransformCoverage)(
|
|
1520
1592
|
this.operations.map((op) => op.schemaVersion),
|
|
1521
1593
|
schema.version,
|
|
1522
1594
|
options.operationTransforms ?? this.operationTransforms,
|
|
@@ -1546,7 +1618,7 @@ var MemoryServerStore = class {
|
|
|
1546
1618
|
async setOperationTransforms(transforms) {
|
|
1547
1619
|
this.assertOpen();
|
|
1548
1620
|
if (this.schema) {
|
|
1549
|
-
(0,
|
|
1621
|
+
(0, import_core11.assertOperationTransformCoverage)(
|
|
1550
1622
|
this.operations.map((op) => op.schemaVersion),
|
|
1551
1623
|
this.schema.version,
|
|
1552
1624
|
transforms,
|
|
@@ -1684,7 +1756,7 @@ var MemoryServerStore = class {
|
|
|
1684
1756
|
this.assertOpen();
|
|
1685
1757
|
let latest = null;
|
|
1686
1758
|
for (const op of this.recordOps.get(recordKey(collection, recordId)) ?? []) {
|
|
1687
|
-
if (latest === null ||
|
|
1759
|
+
if (latest === null || import_core11.HybridLogicalClock.compare(op.timestamp, latest) > 0) {
|
|
1688
1760
|
latest = op.timestamp;
|
|
1689
1761
|
}
|
|
1690
1762
|
}
|
|
@@ -1999,6 +2071,16 @@ var MemoryServerStore = class {
|
|
|
1999
2071
|
}
|
|
2000
2072
|
return owner === userId;
|
|
2001
2073
|
}
|
|
2074
|
+
async claimUnownedNode(nodeId, userId) {
|
|
2075
|
+
this.assertOpen();
|
|
2076
|
+
if (userId === RELEASED_NODE_OWNER) return false;
|
|
2077
|
+
const owner = this.nodeOwners.get(nodeId);
|
|
2078
|
+
if (owner === void 0 || owner === RELEASED_NODE_OWNER) {
|
|
2079
|
+
this.nodeOwners.set(nodeId, userId);
|
|
2080
|
+
return true;
|
|
2081
|
+
}
|
|
2082
|
+
return owner === userId;
|
|
2083
|
+
}
|
|
2002
2084
|
async getNodeClaimOwner(nodeId) {
|
|
2003
2085
|
this.assertOpen();
|
|
2004
2086
|
return this.nodeOwners.get(nodeId) ?? null;
|
|
@@ -2218,7 +2300,7 @@ function keyRecordKey(owner, keyring) {
|
|
|
2218
2300
|
|
|
2219
2301
|
// src/store/postgres-server-store.ts
|
|
2220
2302
|
init_cjs_shims();
|
|
2221
|
-
var
|
|
2303
|
+
var import_core15 = require("@korajs/core");
|
|
2222
2304
|
var import_internal3 = require("@korajs/core/internal");
|
|
2223
2305
|
var import_drizzle_orm = require("drizzle-orm");
|
|
2224
2306
|
|
|
@@ -2309,14 +2391,14 @@ var KEY_ID_SAMPLE_ROWS = 512;
|
|
|
2309
2391
|
|
|
2310
2392
|
// src/store/legacy-bodies.ts
|
|
2311
2393
|
init_cjs_shims();
|
|
2312
|
-
var
|
|
2394
|
+
var import_core12 = require("@korajs/core");
|
|
2313
2395
|
var LEGACY_BODIES_META_KEY = "legacy_bodies_canonical";
|
|
2314
2396
|
async function provenLegacyClears(stored) {
|
|
2315
2397
|
const changed = [];
|
|
2316
2398
|
for (const op of stored) {
|
|
2317
2399
|
if (op.type !== "update" || op.hashVersion !== void 0 || op.encrypted !== void 0) continue;
|
|
2318
|
-
if ((0,
|
|
2319
|
-
const canonical = await (0,
|
|
2400
|
+
if ((0, import_core12.canonicalizeLegacyOperation)(op) === op) continue;
|
|
2401
|
+
const canonical = await (0, import_core12.canonicalizeProvenLegacyClear)(op);
|
|
2320
2402
|
if (canonical !== op) changed.push(canonical);
|
|
2321
2403
|
}
|
|
2322
2404
|
return changed;
|
|
@@ -2401,7 +2483,7 @@ function isJsonStringArray(value) {
|
|
|
2401
2483
|
|
|
2402
2484
|
// src/store/quarantine-base.ts
|
|
2403
2485
|
init_cjs_shims();
|
|
2404
|
-
var
|
|
2486
|
+
var import_core13 = require("@korajs/core");
|
|
2405
2487
|
function quarantineKey(collection, recordId) {
|
|
2406
2488
|
return JSON.stringify([collection, recordId]);
|
|
2407
2489
|
}
|
|
@@ -2442,7 +2524,7 @@ function buildQuarantineScope(rowJsons) {
|
|
|
2442
2524
|
nodeId: typeof row.timestamp_node_id === "string" ? row.timestamp_node_id : ""
|
|
2443
2525
|
};
|
|
2444
2526
|
const prior = scope.latest.get(key);
|
|
2445
|
-
if (!prior ||
|
|
2527
|
+
if (!prior || import_core13.HybridLogicalClock.compare(ts, prior) > 0) scope.latest.set(key, ts);
|
|
2446
2528
|
}
|
|
2447
2529
|
}
|
|
2448
2530
|
return scope;
|
|
@@ -2454,12 +2536,12 @@ function rebuildFromSnapshot(input, schema, options) {
|
|
|
2454
2536
|
}
|
|
2455
2537
|
if (!input.row) return input.ops.length > 0 ? refoldRecord(input.ops, schema, options) : null;
|
|
2456
2538
|
const fromLog = input.ops.length > 0 ? refoldRecord(input.ops, schema, options) : null;
|
|
2457
|
-
const versions = fromLog ? (0,
|
|
2539
|
+
const versions = fromLog ? (0, import_core13.getFoldFieldVersions)(fromLog) : null;
|
|
2458
2540
|
const floor = input.quarantinedLatest;
|
|
2459
2541
|
const atLeast = (ts) => {
|
|
2460
2542
|
if (!ts) return floor;
|
|
2461
2543
|
if (!floor) return ts;
|
|
2462
|
-
return
|
|
2544
|
+
return import_core13.HybridLogicalClock.compare(ts, floor) >= 0 ? ts : floor;
|
|
2463
2545
|
};
|
|
2464
2546
|
const rowWall = (wall) => ({ wallTime: wall, logical: 0, nodeId: "" });
|
|
2465
2547
|
const latest = atLeast(versions?.latest) ?? rowWall(Math.max(input.row.updatedAt, input.row.createdAt));
|
|
@@ -2467,7 +2549,7 @@ function rebuildFromSnapshot(input, schema, options) {
|
|
|
2467
2549
|
for (const field of Object.keys(input.row.values)) {
|
|
2468
2550
|
fieldVersions[field] = atLeast(versions?.fields[field]) ?? latest;
|
|
2469
2551
|
}
|
|
2470
|
-
const base = (0,
|
|
2552
|
+
const base = (0, import_core13.createSnapshotState)(
|
|
2471
2553
|
{
|
|
2472
2554
|
collection: input.collection,
|
|
2473
2555
|
recordId: input.recordId,
|
|
@@ -2488,12 +2570,12 @@ init_server_store();
|
|
|
2488
2570
|
|
|
2489
2571
|
// src/store/text-codec.ts
|
|
2490
2572
|
init_cjs_shims();
|
|
2491
|
-
var
|
|
2573
|
+
var import_core14 = require("@korajs/core");
|
|
2492
2574
|
var PG_TEXT_CODEC_MIGRATION_KEY = "pg_text_codec_v1:";
|
|
2493
|
-
var encodePgText =
|
|
2494
|
-
var decodePgText =
|
|
2495
|
-
var encodePgJsonValue =
|
|
2496
|
-
var decodePgJsonValue =
|
|
2575
|
+
var encodePgText = import_core14.encodeStoredText;
|
|
2576
|
+
var decodePgText = import_core14.decodeStoredText;
|
|
2577
|
+
var encodePgJsonValue = import_core14.encodeStoredJsonValue;
|
|
2578
|
+
var decodePgJsonValue = import_core14.decodeStoredJsonValue;
|
|
2497
2579
|
function pgTextCodecMigrationSql(table, textColumns, jsonbColumns) {
|
|
2498
2580
|
const esc = "U&'\\FFFF'";
|
|
2499
2581
|
const statements = [];
|
|
@@ -2856,14 +2938,14 @@ var PostgresServerStore = class {
|
|
|
2856
2938
|
* log has no transform path to `version` (RT-103): those operations would fold as
|
|
2857
2939
|
* absent, silently erasing them from their records.
|
|
2858
2940
|
*/
|
|
2859
|
-
async assertTransformCoverage(
|
|
2941
|
+
async assertTransformCoverage(version2, transforms) {
|
|
2860
2942
|
if (transforms.length === 0) return;
|
|
2861
2943
|
const rows = await this.db.execute(
|
|
2862
2944
|
import_drizzle_orm.sql`SELECT DISTINCT schema_version AS v FROM operations`
|
|
2863
2945
|
);
|
|
2864
|
-
(0,
|
|
2946
|
+
(0, import_core15.assertOperationTransformCoverage)(
|
|
2865
2947
|
rows.map((row) => Number(row.v)),
|
|
2866
|
-
|
|
2948
|
+
version2,
|
|
2867
2949
|
transforms,
|
|
2868
2950
|
"Postgres server store"
|
|
2869
2951
|
);
|
|
@@ -2945,9 +3027,9 @@ var PostgresServerStore = class {
|
|
|
2945
3027
|
if (done.length > 0) return;
|
|
2946
3028
|
const fields = Object.entries(collection.fields);
|
|
2947
3029
|
const statements = pgTextCodecMigrationSql(
|
|
2948
|
-
(0,
|
|
2949
|
-
fields.filter(([, d]) => isPgRawTextKind(d.kind)).map(([f]) => (0,
|
|
2950
|
-
fields.filter(([, d]) => isPgJsonbKind(d.kind)).map(([f]) => (0,
|
|
3030
|
+
(0, import_core15.quoteIdent)(name),
|
|
3031
|
+
fields.filter(([, d]) => isPgRawTextKind(d.kind)).map(([f]) => (0, import_core15.quoteIdent)(f)),
|
|
3032
|
+
fields.filter(([, d]) => isPgJsonbKind(d.kind)).map(([f]) => (0, import_core15.quoteIdent)(f))
|
|
2951
3033
|
);
|
|
2952
3034
|
for (const statement of statements) await tx.execute(import_drizzle_orm.sql.raw(statement));
|
|
2953
3035
|
await tx.execute(
|
|
@@ -3094,20 +3176,20 @@ var PostgresServerStore = class {
|
|
|
3094
3176
|
await tx.execute(import_drizzle_orm.sql`SELECT value FROM delivery_counter WHERE id = 1 FOR UPDATE`);
|
|
3095
3177
|
if (input.idempotencyKey) {
|
|
3096
3178
|
const existing = await tx.execute(
|
|
3097
|
-
import_drizzle_orm.sql`SELECT 1 FROM ${import_drizzle_orm.sql.raw((0,
|
|
3179
|
+
import_drizzle_orm.sql`SELECT 1 FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(input.idempotencyKey.collection))} WHERE id = ${input.idempotencyKey.id} AND _deleted = 0 LIMIT 1`
|
|
3098
3180
|
);
|
|
3099
3181
|
if (existing.length > 0) {
|
|
3100
3182
|
return { admitted: true, idempotent: true, applied: [] };
|
|
3101
3183
|
}
|
|
3102
3184
|
}
|
|
3103
3185
|
const rows = await tx.execute(
|
|
3104
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
3186
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(input.target.collection))} WHERE id = ${input.target.id} AND _deleted = 0`
|
|
3105
3187
|
);
|
|
3106
3188
|
const current = rows.length > 0 ? this.deserializeRow(rows[0], collectionDef) : null;
|
|
3107
3189
|
if (!input.admit(current)) {
|
|
3108
3190
|
return { admitted: false, idempotent: false, applied: [] };
|
|
3109
3191
|
}
|
|
3110
|
-
const clock = new
|
|
3192
|
+
const clock = new import_core15.HybridLogicalClock(this.nodeId, this.timeSource);
|
|
3111
3193
|
const latest = await this.latestOperationTimestamp(
|
|
3112
3194
|
tx,
|
|
3113
3195
|
input.target.collection,
|
|
@@ -3295,7 +3377,7 @@ var PostgresServerStore = class {
|
|
|
3295
3377
|
const collectionDef = this.schema?.collections[collection];
|
|
3296
3378
|
if (!collectionDef) return null;
|
|
3297
3379
|
const rows = await txOrDb.execute(
|
|
3298
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
3380
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE id = ${recordId} LIMIT 1`
|
|
3299
3381
|
);
|
|
3300
3382
|
const row = rows[0];
|
|
3301
3383
|
if (!row) return null;
|
|
@@ -3484,7 +3566,7 @@ var PostgresServerStore = class {
|
|
|
3484
3566
|
const chunk = ids.slice(i, i + 1e3);
|
|
3485
3567
|
if (chunk.length === 0) continue;
|
|
3486
3568
|
const rows = await this.db.execute(
|
|
3487
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
3569
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE id IN (${import_drizzle_orm.sql.join(
|
|
3488
3570
|
chunk.map((id) => import_drizzle_orm.sql`${id}`),
|
|
3489
3571
|
import_drizzle_orm.sql.raw(", ")
|
|
3490
3572
|
)})`
|
|
@@ -3522,7 +3604,7 @@ var PostgresServerStore = class {
|
|
|
3522
3604
|
this.assertCollection(collection);
|
|
3523
3605
|
const schema = this.schema;
|
|
3524
3606
|
const collectionDef = schema.collections[collection];
|
|
3525
|
-
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
3607
|
+
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
|
|
3526
3608
|
const rows = await this.db.execute(query);
|
|
3527
3609
|
if (rows.length === 0) return null;
|
|
3528
3610
|
return this.deserializeRow(rows[0], collectionDef);
|
|
@@ -3539,7 +3621,7 @@ var PostgresServerStore = class {
|
|
|
3539
3621
|
}
|
|
3540
3622
|
}
|
|
3541
3623
|
const whereClause = this.buildWhereClause(where ?? {}, false, schema.collections[collection]);
|
|
3542
|
-
const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw((0,
|
|
3624
|
+
const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE ${whereClause}`;
|
|
3543
3625
|
const rows = await this.db.execute(query);
|
|
3544
3626
|
const cnt = rows[0]?.cnt;
|
|
3545
3627
|
return typeof cnt === "string" ? Number.parseInt(cnt, 10) : cnt ?? 0;
|
|
@@ -3601,7 +3683,7 @@ var PostgresServerStore = class {
|
|
|
3601
3683
|
await this.db.transaction(async (tx) => {
|
|
3602
3684
|
await tx.execute(import_drizzle_orm.sql`DELETE FROM kora_fold_state`);
|
|
3603
3685
|
for (const collection of Object.keys(this.schema?.collections ?? {})) {
|
|
3604
|
-
await tx.execute(import_drizzle_orm.sql.raw(`DELETE FROM ${(0,
|
|
3686
|
+
await tx.execute(import_drizzle_orm.sql.raw(`DELETE FROM ${(0, import_core15.quoteIdent)(collection)}`));
|
|
3605
3687
|
}
|
|
3606
3688
|
});
|
|
3607
3689
|
if (this.schema) this.foldMigration = await this.rematerialize();
|
|
@@ -3711,6 +3793,21 @@ var PostgresServerStore = class {
|
|
|
3711
3793
|
);
|
|
3712
3794
|
return rows[0]?.user_id === userId;
|
|
3713
3795
|
}
|
|
3796
|
+
async claimUnownedNode(nodeId, userId) {
|
|
3797
|
+
this.assertOpen();
|
|
3798
|
+
await this.ready;
|
|
3799
|
+
if (userId === RELEASED_NODE_OWNER) return false;
|
|
3800
|
+
await this.db.execute(
|
|
3801
|
+
import_drizzle_orm.sql`INSERT INTO node_claims (node_id, user_id, claimed_at)
|
|
3802
|
+
VALUES (${nodeId}, ${userId}, ${Date.now()})
|
|
3803
|
+
ON CONFLICT (node_id) DO UPDATE SET user_id = EXCLUDED.user_id, claimed_at = EXCLUDED.claimed_at
|
|
3804
|
+
WHERE node_claims.user_id = ${RELEASED_NODE_OWNER}`
|
|
3805
|
+
);
|
|
3806
|
+
const rows = await this.db.execute(
|
|
3807
|
+
import_drizzle_orm.sql`SELECT user_id FROM node_claims WHERE node_id = ${nodeId} LIMIT 1`
|
|
3808
|
+
);
|
|
3809
|
+
return rows[0]?.user_id === userId;
|
|
3810
|
+
}
|
|
3714
3811
|
async getNodeClaimOwner(nodeId) {
|
|
3715
3812
|
this.assertOpen();
|
|
3716
3813
|
await this.ready;
|
|
@@ -3752,7 +3849,7 @@ var PostgresServerStore = class {
|
|
|
3752
3849
|
const collectionDef = this.schema?.collections[collection];
|
|
3753
3850
|
if (collectionDef) {
|
|
3754
3851
|
const rows = await txOrDb.execute(
|
|
3755
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
3852
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE id = ${recordId} LIMIT 1`
|
|
3756
3853
|
);
|
|
3757
3854
|
const row = rows[0];
|
|
3758
3855
|
return row ? this.deserializeRow(row, collectionDef) : null;
|
|
@@ -3830,7 +3927,7 @@ var PostgresServerStore = class {
|
|
|
3830
3927
|
const schema = this.schema ?? EMPTY_FOLD_SCHEMA;
|
|
3831
3928
|
const collectionDef = schema.collections[collection];
|
|
3832
3929
|
const raw = collectionDef ? (await txOrDb.execute(
|
|
3833
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
3930
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE id = ${recordId} LIMIT 1`
|
|
3834
3931
|
))[0] : void 0;
|
|
3835
3932
|
let row = null;
|
|
3836
3933
|
if (raw && collectionDef) {
|
|
@@ -3928,7 +4025,7 @@ var PostgresServerStore = class {
|
|
|
3928
4025
|
const row = state ? projectFoldState(state, this.foldOptions) : null;
|
|
3929
4026
|
if (!row) {
|
|
3930
4027
|
await txOrDb.execute(
|
|
3931
|
-
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0,
|
|
4028
|
+
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} SET _deleted = 1 WHERE id = ${recordId}`
|
|
3932
4029
|
);
|
|
3933
4030
|
return;
|
|
3934
4031
|
}
|
|
@@ -3959,16 +4056,16 @@ var PostgresServerStore = class {
|
|
|
3959
4056
|
updatedAt,
|
|
3960
4057
|
deleted ? 1 : 0
|
|
3961
4058
|
];
|
|
3962
|
-
const columnsSql = import_drizzle_orm.sql.raw(allColumns.map((c) => (0,
|
|
4059
|
+
const columnsSql = import_drizzle_orm.sql.raw(allColumns.map((c) => (0, import_core15.quoteIdent)(c)).join(", "));
|
|
3963
4060
|
const valuesSql = import_drizzle_orm.sql.join(
|
|
3964
4061
|
values.map((v) => import_drizzle_orm.sql`${v}`),
|
|
3965
4062
|
import_drizzle_orm.sql.raw(", ")
|
|
3966
4063
|
);
|
|
3967
4064
|
const updateSet = import_drizzle_orm.sql.raw(
|
|
3968
|
-
allColumns.slice(1).map((c) => `${(0,
|
|
4065
|
+
allColumns.slice(1).map((c) => `${(0, import_core15.quoteIdent)(c)} = excluded.${(0, import_core15.quoteIdent)(c)}`).join(", ")
|
|
3969
4066
|
);
|
|
3970
4067
|
await txOrDb.execute(
|
|
3971
|
-
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0,
|
|
4068
|
+
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
3972
4069
|
);
|
|
3973
4070
|
}
|
|
3974
4071
|
/**
|
|
@@ -4024,9 +4121,9 @@ var PostgresServerStore = class {
|
|
|
4024
4121
|
]);
|
|
4025
4122
|
}
|
|
4026
4123
|
const perStatement = Math.max(1, Math.floor(3e4 / columns.length));
|
|
4027
|
-
const columnsSql = import_drizzle_orm.sql.raw(columns.map((c) => (0,
|
|
4124
|
+
const columnsSql = import_drizzle_orm.sql.raw(columns.map((c) => (0, import_core15.quoteIdent)(c)).join(", "));
|
|
4028
4125
|
const updateSet = import_drizzle_orm.sql.raw(
|
|
4029
|
-
columns.slice(1).map((c) => `${(0,
|
|
4126
|
+
columns.slice(1).map((c) => `${(0, import_core15.quoteIdent)(c)} = excluded.${(0, import_core15.quoteIdent)(c)}`).join(", ")
|
|
4030
4127
|
);
|
|
4031
4128
|
for (let i = 0; i < rows.length; i += perStatement) {
|
|
4032
4129
|
const tuples = rows.slice(i, i + perStatement).map(
|
|
@@ -4036,12 +4133,12 @@ var PostgresServerStore = class {
|
|
|
4036
4133
|
)})`
|
|
4037
4134
|
);
|
|
4038
4135
|
await tx.execute(
|
|
4039
|
-
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0,
|
|
4136
|
+
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} (${columnsSql}) VALUES ${import_drizzle_orm.sql.join(tuples, import_drizzle_orm.sql.raw(", "))} ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
4040
4137
|
);
|
|
4041
4138
|
}
|
|
4042
4139
|
if (hidden.length > 0) {
|
|
4043
4140
|
await tx.execute(
|
|
4044
|
-
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0,
|
|
4141
|
+
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} SET _deleted = 1 WHERE id IN (${import_drizzle_orm.sql.join(
|
|
4045
4142
|
hidden.map((id) => import_drizzle_orm.sql`${id}`),
|
|
4046
4143
|
import_drizzle_orm.sql.raw(", ")
|
|
4047
4144
|
)})`
|
|
@@ -4133,7 +4230,7 @@ var PostgresServerStore = class {
|
|
|
4133
4230
|
GROUP BY record_id ORDER BY record_id LIMIT ${FOLD_MIGRATION_BATCH}
|
|
4134
4231
|
)
|
|
4135
4232
|
SELECT r.record_id, r.max_seq, f.covered_seq AS covered,
|
|
4136
|
-
EXISTS (SELECT 1 FROM ${import_drizzle_orm.sql.raw((0,
|
|
4233
|
+
EXISTS (SELECT 1 FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} c WHERE c.id = r.record_id) AS has_row,
|
|
4137
4234
|
EXISTS (
|
|
4138
4235
|
SELECT 1 FROM operations x
|
|
4139
4236
|
WHERE x.id = f.covered_op_id AND x.delivery_seq = f.covered_seq
|
|
@@ -4205,7 +4302,7 @@ var PostgresServerStore = class {
|
|
|
4205
4302
|
await this.db.transaction(async (tx) => {
|
|
4206
4303
|
await tx.execute(import_drizzle_orm.sql`SELECT value FROM delivery_counter WHERE id = 1 FOR UPDATE`);
|
|
4207
4304
|
const orphans = await tx.execute(
|
|
4208
|
-
import_drizzle_orm.sql`SELECT c.id AS id FROM ${import_drizzle_orm.sql.raw((0,
|
|
4305
|
+
import_drizzle_orm.sql`SELECT c.id AS id FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} c
|
|
4209
4306
|
WHERE NOT EXISTS (SELECT 1 FROM kora_fold_state f WHERE f.collection = ${collection} AND f.record_id = c.id)
|
|
4210
4307
|
AND NOT EXISTS (SELECT 1 FROM operations o WHERE o.collection = ${collection} AND o.record_id = c.id)`
|
|
4211
4308
|
);
|
|
@@ -4239,11 +4336,11 @@ var PostgresServerStore = class {
|
|
|
4239
4336
|
this.schema?.collections[collection]
|
|
4240
4337
|
);
|
|
4241
4338
|
const parts = [
|
|
4242
|
-
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0,
|
|
4339
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(collection))} WHERE ${whereClause}`
|
|
4243
4340
|
];
|
|
4244
4341
|
if (options?.orderBy) {
|
|
4245
4342
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
4246
|
-
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${(0,
|
|
4343
|
+
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${(0, import_core15.quoteIdent)(options.orderBy)} ${dir}`));
|
|
4247
4344
|
}
|
|
4248
4345
|
if (options?.limit !== void 0) {
|
|
4249
4346
|
parts.push(import_drizzle_orm.sql` LIMIT ${options.limit}`);
|
|
@@ -4261,7 +4358,7 @@ var PostgresServerStore = class {
|
|
|
4261
4358
|
for (const [key, value] of Object.entries(where)) {
|
|
4262
4359
|
const kind = collectionDef?.fields[key]?.kind;
|
|
4263
4360
|
const param = kind && isPgRawTextKind(kind) && typeof value === "string" ? encodePgText(value) : value;
|
|
4264
|
-
conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw((0,
|
|
4361
|
+
conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw((0, import_core15.quoteIdent)(key))} = ${param}`);
|
|
4265
4362
|
}
|
|
4266
4363
|
if (conditions.length === 0) {
|
|
4267
4364
|
return import_drizzle_orm.sql.raw("1 = 1");
|
|
@@ -4486,7 +4583,7 @@ var PostgresServerStore = class {
|
|
|
4486
4583
|
for (const column of narrowColumns) {
|
|
4487
4584
|
await tx.execute(
|
|
4488
4585
|
import_drizzle_orm.sql.raw(
|
|
4489
|
-
`ALTER TABLE ${(0,
|
|
4586
|
+
`ALTER TABLE ${(0, import_core15.quoteIdent)(column.table_name)} ALTER COLUMN ${(0, import_core15.quoteIdent)(column.column_name)} TYPE BIGINT`
|
|
4490
4587
|
)
|
|
4491
4588
|
);
|
|
4492
4589
|
}
|
|
@@ -4695,7 +4792,7 @@ async function createPostgresServerStore(options) {
|
|
|
4695
4792
|
}
|
|
4696
4793
|
return store;
|
|
4697
4794
|
}
|
|
4698
|
-
var ServerStoreUnavailableError = class extends
|
|
4795
|
+
var ServerStoreUnavailableError = class extends import_core15.KoraError {
|
|
4699
4796
|
constructor(connectionString, cause) {
|
|
4700
4797
|
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
4701
4798
|
super(
|
|
@@ -4753,8 +4850,10 @@ var POSTGRES_DRIVER_SPECIFIERS = [
|
|
|
4753
4850
|
|
|
4754
4851
|
// src/store/sqlite-server-store.ts
|
|
4755
4852
|
init_cjs_shims();
|
|
4853
|
+
var import_node_fs = require("fs");
|
|
4756
4854
|
var import_node_module = require("module");
|
|
4757
|
-
var
|
|
4855
|
+
var import_node_path = require("path");
|
|
4856
|
+
var import_core16 = require("@korajs/core");
|
|
4758
4857
|
var import_internal4 = require("@korajs/core/internal");
|
|
4759
4858
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
4760
4859
|
|
|
@@ -5096,14 +5195,14 @@ var SqliteServerStore = class {
|
|
|
5096
5195
|
* log has no transform path to `version` (RT-103): those operations would fold as
|
|
5097
5196
|
* absent, silently erasing them from their records.
|
|
5098
5197
|
*/
|
|
5099
|
-
assertTransformCoverage(
|
|
5198
|
+
assertTransformCoverage(version2, transforms) {
|
|
5100
5199
|
if (transforms.length === 0) return;
|
|
5101
5200
|
const rows = this.db.all(
|
|
5102
5201
|
import_drizzle_orm2.sql`SELECT DISTINCT schema_version AS v FROM operations`
|
|
5103
5202
|
);
|
|
5104
|
-
(0,
|
|
5203
|
+
(0, import_core16.assertOperationTransformCoverage)(
|
|
5105
5204
|
rows.map((row) => Number(row.v)),
|
|
5106
|
-
|
|
5205
|
+
version2,
|
|
5107
5206
|
transforms,
|
|
5108
5207
|
"SQLite server store"
|
|
5109
5208
|
);
|
|
@@ -5329,7 +5428,7 @@ var SqliteServerStore = class {
|
|
|
5329
5428
|
const collectionDef = this.schema?.collections[collection];
|
|
5330
5429
|
if (!collectionDef) return null;
|
|
5331
5430
|
const rows = txOrDb.all(
|
|
5332
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0,
|
|
5431
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE id = ${recordId} LIMIT 1`
|
|
5333
5432
|
);
|
|
5334
5433
|
const row = rows[0];
|
|
5335
5434
|
if (!row) return null;
|
|
@@ -5467,7 +5566,7 @@ var SqliteServerStore = class {
|
|
|
5467
5566
|
const chunk = ids.slice(i, i + 500);
|
|
5468
5567
|
if (chunk.length === 0) continue;
|
|
5469
5568
|
const rows = this.db.all(
|
|
5470
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0,
|
|
5569
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE id IN (${import_drizzle_orm2.sql.join(
|
|
5471
5570
|
chunk.map((id) => import_drizzle_orm2.sql`${id}`),
|
|
5472
5571
|
import_drizzle_orm2.sql.raw(", ")
|
|
5473
5572
|
)})`
|
|
@@ -5503,7 +5602,7 @@ var SqliteServerStore = class {
|
|
|
5503
5602
|
this.assertCollection(collection);
|
|
5504
5603
|
const schema = this.schema;
|
|
5505
5604
|
const collectionDef = schema.collections[collection];
|
|
5506
|
-
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0,
|
|
5605
|
+
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
|
|
5507
5606
|
const rows = this.db.all(query);
|
|
5508
5607
|
if (rows.length === 0) return null;
|
|
5509
5608
|
return this.deserializeRow(rows[0], collectionDef);
|
|
@@ -5519,7 +5618,7 @@ var SqliteServerStore = class {
|
|
|
5519
5618
|
}
|
|
5520
5619
|
}
|
|
5521
5620
|
const whereClause = this.buildWhereClause(where ?? {}, false, schema.collections[collection]);
|
|
5522
|
-
const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw((0,
|
|
5621
|
+
const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE ${whereClause}`;
|
|
5523
5622
|
const rows = this.db.all(query);
|
|
5524
5623
|
return rows[0]?.cnt ?? 0;
|
|
5525
5624
|
}
|
|
@@ -5591,6 +5690,20 @@ var SqliteServerStore = class {
|
|
|
5591
5690
|
);
|
|
5592
5691
|
return rows[0]?.user_id === userId;
|
|
5593
5692
|
}
|
|
5693
|
+
async claimUnownedNode(nodeId, userId) {
|
|
5694
|
+
this.assertOpen();
|
|
5695
|
+
if (userId === RELEASED_NODE_OWNER) return false;
|
|
5696
|
+
this.db.run(
|
|
5697
|
+
import_drizzle_orm2.sql`INSERT INTO node_claims (node_id, user_id, claimed_at)
|
|
5698
|
+
VALUES (${nodeId}, ${userId}, ${Date.now()})
|
|
5699
|
+
ON CONFLICT (node_id) DO UPDATE SET user_id = excluded.user_id, claimed_at = excluded.claimed_at
|
|
5700
|
+
WHERE node_claims.user_id = ${RELEASED_NODE_OWNER}`
|
|
5701
|
+
);
|
|
5702
|
+
const rows = this.db.all(
|
|
5703
|
+
import_drizzle_orm2.sql`SELECT user_id FROM node_claims WHERE node_id = ${nodeId} LIMIT 1`
|
|
5704
|
+
);
|
|
5705
|
+
return rows[0]?.user_id === userId;
|
|
5706
|
+
}
|
|
5594
5707
|
async getNodeClaimOwner(nodeId) {
|
|
5595
5708
|
this.assertOpen();
|
|
5596
5709
|
const rows = this.db.all(
|
|
@@ -5629,7 +5742,7 @@ var SqliteServerStore = class {
|
|
|
5629
5742
|
const collectionDef = this.schema?.collections[collection];
|
|
5630
5743
|
if (collectionDef) {
|
|
5631
5744
|
const rows = txOrDb.all(
|
|
5632
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0,
|
|
5745
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE id = ${recordId} LIMIT 1`
|
|
5633
5746
|
);
|
|
5634
5747
|
const row = rows[0];
|
|
5635
5748
|
return row ? this.deserializeRow(row, collectionDef) : null;
|
|
@@ -5649,7 +5762,7 @@ var SqliteServerStore = class {
|
|
|
5649
5762
|
const schema = this.schema ?? EMPTY_FOLD_SCHEMA;
|
|
5650
5763
|
const collectionDef = schema.collections[collection];
|
|
5651
5764
|
const raw = collectionDef ? txOrDb.all(
|
|
5652
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0,
|
|
5765
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE id = ${recordId} LIMIT 1`
|
|
5653
5766
|
)[0] : void 0;
|
|
5654
5767
|
let row = null;
|
|
5655
5768
|
if (raw && collectionDef) {
|
|
@@ -5720,7 +5833,7 @@ var SqliteServerStore = class {
|
|
|
5720
5833
|
this.db.transaction((tx) => {
|
|
5721
5834
|
tx.run(import_drizzle_orm2.sql.raw("DELETE FROM kora_fold_state"));
|
|
5722
5835
|
for (const collection of Object.keys(this.schema?.collections ?? {})) {
|
|
5723
|
-
tx.run(import_drizzle_orm2.sql.raw(`DELETE FROM ${(0,
|
|
5836
|
+
tx.run(import_drizzle_orm2.sql.raw(`DELETE FROM ${(0, import_core16.quoteIdent)(collection)}`));
|
|
5724
5837
|
}
|
|
5725
5838
|
});
|
|
5726
5839
|
if (this.schema) this.foldMigration = this.rematerialize();
|
|
@@ -5842,7 +5955,7 @@ var SqliteServerStore = class {
|
|
|
5842
5955
|
const row = state ? projectFoldState(state, this.foldOptions) : null;
|
|
5843
5956
|
if (!row) {
|
|
5844
5957
|
txOrDb.run(
|
|
5845
|
-
import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw((0,
|
|
5958
|
+
import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} SET _deleted = 1 WHERE id = ${recordId}`
|
|
5846
5959
|
);
|
|
5847
5960
|
return;
|
|
5848
5961
|
}
|
|
@@ -5874,16 +5987,16 @@ var SqliteServerStore = class {
|
|
|
5874
5987
|
updatedAt,
|
|
5875
5988
|
deleted ? 1 : 0
|
|
5876
5989
|
];
|
|
5877
|
-
const columnsSql = import_drizzle_orm2.sql.raw(allColumns.map((c) => (0,
|
|
5990
|
+
const columnsSql = import_drizzle_orm2.sql.raw(allColumns.map((c) => (0, import_core16.quoteIdent)(c)).join(", "));
|
|
5878
5991
|
const valuesSql = import_drizzle_orm2.sql.join(
|
|
5879
5992
|
values.map((v) => import_drizzle_orm2.sql`${v}`),
|
|
5880
5993
|
import_drizzle_orm2.sql.raw(", ")
|
|
5881
5994
|
);
|
|
5882
5995
|
const updateSet = import_drizzle_orm2.sql.raw(
|
|
5883
|
-
allColumns.slice(1).map((c) => `${(0,
|
|
5996
|
+
allColumns.slice(1).map((c) => `${(0, import_core16.quoteIdent)(c)} = excluded.${(0, import_core16.quoteIdent)(c)}`).join(", ")
|
|
5884
5997
|
);
|
|
5885
5998
|
txOrDb.run(
|
|
5886
|
-
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0,
|
|
5999
|
+
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
5887
6000
|
);
|
|
5888
6001
|
}
|
|
5889
6002
|
/**
|
|
@@ -5914,8 +6027,8 @@ var SqliteServerStore = class {
|
|
|
5914
6027
|
const key = `${PG_TEXT_CODEC_MIGRATION_KEY}${name}`;
|
|
5915
6028
|
this.db.transaction((tx) => {
|
|
5916
6029
|
if (tx.all(import_drizzle_orm2.sql`SELECT 1 AS one FROM kora_server_meta WHERE key = ${key}`).length > 0) return;
|
|
5917
|
-
const textColumns = Object.entries(collection.fields).filter(([, descriptor]) => isPgRawTextKind(descriptor.kind)).map(([field]) => (0,
|
|
5918
|
-
for (const statement of sqliteTextCodecMigrationSql((0,
|
|
6030
|
+
const textColumns = Object.entries(collection.fields).filter(([, descriptor]) => isPgRawTextKind(descriptor.kind)).map(([field]) => (0, import_core16.quoteIdent)(field));
|
|
6031
|
+
for (const statement of sqliteTextCodecMigrationSql((0, import_core16.quoteIdent)(name), textColumns)) {
|
|
5919
6032
|
tx.run(import_drizzle_orm2.sql.raw(statement));
|
|
5920
6033
|
}
|
|
5921
6034
|
tx.run(
|
|
@@ -5987,7 +6100,7 @@ var SqliteServerStore = class {
|
|
|
5987
6100
|
import_drizzle_orm2.sql`SELECT o.record_id AS record_id, MAX(o.delivery_seq) AS max_seq,
|
|
5988
6101
|
(SELECT f.covered_seq FROM kora_fold_state f WHERE f.collection = o.collection AND f.record_id = o.record_id) AS covered,
|
|
5989
6102
|
(SELECT f.covered_op_id FROM kora_fold_state f WHERE f.collection = o.collection AND f.record_id = o.record_id) AS covered_op_id,
|
|
5990
|
-
EXISTS (SELECT 1 FROM ${import_drizzle_orm2.sql.raw((0,
|
|
6103
|
+
EXISTS (SELECT 1 FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} c WHERE c.id = o.record_id) AS has_row
|
|
5991
6104
|
FROM operations o
|
|
5992
6105
|
WHERE o.collection = ${collection} AND o.record_id > ${cursor}
|
|
5993
6106
|
GROUP BY o.record_id
|
|
@@ -6053,7 +6166,7 @@ var SqliteServerStore = class {
|
|
|
6053
6166
|
if (!clean) {
|
|
6054
6167
|
this.db.transaction((tx) => {
|
|
6055
6168
|
const orphans = tx.all(
|
|
6056
|
-
import_drizzle_orm2.sql`SELECT c.id AS id FROM ${import_drizzle_orm2.sql.raw((0,
|
|
6169
|
+
import_drizzle_orm2.sql`SELECT c.id AS id FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} c
|
|
6057
6170
|
WHERE NOT EXISTS (SELECT 1 FROM kora_fold_state f WHERE f.collection = ${collection} AND f.record_id = c.id)
|
|
6058
6171
|
AND NOT EXISTS (SELECT 1 FROM operations o WHERE o.collection = ${collection} AND o.record_id = c.id)`
|
|
6059
6172
|
);
|
|
@@ -6085,11 +6198,11 @@ var SqliteServerStore = class {
|
|
|
6085
6198
|
this.schema?.collections[collection]
|
|
6086
6199
|
);
|
|
6087
6200
|
const parts = [
|
|
6088
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0,
|
|
6201
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(collection))} WHERE ${whereClause}`
|
|
6089
6202
|
];
|
|
6090
6203
|
if (options?.orderBy) {
|
|
6091
6204
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
6092
|
-
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${(0,
|
|
6205
|
+
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${(0, import_core16.quoteIdent)(options.orderBy)} ${dir}`));
|
|
6093
6206
|
}
|
|
6094
6207
|
if (options?.limit !== void 0) {
|
|
6095
6208
|
parts.push(import_drizzle_orm2.sql` LIMIT ${options.limit}`);
|
|
@@ -6107,7 +6220,7 @@ var SqliteServerStore = class {
|
|
|
6107
6220
|
for (const [key, value] of Object.entries(where)) {
|
|
6108
6221
|
const kind = collectionDef?.fields[key]?.kind;
|
|
6109
6222
|
const param = kind && isPgRawTextKind(kind) && typeof value === "string" ? encodePgText(value) : value;
|
|
6110
|
-
conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw((0,
|
|
6223
|
+
conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw((0, import_core16.quoteIdent)(key))} = ${param}`);
|
|
6111
6224
|
}
|
|
6112
6225
|
if (conditions.length === 0) {
|
|
6113
6226
|
return import_drizzle_orm2.sql.raw("1 = 1");
|
|
@@ -6593,6 +6706,7 @@ function createSqliteServerStore(options) {
|
|
|
6593
6706
|
const Database = esmRequire("better-sqlite3");
|
|
6594
6707
|
const { drizzle } = esmRequire("drizzle-orm/better-sqlite3");
|
|
6595
6708
|
const filename = options.filename ?? ":memory:";
|
|
6709
|
+
ensureDatabaseDirectory(filename);
|
|
6596
6710
|
const sqlite = new Database(filename);
|
|
6597
6711
|
sqlite.pragma("journal_mode = WAL");
|
|
6598
6712
|
const db = drizzle(sqlite);
|
|
@@ -6602,10 +6716,14 @@ function createSqliteServerStore(options) {
|
|
|
6602
6716
|
...options.instanceId !== void 0 ? { instanceId: options.instanceId } : {}
|
|
6603
6717
|
});
|
|
6604
6718
|
}
|
|
6719
|
+
function ensureDatabaseDirectory(filename) {
|
|
6720
|
+
if (filename === "" || filename === ":memory:" || filename.startsWith("file:")) return;
|
|
6721
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(filename), { recursive: true });
|
|
6722
|
+
}
|
|
6605
6723
|
|
|
6606
6724
|
// src/transport/http-server-transport.ts
|
|
6607
6725
|
init_cjs_shims();
|
|
6608
|
-
var
|
|
6726
|
+
var import_core17 = require("@korajs/core");
|
|
6609
6727
|
var DEFAULT_HTTP_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
|
|
6610
6728
|
var HttpServerTransport = class {
|
|
6611
6729
|
serializer;
|
|
@@ -6677,7 +6795,7 @@ var HttpServerTransport = class {
|
|
|
6677
6795
|
}
|
|
6678
6796
|
receive(payload) {
|
|
6679
6797
|
if (!this.connected) {
|
|
6680
|
-
throw new
|
|
6798
|
+
throw new import_core17.SyncError("HTTP server transport is closed");
|
|
6681
6799
|
}
|
|
6682
6800
|
try {
|
|
6683
6801
|
const message = this.serializer.decode(payload);
|
|
@@ -6718,7 +6836,7 @@ var HttpServerTransport = class {
|
|
|
6718
6836
|
|
|
6719
6837
|
// src/transport/ws-server-transport.ts
|
|
6720
6838
|
init_cjs_shims();
|
|
6721
|
-
var
|
|
6839
|
+
var import_core18 = require("@korajs/core");
|
|
6722
6840
|
var import_sync2 = require("@korajs/sync");
|
|
6723
6841
|
var WS_OPEN = 1;
|
|
6724
6842
|
var DEFAULT_WS_HEARTBEAT_INTERVAL_MS = 25e3;
|
|
@@ -6746,7 +6864,7 @@ var WsServerTransport = class {
|
|
|
6746
6864
|
}
|
|
6747
6865
|
send(message) {
|
|
6748
6866
|
if (this.ws.readyState !== WS_OPEN) {
|
|
6749
|
-
throw new
|
|
6867
|
+
throw new import_core18.SyncError("Cannot send message: WebSocket is not open", {
|
|
6750
6868
|
readyState: this.ws.readyState,
|
|
6751
6869
|
messageType: message.type
|
|
6752
6870
|
});
|
|
@@ -6825,7 +6943,7 @@ var WsServerTransport = class {
|
|
|
6825
6943
|
this.missedPongs = 0;
|
|
6826
6944
|
try {
|
|
6827
6945
|
if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) {
|
|
6828
|
-
throw new
|
|
6946
|
+
throw new import_core18.SyncError("Unsupported WebSocket payload type", {
|
|
6829
6947
|
payloadType: typeof data
|
|
6830
6948
|
});
|
|
6831
6949
|
}
|
|
@@ -6850,8 +6968,8 @@ var WsServerTransport = class {
|
|
|
6850
6968
|
|
|
6851
6969
|
// src/session/client-session.ts
|
|
6852
6970
|
init_cjs_shims();
|
|
6853
|
-
var import_core29 = require("@korajs/core");
|
|
6854
6971
|
var import_core30 = require("@korajs/core");
|
|
6972
|
+
var import_core31 = require("@korajs/core");
|
|
6855
6973
|
var import_internal7 = require("@korajs/core/internal");
|
|
6856
6974
|
var import_sync4 = require("@korajs/sync");
|
|
6857
6975
|
var import_sync5 = require("@korajs/sync");
|
|
@@ -6859,22 +6977,22 @@ var import_internal8 = require("@korajs/sync/internal");
|
|
|
6859
6977
|
|
|
6860
6978
|
// src/apply/apply-server-operation.ts
|
|
6861
6979
|
init_cjs_shims();
|
|
6862
|
-
var
|
|
6980
|
+
var import_core22 = require("@korajs/core");
|
|
6863
6981
|
var import_merge4 = require("@korajs/merge");
|
|
6864
6982
|
|
|
6865
6983
|
// src/constraints/constraint-authority.ts
|
|
6866
6984
|
init_cjs_shims();
|
|
6867
|
-
var
|
|
6985
|
+
var import_core20 = require("@korajs/core");
|
|
6868
6986
|
var import_merge2 = require("@korajs/merge");
|
|
6869
6987
|
|
|
6870
6988
|
// src/apply/derive-server-op-id.ts
|
|
6871
6989
|
init_cjs_shims();
|
|
6872
|
-
var
|
|
6990
|
+
var import_core19 = require("@korajs/core");
|
|
6873
6991
|
async function deriveServerOpId(store, parentOpId, ruleId, targetRecordId) {
|
|
6874
6992
|
if (store.deriveServerOperationId) {
|
|
6875
6993
|
return store.deriveServerOperationId(parentOpId, ruleId, targetRecordId);
|
|
6876
6994
|
}
|
|
6877
|
-
return (0,
|
|
6995
|
+
return (0, import_core19.deriveSideEffectOpId)(parentOpId, ruleId, targetRecordId);
|
|
6878
6996
|
}
|
|
6879
6997
|
|
|
6880
6998
|
// src/constraints/server-constraint-context.ts
|
|
@@ -6920,12 +7038,12 @@ async function buildCorrection(store, spec, schemaVersion, nextSequence) {
|
|
|
6920
7038
|
};
|
|
6921
7039
|
}
|
|
6922
7040
|
function stampTimestamp(stamp) {
|
|
6923
|
-
return stamp ?
|
|
7041
|
+
return stamp ? import_core20.HybridLogicalClock.deserialize(stamp.t) : null;
|
|
6924
7042
|
}
|
|
6925
7043
|
function maxTimestamp(values) {
|
|
6926
7044
|
let best = null;
|
|
6927
7045
|
for (const value of values) {
|
|
6928
|
-
if (value && (best === null ||
|
|
7046
|
+
if (value && (best === null || import_core20.HybridLogicalClock.compare(value, best) > 0)) best = value;
|
|
6929
7047
|
}
|
|
6930
7048
|
return best;
|
|
6931
7049
|
}
|
|
@@ -6939,7 +7057,7 @@ function comparePriority(a, b) {
|
|
|
6939
7057
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
6940
7058
|
}
|
|
6941
7059
|
function chooseWinner(members, constraint) {
|
|
6942
|
-
const byStamp = (a, b) =>
|
|
7060
|
+
const byStamp = (a, b) => import_core20.HybridLogicalClock.compare(a.key, b.key) || (a.row.id < b.row.id ? -1 : 1);
|
|
6943
7061
|
const sorted = [...members].sort(byStamp);
|
|
6944
7062
|
if (constraint.onConflict === "last-write-wins") return sorted[sorted.length - 1];
|
|
6945
7063
|
if (constraint.onConflict === "priority-field" && constraint.priorityField) {
|
|
@@ -6952,8 +7070,11 @@ function chooseWinner(members, constraint) {
|
|
|
6952
7070
|
}
|
|
6953
7071
|
return sorted[0];
|
|
6954
7072
|
}
|
|
7073
|
+
function groupKeys(constraint) {
|
|
7074
|
+
return [.../* @__PURE__ */ new Set([...constraint.fields, ...Object.keys(constraint.where ?? {})])];
|
|
7075
|
+
}
|
|
6955
7076
|
function groupWhere(constraint, row) {
|
|
6956
|
-
const where =
|
|
7077
|
+
const where = { ...constraint.where ?? {} };
|
|
6957
7078
|
for (const field of constraint.fields) {
|
|
6958
7079
|
const value = row[field];
|
|
6959
7080
|
if (value === null || value === void 0) return null;
|
|
@@ -6962,22 +7083,25 @@ function groupWhere(constraint, row) {
|
|
|
6962
7083
|
return where;
|
|
6963
7084
|
}
|
|
6964
7085
|
async function undoConstrainedWrite(store, schema, collection, constraintIndex, constraint, loser, winner) {
|
|
7086
|
+
const keys = groupKeys(constraint);
|
|
6965
7087
|
const ops = await store.getRecordOperations?.(collection, loser.row.id) ?? [];
|
|
6966
7088
|
const state = await store.getRecordFoldState?.(collection, loser.row.id);
|
|
6967
7089
|
const newest = stampTimestamp(state?.u ?? null) ?? loser.key;
|
|
6968
|
-
const parent = `hlc:${
|
|
7090
|
+
const parent = `hlc:${import_core20.HybridLogicalClock.serialize(loser.key)}`;
|
|
6969
7091
|
const rule = `constraint:${collection}:${constraintIndex}:${constraint.type}`;
|
|
6970
7092
|
const transforms = store.getOperationTransforms?.() ?? [];
|
|
6971
7093
|
const losing = new Set(
|
|
6972
7094
|
ops.filter((stored) => {
|
|
6973
|
-
if (
|
|
6974
|
-
const op = (0,
|
|
6975
|
-
return op !== null && op.data !== null &&
|
|
7095
|
+
if (import_core20.HybridLogicalClock.compare(stored.timestamp, loser.key) !== 0) return false;
|
|
7096
|
+
const op = (0, import_core20.operationSchemaView)(stored, schema.version, transforms);
|
|
7097
|
+
return op !== null && op.data !== null && keys.some((field) => op.data !== null && field in op.data);
|
|
6976
7098
|
}).map((op) => op.id)
|
|
6977
7099
|
);
|
|
6978
|
-
const without = (0,
|
|
6979
|
-
const previous = without && (0,
|
|
6980
|
-
const collides = previous !== null && constraint.fields.every((field) => sameValue(previous[field], winner.row[field]))
|
|
7100
|
+
const without = (0, import_core20.foldRecord)(ops, schema, { exclude: losing, transforms }).state;
|
|
7101
|
+
const previous = without && (0, import_core20.isFoldStateLive)(without) ? (0, import_core20.materialize)(without) : null;
|
|
7102
|
+
const collides = previous !== null && constraint.fields.every((field) => sameValue(previous[field], winner.row[field])) && Object.entries(constraint.where ?? {}).every(
|
|
7103
|
+
([field, value]) => sameValue(previous[field], value)
|
|
7104
|
+
);
|
|
6981
7105
|
if (previous === null || collides) {
|
|
6982
7106
|
return {
|
|
6983
7107
|
parent,
|
|
@@ -6992,10 +7116,17 @@ async function undoConstrainedWrite(store, schema, collection, constraintIndex,
|
|
|
6992
7116
|
}
|
|
6993
7117
|
const data = {};
|
|
6994
7118
|
const previousData = {};
|
|
6995
|
-
for (const field of
|
|
7119
|
+
for (const field of keys) {
|
|
7120
|
+
if (sameValue(previous[field], loser.row[field])) continue;
|
|
6996
7121
|
data[field] = previous[field] ?? null;
|
|
6997
7122
|
previousData[field] = loser.row[field] ?? null;
|
|
6998
7123
|
}
|
|
7124
|
+
if (Object.keys(data).length === 0) {
|
|
7125
|
+
for (const field of constraint.fields) {
|
|
7126
|
+
data[field] = previous[field] ?? null;
|
|
7127
|
+
previousData[field] = loser.row[field] ?? null;
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
6999
7130
|
return {
|
|
7000
7131
|
parent,
|
|
7001
7132
|
rule,
|
|
@@ -7014,7 +7145,7 @@ async function uniqueAndCapacityCorrections(store, schema, op) {
|
|
|
7014
7145
|
const definition = schema.collections[op.collection];
|
|
7015
7146
|
if (!definition || op.type === "delete") return [];
|
|
7016
7147
|
const crossRecord = definition.constraints.filter(
|
|
7017
|
-
(constraint) => (constraint.type === "unique" || constraint.type === "capacity") && (op.type === "insert" || constraint.
|
|
7148
|
+
(constraint) => (constraint.type === "unique" || constraint.type === "capacity") && (op.type === "insert" || groupKeys(constraint).some((field) => op.data !== null && field in op.data))
|
|
7018
7149
|
);
|
|
7019
7150
|
if (crossRecord.length === 0 || !store.getRecordFieldVersions) return [];
|
|
7020
7151
|
const row = await store.findRecord(op.collection, op.recordId);
|
|
@@ -7040,7 +7171,7 @@ async function uniqueAndCapacityCorrections(store, schema, op) {
|
|
|
7040
7171
|
members.push({
|
|
7041
7172
|
row: candidate,
|
|
7042
7173
|
versions,
|
|
7043
|
-
key: constrainedVersion(versions, constraint
|
|
7174
|
+
key: constrainedVersion(versions, groupKeys(constraint))
|
|
7044
7175
|
});
|
|
7045
7176
|
}
|
|
7046
7177
|
if (members.length < 2) continue;
|
|
@@ -7074,7 +7205,7 @@ async function referentialCorrections(store, schema, op) {
|
|
|
7074
7205
|
const parentId = child[relation.field];
|
|
7075
7206
|
if (typeof parentId !== "string" || parentId.length === 0) continue;
|
|
7076
7207
|
const parent = await store.getRecordFoldState(relation.to, parentId);
|
|
7077
|
-
if (!parent || parent.cr === null || (0,
|
|
7208
|
+
if (!parent || parent.cr === null || (0, import_core20.isFoldStateLive)(parent)) continue;
|
|
7078
7209
|
const deletedAt = stampTimestamp(parent.d);
|
|
7079
7210
|
const childState = await store.getRecordFoldState(op.collection, op.recordId);
|
|
7080
7211
|
const after = maxTimestamp([deletedAt, stampTimestamp(childState?.u ?? null)]);
|
|
@@ -7121,7 +7252,7 @@ async function referentialCorrections(store, schema, op) {
|
|
|
7121
7252
|
return specs;
|
|
7122
7253
|
}
|
|
7123
7254
|
const deleted = await store.getRecordFoldState(op.collection, op.recordId);
|
|
7124
|
-
if (!deleted || deleted.cr === null || (0,
|
|
7255
|
+
if (!deleted || deleted.cr === null || (0, import_core20.isFoldStateLive)(deleted) || !deleted.d) return specs;
|
|
7125
7256
|
for (const [name, relation] of relations) {
|
|
7126
7257
|
if (relation.to !== op.collection || relation.onDelete !== "restrict") continue;
|
|
7127
7258
|
const children = await store.queryCollection(relation.from, {
|
|
@@ -7243,7 +7374,7 @@ init_ingest_validation();
|
|
|
7243
7374
|
|
|
7244
7375
|
// src/apply/server-side-effect-operation.ts
|
|
7245
7376
|
init_cjs_shims();
|
|
7246
|
-
var
|
|
7377
|
+
var import_core21 = require("@korajs/core");
|
|
7247
7378
|
async function createServerSideEffectOperation(store, parentOp, effect, schemaVersion, sequenceNumber) {
|
|
7248
7379
|
const nodeId = store.getNodeId();
|
|
7249
7380
|
const policy = effect.type === "delete" ? "cascade" : "set-null";
|
|
@@ -7282,7 +7413,7 @@ var serverClocks = /* @__PURE__ */ new WeakMap();
|
|
|
7282
7413
|
function serverClock(store, after) {
|
|
7283
7414
|
let clock = serverClocks.get(store);
|
|
7284
7415
|
if (!clock) {
|
|
7285
|
-
clock = new
|
|
7416
|
+
clock = new import_core21.HybridLogicalClock(store.getNodeId());
|
|
7286
7417
|
serverClocks.set(store, clock);
|
|
7287
7418
|
}
|
|
7288
7419
|
if (after) clock.advanceTo(after);
|
|
@@ -7565,7 +7696,7 @@ function validateOperationShape(op, schema) {
|
|
|
7565
7696
|
for (const [field, value] of Object.entries(op.data)) {
|
|
7566
7697
|
const descriptor = collection.fields[field];
|
|
7567
7698
|
if (!descriptor) continue;
|
|
7568
|
-
const violation2 = (0,
|
|
7699
|
+
const violation2 = (0, import_core22.operationValueViolation)(descriptor, value);
|
|
7569
7700
|
if (violation2 !== null) {
|
|
7570
7701
|
return {
|
|
7571
7702
|
valid: false,
|
|
@@ -7614,9 +7745,152 @@ var NoAuthProvider = class {
|
|
|
7614
7745
|
}
|
|
7615
7746
|
};
|
|
7616
7747
|
|
|
7748
|
+
// src/awareness/presence-records.ts
|
|
7749
|
+
init_cjs_shims();
|
|
7750
|
+
var CACHE_TTL_MS = 1e3;
|
|
7751
|
+
var MAX_TRACKED_RECORDS = 1e4;
|
|
7752
|
+
var MAX_CONCURRENT_READS = 16;
|
|
7753
|
+
var PresenceRecords = class {
|
|
7754
|
+
constructor(store, now = Date.now) {
|
|
7755
|
+
this.store = store;
|
|
7756
|
+
this.now = now;
|
|
7757
|
+
}
|
|
7758
|
+
store;
|
|
7759
|
+
now;
|
|
7760
|
+
clock = 0;
|
|
7761
|
+
/** The clock value of each tracked record's last touch, oldest first. */
|
|
7762
|
+
lastTouch = /* @__PURE__ */ new Map();
|
|
7763
|
+
/** Touches forgotten to bound memory count as having happened at this clock. */
|
|
7764
|
+
forgottenTouchesAt = 0;
|
|
7765
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
7766
|
+
cache = /* @__PURE__ */ new Map();
|
|
7767
|
+
running = 0;
|
|
7768
|
+
waiting = [];
|
|
7769
|
+
/**
|
|
7770
|
+
* Record that writes to these records committed: reads that began before are no
|
|
7771
|
+
* longer current. Null means any record may have changed.
|
|
7772
|
+
*
|
|
7773
|
+
* @param keys - Record keys from {@link presenceRecordKey}, or null for every record
|
|
7774
|
+
*/
|
|
7775
|
+
touch(keys) {
|
|
7776
|
+
this.clock += 1;
|
|
7777
|
+
if (keys === null) {
|
|
7778
|
+
this.forgottenTouchesAt = this.clock;
|
|
7779
|
+
this.lastTouch.clear();
|
|
7780
|
+
this.cache.clear();
|
|
7781
|
+
return;
|
|
7782
|
+
}
|
|
7783
|
+
for (const key of keys) {
|
|
7784
|
+
this.lastTouch.delete(key);
|
|
7785
|
+
this.lastTouch.set(key, this.clock);
|
|
7786
|
+
this.cache.delete(key);
|
|
7787
|
+
}
|
|
7788
|
+
this.forget(this.lastTouch, (value) => {
|
|
7789
|
+
this.forgottenTouchesAt = Math.max(this.forgottenTouchesAt, value);
|
|
7790
|
+
});
|
|
7791
|
+
}
|
|
7792
|
+
/**
|
|
7793
|
+
* True when a write touched the record after a read that began at `asOf`.
|
|
7794
|
+
*
|
|
7795
|
+
* @param key - Record key from {@link presenceRecordKey}
|
|
7796
|
+
* @param asOf - The read's {@link PresenceRecordRead.asOf}
|
|
7797
|
+
*/
|
|
7798
|
+
touchedSince(key, asOf) {
|
|
7799
|
+
return (this.lastTouch.get(key) ?? this.forgottenTouchesAt) > asOf;
|
|
7800
|
+
}
|
|
7801
|
+
/**
|
|
7802
|
+
* A current read of the record that is still fresh enough to reuse, without a store
|
|
7803
|
+
* read; undefined when the record must be read.
|
|
7804
|
+
*
|
|
7805
|
+
* @param collection - The record's collection
|
|
7806
|
+
* @param recordId - The record's id
|
|
7807
|
+
*/
|
|
7808
|
+
peek(collection, recordId) {
|
|
7809
|
+
const key = presenceRecordKey(collection, recordId);
|
|
7810
|
+
const cached = this.cache.get(key);
|
|
7811
|
+
if (!cached) return void 0;
|
|
7812
|
+
if (this.now() - cached.atMs >= CACHE_TTL_MS || this.touchedSince(key, cached.startedAt)) {
|
|
7813
|
+
this.cache.delete(key);
|
|
7814
|
+
return void 0;
|
|
7815
|
+
}
|
|
7816
|
+
return { stored: cached.stored, asOf: cached.startedAt };
|
|
7817
|
+
}
|
|
7818
|
+
/**
|
|
7819
|
+
* Read the record from the store, joining a read of it already running when no
|
|
7820
|
+
* write touched the record since that read began. Rejects when the store read fails.
|
|
7821
|
+
*
|
|
7822
|
+
* @param collection - The record's collection
|
|
7823
|
+
* @param recordId - The record's id
|
|
7824
|
+
*/
|
|
7825
|
+
async read(collection, recordId) {
|
|
7826
|
+
const key = presenceRecordKey(collection, recordId);
|
|
7827
|
+
let entry = this.inFlight.get(key);
|
|
7828
|
+
if (!entry || this.touchedSince(key, entry.startedAt)) {
|
|
7829
|
+
const startedAt = this.clock;
|
|
7830
|
+
const created = {
|
|
7831
|
+
startedAt,
|
|
7832
|
+
promise: this.readStore(collection, recordId)
|
|
7833
|
+
};
|
|
7834
|
+
entry = created;
|
|
7835
|
+
this.inFlight.set(key, created);
|
|
7836
|
+
void created.promise.then(
|
|
7837
|
+
(stored2) => {
|
|
7838
|
+
if (this.inFlight.get(key) === created) this.inFlight.delete(key);
|
|
7839
|
+
if (this.touchedSince(key, startedAt)) return;
|
|
7840
|
+
this.cache.delete(key);
|
|
7841
|
+
this.cache.set(key, { stored: stored2, startedAt, atMs: this.now() });
|
|
7842
|
+
this.forget(this.cache, () => {
|
|
7843
|
+
});
|
|
7844
|
+
},
|
|
7845
|
+
() => {
|
|
7846
|
+
if (this.inFlight.get(key) === created) this.inFlight.delete(key);
|
|
7847
|
+
}
|
|
7848
|
+
);
|
|
7849
|
+
}
|
|
7850
|
+
const stored = await entry.promise;
|
|
7851
|
+
return { stored, asOf: entry.startedAt };
|
|
7852
|
+
}
|
|
7853
|
+
/** Forget every read and touch (the server stopped). */
|
|
7854
|
+
clear() {
|
|
7855
|
+
this.touch(null);
|
|
7856
|
+
this.inFlight.clear();
|
|
7857
|
+
}
|
|
7858
|
+
async readStore(collection, recordId) {
|
|
7859
|
+
if (this.running < MAX_CONCURRENT_READS) {
|
|
7860
|
+
this.running += 1;
|
|
7861
|
+
} else {
|
|
7862
|
+
await new Promise((resolve2) => this.waiting.push(resolve2));
|
|
7863
|
+
}
|
|
7864
|
+
try {
|
|
7865
|
+
const rows = await this.store.queryCollection(collection, {
|
|
7866
|
+
where: { id: recordId },
|
|
7867
|
+
includeDeleted: true,
|
|
7868
|
+
limit: 1
|
|
7869
|
+
});
|
|
7870
|
+
return rows[0] ?? null;
|
|
7871
|
+
} finally {
|
|
7872
|
+
const next = this.waiting.shift();
|
|
7873
|
+
if (next) next();
|
|
7874
|
+
else this.running -= 1;
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
forget(map, onForget) {
|
|
7878
|
+
while (map.size > MAX_TRACKED_RECORDS) {
|
|
7879
|
+
const oldest = map.keys().next();
|
|
7880
|
+
if (oldest.done) return;
|
|
7881
|
+
const value = map.get(oldest.value);
|
|
7882
|
+
map.delete(oldest.value);
|
|
7883
|
+
if (value !== void 0) onForget(value);
|
|
7884
|
+
}
|
|
7885
|
+
}
|
|
7886
|
+
};
|
|
7887
|
+
function presenceRecordKey(collection, recordId) {
|
|
7888
|
+
return `${collection}\0${recordId}`;
|
|
7889
|
+
}
|
|
7890
|
+
|
|
7617
7891
|
// src/encryption/key-record-service.ts
|
|
7618
7892
|
init_cjs_shims();
|
|
7619
|
-
var
|
|
7893
|
+
var import_core23 = require("@korajs/core");
|
|
7620
7894
|
var import_sync3 = require("@korajs/sync");
|
|
7621
7895
|
var ANONYMOUS_KEY_OWNER = "*";
|
|
7622
7896
|
function userKeyOwner(userId) {
|
|
@@ -7646,7 +7920,7 @@ var EncryptionKeyService = class {
|
|
|
7646
7920
|
const reply = (status, record, text3) => ({
|
|
7647
7921
|
response: {
|
|
7648
7922
|
type: "encryption-key-response",
|
|
7649
|
-
messageId: (0,
|
|
7923
|
+
messageId: (0, import_core23.generateUUIDv7)(),
|
|
7650
7924
|
requestId: message.requestId,
|
|
7651
7925
|
keyring: message.keyring,
|
|
7652
7926
|
status,
|
|
@@ -7756,7 +8030,7 @@ init_cjs_shims();
|
|
|
7756
8030
|
|
|
7757
8031
|
// src/richtext/blob-access-index.ts
|
|
7758
8032
|
init_cjs_shims();
|
|
7759
|
-
var
|
|
8033
|
+
var import_core24 = require("@korajs/core");
|
|
7760
8034
|
var DEFAULT_BLOB_ACCESS_CACHE_TTL_MS = 5e3;
|
|
7761
8035
|
var MAX_KNOWN_MANIFESTS = 1e4;
|
|
7762
8036
|
var MAX_MEMORY_BLOB_OWNERS = 1e5;
|
|
@@ -7970,7 +8244,7 @@ var BlobAccessIndex = class {
|
|
|
7970
8244
|
bytes = null;
|
|
7971
8245
|
}
|
|
7972
8246
|
if (bytes === null) continue;
|
|
7973
|
-
if (await (0,
|
|
8247
|
+
if (await (0, import_core24.hashBlob)(bytes) !== manifestHash) continue;
|
|
7974
8248
|
this.observeVerifiedBytes(manifestHash, bytes);
|
|
7975
8249
|
}
|
|
7976
8250
|
}
|
|
@@ -8050,7 +8324,7 @@ function asBlobRef(value) {
|
|
|
8050
8324
|
return null;
|
|
8051
8325
|
}
|
|
8052
8326
|
}
|
|
8053
|
-
return (0,
|
|
8327
|
+
return (0, import_core24.isBlobRef)(candidate) ? candidate : null;
|
|
8054
8328
|
}
|
|
8055
8329
|
function parseManifestChunks(bytes) {
|
|
8056
8330
|
if (bytes.byteLength > 4 * 1024 * 1024) return null;
|
|
@@ -8151,8 +8425,8 @@ function asRecord2(value) {
|
|
|
8151
8425
|
|
|
8152
8426
|
// src/scopes/resolve-session-scopes.ts
|
|
8153
8427
|
init_cjs_shims();
|
|
8154
|
-
var
|
|
8155
|
-
var ScopeRequiredError = class extends
|
|
8428
|
+
var import_core25 = require("@korajs/core");
|
|
8429
|
+
var ScopeRequiredError = class extends import_core25.KoraError {
|
|
8156
8430
|
constructor(denied) {
|
|
8157
8431
|
super(
|
|
8158
8432
|
`Sync scope required: ${denied.map((d) => `"${d.collection}" needs ${d.missingKeys.map((k) => `"${k}"`).join(", ")}`).join("; ")}. The server could not derive these values from the verified token.`,
|
|
@@ -8176,7 +8450,7 @@ function resolveSessionScopeGrant(schema, options) {
|
|
|
8176
8450
|
const denied = [];
|
|
8177
8451
|
let grant;
|
|
8178
8452
|
if (authScopes !== void 0 || scopeValues !== void 0) {
|
|
8179
|
-
const claims = mergeClaims((0,
|
|
8453
|
+
const claims = mergeClaims((0, import_core25.getScopeClaims)(authScopes), scopeValues);
|
|
8180
8454
|
const explicit = withoutReservedKeys(authScopes ?? {});
|
|
8181
8455
|
assertScopeValuesDefined(explicit);
|
|
8182
8456
|
if (!schema && claims && Object.keys(explicit).length === 0) {
|
|
@@ -8212,10 +8486,10 @@ function resolveSessionScopeGrant(schema, options) {
|
|
|
8212
8486
|
}
|
|
8213
8487
|
function bindClaimsToSchema(schema, claims, denied) {
|
|
8214
8488
|
const result = {};
|
|
8215
|
-
const partialSync = (0,
|
|
8489
|
+
const partialSync = (0, import_core25.hasSchemaSyncRules)(schema);
|
|
8216
8490
|
for (const collection of Object.keys(schema.collections)) {
|
|
8217
|
-
if (partialSync && !(0,
|
|
8218
|
-
const bindings = (0,
|
|
8491
|
+
if (partialSync && !(0, import_core25.isCollectionSyncScoped)(schema, collection)) continue;
|
|
8492
|
+
const bindings = (0, import_core25.getCollectionScopeBindings)(schema, collection);
|
|
8219
8493
|
if (!bindings) {
|
|
8220
8494
|
result[collection] = {};
|
|
8221
8495
|
continue;
|
|
@@ -8271,7 +8545,7 @@ function mergeClaims(fromGrant, scopeValues) {
|
|
|
8271
8545
|
function withoutReservedKeys(scopes) {
|
|
8272
8546
|
const result = {};
|
|
8273
8547
|
for (const [collection, predicate] of Object.entries(scopes)) {
|
|
8274
|
-
if (collection ===
|
|
8548
|
+
if (collection === import_core25.SCOPE_CLAIMS_KEY) continue;
|
|
8275
8549
|
result[collection] = { ...predicate ?? {} };
|
|
8276
8550
|
}
|
|
8277
8551
|
return result;
|
|
@@ -8301,7 +8575,7 @@ init_server_store();
|
|
|
8301
8575
|
|
|
8302
8576
|
// src/session/duplicate-identity.ts
|
|
8303
8577
|
init_cjs_shims();
|
|
8304
|
-
var
|
|
8578
|
+
var import_core26 = require("@korajs/core");
|
|
8305
8579
|
var import_internal5 = require("@korajs/core/internal");
|
|
8306
8580
|
var import_internal6 = require("@korajs/sync/internal");
|
|
8307
8581
|
var FORGED_DUPLICATE_CODE = "FORGED_DUPLICATE";
|
|
@@ -8313,11 +8587,11 @@ function isSameStoredOperation(upload, stored) {
|
|
|
8313
8587
|
const b = contentKey(stored, storedVersion);
|
|
8314
8588
|
return a !== null && a === b;
|
|
8315
8589
|
}
|
|
8316
|
-
function contentKey(op,
|
|
8590
|
+
function contentKey(op, version2) {
|
|
8317
8591
|
let body;
|
|
8318
8592
|
try {
|
|
8319
|
-
body = (0,
|
|
8320
|
-
|
|
8593
|
+
body = (0, import_core26.canonicalizeOperationBody)(
|
|
8594
|
+
version2 === 1 ? (0, import_core26.canonicalizeLegacyOperation)({ ...op, hashVersion: void 0 }) : op
|
|
8321
8595
|
);
|
|
8322
8596
|
} catch {
|
|
8323
8597
|
return null;
|
|
@@ -8328,12 +8602,12 @@ function contentKey(op, version) {
|
|
|
8328
8602
|
collection: op.collection,
|
|
8329
8603
|
recordId: op.recordId,
|
|
8330
8604
|
data: body.data,
|
|
8331
|
-
timestamp:
|
|
8605
|
+
timestamp: import_core26.HybridLogicalClock.serialize(op.timestamp),
|
|
8332
8606
|
nodeId: op.nodeId,
|
|
8333
8607
|
atomicOps,
|
|
8334
8608
|
sealed: op.encrypted !== void 0
|
|
8335
8609
|
};
|
|
8336
|
-
if (
|
|
8610
|
+
if (version2 === 2) {
|
|
8337
8611
|
key.previousData = body.previousData;
|
|
8338
8612
|
key.sequenceNumber = op.sequenceNumber;
|
|
8339
8613
|
key.causalDeps = [...op.causalDeps].sort();
|
|
@@ -8354,7 +8628,7 @@ async function isRewrittenEcho(upload, stored, schema) {
|
|
|
8354
8628
|
return !await (0, import_internal6.operationIdMatches)(upload, schema);
|
|
8355
8629
|
}
|
|
8356
8630
|
function sameHeader(a, b) {
|
|
8357
|
-
return a.nodeId === b.nodeId && a.type === b.type && a.collection === b.collection && a.recordId === b.recordId &&
|
|
8631
|
+
return a.nodeId === b.nodeId && a.type === b.type && a.collection === b.collection && a.recordId === b.recordId && import_core26.HybridLogicalClock.serialize(a.timestamp) === import_core26.HybridLogicalClock.serialize(b.timestamp);
|
|
8358
8632
|
}
|
|
8359
8633
|
|
|
8360
8634
|
// src/session/client-session.ts
|
|
@@ -8362,14 +8636,14 @@ init_operation_validation();
|
|
|
8362
8636
|
|
|
8363
8637
|
// src/session/scope-entry.ts
|
|
8364
8638
|
init_cjs_shims();
|
|
8365
|
-
var
|
|
8639
|
+
var import_core27 = require("@korajs/core");
|
|
8366
8640
|
var SCOPE_ENTRY_NODE_ID = "kora:scope-entry";
|
|
8367
8641
|
async function buildScopeEntryOperation(input) {
|
|
8368
8642
|
const { trigger, row, schema } = input;
|
|
8369
8643
|
const definition = schema.collections[trigger.collection];
|
|
8370
8644
|
const data = {};
|
|
8371
8645
|
const foldState = input.foldState ?? null;
|
|
8372
|
-
const versioned = input.fieldVersions ?? (foldState ? (0,
|
|
8646
|
+
const versioned = input.fieldVersions ?? (foldState ? (0, import_core27.getFoldFieldVersions)(foldState) : null) ?? null;
|
|
8373
8647
|
const fieldVersions = {};
|
|
8374
8648
|
for (const field of Object.keys(definition?.fields ?? {})) {
|
|
8375
8649
|
if (!(field in row)) continue;
|
|
@@ -8380,7 +8654,7 @@ async function buildScopeEntryOperation(input) {
|
|
|
8380
8654
|
fieldVersions[field] = { ...versioned.fields[field] ?? versioned.created };
|
|
8381
8655
|
}
|
|
8382
8656
|
}
|
|
8383
|
-
const id = await (0,
|
|
8657
|
+
const id = await (0, import_core27.hashBlob)(
|
|
8384
8658
|
new TextEncoder().encode(
|
|
8385
8659
|
`scope-entry\0${trigger.collection}\0${trigger.recordId}\0${trigger.id}`
|
|
8386
8660
|
)
|
|
@@ -8413,15 +8687,15 @@ function toWireValue(value) {
|
|
|
8413
8687
|
|
|
8414
8688
|
// src/session/session-operation-limits.ts
|
|
8415
8689
|
init_cjs_shims();
|
|
8416
|
-
var import_core27 = require("@korajs/core");
|
|
8417
8690
|
var import_core28 = require("@korajs/core");
|
|
8418
|
-
var
|
|
8691
|
+
var import_core29 = require("@korajs/core");
|
|
8692
|
+
var DEFAULT_MAX_OPERATION_BYTES = import_core28.DEFAULT_MAX_OPERATION_BYTES;
|
|
8419
8693
|
var DEFAULT_MAX_OPS_PER_MINUTE = 600;
|
|
8420
8694
|
var DEFAULT_MAX_BLOB_REQUESTS_PER_MINUTE = 6e3;
|
|
8421
8695
|
var BATCH_LOOKUP_RATE_COST = 1;
|
|
8422
8696
|
var DEFAULT_MAX_OPS_PER_BATCH = 1e3;
|
|
8423
8697
|
function measureOperationBytes(op) {
|
|
8424
|
-
return (0,
|
|
8698
|
+
return (0, import_core29.measureOperationBytes)(op);
|
|
8425
8699
|
}
|
|
8426
8700
|
function validateOperationSize(op, maxBytes = DEFAULT_MAX_OPERATION_BYTES) {
|
|
8427
8701
|
const bytes = measureOperationBytes(op);
|
|
@@ -8542,7 +8816,7 @@ function generateNodeToken() {
|
|
|
8542
8816
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8543
8817
|
}
|
|
8544
8818
|
async function nodeTokenHash(nodeToken) {
|
|
8545
|
-
return (0,
|
|
8819
|
+
return (0, import_core31.hashBlob)(new TextEncoder().encode(nodeToken));
|
|
8546
8820
|
}
|
|
8547
8821
|
function samePrincipal(a, b) {
|
|
8548
8822
|
if (a.anonymous === true || b.anonymous === true)
|
|
@@ -8558,24 +8832,24 @@ function revocationMatches(principal, filter) {
|
|
|
8558
8832
|
return true;
|
|
8559
8833
|
}
|
|
8560
8834
|
var warnedUnscopedProviders = /* @__PURE__ */ new WeakSet();
|
|
8561
|
-
function warnIfMultiTenantWithoutScopes(auth
|
|
8562
|
-
if (!auth || auth instanceof NoAuthProvider) {
|
|
8563
|
-
return;
|
|
8564
|
-
}
|
|
8565
|
-
if (resolvedScopes) {
|
|
8566
|
-
return;
|
|
8567
|
-
}
|
|
8568
|
-
if (!schema || Object.keys(schema.collections).length === 0) {
|
|
8569
|
-
return;
|
|
8570
|
-
}
|
|
8835
|
+
function warnIfMultiTenantWithoutScopes(auth) {
|
|
8571
8836
|
if (warnedUnscopedProviders.has(auth)) {
|
|
8572
8837
|
return;
|
|
8573
8838
|
}
|
|
8574
8839
|
warnedUnscopedProviders.add(auth);
|
|
8575
8840
|
console.warn(
|
|
8576
|
-
"[kora] An authenticated session resolved to no sync scopes, so every user will sync every other user's data. Return per-user sync scopes from your auth provider (for example KoraAuthProvider's resolveScopes
|
|
8841
|
+
"[kora] An authenticated session resolved to no sync scopes: its grant restricts no collection, so every user will sync every other user's data. Return per-user sync scopes from your auth provider (for example KoraAuthProvider's resolveScopes or scopeValues) and declare schema sync rules that bind them, to isolate tenants. If all signed-in users are meant to share the same data, set unscopedSharing: 'allow' in the sync server options to silence this; 'refuse' turns it into a handshake refusal."
|
|
8577
8842
|
);
|
|
8578
8843
|
}
|
|
8844
|
+
function sharesEveryUsersData(auth, principal, downlink, schema) {
|
|
8845
|
+
if (downlink === null) return false;
|
|
8846
|
+
if (!auth || auth instanceof NoAuthProvider) return false;
|
|
8847
|
+
if (!principal || principal.anonymous === true) return false;
|
|
8848
|
+
if (!schema || Object.keys(schema.collections).length === 0) return false;
|
|
8849
|
+
if (downlink === void 0) return true;
|
|
8850
|
+
const collections = Object.values(downlink);
|
|
8851
|
+
return collections.length > 0 && collections.every((predicate) => Object.keys(predicate).length === 0);
|
|
8852
|
+
}
|
|
8579
8853
|
function stableStringify(value) {
|
|
8580
8854
|
if (Array.isArray(value)) {
|
|
8581
8855
|
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
@@ -8587,10 +8861,16 @@ function stableStringify(value) {
|
|
|
8587
8861
|
}
|
|
8588
8862
|
return JSON.stringify(value);
|
|
8589
8863
|
}
|
|
8590
|
-
function
|
|
8591
|
-
|
|
8864
|
+
function sameNormalizedScopeMap(a, b) {
|
|
8865
|
+
if (a === b) return true;
|
|
8866
|
+
return stableStringify(a ?? null) === stableStringify(b ?? null);
|
|
8867
|
+
}
|
|
8868
|
+
function sameScopeMap(a, b, maxValues) {
|
|
8869
|
+
const normalize = (value) => value && typeof value === "object" ? normalizeScopeMap(value, maxValues) : value;
|
|
8592
8870
|
return stableStringify(normalize(a) ?? null) === stableStringify(normalize(b) ?? null);
|
|
8593
8871
|
}
|
|
8872
|
+
var MAX_PRESENCE_LOOKUPS_PER_MINUTE = 1200;
|
|
8873
|
+
var MAX_PRESENCE_REREADS = 2;
|
|
8594
8874
|
var DEFAULT_MAX_BLOB_CHUNK_BYTES = 1024 * 1024;
|
|
8595
8875
|
var DEFAULT_MAX_BLOB_BYTES_PER_SESSION = 256 * 1024 * 1024;
|
|
8596
8876
|
var ClientSession = class _ClientSession {
|
|
@@ -8614,6 +8894,8 @@ var ClientSession = class _ClientSession {
|
|
|
8614
8894
|
/** A provisional anonymous claim awaiting the device's confirmation (RT-21). */
|
|
8615
8895
|
pendingNodeClaim = null;
|
|
8616
8896
|
allowLegacyAnonymousClaims;
|
|
8897
|
+
deviceNodeHandover;
|
|
8898
|
+
unscopedSharing;
|
|
8617
8899
|
anonymousClaimTtlMs;
|
|
8618
8900
|
isNodeLive;
|
|
8619
8901
|
/** The credential presented at handshake, kept to re-validate the session (RT-18). */
|
|
@@ -8686,7 +8968,6 @@ var ClientSession = class _ClientSession {
|
|
|
8686
8968
|
* `collection\u0000id`; null marks a record known to be absent. Only set while a
|
|
8687
8969
|
* chunk is filtered.
|
|
8688
8970
|
*/
|
|
8689
|
-
recordLookupCache = null;
|
|
8690
8971
|
deliveryHighWaterBytes;
|
|
8691
8972
|
handshakeTimeoutMs;
|
|
8692
8973
|
handshakeTimer = null;
|
|
@@ -8737,6 +9018,15 @@ var ClientSession = class _ClientSession {
|
|
|
8737
9018
|
maxOperationBytes;
|
|
8738
9019
|
maxOpsPerMinute;
|
|
8739
9020
|
maxOpsPerBatch;
|
|
9021
|
+
maxScopePredicateValues;
|
|
9022
|
+
/** {@link getScopePartitionKey}, cached for the scope map it was computed from. */
|
|
9023
|
+
scopePartitionKeyCache = null;
|
|
9024
|
+
/** Store reads for presence cursors (F16), per minute. */
|
|
9025
|
+
presenceLookupLimiter = new SessionRateLimiter(MAX_PRESENCE_LOOKUPS_PER_MINUTE);
|
|
9026
|
+
/** Reads the records presence cursors name (shared by the server's sessions). */
|
|
9027
|
+
presenceRecords;
|
|
9028
|
+
/** A {@link refreshScopes} request arrived during the handshake: re-check once streaming. */
|
|
9029
|
+
scopeRefreshPending = false;
|
|
8740
9030
|
rateLimiter;
|
|
8741
9031
|
/** Separate budget for blob chunk requests (RT-24). */
|
|
8742
9032
|
blobRateLimiter;
|
|
@@ -8789,6 +9079,7 @@ var ClientSession = class _ClientSession {
|
|
|
8789
9079
|
this.operationTransforms = options.operationTransforms ?? [];
|
|
8790
9080
|
this.onRelay = options.onRelay ?? null;
|
|
8791
9081
|
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
9082
|
+
this.presenceRecords = options.presenceRecords ?? new PresenceRecords(options.store);
|
|
8792
9083
|
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
8793
9084
|
this.onBlobChunkRequest = options.onBlobChunkRequest ?? null;
|
|
8794
9085
|
this.onBlobChunkResponse = options.onBlobChunkResponse ?? null;
|
|
@@ -8815,10 +9106,13 @@ var ClientSession = class _ClientSession {
|
|
|
8815
9106
|
options.blobRequestWindowMs ?? 6e4
|
|
8816
9107
|
);
|
|
8817
9108
|
this.maxOpsPerBatch = options.maxOpsPerBatch ?? DEFAULT_MAX_OPS_PER_BATCH;
|
|
9109
|
+
this.maxScopePredicateValues = options.maxScopePredicateValues ?? DEFAULT_MAX_SCOPE_PREDICATE_VALUES;
|
|
8818
9110
|
this.validateOperation = options.validateOperation ?? null;
|
|
8819
9111
|
this.koraContext = options.koraContext ?? null;
|
|
8820
9112
|
this.blobAccess = options.blobAccess ?? null;
|
|
8821
9113
|
this.allowLegacyAnonymousClaims = options.allowLegacyAnonymousClaims ?? true;
|
|
9114
|
+
this.deviceNodeHandover = options.deviceNodeHandover ?? true;
|
|
9115
|
+
this.unscopedSharing = options.unscopedSharing ?? "warn";
|
|
8822
9116
|
this.anonymousClaimTtlMs = options.anonymousClaimTtlMs ?? DEFAULT_ANONYMOUS_CLAIM_TTL_MS;
|
|
8823
9117
|
this.isNodeLive = options.isNodeLive ?? null;
|
|
8824
9118
|
this.deliveryHighWaterBytes = options.deliveryHighWaterBytes ?? DEFAULT_DELIVERY_HIGH_WATER_BYTES;
|
|
@@ -8919,7 +9213,7 @@ var ClientSession = class _ClientSession {
|
|
|
8919
9213
|
const serializedOps = visibleOperations.map((op) => this.serializer.encodeOperation(op));
|
|
8920
9214
|
const msg = {
|
|
8921
9215
|
type: "operation-batch",
|
|
8922
|
-
messageId: (0,
|
|
9216
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
8923
9217
|
operations: serializedOps,
|
|
8924
9218
|
...retractions.length > 0 ? { retractions } : {},
|
|
8925
9219
|
isFinal: true,
|
|
@@ -9218,6 +9512,44 @@ var ClientSession = class _ClientSession {
|
|
|
9218
9512
|
this.terminate(expired ? "AUTH_EXPIRED" : "AUTH_REVOKED");
|
|
9219
9513
|
return "terminated";
|
|
9220
9514
|
}
|
|
9515
|
+
/**
|
|
9516
|
+
* Re-resolve this session's grant on request (`KoraSyncServer.refreshScopes`), for
|
|
9517
|
+
* the user named. A streaming session whose principal is that user re-authenticates
|
|
9518
|
+
* now and ends with a retriable `SCOPE_CHANGED` when its grant changed (or
|
|
9519
|
+
* `AUTH_REVOKED` when the credential is no longer accepted). A session still in its
|
|
9520
|
+
* handshake may have resolved its grant from membership read before the change, so
|
|
9521
|
+
* it re-checks as soon as it reaches streaming; a session whose principal is not
|
|
9522
|
+
* known yet does the same (the re-check is a no-op for a grant that did not change).
|
|
9523
|
+
*
|
|
9524
|
+
* @param userId - The user whose grant changed
|
|
9525
|
+
* @returns The revalidation outcome, `'deferred'` when the check waits for the
|
|
9526
|
+
* handshake, or `'skipped'` for another user's (or a closed) session
|
|
9527
|
+
*/
|
|
9528
|
+
async refreshScopes(userId) {
|
|
9529
|
+
if (this.state === "closed") return "skipped";
|
|
9530
|
+
const principal = this.principal;
|
|
9531
|
+
if (principal && principal.userId !== userId) return "skipped";
|
|
9532
|
+
if (!principal || this.state !== "streaming" && this.state !== "syncing") {
|
|
9533
|
+
this.scopeRefreshPending = true;
|
|
9534
|
+
return "deferred";
|
|
9535
|
+
}
|
|
9536
|
+
return this.revalidateCredential();
|
|
9537
|
+
}
|
|
9538
|
+
/** Run a re-check deferred by {@link refreshScopes} once the session streams. */
|
|
9539
|
+
runDeferredScopeRefresh() {
|
|
9540
|
+
if (!this.scopeRefreshPending) return;
|
|
9541
|
+
this.scopeRefreshPending = false;
|
|
9542
|
+
void this.revalidateCredential().then((outcome) => {
|
|
9543
|
+
if (outcome !== "error") return;
|
|
9544
|
+
this.logger?.log({
|
|
9545
|
+
timestamp: Date.now(),
|
|
9546
|
+
level: "warn",
|
|
9547
|
+
event: "session.revalidation_failed",
|
|
9548
|
+
sessionId: this.sessionId,
|
|
9549
|
+
details: { reason: "deferred scope refresh" }
|
|
9550
|
+
});
|
|
9551
|
+
});
|
|
9552
|
+
}
|
|
9221
9553
|
/**
|
|
9222
9554
|
* True when `context` (a fresh authentication of this session's principal)
|
|
9223
9555
|
* resolves to download or upload scopes other than the ones this session holds.
|
|
@@ -9227,7 +9559,7 @@ var ClientSession = class _ClientSession {
|
|
|
9227
9559
|
if (this.state !== "streaming" && this.state !== "syncing") return false;
|
|
9228
9560
|
const resolution = this.computeSessionScopes(context, this.handshakeScope);
|
|
9229
9561
|
if (!resolution.ok) return true;
|
|
9230
|
-
return !
|
|
9562
|
+
return !sameNormalizedScopeMap(resolution.downlink, this.authContext?.downlinkScopes) || !sameNormalizedScopeMap(resolution.uplink, this.authContext?.uplinkScopes);
|
|
9231
9563
|
}
|
|
9232
9564
|
/**
|
|
9233
9565
|
* End this session because an admin released its node id (RT-5). Retriable: a
|
|
@@ -9277,7 +9609,7 @@ var ClientSession = class _ClientSession {
|
|
|
9277
9609
|
this.appHeartbeatTimer = setInterval(() => {
|
|
9278
9610
|
if (this.state === "closed") return;
|
|
9279
9611
|
if (Date.now() - this.lastClientSendAtMs < interval / 2) return;
|
|
9280
|
-
this.sendToClient({ type: "heartbeat", messageId: (0,
|
|
9612
|
+
this.sendToClient({ type: "heartbeat", messageId: (0, import_core31.generateUUIDv7)() });
|
|
9281
9613
|
}, interval);
|
|
9282
9614
|
this.appHeartbeatTimer.unref?.();
|
|
9283
9615
|
}
|
|
@@ -9370,18 +9702,35 @@ var ClientSession = class _ClientSession {
|
|
|
9370
9702
|
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
9371
9703
|
return recordMatchesScopes(collection, { ...storedRecord ?? {}, id: recordId }, scopes);
|
|
9372
9704
|
}
|
|
9373
|
-
/**
|
|
9374
|
-
* A stable key for this session's download scope. Presence (awareness) is only
|
|
9375
|
-
* relayed between sessions that share exactly the same key, so it never crosses a
|
|
9376
|
-
* tenant boundary. Unscoped sessions share the key `"*"`.
|
|
9377
|
-
*/
|
|
9378
9705
|
/** This session's download scope, or undefined when it is unscoped. */
|
|
9379
9706
|
getDownlinkScopes() {
|
|
9380
9707
|
return this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
9381
9708
|
}
|
|
9709
|
+
/**
|
|
9710
|
+
* A stable key for this session's download scope: sessions with the same key see
|
|
9711
|
+
* exactly the same data. Unscoped sessions share the key `"*"`. Cached per scope
|
|
9712
|
+
* map, since a large `$in` grant makes the key costly and the relays read it often.
|
|
9713
|
+
*/
|
|
9382
9714
|
getScopePartitionKey() {
|
|
9383
9715
|
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
9384
|
-
|
|
9716
|
+
if (!scopes) return "*";
|
|
9717
|
+
if (this.scopePartitionKeyCache?.scopes !== scopes) {
|
|
9718
|
+
this.scopePartitionKeyCache = {
|
|
9719
|
+
scopes,
|
|
9720
|
+
key: stableStringify(normalizeScopeMap(scopes, this.maxScopePredicateValues))
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9723
|
+
return this.scopePartitionKeyCache.key;
|
|
9724
|
+
}
|
|
9725
|
+
/**
|
|
9726
|
+
* Partition for presence without a cursor (a "who is online" state that names no
|
|
9727
|
+
* record): only sessions with the same key see each other's. Signed-in sessions
|
|
9728
|
+
* use their download scope key; every anonymous session is its own partition,
|
|
9729
|
+
* because sessions holding the same anonymous grant are unrelated people.
|
|
9730
|
+
*/
|
|
9731
|
+
getPresencePartitionKey() {
|
|
9732
|
+
const key = this.getScopePartitionKey();
|
|
9733
|
+
return this.principal?.anonymous === true ? `anonymous:${this.sessionId}|${key}` : key;
|
|
9385
9734
|
}
|
|
9386
9735
|
/**
|
|
9387
9736
|
* Get the transport for this session.
|
|
@@ -9446,7 +9795,7 @@ var ClientSession = class _ClientSession {
|
|
|
9446
9795
|
case "heartbeat":
|
|
9447
9796
|
break;
|
|
9448
9797
|
case "awareness-update":
|
|
9449
|
-
this.handleAwarenessUpdate(message);
|
|
9798
|
+
await this.handleAwarenessUpdate(message);
|
|
9450
9799
|
break;
|
|
9451
9800
|
case "yjs-doc-update":
|
|
9452
9801
|
await this.handleYjsDocUpdate(message);
|
|
@@ -9497,7 +9846,7 @@ var ClientSession = class _ClientSession {
|
|
|
9497
9846
|
if (this.state !== "syncing" && this.state !== "streaming") return;
|
|
9498
9847
|
this.sendToClient({
|
|
9499
9848
|
type: "encryption-key-response",
|
|
9500
|
-
messageId: (0,
|
|
9849
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
9501
9850
|
keyring,
|
|
9502
9851
|
status: "ok",
|
|
9503
9852
|
record
|
|
@@ -9507,7 +9856,7 @@ var ClientSession = class _ClientSession {
|
|
|
9507
9856
|
const refuse = (status, text3) => {
|
|
9508
9857
|
this.sendToClient({
|
|
9509
9858
|
type: "encryption-key-response",
|
|
9510
|
-
messageId: (0,
|
|
9859
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
9511
9860
|
requestId: message.requestId,
|
|
9512
9861
|
keyring: message.keyring,
|
|
9513
9862
|
status,
|
|
@@ -9557,7 +9906,7 @@ var ClientSession = class _ClientSession {
|
|
|
9557
9906
|
);
|
|
9558
9907
|
return;
|
|
9559
9908
|
}
|
|
9560
|
-
const actual = await (0,
|
|
9909
|
+
const actual = await (0, import_core31.hashBlob)(bytes);
|
|
9561
9910
|
if (actual !== message.hash) {
|
|
9562
9911
|
return;
|
|
9563
9912
|
}
|
|
@@ -9622,10 +9971,15 @@ var ClientSession = class _ClientSession {
|
|
|
9622
9971
|
if (!store.getNodeClaimOwner || !store.replaceNodeClaim) return { ok: false };
|
|
9623
9972
|
const current = await store.getNodeClaimOwner(nodeId);
|
|
9624
9973
|
if (current === null) {
|
|
9625
|
-
if (!this.allowLegacyAnonymousClaims
|
|
9974
|
+
if (!this.allowLegacyAnonymousClaims) return { ok: false };
|
|
9626
9975
|
if (this.isNodeLive?.(nodeId, this.sessionId) === true) return { ok: false };
|
|
9627
|
-
if (
|
|
9628
|
-
|
|
9976
|
+
if (store.claimUnownedNode) {
|
|
9977
|
+
if (!await store.claimUnownedNode(nodeId, owner)) return { ok: false };
|
|
9978
|
+
} else {
|
|
9979
|
+
if (!store.releaseNodeClaim) return { ok: false };
|
|
9980
|
+
if (!await store.releaseNodeClaim(nodeId)) return { ok: false };
|
|
9981
|
+
if (!await store.claimNode(nodeId, owner)) return { ok: false };
|
|
9982
|
+
}
|
|
9629
9983
|
this.warnLegacyAnonymousClaim(nodeId);
|
|
9630
9984
|
return issued;
|
|
9631
9985
|
}
|
|
@@ -9647,6 +10001,68 @@ var ClientSession = class _ClientSession {
|
|
|
9647
10001
|
if (legacy) this.warnLegacyAnonymousClaim(nodeId);
|
|
9648
10002
|
return issued;
|
|
9649
10003
|
}
|
|
10004
|
+
/**
|
|
10005
|
+
* Automatic device handover after an upgrade from beta.12 (F1). A node with
|
|
10006
|
+
* history but no owner (written before node claims existed) or released by an
|
|
10007
|
+
* admin is claimed for the signed-in user when the node id IS the device id the
|
|
10008
|
+
* auth provider verified for this credential (`metadata.deviceId`; the built-in
|
|
10009
|
+
* provider takes it from the token's `dev` claim, which names a device registered
|
|
10010
|
+
* to that user). `@korajs/auth` clients use that device id as their node id and
|
|
10011
|
+
* cannot change it, so without this they stay refused until an operator binds the
|
|
10012
|
+
* node. The claim is one atomic store step; a node another principal owns is never
|
|
10013
|
+
* taken, and another user presenting the node id is still refused.
|
|
10014
|
+
*
|
|
10015
|
+
* The claim decides ownership from now on only. History written under the node on
|
|
10016
|
+
* beta.12 may include operations another user forged (beta.12 did not verify node
|
|
10017
|
+
* ids); the operation log integrity scan is the tool for auditing it.
|
|
10018
|
+
*/
|
|
10019
|
+
async handOverDeviceNode(nodeId, context) {
|
|
10020
|
+
if (!this.deviceNodeHandover || context.anonymous === true) return false;
|
|
10021
|
+
if (!this.store.claimUnownedNode) return false;
|
|
10022
|
+
const deviceId = context.metadata?.deviceId;
|
|
10023
|
+
if (typeof deviceId !== "string" || deviceId.length === 0 || deviceId !== nodeId) return false;
|
|
10024
|
+
const previousOwner = await this.store.getNodeClaimOwner?.(nodeId) ?? null;
|
|
10025
|
+
if (previousOwner !== null && previousOwner !== RELEASED_NODE_OWNER) return false;
|
|
10026
|
+
if (!await this.store.claimUnownedNode(nodeId, context.userId)) return false;
|
|
10027
|
+
this.logger?.log({
|
|
10028
|
+
timestamp: Date.now(),
|
|
10029
|
+
level: "info",
|
|
10030
|
+
event: "node_claim.handover",
|
|
10031
|
+
sessionId: this.sessionId,
|
|
10032
|
+
nodeId,
|
|
10033
|
+
details: {
|
|
10034
|
+
userId: context.userId,
|
|
10035
|
+
previousOwner: previousOwner === null ? "none (pre-claims history)" : "released",
|
|
10036
|
+
message: "A signed-in device took over its own node id, which had history but no owner (a database upgraded from beta.12, or a node an administrator released). The node id equals the device id the auth provider verified for this user."
|
|
10037
|
+
}
|
|
10038
|
+
});
|
|
10039
|
+
return true;
|
|
10040
|
+
}
|
|
10041
|
+
/**
|
|
10042
|
+
* The download grant the server itself gives this session, before the client's
|
|
10043
|
+
* handshake scope narrows it (F4). A client may narrow its own view, but that does
|
|
10044
|
+
* not isolate tenants: another client of the same user set can ask for everything.
|
|
10045
|
+
* `null` when the grant cannot be resolved here (the handshake already refused or
|
|
10046
|
+
* accepted it on the full path); then nothing is judged.
|
|
10047
|
+
*/
|
|
10048
|
+
grantWithoutHandshakeNarrowing(downlinkAuthScopes, authenticated) {
|
|
10049
|
+
try {
|
|
10050
|
+
return resolveSessionScopes(this.store.getSchema(), {
|
|
10051
|
+
handshakeScope: void 0,
|
|
10052
|
+
authScopes: downlinkAuthScopes,
|
|
10053
|
+
authenticated,
|
|
10054
|
+
onUnresolved: "deny"
|
|
10055
|
+
});
|
|
10056
|
+
} catch {
|
|
10057
|
+
return null;
|
|
10058
|
+
}
|
|
10059
|
+
}
|
|
10060
|
+
/** Who holds a refused node id, as `NODE_ID_CLAIMED` reports it (F9). */
|
|
10061
|
+
async nodeOwnershipOf(nodeId) {
|
|
10062
|
+
if (!this.store.getNodeClaimOwner) return void 0;
|
|
10063
|
+
const owner = await this.store.getNodeClaimOwner(nodeId);
|
|
10064
|
+
return owner === null || owner === RELEASED_NODE_OWNER ? "unowned" : "other-principal";
|
|
10065
|
+
}
|
|
9650
10066
|
/** The deprecation warning for an anonymous device adopted under a legacy claim (RT-21, RT-91). */
|
|
9651
10067
|
warnLegacyAnonymousClaim(nodeId) {
|
|
9652
10068
|
this.logger?.log({
|
|
@@ -9656,7 +10072,7 @@ var ClientSession = class _ClientSession {
|
|
|
9656
10072
|
sessionId: this.sessionId,
|
|
9657
10073
|
nodeId,
|
|
9658
10074
|
details: {
|
|
9659
|
-
message: "An anonymous device re-claimed a node id under a legacy claim (no confirmed node token, or history from before node claims). Upgrade the client; allowLegacyAnonymousClaims will default to false in
|
|
10075
|
+
message: "An anonymous device re-claimed a node id under a legacy claim (no confirmed node token, or history from before node claims). Upgrade the client; allowLegacyAnonymousClaims will default to false in a later release, announced in its release notes."
|
|
9660
10076
|
}
|
|
9661
10077
|
});
|
|
9662
10078
|
}
|
|
@@ -9800,14 +10216,19 @@ var ClientSession = class _ClientSession {
|
|
|
9800
10216
|
} else {
|
|
9801
10217
|
this.nodeOwnerKey = context.userId;
|
|
9802
10218
|
claimed = await this.store.claimNode(msg.nodeId, context.userId);
|
|
10219
|
+
if (!claimed) claimed = await this.handOverDeviceNode(msg.nodeId, context);
|
|
9803
10220
|
}
|
|
9804
10221
|
if (!claimed) {
|
|
9805
10222
|
this.issuedNodeToken = null;
|
|
9806
|
-
this.
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
10223
|
+
const nodeOwnership = context.anonymous === true ? void 0 : await this.nodeOwnershipOf(msg.nodeId);
|
|
10224
|
+
this.sendToClient({
|
|
10225
|
+
type: "error",
|
|
10226
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
10227
|
+
code: "NODE_ID_CLAIMED",
|
|
10228
|
+
message: `Node id "${msg.nodeId}" belongs to another principal, or has operation history with no recorded owner (an administrator can release it with KoraSyncServer.releaseNodeClaim). Use a fresh node id per signed-in user.`,
|
|
10229
|
+
retriable: false,
|
|
10230
|
+
...nodeOwnership ? { nodeOwnership } : {}
|
|
10231
|
+
});
|
|
9811
10232
|
this.close("node id claimed by another user");
|
|
9812
10233
|
return;
|
|
9813
10234
|
}
|
|
@@ -9873,11 +10294,23 @@ var ClientSession = class _ClientSession {
|
|
|
9873
10294
|
} = this.authContext;
|
|
9874
10295
|
this.authContext = rest;
|
|
9875
10296
|
}
|
|
9876
|
-
|
|
10297
|
+
if (this.unscopedSharing !== "allow" && this.auth && sharesEveryUsersData(
|
|
9877
10298
|
this.auth,
|
|
9878
|
-
|
|
10299
|
+
this.principal,
|
|
10300
|
+
this.grantWithoutHandshakeNarrowing(downlinkAuthScopes, authenticated),
|
|
9879
10301
|
this.store.getSchema()
|
|
9880
|
-
)
|
|
10302
|
+
)) {
|
|
10303
|
+
if (this.unscopedSharing === "refuse") {
|
|
10304
|
+
this.sendError(
|
|
10305
|
+
"UNSCOPED_SHARING_REFUSED",
|
|
10306
|
+
"This server refuses signed-in sessions whose grant restricts no collection (unscopedSharing: 'refuse'). Return per-user sync scopes from the auth provider.",
|
|
10307
|
+
false
|
|
10308
|
+
);
|
|
10309
|
+
this.close("unscoped sharing refused");
|
|
10310
|
+
return;
|
|
10311
|
+
}
|
|
10312
|
+
warnIfMultiTenantWithoutScopes(this.auth);
|
|
10313
|
+
}
|
|
9881
10314
|
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
9882
10315
|
this.syncQuerySubsets = (0, import_sync5.dedupeQuerySubsets)(msg.syncQueries);
|
|
9883
10316
|
} else {
|
|
@@ -9885,7 +10318,7 @@ var ClientSession = class _ClientSession {
|
|
|
9885
10318
|
}
|
|
9886
10319
|
this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync5.decodeDeltaCursor)(msg.deltaCursor) : null;
|
|
9887
10320
|
this.clientDeliveryWatermark = msg.lastDeliverySequence ?? null;
|
|
9888
|
-
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.downlinkScopes)) {
|
|
10321
|
+
if (this.clientDeliveryWatermark !== null && !sameScopeMap(msg.syncScope, this.authContext?.downlinkScopes, this.maxScopePredicateValues)) {
|
|
9889
10322
|
const acceptedWatermark = msg.acceptedScopeWatermark;
|
|
9890
10323
|
const resumable = typeof msg.acceptedScopeKey === "string" && typeof acceptedWatermark === "number" && Number.isSafeInteger(acceptedWatermark) && acceptedWatermark >= 0 && msg.acceptedScopeKey === (0, import_internal8.scopeViewKey)(this.authContext?.downlinkScopes);
|
|
9891
10324
|
this.clientDeliveryWatermark = resumable ? acceptedWatermark : 0;
|
|
@@ -9911,7 +10344,7 @@ var ClientSession = class _ClientSession {
|
|
|
9911
10344
|
});
|
|
9912
10345
|
const response2 = {
|
|
9913
10346
|
type: "handshake-response",
|
|
9914
|
-
messageId: (0,
|
|
10347
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
9915
10348
|
nodeId: this.store.getNodeId(),
|
|
9916
10349
|
versionVector: {},
|
|
9917
10350
|
schemaVersion: this.schemaVersion,
|
|
@@ -9952,7 +10385,7 @@ var ClientSession = class _ClientSession {
|
|
|
9952
10385
|
const heartbeat = msg.supportsHeartbeat === true && this.appHeartbeatIntervalMs > 0;
|
|
9953
10386
|
const response = {
|
|
9954
10387
|
type: "handshake-response",
|
|
9955
|
-
messageId: (0,
|
|
10388
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
9956
10389
|
nodeId: this.store.getNodeId(),
|
|
9957
10390
|
versionVector: (0, import_sync5.versionVectorToWire)(visibleServerVector),
|
|
9958
10391
|
schemaVersion: this.schemaVersion,
|
|
@@ -10009,6 +10442,7 @@ var ClientSession = class _ClientSession {
|
|
|
10009
10442
|
}
|
|
10010
10443
|
this.onReady?.(this.sessionId);
|
|
10011
10444
|
if (heartbeat) this.startAppHeartbeat();
|
|
10445
|
+
this.runDeferredScopeRefresh();
|
|
10012
10446
|
if (this.takeOrphanedRelays && this.clientNodeId) {
|
|
10013
10447
|
const buffered = this.takeOrphanedRelays(this.clientNodeId);
|
|
10014
10448
|
if (buffered.length > 0) {
|
|
@@ -10115,8 +10549,8 @@ var ClientSession = class _ClientSession {
|
|
|
10115
10549
|
};
|
|
10116
10550
|
}
|
|
10117
10551
|
try {
|
|
10118
|
-
const downlink = rawDownlink ? normalizeScopeMap(rawDownlink) : directionalScopesConfigured ? {} : void 0;
|
|
10119
|
-
const uplink = rawUplink ? normalizeScopeMap(rawUplink) : directionalScopesConfigured ? {} : void 0;
|
|
10552
|
+
const downlink = rawDownlink ? normalizeScopeMap(rawDownlink, this.maxScopePredicateValues) : directionalScopesConfigured ? {} : void 0;
|
|
10553
|
+
const uplink = rawUplink !== void 0 && rawUplink === rawDownlink ? downlink : rawUplink ? normalizeScopeMap(rawUplink, this.maxScopePredicateValues) : directionalScopesConfigured ? {} : void 0;
|
|
10120
10554
|
return { ok: true, downlink, uplink, downlinkAuthScopes, authenticated };
|
|
10121
10555
|
} catch (error) {
|
|
10122
10556
|
return {
|
|
@@ -10410,7 +10844,7 @@ var ClientSession = class _ClientSession {
|
|
|
10410
10844
|
const lastOp = operations2[operations2.length - 1];
|
|
10411
10845
|
const ack = {
|
|
10412
10846
|
type: "acknowledgment",
|
|
10413
|
-
messageId: (0,
|
|
10847
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
10414
10848
|
acknowledgedMessageId: msg.messageId,
|
|
10415
10849
|
lastSequenceNumber: lastOp ? Math.min(acknowledgedThrough, lastOp.sequenceNumber) : 0
|
|
10416
10850
|
};
|
|
@@ -10510,7 +10944,7 @@ var ClientSession = class _ClientSession {
|
|
|
10510
10944
|
schemaView(op) {
|
|
10511
10945
|
let view;
|
|
10512
10946
|
try {
|
|
10513
|
-
view = (0,
|
|
10947
|
+
view = (0, import_core30.operationSchemaView)(op, this.schemaVersion, this.operationTransforms);
|
|
10514
10948
|
} catch (error) {
|
|
10515
10949
|
return {
|
|
10516
10950
|
ok: false,
|
|
@@ -10571,7 +11005,7 @@ var ClientSession = class _ClientSession {
|
|
|
10571
11005
|
if (integrity.ok && integrity.restoredData !== void 0) {
|
|
10572
11006
|
this.restoredLegacyData.set(op, integrity.restoredData);
|
|
10573
11007
|
}
|
|
10574
|
-
if (integrity.ok && declared === void 0 && integrity.restoredData === void 0 && (0,
|
|
11008
|
+
if (integrity.ok && declared === void 0 && integrity.restoredData === void 0 && (0, import_core30.canonicalizeLegacyOperation)(op) !== op) {
|
|
10575
11009
|
return {
|
|
10576
11010
|
code: import_sync5.INVALID_OPERATION_ID,
|
|
10577
11011
|
message: `Operation "${op.id}" is a version-1 update whose previousData names fields its data lacks, but its id does not cover clearing them. No Kora client writes this shape; it is refused and never stored or relayed.`
|
|
@@ -10659,12 +11093,12 @@ var ClientSession = class _ClientSession {
|
|
|
10659
11093
|
/** The handshake's explicit revocations (RT-81), when the deployment has any. */
|
|
10660
11094
|
advertisedRevocations() {
|
|
10661
11095
|
const revoked = (this.store.getRevokedAuthoritativeNodeIds?.() ?? []).filter(
|
|
10662
|
-
(id) => !(0,
|
|
11096
|
+
(id) => !(0, import_core30.isServerNodeId)(id)
|
|
10663
11097
|
);
|
|
10664
11098
|
return revoked.length > 0 ? { revokedAuthoritativeNodeIds: revoked } : {};
|
|
10665
11099
|
}
|
|
10666
11100
|
advertisedAuthoritativeNodeIds() {
|
|
10667
|
-
return [...new Set(this.serverAuthoritativeNodeIds())].filter((id) => !(0,
|
|
11101
|
+
return [...new Set(this.serverAuthoritativeNodeIds())].filter((id) => !(0, import_core30.isServerNodeId)(id));
|
|
10668
11102
|
}
|
|
10669
11103
|
/** Node ids this server authors operations under (protocol v2 handshake response). */
|
|
10670
11104
|
serverAuthoritativeNodeIds() {
|
|
@@ -10677,7 +11111,7 @@ var ClientSession = class _ClientSession {
|
|
|
10677
11111
|
* payloads have no envelope binding.
|
|
10678
11112
|
*/
|
|
10679
11113
|
warnLegacyProtocol(nodeId) {
|
|
10680
|
-
const message = `Client node "${nodeId}" speaks sync protocol ${String(this.clientProtocolVersion)}; this server speaks ${String(import_sync5.SYNC_PROTOCOL_VERSION)}. Protocol 1 clients (Kora <= beta.12) are
|
|
11114
|
+
const message = `Client node "${nodeId}" speaks sync protocol ${String(this.clientProtocolVersion)}; this server speaks ${String(import_sync5.SYNC_PROTOCOL_VERSION)}. Protocol 1 clients (Kora <= beta.12) are deprecated and will be refused by a later release, announced in its release notes. Upgrade the client.`;
|
|
10681
11115
|
this.logger?.log({
|
|
10682
11116
|
timestamp: Date.now(),
|
|
10683
11117
|
level: "warn",
|
|
@@ -10727,7 +11161,7 @@ var ClientSession = class _ClientSession {
|
|
|
10727
11161
|
if (missing.length === 0) {
|
|
10728
11162
|
const emptyBatch = {
|
|
10729
11163
|
type: "operation-batch",
|
|
10730
|
-
messageId: (0,
|
|
11164
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
10731
11165
|
operations: [],
|
|
10732
11166
|
isFinal: true,
|
|
10733
11167
|
batchIndex: 0,
|
|
@@ -10742,7 +11176,7 @@ var ClientSession = class _ClientSession {
|
|
|
10742
11176
|
if (afterCursor.length === 0) {
|
|
10743
11177
|
const emptyBatch = {
|
|
10744
11178
|
type: "operation-batch",
|
|
10745
|
-
messageId: (0,
|
|
11179
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
10746
11180
|
operations: [],
|
|
10747
11181
|
isFinal: true,
|
|
10748
11182
|
batchIndex: this.resumeDeltaCursor?.batchIndex ?? 0,
|
|
@@ -10758,7 +11192,7 @@ var ClientSession = class _ClientSession {
|
|
|
10758
11192
|
const batchCursor = (0, import_sync5.createDeltaCursorFromBatch)(batchOps, i);
|
|
10759
11193
|
const batchMsg = {
|
|
10760
11194
|
type: "operation-batch",
|
|
10761
|
-
messageId: (0,
|
|
11195
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
10762
11196
|
operations: serializedOps,
|
|
10763
11197
|
isFinal: i === totalBatches - 1,
|
|
10764
11198
|
batchIndex: i,
|
|
@@ -10868,26 +11302,22 @@ var ClientSession = class _ClientSession {
|
|
|
10868
11302
|
!(clientNodeId !== null && delivered.operation.nodeId === clientNodeId && delivered.deliverySequence > this.ownOperationsIncludedThrough)
|
|
10869
11303
|
)
|
|
10870
11304
|
);
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
});
|
|
10883
|
-
}
|
|
10884
|
-
deliverable.push(delivered);
|
|
10885
|
-
} else if (await this.scopeRetractionFor(delivered.operation, snapshot)) {
|
|
10886
|
-
deliverable.push({ ...delivered, retraction: true });
|
|
11305
|
+
const rows = await this.prefetchRecordsFor(candidates);
|
|
11306
|
+
for (const delivered of candidates) {
|
|
11307
|
+
const snapshot = delivered.scopeSnapshot ?? null;
|
|
11308
|
+
if (await this.operationVisibleToClient(delivered.operation, snapshot, rows)) {
|
|
11309
|
+
const entry = await this.scopeEntryFor(delivered.operation, snapshot, rows);
|
|
11310
|
+
if (entry) {
|
|
11311
|
+
deliverable.push({
|
|
11312
|
+
operation: entry,
|
|
11313
|
+
deliverySequence: delivered.deliverySequence,
|
|
11314
|
+
scopeEntry: true
|
|
11315
|
+
});
|
|
10887
11316
|
}
|
|
11317
|
+
deliverable.push(delivered);
|
|
11318
|
+
} else if (await this.scopeRetractionFor(delivered.operation, snapshot, rows)) {
|
|
11319
|
+
deliverable.push({ ...delivered, retraction: true });
|
|
10888
11320
|
}
|
|
10889
|
-
} finally {
|
|
10890
|
-
this.recordLookupCache = null;
|
|
10891
11321
|
}
|
|
10892
11322
|
return deliverable;
|
|
10893
11323
|
}
|
|
@@ -10930,7 +11360,7 @@ var ClientSession = class _ClientSession {
|
|
|
10930
11360
|
sendDeliveryBatch(slice, base, max, batchIndex, isFinal) {
|
|
10931
11361
|
const batchMsg = {
|
|
10932
11362
|
type: "operation-batch",
|
|
10933
|
-
messageId: (0,
|
|
11363
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
10934
11364
|
operations: slice.filter((delivered) => !delivered.retraction).map((delivered) => this.serializer.encodeOperation(delivered.operation)),
|
|
10935
11365
|
...slice.some((delivered) => delivered.retraction) ? {
|
|
10936
11366
|
retractions: slice.filter((delivered) => delivered.retraction).map((delivered) => ({
|
|
@@ -10972,7 +11402,7 @@ var ClientSession = class _ClientSession {
|
|
|
10972
11402
|
* row. Operations without a snapshot (legacy rows, custom stores) fall back to the
|
|
10973
11403
|
* current row plus `op.data`.
|
|
10974
11404
|
*/
|
|
10975
|
-
async operationVisibleToClient(stored, snapshot = null) {
|
|
11405
|
+
async operationVisibleToClient(stored, snapshot = null, rows = null) {
|
|
10976
11406
|
const judged = this.schemaView(stored);
|
|
10977
11407
|
const op = judged.ok ? judged.op : stored;
|
|
10978
11408
|
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
@@ -10981,18 +11411,18 @@ var ClientSession = class _ClientSession {
|
|
|
10981
11411
|
let current;
|
|
10982
11412
|
let post = snapshot.post;
|
|
10983
11413
|
if (snapshotLacksScopeFields(op.collection, { pre: null, post }, scopes)) {
|
|
10984
|
-
current = await this.lookupRecordFields(op.collection, op.recordId);
|
|
11414
|
+
current = await this.lookupRecordFields(op.collection, op.recordId, rows);
|
|
10985
11415
|
post = snapshotValuesWithFallback(op.collection, post, scopes, current);
|
|
10986
11416
|
}
|
|
10987
11417
|
if (!post || !recordMatchesScopes(op.collection, { ...post, id: op.recordId }, scopes)) {
|
|
10988
11418
|
return false;
|
|
10989
11419
|
}
|
|
10990
11420
|
if (subsets.length === 0) return true;
|
|
10991
|
-
current ??= await this.lookupRecordFields(op.collection, op.recordId);
|
|
11421
|
+
current ??= await this.lookupRecordFields(op.collection, op.recordId, rows);
|
|
10992
11422
|
return (0, import_sync5.operationMatchesQuerySubsets)(op, subsets, current);
|
|
10993
11423
|
}
|
|
10994
11424
|
const needsBackfill = missingScopeFields(op, scopes).length > 0 || subsets !== void 0 && subsets.length > 0;
|
|
10995
|
-
const fullRecord = needsBackfill ? await this.lookupRecordFields(op.collection, op.recordId) : void 0;
|
|
11425
|
+
const fullRecord = needsBackfill ? await this.lookupRecordFields(op.collection, op.recordId, rows) : void 0;
|
|
10996
11426
|
if (!operationMatchesScopes(op, scopes, fullRecord)) {
|
|
10997
11427
|
return false;
|
|
10998
11428
|
}
|
|
@@ -11218,11 +11648,11 @@ var ClientSession = class _ClientSession {
|
|
|
11218
11648
|
* operation without a snapshot never produces a retraction (fail quiet: the
|
|
11219
11649
|
* client keeps what it already had, and no other tenant's record id leaks).
|
|
11220
11650
|
*/
|
|
11221
|
-
async scopeRetractionFor(op, snapshot = null) {
|
|
11651
|
+
async scopeRetractionFor(op, snapshot = null, rows = null) {
|
|
11222
11652
|
if (this.scopeExitPolicy !== "retract") return false;
|
|
11223
11653
|
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
11224
11654
|
if (!scopes || !snapshot) return false;
|
|
11225
|
-
const current = snapshotLacksScopeFields(op.collection, snapshot, scopes) ? await this.lookupRecordFields(op.collection, op.recordId) : void 0;
|
|
11655
|
+
const current = snapshotLacksScopeFields(op.collection, snapshot, scopes) ? await this.lookupRecordFields(op.collection, op.recordId, rows) : void 0;
|
|
11226
11656
|
return snapshotExitsScopes(op, snapshot, scopes, current);
|
|
11227
11657
|
}
|
|
11228
11658
|
/**
|
|
@@ -11233,14 +11663,14 @@ var ClientSession = class _ClientSession {
|
|
|
11233
11663
|
* deleted, gets no entry: its later operations retract or delete it anyway).
|
|
11234
11664
|
* Operations without a snapshot (legacy rows, custom stores) never produce one.
|
|
11235
11665
|
*/
|
|
11236
|
-
async scopeEntryFor(op, snapshot) {
|
|
11666
|
+
async scopeEntryFor(op, snapshot, rows = null) {
|
|
11237
11667
|
const scopes = this.authContext?.downlinkScopes ?? this.authContext?.scopes;
|
|
11238
11668
|
const schema = this.store.getSchema();
|
|
11239
11669
|
if (!scopes || !snapshot || !schema || op.type !== "update") return null;
|
|
11240
11670
|
if (!snapshotLacksScopeFields(op.collection, snapshot, scopes) && !snapshotEntersScopes(op, snapshot, scopes)) {
|
|
11241
11671
|
return null;
|
|
11242
11672
|
}
|
|
11243
|
-
const current = await this.lookupRecordFields(op.collection, op.recordId);
|
|
11673
|
+
const current = await this.lookupRecordFields(op.collection, op.recordId, rows);
|
|
11244
11674
|
if (!current || current._deleted === 1 || current._deleted === true) return null;
|
|
11245
11675
|
if (!snapshotEntersScopes(op, snapshot, scopes, current)) return null;
|
|
11246
11676
|
if (!recordMatchesScopes(op.collection, { ...current, id: op.recordId }, scopes)) return null;
|
|
@@ -11274,23 +11704,65 @@ var ClientSession = class _ClientSession {
|
|
|
11274
11704
|
* query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
|
|
11275
11705
|
* carries no fields) is still judged against the record's actual scope. Returns
|
|
11276
11706
|
* undefined when the record cannot be read (never materialized, or no schema).
|
|
11707
|
+
* `rows` are the rows one delivery pass prefetched for its own decisions.
|
|
11277
11708
|
*/
|
|
11278
|
-
async lookupRecordFields(collection, recordId) {
|
|
11279
|
-
const cached =
|
|
11709
|
+
async lookupRecordFields(collection, recordId, rows = null) {
|
|
11710
|
+
const cached = rows?.get(recordCacheKey(collection, recordId));
|
|
11280
11711
|
if (cached !== void 0) return cached ?? void 0;
|
|
11281
11712
|
try {
|
|
11282
|
-
const
|
|
11713
|
+
const rows2 = await this.store.queryCollection(collection, {
|
|
11283
11714
|
where: { id: recordId },
|
|
11284
11715
|
includeDeleted: true,
|
|
11285
11716
|
limit: 1
|
|
11286
11717
|
});
|
|
11287
|
-
return
|
|
11718
|
+
return rows2[0];
|
|
11288
11719
|
} catch {
|
|
11289
11720
|
return void 0;
|
|
11290
11721
|
}
|
|
11291
11722
|
}
|
|
11292
|
-
|
|
11293
|
-
|
|
11723
|
+
/**
|
|
11724
|
+
* Hand an awareness update to the server's relay, with the record its cursor
|
|
11725
|
+
* names, read from the store, so the relay can deliver it only to sessions whose
|
|
11726
|
+
* download scope contains that record (the rule of the Yjs doc channel).
|
|
11727
|
+
* Awareness is purely ephemeral: nothing is persisted.
|
|
11728
|
+
*/
|
|
11729
|
+
async handleAwarenessUpdate(msg) {
|
|
11730
|
+
if (!this.onAwarenessUpdate) return;
|
|
11731
|
+
const state = msg.states[String(msg.clientId)];
|
|
11732
|
+
const cursor = state ? state.cursor : void 0;
|
|
11733
|
+
if (cursor === void 0 || cursor === null) {
|
|
11734
|
+
this.onAwarenessUpdate(this.sessionId, msg);
|
|
11735
|
+
return;
|
|
11736
|
+
}
|
|
11737
|
+
const collection = cursor.collection;
|
|
11738
|
+
const recordId = cursor.recordId;
|
|
11739
|
+
if (typeof cursor !== "object" || typeof collection !== "string" || typeof recordId !== "string" || !isStorableIdentifier(collection) || !isStorableIdentifier(recordId)) {
|
|
11740
|
+
this.onAwarenessUpdate(this.sessionId, msg, { invalid: true });
|
|
11741
|
+
return;
|
|
11742
|
+
}
|
|
11743
|
+
let read = this.presenceRecords.peek(collection, recordId);
|
|
11744
|
+
if (read === void 0) {
|
|
11745
|
+
if (!this.presenceLookupLimiter.allow(1)) {
|
|
11746
|
+
this.onAwarenessUpdate(this.sessionId, msg, { invalid: true });
|
|
11747
|
+
return;
|
|
11748
|
+
}
|
|
11749
|
+
try {
|
|
11750
|
+
read = await this.presenceRecords.read(collection, recordId);
|
|
11751
|
+
const key = presenceRecordKey(collection, recordId);
|
|
11752
|
+
for (let attempt = 0; attempt < MAX_PRESENCE_REREADS && this.presenceRecords.touchedSince(key, read.asOf); attempt++) {
|
|
11753
|
+
read = await this.presenceRecords.read(collection, recordId);
|
|
11754
|
+
}
|
|
11755
|
+
} catch {
|
|
11756
|
+
this.onAwarenessUpdate(this.sessionId, msg, { invalid: true });
|
|
11757
|
+
return;
|
|
11758
|
+
}
|
|
11759
|
+
}
|
|
11760
|
+
this.onAwarenessUpdate(this.sessionId, msg, {
|
|
11761
|
+
collection,
|
|
11762
|
+
recordId,
|
|
11763
|
+
stored: read.stored,
|
|
11764
|
+
asOf: read.asOf
|
|
11765
|
+
});
|
|
11294
11766
|
}
|
|
11295
11767
|
/**
|
|
11296
11768
|
* A Yjs doc-channel update is a write to a richtext field: the sender must be
|
|
@@ -11336,7 +11808,7 @@ var ClientSession = class _ClientSession {
|
|
|
11336
11808
|
sendError(code, message, retriable) {
|
|
11337
11809
|
const errorMsg = {
|
|
11338
11810
|
type: "error",
|
|
11339
|
-
messageId: (0,
|
|
11811
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
11340
11812
|
code,
|
|
11341
11813
|
message,
|
|
11342
11814
|
retriable
|
|
@@ -11352,7 +11824,7 @@ var ClientSession = class _ClientSession {
|
|
|
11352
11824
|
sendOperationRejected(op, code, message, retriable) {
|
|
11353
11825
|
const rejectedMsg = {
|
|
11354
11826
|
type: "operation-rejected",
|
|
11355
|
-
messageId: (0,
|
|
11827
|
+
messageId: (0, import_core31.generateUUIDv7)(),
|
|
11356
11828
|
operationId: op.id,
|
|
11357
11829
|
collection: op.collection,
|
|
11358
11830
|
recordId: op.recordId,
|
|
@@ -11385,13 +11857,16 @@ function operationIdentifiersStorable(op) {
|
|
|
11385
11857
|
|
|
11386
11858
|
// src/server/kora-sync-server.ts
|
|
11387
11859
|
init_cjs_shims();
|
|
11388
|
-
var
|
|
11860
|
+
var import_core35 = require("@korajs/core");
|
|
11389
11861
|
var import_internal9 = require("@korajs/core/internal");
|
|
11390
11862
|
var import_sync7 = require("@korajs/sync");
|
|
11391
11863
|
|
|
11864
|
+
// package.json
|
|
11865
|
+
var version = "1.0.0-beta.14";
|
|
11866
|
+
|
|
11392
11867
|
// src/awareness/awareness-relay.ts
|
|
11393
11868
|
init_cjs_shims();
|
|
11394
|
-
var
|
|
11869
|
+
var import_core32 = require("@korajs/core");
|
|
11395
11870
|
var DEFAULT_PARTITION = "*";
|
|
11396
11871
|
var AwarenessRelay = class {
|
|
11397
11872
|
clients = /* @__PURE__ */ new Map();
|
|
@@ -11400,34 +11875,41 @@ var AwarenessRelay = class {
|
|
|
11400
11875
|
* calls for an already-registered session are ignored, so a client cannot rebind
|
|
11401
11876
|
* itself to another client's id.
|
|
11402
11877
|
*
|
|
11878
|
+
* The new client is sent every existing state it may see (catch-up): a state with
|
|
11879
|
+
* an audience when the audience admits the new session, otherwise a state of the
|
|
11880
|
+
* same partition.
|
|
11881
|
+
*
|
|
11403
11882
|
* @param sessionId - Unique session identifier
|
|
11404
11883
|
* @param clientId - Client-assigned awareness ID
|
|
11405
11884
|
* @param transport - Transport for sending messages to this client
|
|
11406
|
-
* @param partition - Presence partition
|
|
11885
|
+
* @param partition - Presence partition for states relayed without an audience
|
|
11407
11886
|
*/
|
|
11408
11887
|
addClient(sessionId, clientId, transport, partition = DEFAULT_PARTITION) {
|
|
11409
11888
|
if (this.clients.has(sessionId)) return;
|
|
11410
|
-
|
|
11889
|
+
const joining = {
|
|
11411
11890
|
sessionId,
|
|
11412
11891
|
clientId,
|
|
11413
11892
|
transport,
|
|
11414
11893
|
state: null,
|
|
11415
|
-
partition
|
|
11416
|
-
|
|
11894
|
+
partition,
|
|
11895
|
+
audience: null,
|
|
11896
|
+
deliveredTo: /* @__PURE__ */ new Set()
|
|
11897
|
+
};
|
|
11898
|
+
this.clients.set(sessionId, joining);
|
|
11417
11899
|
const existingStates = {};
|
|
11418
11900
|
let hasStates = false;
|
|
11419
11901
|
for (const [, client] of this.clients) {
|
|
11420
11902
|
if (client.sessionId === sessionId) continue;
|
|
11421
|
-
if (client.
|
|
11422
|
-
if (client
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11903
|
+
if (!client.state) continue;
|
|
11904
|
+
if (!this.mayReceive(client, joining)) continue;
|
|
11905
|
+
existingStates[String(client.clientId)] = client.state;
|
|
11906
|
+
client.deliveredTo.add(sessionId);
|
|
11907
|
+
hasStates = true;
|
|
11426
11908
|
}
|
|
11427
11909
|
if (hasStates) {
|
|
11428
11910
|
const catchUpMsg = {
|
|
11429
11911
|
type: "awareness-update",
|
|
11430
|
-
messageId: (0,
|
|
11912
|
+
messageId: (0, import_core32.generateUUIDv7)(),
|
|
11431
11913
|
clientId: 0,
|
|
11432
11914
|
// Server-sourced
|
|
11433
11915
|
states: existingStates
|
|
@@ -11444,7 +11926,7 @@ var AwarenessRelay = class {
|
|
|
11444
11926
|
return this.clients.has(sessionId);
|
|
11445
11927
|
}
|
|
11446
11928
|
/**
|
|
11447
|
-
* Remove a client and
|
|
11929
|
+
* Remove a client and send its removal to every client that was shown its state.
|
|
11448
11930
|
*
|
|
11449
11931
|
* @param sessionId - Session ID of the disconnecting client
|
|
11450
11932
|
*/
|
|
@@ -11452,17 +11934,26 @@ var AwarenessRelay = class {
|
|
|
11452
11934
|
const client = this.clients.get(sessionId);
|
|
11453
11935
|
if (!client) return;
|
|
11454
11936
|
this.clients.delete(sessionId);
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
11937
|
+
for (const [, other] of this.clients) other.deliveredTo.delete(sessionId);
|
|
11938
|
+
if (client.deliveredTo.size === 0) return;
|
|
11939
|
+
const msg = this.removalMessage(client);
|
|
11940
|
+
for (const targetId of client.deliveredTo) {
|
|
11941
|
+
const target = this.clients.get(targetId);
|
|
11942
|
+
if (target?.transport.isConnected()) target.transport.send(msg);
|
|
11943
|
+
}
|
|
11944
|
+
client.deliveredTo.clear();
|
|
11945
|
+
}
|
|
11946
|
+
/**
|
|
11947
|
+
* True when {@link handleUpdate} would take this update: the session is registered,
|
|
11948
|
+
* the update is stamped with its bound `clientId` and carries that client's entry.
|
|
11949
|
+
*
|
|
11950
|
+
* @param sessionId - Session ID of the sending client
|
|
11951
|
+
* @param message - The awareness update message
|
|
11952
|
+
*/
|
|
11953
|
+
accepts(sessionId, message) {
|
|
11954
|
+
const sender = this.clients.get(sessionId);
|
|
11955
|
+
if (!sender || message.clientId !== sender.clientId) return false;
|
|
11956
|
+
return String(sender.clientId) in message.states;
|
|
11466
11957
|
}
|
|
11467
11958
|
/**
|
|
11468
11959
|
* Handle an incoming awareness update from a registered client.
|
|
@@ -11470,12 +11961,17 @@ var AwarenessRelay = class {
|
|
|
11470
11961
|
* Only the sender's own entry is accepted: the message must be stamped with the
|
|
11471
11962
|
* sender's bound `clientId`, and only `states[clientId]` is stored and relayed.
|
|
11472
11963
|
* Entries for other clients are dropped, so a sender cannot overwrite or remove
|
|
11473
|
-
* anyone else's presence.
|
|
11964
|
+
* anyone else's presence.
|
|
11965
|
+
*
|
|
11966
|
+
* The state is sent to every other client the audience admits (without an
|
|
11967
|
+
* audience: every client of the sender's partition). A client that was shown an
|
|
11968
|
+
* earlier state of the sender but is not admitted now receives a removal.
|
|
11474
11969
|
*
|
|
11475
11970
|
* @param sessionId - Session ID of the sending client
|
|
11476
11971
|
* @param message - The awareness update message
|
|
11972
|
+
* @param audience - Who may see this state; omitted means the sender's partition
|
|
11477
11973
|
*/
|
|
11478
|
-
handleUpdate(sessionId, message) {
|
|
11974
|
+
handleUpdate(sessionId, message, audience) {
|
|
11479
11975
|
const sender = this.clients.get(sessionId);
|
|
11480
11976
|
if (!sender) return;
|
|
11481
11977
|
if (message.clientId !== sender.clientId) return;
|
|
@@ -11483,13 +11979,66 @@ var AwarenessRelay = class {
|
|
|
11483
11979
|
if (!(key in message.states)) return;
|
|
11484
11980
|
const senderState = message.states[key] ?? null;
|
|
11485
11981
|
sender.state = senderState;
|
|
11982
|
+
sender.audience = audience ?? null;
|
|
11486
11983
|
const sanitized = {
|
|
11487
11984
|
type: "awareness-update",
|
|
11488
11985
|
messageId: message.messageId,
|
|
11489
11986
|
clientId: sender.clientId,
|
|
11490
11987
|
states: { [key]: senderState }
|
|
11491
11988
|
};
|
|
11492
|
-
|
|
11989
|
+
let removal = null;
|
|
11990
|
+
for (const [, client] of this.clients) {
|
|
11991
|
+
if (client.sessionId === sessionId) continue;
|
|
11992
|
+
const visible = senderState !== null && this.mayReceive(sender, client);
|
|
11993
|
+
if (visible) {
|
|
11994
|
+
if (!client.transport.isConnected()) continue;
|
|
11995
|
+
client.transport.send(sanitized);
|
|
11996
|
+
sender.deliveredTo.add(client.sessionId);
|
|
11997
|
+
} else if (sender.deliveredTo.delete(client.sessionId)) {
|
|
11998
|
+
if (!client.transport.isConnected()) continue;
|
|
11999
|
+
removal ??= senderState === null ? sanitized : this.removalMessage(sender);
|
|
12000
|
+
client.transport.send(removal);
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
12003
|
+
}
|
|
12004
|
+
/**
|
|
12005
|
+
* Re-decide who may see a client's current state after the record its cursor names
|
|
12006
|
+
* changed (F16): it moved into or out of a session's grant. Sessions that may no
|
|
12007
|
+
* longer see the state get a removal; sessions that may see it now and were not
|
|
12008
|
+
* shown it get the state. Later catch-ups use the new audience.
|
|
12009
|
+
*
|
|
12010
|
+
* @param sessionId - The client whose audience changed
|
|
12011
|
+
* @param audience - The new audience
|
|
12012
|
+
*/
|
|
12013
|
+
updateAudience(sessionId, audience) {
|
|
12014
|
+
const sender = this.clients.get(sessionId);
|
|
12015
|
+
if (!sender) return;
|
|
12016
|
+
sender.audience = audience;
|
|
12017
|
+
if (sender.state === null) return;
|
|
12018
|
+
const key = String(sender.clientId);
|
|
12019
|
+
let current = null;
|
|
12020
|
+
let removal = null;
|
|
12021
|
+
for (const [, client] of this.clients) {
|
|
12022
|
+
if (client.sessionId === sessionId) continue;
|
|
12023
|
+
const visible = this.mayReceive(sender, client);
|
|
12024
|
+
const shown = sender.deliveredTo.has(client.sessionId);
|
|
12025
|
+
if (visible && !shown) {
|
|
12026
|
+
if (!client.transport.isConnected()) continue;
|
|
12027
|
+
current ??= {
|
|
12028
|
+
type: "awareness-update",
|
|
12029
|
+
messageId: (0, import_core32.generateUUIDv7)(),
|
|
12030
|
+
clientId: sender.clientId,
|
|
12031
|
+
states: { [key]: sender.state }
|
|
12032
|
+
};
|
|
12033
|
+
client.transport.send(current);
|
|
12034
|
+
sender.deliveredTo.add(client.sessionId);
|
|
12035
|
+
} else if (!visible && shown) {
|
|
12036
|
+
sender.deliveredTo.delete(client.sessionId);
|
|
12037
|
+
if (!client.transport.isConnected()) continue;
|
|
12038
|
+
removal ??= this.removalMessage(sender);
|
|
12039
|
+
client.transport.send(removal);
|
|
12040
|
+
}
|
|
12041
|
+
}
|
|
11493
12042
|
}
|
|
11494
12043
|
/**
|
|
11495
12044
|
* Get the number of registered awareness clients.
|
|
@@ -11504,14 +12053,23 @@ var AwarenessRelay = class {
|
|
|
11504
12053
|
this.clients.clear();
|
|
11505
12054
|
}
|
|
11506
12055
|
// --- Private ---
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
12056
|
+
/** True when `target` may see `source`'s current state. Fails closed. */
|
|
12057
|
+
mayReceive(source, target) {
|
|
12058
|
+
if (source.audience === null) return source.partition === target.partition;
|
|
12059
|
+
try {
|
|
12060
|
+
return source.audience(target.sessionId);
|
|
12061
|
+
} catch {
|
|
12062
|
+
return false;
|
|
11513
12063
|
}
|
|
11514
12064
|
}
|
|
12065
|
+
removalMessage(client) {
|
|
12066
|
+
return {
|
|
12067
|
+
type: "awareness-update",
|
|
12068
|
+
messageId: (0, import_core32.generateUUIDv7)(),
|
|
12069
|
+
clientId: client.clientId,
|
|
12070
|
+
states: { [String(client.clientId)]: null }
|
|
12071
|
+
};
|
|
12072
|
+
}
|
|
11515
12073
|
};
|
|
11516
12074
|
|
|
11517
12075
|
// src/diagnostics/server-metrics-collector.ts
|
|
@@ -11626,8 +12184,8 @@ var ServerMetricsCollector = class {
|
|
|
11626
12184
|
this.errorCount++;
|
|
11627
12185
|
}
|
|
11628
12186
|
/** Set the schema version. */
|
|
11629
|
-
setSchemaVersion(
|
|
11630
|
-
this.schemaVersion =
|
|
12187
|
+
setSchemaVersion(version2) {
|
|
12188
|
+
this.schemaVersion = version2;
|
|
11631
12189
|
}
|
|
11632
12190
|
/** Return a full snapshot of current metrics. */
|
|
11633
12191
|
getSnapshot(totalOperations) {
|
|
@@ -11675,7 +12233,7 @@ var ServerMetricsCollector = class {
|
|
|
11675
12233
|
|
|
11676
12234
|
// src/richtext/blob-chunk-relay.ts
|
|
11677
12235
|
init_cjs_shims();
|
|
11678
|
-
var
|
|
12236
|
+
var import_core33 = require("@korajs/core");
|
|
11679
12237
|
var import_sync6 = require("@korajs/sync");
|
|
11680
12238
|
var DEFAULT_MAX_PENDING_BLOB_REQUESTS_PER_SESSION = 256;
|
|
11681
12239
|
var DEFAULT_BLOB_REQUEST_TTL_MS = 6e4;
|
|
@@ -11766,7 +12324,7 @@ var BlobChunkRelay = class {
|
|
|
11766
12324
|
} catch {
|
|
11767
12325
|
return;
|
|
11768
12326
|
}
|
|
11769
|
-
if (await (0,
|
|
12327
|
+
if (await (0, import_core33.hashBlob)(decoded) !== entry.hash) {
|
|
11770
12328
|
return;
|
|
11771
12329
|
}
|
|
11772
12330
|
if (this.pending.get(message.requestId) !== entry) {
|
|
@@ -11904,7 +12462,7 @@ var YjsDocRelay = class {
|
|
|
11904
12462
|
|
|
11905
12463
|
// src/server/route-context.ts
|
|
11906
12464
|
init_cjs_shims();
|
|
11907
|
-
var
|
|
12465
|
+
var import_core34 = require("@korajs/core");
|
|
11908
12466
|
init_ingest_validation();
|
|
11909
12467
|
|
|
11910
12468
|
// src/apply/route-predicate.ts
|
|
@@ -11990,12 +12548,12 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
11990
12548
|
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
11991
12549
|
const sequenceNumber = nextServerSequenceNumber(store);
|
|
11992
12550
|
if (mutation.type === "insert") {
|
|
11993
|
-
return (0,
|
|
12551
|
+
return (0, import_core34.createOperation)(
|
|
11994
12552
|
{
|
|
11995
12553
|
nodeId,
|
|
11996
12554
|
type: "insert",
|
|
11997
12555
|
collection: mutation.collection,
|
|
11998
|
-
recordId: mutation.recordId ?? (0,
|
|
12556
|
+
recordId: mutation.recordId ?? (0, import_core34.generateUUIDv7)(),
|
|
11999
12557
|
data: mutation.data ?? {},
|
|
12000
12558
|
previousData: null,
|
|
12001
12559
|
sequenceNumber,
|
|
@@ -12018,14 +12576,14 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
12018
12576
|
const atomicOps = {};
|
|
12019
12577
|
for (const key of Object.keys(rawData)) {
|
|
12020
12578
|
const value = rawData[key];
|
|
12021
|
-
if ((0,
|
|
12022
|
-
data[key] = (0,
|
|
12023
|
-
atomicOps[key] = (0,
|
|
12579
|
+
if ((0, import_core34.isAtomicOp)(value)) {
|
|
12580
|
+
data[key] = (0, import_core34.resolveAtomicOp)(current ? current[key] : void 0, value);
|
|
12581
|
+
atomicOps[key] = (0, import_core34.toAtomicOp)(value);
|
|
12024
12582
|
} else {
|
|
12025
12583
|
data[key] = value;
|
|
12026
12584
|
}
|
|
12027
12585
|
}
|
|
12028
|
-
return (0,
|
|
12586
|
+
return (0, import_core34.createOperation)(
|
|
12029
12587
|
{
|
|
12030
12588
|
nodeId,
|
|
12031
12589
|
type: "update",
|
|
@@ -12041,7 +12599,7 @@ async function buildRouteOperation(store, mutation, currentOverride, clockOverri
|
|
|
12041
12599
|
clock
|
|
12042
12600
|
);
|
|
12043
12601
|
}
|
|
12044
|
-
return (0,
|
|
12602
|
+
return (0, import_core34.createOperation)(
|
|
12045
12603
|
{
|
|
12046
12604
|
nodeId,
|
|
12047
12605
|
type: "delete",
|
|
@@ -12411,7 +12969,7 @@ function resolvePerMessageDeflate(option) {
|
|
|
12411
12969
|
}
|
|
12412
12970
|
function validateIntervalOption(name, value) {
|
|
12413
12971
|
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
12414
|
-
throw new
|
|
12972
|
+
throw new import_core35.SyncError(`${name} must be a non-negative integer number of milliseconds`, {
|
|
12415
12973
|
[name]: value
|
|
12416
12974
|
});
|
|
12417
12975
|
}
|
|
@@ -12437,6 +12995,18 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12437
12995
|
logger;
|
|
12438
12996
|
metrics;
|
|
12439
12997
|
awarenessRelay = new AwarenessRelay();
|
|
12998
|
+
/** Reads the records presence cursors name, for every session (F16). */
|
|
12999
|
+
presenceRecords;
|
|
13000
|
+
/** The record each session's current presence cursor names (F16). */
|
|
13001
|
+
presenceTargets = /* @__PURE__ */ new Map();
|
|
13002
|
+
/** The sessions whose presence cursor names each record, by record key. */
|
|
13003
|
+
presenceTargetsByKey = /* @__PURE__ */ new Map();
|
|
13004
|
+
/**
|
|
13005
|
+
* Audience re-decisions running, one per record: a write while one runs marks it
|
|
13006
|
+
* dirty, so it reads again and the last decision applied is always from a read that
|
|
13007
|
+
* began after the last write (never an older read finishing late).
|
|
13008
|
+
*/
|
|
13009
|
+
presenceRefreshes = /* @__PURE__ */ new Map();
|
|
12440
13010
|
yjsDocRelay = new YjsDocRelay();
|
|
12441
13011
|
blobChunkRelay;
|
|
12442
13012
|
/** Wrapped encryption key records (ENC-1, D4b), persisted in the server store. */
|
|
@@ -12449,8 +13019,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12449
13019
|
/** Per-principal ingest budget per minute; 0 when disabled. */
|
|
12450
13020
|
maxOpsPerMinutePerUser;
|
|
12451
13021
|
allowLegacyAnonymousClaims;
|
|
13022
|
+
deviceNodeHandover;
|
|
13023
|
+
unscopedSharing;
|
|
12452
13024
|
anonymousClaimTtlMs;
|
|
12453
13025
|
maxOpsPerBatch;
|
|
13026
|
+
maxScopePredicateValues;
|
|
12454
13027
|
maxMessageBytes;
|
|
12455
13028
|
heartbeatIntervalMs;
|
|
12456
13029
|
appHeartbeatIntervalMs;
|
|
@@ -12482,9 +13055,10 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12482
13055
|
/** Internal ClientSession id -> HTTP session id, for cleanup on close. */
|
|
12483
13056
|
httpSessionIdBySession = /* @__PURE__ */ new Map();
|
|
12484
13057
|
httpSessionIdleTimeoutMs;
|
|
12485
|
-
//
|
|
12486
|
-
//
|
|
12487
|
-
|
|
13058
|
+
// The @korajs/server package version, reported by getStatus() and /health so
|
|
13059
|
+
// operators can tell which release is deployed. Read from package.json (inlined by
|
|
13060
|
+
// the build), never hand-maintained. Not used for protocol negotiation.
|
|
13061
|
+
serverVersion = version;
|
|
12488
13062
|
wsServer = null;
|
|
12489
13063
|
running = false;
|
|
12490
13064
|
/**
|
|
@@ -12517,9 +13091,10 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12517
13091
|
static ORPHANED_RELAY_TTL_MS = 5 * 6e4;
|
|
12518
13092
|
constructor(config) {
|
|
12519
13093
|
this.store = config.store;
|
|
13094
|
+
this.presenceRecords = new PresenceRecords(config.store);
|
|
12520
13095
|
const storeSchemaVersion = this.store.getSchema()?.version;
|
|
12521
13096
|
if (config.schemaVersion !== void 0 && storeSchemaVersion !== void 0 && config.schemaVersion !== storeSchemaVersion) {
|
|
12522
|
-
throw new
|
|
13097
|
+
throw new import_core35.SyncError("Sync schema version does not match the configured store schema.", {
|
|
12523
13098
|
syncSchemaVersion: config.schemaVersion,
|
|
12524
13099
|
storeSchemaVersion
|
|
12525
13100
|
});
|
|
@@ -12610,13 +13185,21 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12610
13185
|
}
|
|
12611
13186
|
this.maxOpsPerMinutePerUser = config.maxOpsPerMinutePerUser ?? (config.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE) * DEFAULT_USER_BUDGET_MULTIPLIER;
|
|
12612
13187
|
this.allowLegacyAnonymousClaims = config.allowLegacyAnonymousClaims;
|
|
13188
|
+
this.deviceNodeHandover = config.deviceNodeHandover;
|
|
13189
|
+
this.unscopedSharing = config.unscopedSharing;
|
|
12613
13190
|
this.anonymousClaimTtlMs = config.anonymousClaimTtlMs;
|
|
12614
13191
|
if (config.maxOpsPerBatch !== void 0 && (!Number.isInteger(config.maxOpsPerBatch) || config.maxOpsPerBatch < 1)) {
|
|
12615
|
-
throw new
|
|
13192
|
+
throw new import_core35.SyncError("maxOpsPerBatch must be a positive integer", {
|
|
12616
13193
|
maxOpsPerBatch: config.maxOpsPerBatch
|
|
12617
13194
|
});
|
|
12618
13195
|
}
|
|
12619
13196
|
this.maxOpsPerBatch = config.maxOpsPerBatch;
|
|
13197
|
+
if (config.maxScopePredicateValues !== void 0 && (!Number.isInteger(config.maxScopePredicateValues) || config.maxScopePredicateValues < 1)) {
|
|
13198
|
+
throw new import_core35.SyncError("maxScopePredicateValues must be a positive integer", {
|
|
13199
|
+
maxScopePredicateValues: config.maxScopePredicateValues
|
|
13200
|
+
});
|
|
13201
|
+
}
|
|
13202
|
+
this.maxScopePredicateValues = config.maxScopePredicateValues;
|
|
12620
13203
|
this.validateOperation = config.validateOperation;
|
|
12621
13204
|
this.koraContext = createRouteContext(this, this.store);
|
|
12622
13205
|
if (!this.emitter) {
|
|
@@ -12661,7 +13244,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12661
13244
|
*/
|
|
12662
13245
|
terminateSessions(filter) {
|
|
12663
13246
|
if (filter.userId === void 0 && filter.deviceId === void 0) {
|
|
12664
|
-
throw new
|
|
13247
|
+
throw new import_core35.KoraError(
|
|
12665
13248
|
"terminateSessions needs a userId or a deviceId; refusing to end every session.",
|
|
12666
13249
|
"INVALID_TERMINATE_FILTER",
|
|
12667
13250
|
{ fix: "Pass { userId } to end a user, or { userId, deviceId } to end one device." }
|
|
@@ -12799,6 +13382,65 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12799
13382
|
});
|
|
12800
13383
|
return run;
|
|
12801
13384
|
}
|
|
13385
|
+
/**
|
|
13386
|
+
* Re-resolve one user's grant on every live session of that user now, instead of
|
|
13387
|
+
* waiting for the next revalidation pass (`sessionRevalidationIntervalMs`). Call it
|
|
13388
|
+
* after a membership change the auth provider's grant depends on: an invitation
|
|
13389
|
+
* accepted, a collaborator removed, a role changed.
|
|
13390
|
+
*
|
|
13391
|
+
* Each of the user's sessions re-authenticates its credential with the provider. A
|
|
13392
|
+
* session whose resolved download or upload scope changed ends with a retriable
|
|
13393
|
+
* `SCOPE_CHANGED`; its client reconnects at once and receives the new grant. A
|
|
13394
|
+
* narrowed grant applies the client's `scopeExit` policy at that handshake
|
|
13395
|
+
* (`'retract'` hides the rows that left the scope), and the device's unsynced writes
|
|
13396
|
+
* outside the new upload scope are refused. A session still in its handshake
|
|
13397
|
+
* re-checks as soon as it is established, so a grant read before the change never
|
|
13398
|
+
* outlives this call. Sessions whose grant did not change are kept.
|
|
13399
|
+
*
|
|
13400
|
+
* Only this process's sessions are refreshed. Other instances apply the change at
|
|
13401
|
+
* their next revalidation pass, or call this too (for example from a pub/sub
|
|
13402
|
+
* message).
|
|
13403
|
+
*
|
|
13404
|
+
* @param userId - The user whose grant changed (the auth provider's `userId`)
|
|
13405
|
+
* @returns The number of sessions this call ended
|
|
13406
|
+
*
|
|
13407
|
+
* @example
|
|
13408
|
+
* ```typescript
|
|
13409
|
+
* await removeCollaborator(documentId, bobId)
|
|
13410
|
+
* await server.refreshScopes(bobId)
|
|
13411
|
+
* ```
|
|
13412
|
+
*/
|
|
13413
|
+
async refreshScopes(userId) {
|
|
13414
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
13415
|
+
throw new import_core35.KoraError(
|
|
13416
|
+
"refreshScopes needs the user id whose grant changed.",
|
|
13417
|
+
"INVALID_REFRESH_SCOPES_USER",
|
|
13418
|
+
{ fix: "Pass the userId your auth provider returns for that user." }
|
|
13419
|
+
);
|
|
13420
|
+
}
|
|
13421
|
+
let terminated = 0;
|
|
13422
|
+
for (const session of [...this.sessions.values()]) {
|
|
13423
|
+
const outcome = await session.refreshScopes(userId);
|
|
13424
|
+
if (outcome === "terminated") terminated++;
|
|
13425
|
+
if (outcome === "error") {
|
|
13426
|
+
this.logger.log({
|
|
13427
|
+
timestamp: Date.now(),
|
|
13428
|
+
level: "warn",
|
|
13429
|
+
event: "session.revalidation_failed",
|
|
13430
|
+
sessionId: session.getSessionId(),
|
|
13431
|
+
details: { reason: "refreshScopes", userId }
|
|
13432
|
+
});
|
|
13433
|
+
}
|
|
13434
|
+
}
|
|
13435
|
+
this.logger.log({
|
|
13436
|
+
timestamp: Date.now(),
|
|
13437
|
+
level: "info",
|
|
13438
|
+
event: "sessions.scopes_refreshed",
|
|
13439
|
+
count: terminated,
|
|
13440
|
+
details: { userId }
|
|
13441
|
+
});
|
|
13442
|
+
return terminated;
|
|
13443
|
+
}
|
|
12802
13444
|
/** Run {@link revalidateSessions} from the delivery poll tick once the interval elapsed. */
|
|
12803
13445
|
maybeRevalidateSessions(now = Date.now()) {
|
|
12804
13446
|
if (this.sessionRevalidationIntervalMs <= 0) return;
|
|
@@ -12904,13 +13546,17 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
12904
13546
|
const MAX_SCANNED = 1e3;
|
|
12905
13547
|
if (to - from > MAX_SCANNED) {
|
|
12906
13548
|
this.blobAccess.invalidate();
|
|
13549
|
+
this.refreshPresence(null);
|
|
12907
13550
|
return;
|
|
12908
13551
|
}
|
|
12909
13552
|
try {
|
|
12910
13553
|
const delivered = await this.store.getOperationsAfterDelivery(from, MAX_SCANNED);
|
|
12911
|
-
|
|
13554
|
+
const operations2 = delivered.map((d) => d.operation);
|
|
13555
|
+
this.blobAccess.invalidate(collectionsOf(operations2));
|
|
13556
|
+
this.refreshPresence(operations2);
|
|
12912
13557
|
} catch {
|
|
12913
13558
|
this.blobAccess.invalidate();
|
|
13559
|
+
this.refreshPresence(null);
|
|
12914
13560
|
}
|
|
12915
13561
|
}
|
|
12916
13562
|
/**
|
|
@@ -13057,11 +13703,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13057
13703
|
*/
|
|
13058
13704
|
async start(wsServerImpl) {
|
|
13059
13705
|
if (this.running) {
|
|
13060
|
-
throw new
|
|
13706
|
+
throw new import_core35.SyncError("Server is already running", { port: this.port });
|
|
13061
13707
|
}
|
|
13062
13708
|
await this.storeTransformsReady;
|
|
13063
13709
|
if (!wsServerImpl && this.port === void 0) {
|
|
13064
|
-
throw new
|
|
13710
|
+
throw new import_core35.SyncError(
|
|
13065
13711
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
13066
13712
|
{}
|
|
13067
13713
|
);
|
|
@@ -13126,6 +13772,10 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13126
13772
|
this.revocationUnsubscribe?.();
|
|
13127
13773
|
this.revocationUnsubscribe = null;
|
|
13128
13774
|
this.awarenessRelay.clear();
|
|
13775
|
+
this.presenceTargets.clear();
|
|
13776
|
+
this.presenceTargetsByKey.clear();
|
|
13777
|
+
this.presenceRefreshes.clear();
|
|
13778
|
+
this.presenceRecords.clear();
|
|
13129
13779
|
this.yjsDocRelay.clear();
|
|
13130
13780
|
this.blobChunkRelay.clear();
|
|
13131
13781
|
for (const session of this.sessions.values()) {
|
|
@@ -13271,7 +13921,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13271
13921
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
13272
13922
|
transport.send({
|
|
13273
13923
|
type: "error",
|
|
13274
|
-
messageId: (0,
|
|
13924
|
+
messageId: (0, import_core35.generateUUIDv7)(),
|
|
13275
13925
|
code: "MAX_CONNECTIONS",
|
|
13276
13926
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
13277
13927
|
retriable: true
|
|
@@ -13284,13 +13934,13 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13284
13934
|
event: "connection.rejected",
|
|
13285
13935
|
details: { reason: "max_connections", max: this.maxConnections }
|
|
13286
13936
|
});
|
|
13287
|
-
throw new
|
|
13937
|
+
throw new import_core35.SyncError("Maximum connections reached", {
|
|
13288
13938
|
current: this.sessions.size,
|
|
13289
13939
|
max: this.maxConnections
|
|
13290
13940
|
});
|
|
13291
13941
|
}
|
|
13292
13942
|
this.ensureBackgroundTimersStarted();
|
|
13293
|
-
const sessionId = (0,
|
|
13943
|
+
const sessionId = (0, import_core35.generateUUIDv7)();
|
|
13294
13944
|
this.metrics.recordConnection(sessionId);
|
|
13295
13945
|
const sessionEmitter = new import_internal9.SimpleEventEmitter();
|
|
13296
13946
|
sessionEmitter.on("sync:connected", (event) => {
|
|
@@ -13390,9 +14040,10 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13390
14040
|
onRelay: (sourceSessionId, operations2) => {
|
|
13391
14041
|
this.handleRelay(sourceSessionId, operations2);
|
|
13392
14042
|
},
|
|
13393
|
-
onAwarenessUpdate: (sourceSessionId, message) => {
|
|
13394
|
-
this.handleAwarenessRelay(sourceSessionId, message);
|
|
14043
|
+
onAwarenessUpdate: (sourceSessionId, message, cursorTarget) => {
|
|
14044
|
+
this.handleAwarenessRelay(sourceSessionId, message, cursorTarget);
|
|
13395
14045
|
},
|
|
14046
|
+
presenceRecords: this.presenceRecords,
|
|
13396
14047
|
onYjsDocUpdate: (sourceSessionId, message, storedRecord) => {
|
|
13397
14048
|
this.handleYjsDocRelay(sourceSessionId, message, storedRecord);
|
|
13398
14049
|
},
|
|
@@ -13425,8 +14076,11 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13425
14076
|
...this.maxOpsPerMinute !== void 0 ? { maxOpsPerMinute: this.maxOpsPerMinute } : {},
|
|
13426
14077
|
...this.blobLimits.maxRequestsPerMinute !== void 0 ? { maxBlobRequestsPerMinute: this.blobLimits.maxRequestsPerMinute } : {},
|
|
13427
14078
|
...this.maxOpsPerBatch !== void 0 ? { maxOpsPerBatch: this.maxOpsPerBatch } : {},
|
|
14079
|
+
...this.maxScopePredicateValues !== void 0 ? { maxScopePredicateValues: this.maxScopePredicateValues } : {},
|
|
13428
14080
|
...this.validateOperation ? { validateOperation: this.validateOperation, koraContext: this.koraContext } : {},
|
|
13429
14081
|
blobAccess: this.blobAccess,
|
|
14082
|
+
...this.deviceNodeHandover !== void 0 ? { deviceNodeHandover: this.deviceNodeHandover } : {},
|
|
14083
|
+
...this.unscopedSharing !== void 0 ? { unscopedSharing: this.unscopedSharing } : {},
|
|
13430
14084
|
...this.allowLegacyAnonymousClaims !== void 0 ? { allowLegacyAnonymousClaims: this.allowLegacyAnonymousClaims } : {},
|
|
13431
14085
|
...this.anonymousClaimTtlMs !== void 0 ? { anonymousClaimTtlMs: this.anonymousClaimTtlMs } : {},
|
|
13432
14086
|
isNodeLive: (nodeId, exceptSessionId) => this.isNodeLive(nodeId, exceptSessionId),
|
|
@@ -13496,6 +14150,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13496
14150
|
const result = await applyServerOperation(this.store, op, void 0, options);
|
|
13497
14151
|
if (result.result === "applied" && result.appliedOperations.length > 0) {
|
|
13498
14152
|
this.blobAccess.invalidate(collectionsOf(result.appliedOperations));
|
|
14153
|
+
this.refreshPresence(result.appliedOperations);
|
|
13499
14154
|
for (const session of this.sessions.values()) {
|
|
13500
14155
|
session.relayOperations(result.appliedOperations);
|
|
13501
14156
|
}
|
|
@@ -13515,6 +14170,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13515
14170
|
return;
|
|
13516
14171
|
}
|
|
13517
14172
|
this.blobAccess.invalidate(collectionsOf(operations2));
|
|
14173
|
+
this.refreshPresence(operations2);
|
|
13518
14174
|
for (const session of this.sessions.values()) {
|
|
13519
14175
|
session.relayOperations(operations2);
|
|
13520
14176
|
}
|
|
@@ -13543,7 +14199,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13543
14199
|
const records = await this.store.materializeCollection(name);
|
|
13544
14200
|
for (const record of records) {
|
|
13545
14201
|
for (const value of Object.values(record)) {
|
|
13546
|
-
if ((0,
|
|
14202
|
+
if ((0, import_core35.isBlobRef)(value)) {
|
|
13547
14203
|
refs.push(value);
|
|
13548
14204
|
}
|
|
13549
14205
|
}
|
|
@@ -13611,6 +14267,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13611
14267
|
}
|
|
13612
14268
|
handleRelay(sourceSessionId, operations2) {
|
|
13613
14269
|
this.blobAccess.invalidate(collectionsOf(operations2));
|
|
14270
|
+
this.refreshPresence(operations2);
|
|
13614
14271
|
const targetCount = this.sessions.size - 1;
|
|
13615
14272
|
const byteSize = estimateOperationByteSize(operations2);
|
|
13616
14273
|
this.metrics.recordSent(
|
|
@@ -13635,6 +14292,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13635
14292
|
handleSessionClose(sessionId) {
|
|
13636
14293
|
this.metrics.recordDisconnection(sessionId);
|
|
13637
14294
|
this.awarenessRelay.removeClient(sessionId);
|
|
14295
|
+
this.setPresenceTarget(sessionId, null);
|
|
13638
14296
|
this.yjsDocRelay.removeClient(sessionId);
|
|
13639
14297
|
this.blobChunkRelay.removeClient(sessionId);
|
|
13640
14298
|
this.sessions.delete(sessionId);
|
|
@@ -13646,7 +14304,7 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13646
14304
|
}
|
|
13647
14305
|
}
|
|
13648
14306
|
}
|
|
13649
|
-
handleAwarenessRelay(sourceSessionId, message) {
|
|
14307
|
+
handleAwarenessRelay(sourceSessionId, message, cursorTarget) {
|
|
13650
14308
|
const session = this.sessions.get(sourceSessionId);
|
|
13651
14309
|
if (!session || !session.isStreaming()) return;
|
|
13652
14310
|
if (!this.awarenessRelay.hasClient(sourceSessionId)) {
|
|
@@ -13654,10 +14312,143 @@ var KoraSyncServer = class _KoraSyncServer {
|
|
|
13654
14312
|
sourceSessionId,
|
|
13655
14313
|
message.clientId,
|
|
13656
14314
|
session.getTransport(),
|
|
13657
|
-
session.
|
|
14315
|
+
session.getPresencePartitionKey()
|
|
13658
14316
|
);
|
|
13659
14317
|
}
|
|
13660
|
-
this.awarenessRelay.
|
|
14318
|
+
if (!this.awarenessRelay.accepts(sourceSessionId, message)) return;
|
|
14319
|
+
let audience = this.awarenessAudience(session, cursorTarget);
|
|
14320
|
+
if (cursorTarget !== void 0 && cursorTarget.invalid !== true) {
|
|
14321
|
+
const key = presenceRecordKey(cursorTarget.collection, cursorTarget.recordId);
|
|
14322
|
+
this.setPresenceTarget(sourceSessionId, {
|
|
14323
|
+
collection: cursorTarget.collection,
|
|
14324
|
+
recordId: cursorTarget.recordId,
|
|
14325
|
+
key
|
|
14326
|
+
});
|
|
14327
|
+
if (cursorTarget.asOf !== void 0 && this.presenceRecords.touchedSince(key, cursorTarget.asOf)) {
|
|
14328
|
+
audience = () => false;
|
|
14329
|
+
this.schedulePresenceRefresh(key);
|
|
14330
|
+
}
|
|
14331
|
+
} else {
|
|
14332
|
+
this.setPresenceTarget(sourceSessionId, null);
|
|
14333
|
+
}
|
|
14334
|
+
this.awarenessRelay.handleUpdate(sourceSessionId, message, audience);
|
|
14335
|
+
}
|
|
14336
|
+
/** Point a session's presence at a record (or at none), keeping the index in step. */
|
|
14337
|
+
setPresenceTarget(sessionId, target) {
|
|
14338
|
+
const previous = this.presenceTargets.get(sessionId);
|
|
14339
|
+
if (previous) {
|
|
14340
|
+
const sessions2 = this.presenceTargetsByKey.get(previous.key);
|
|
14341
|
+
sessions2?.delete(sessionId);
|
|
14342
|
+
if (sessions2?.size === 0) this.presenceTargetsByKey.delete(previous.key);
|
|
14343
|
+
}
|
|
14344
|
+
if (target === null) {
|
|
14345
|
+
this.presenceTargets.delete(sessionId);
|
|
14346
|
+
return;
|
|
14347
|
+
}
|
|
14348
|
+
this.presenceTargets.set(sessionId, target);
|
|
14349
|
+
let sessions = this.presenceTargetsByKey.get(target.key);
|
|
14350
|
+
if (!sessions) {
|
|
14351
|
+
sessions = /* @__PURE__ */ new Set();
|
|
14352
|
+
this.presenceTargetsByKey.set(target.key, sessions);
|
|
14353
|
+
}
|
|
14354
|
+
sessions.add(sessionId);
|
|
14355
|
+
}
|
|
14356
|
+
/**
|
|
14357
|
+
* Records changed (`operations`, or anything when null): re-decide the audience of
|
|
14358
|
+
* every presence cursor on one of them (F16). A record that moved out of a session's
|
|
14359
|
+
* grant takes the presence on it out of that session, and one that moved in brings
|
|
14360
|
+
* it, without waiting for the sender's next update; catch-ups use the new audience.
|
|
14361
|
+
* Every write is noted first, so a presence read that began before it is never
|
|
14362
|
+
* taken as current.
|
|
14363
|
+
*/
|
|
14364
|
+
refreshPresence(operations2) {
|
|
14365
|
+
if (operations2 === null) {
|
|
14366
|
+
this.presenceRecords.touch(null);
|
|
14367
|
+
for (const key of this.presenceTargetsByKey.keys()) this.schedulePresenceRefresh(key);
|
|
14368
|
+
return;
|
|
14369
|
+
}
|
|
14370
|
+
const keys = new Set(operations2.map((op) => presenceRecordKey(op.collection, op.recordId)));
|
|
14371
|
+
this.presenceRecords.touch(keys);
|
|
14372
|
+
for (const key of keys) {
|
|
14373
|
+
if (this.presenceTargetsByKey.has(key)) this.schedulePresenceRefresh(key);
|
|
14374
|
+
}
|
|
14375
|
+
}
|
|
14376
|
+
/** Re-decide the presence on one record, coalesced with a re-decision already running. */
|
|
14377
|
+
schedulePresenceRefresh(key) {
|
|
14378
|
+
const running = this.presenceRefreshes.get(key);
|
|
14379
|
+
if (running) {
|
|
14380
|
+
running.dirty = true;
|
|
14381
|
+
return;
|
|
14382
|
+
}
|
|
14383
|
+
const state = { dirty: true };
|
|
14384
|
+
this.presenceRefreshes.set(key, state);
|
|
14385
|
+
void this.runPresenceRefresh(key, state);
|
|
14386
|
+
}
|
|
14387
|
+
async runPresenceRefresh(key, state) {
|
|
14388
|
+
let staleReads = 0;
|
|
14389
|
+
try {
|
|
14390
|
+
while (state.dirty) {
|
|
14391
|
+
state.dirty = false;
|
|
14392
|
+
const sessions = this.presenceTargetsByKey.get(key);
|
|
14393
|
+
const first = sessions?.values().next();
|
|
14394
|
+
const target = first && !first.done ? this.presenceTargets.get(first.value) : void 0;
|
|
14395
|
+
if (!target) return;
|
|
14396
|
+
let stored;
|
|
14397
|
+
try {
|
|
14398
|
+
const read = await this.presenceRecords.read(target.collection, target.recordId);
|
|
14399
|
+
if (this.presenceRecords.touchedSince(key, read.asOf)) {
|
|
14400
|
+
state.dirty = true;
|
|
14401
|
+
if (staleReads < 2) {
|
|
14402
|
+
staleReads += 1;
|
|
14403
|
+
continue;
|
|
14404
|
+
}
|
|
14405
|
+
stored = void 0;
|
|
14406
|
+
} else {
|
|
14407
|
+
stored = read.stored;
|
|
14408
|
+
}
|
|
14409
|
+
} catch {
|
|
14410
|
+
stored = void 0;
|
|
14411
|
+
}
|
|
14412
|
+
staleReads = 0;
|
|
14413
|
+
for (const sessionId of this.presenceTargetsByKey.get(key) ?? []) {
|
|
14414
|
+
const session = this.sessions.get(sessionId);
|
|
14415
|
+
if (!session) continue;
|
|
14416
|
+
this.awarenessRelay.updateAudience(
|
|
14417
|
+
sessionId,
|
|
14418
|
+
stored === void 0 ? () => false : this.awarenessAudience(session, {
|
|
14419
|
+
collection: target.collection,
|
|
14420
|
+
recordId: target.recordId,
|
|
14421
|
+
stored
|
|
14422
|
+
})
|
|
14423
|
+
);
|
|
14424
|
+
}
|
|
14425
|
+
}
|
|
14426
|
+
} finally {
|
|
14427
|
+
if (this.presenceRefreshes.get(key) === state) this.presenceRefreshes.delete(key);
|
|
14428
|
+
}
|
|
14429
|
+
}
|
|
14430
|
+
/**
|
|
14431
|
+
* Who may see a presence state (F16). A state whose cursor names a record reaches
|
|
14432
|
+
* exactly the sessions whose download scope contains that record, the rule of the
|
|
14433
|
+
* Yjs doc channel, and only when the sender may read the record too: collaborators
|
|
14434
|
+
* with different grants see each other on the documents they share, and nobody
|
|
14435
|
+
* sees presence on a record outside their grant. A malformed cursor reaches
|
|
14436
|
+
* nobody. A state without a cursor names no record: it reaches only sessions of
|
|
14437
|
+
* the same presence partition (identical download scope; anonymous sessions are
|
|
14438
|
+
* each their own partition).
|
|
14439
|
+
*/
|
|
14440
|
+
awarenessAudience(source, cursorTarget) {
|
|
14441
|
+
if (cursorTarget === void 0) {
|
|
14442
|
+
const partition = source.getPresencePartitionKey();
|
|
14443
|
+
return (targetSessionId) => {
|
|
14444
|
+
const target = this.sessions.get(targetSessionId);
|
|
14445
|
+
return target?.isStreaming() === true && target.getPresencePartitionKey() === partition;
|
|
14446
|
+
};
|
|
14447
|
+
}
|
|
14448
|
+
if (cursorTarget.invalid === true) return () => false;
|
|
14449
|
+
const { collection, recordId, stored } = cursorTarget;
|
|
14450
|
+
if (!source.canReceiveRecord(collection, recordId, stored)) return () => false;
|
|
14451
|
+
return (targetSessionId) => this.sessions.get(targetSessionId)?.canReceiveRecord(collection, recordId, stored) ?? false;
|
|
13661
14452
|
}
|
|
13662
14453
|
handleYjsDocRelay(sourceSessionId, message, storedRecord) {
|
|
13663
14454
|
if (!this.sessions.has(sourceSessionId)) {
|
|
@@ -13733,7 +14524,7 @@ function normalizeHttpBody(body, contentType) {
|
|
|
13733
14524
|
|
|
13734
14525
|
// src/auth/token-auth.ts
|
|
13735
14526
|
init_cjs_shims();
|
|
13736
|
-
var
|
|
14527
|
+
var import_core36 = require("@korajs/core");
|
|
13737
14528
|
var TokenAuthProvider = class {
|
|
13738
14529
|
validate;
|
|
13739
14530
|
constructor(options) {
|
|
@@ -13743,7 +14534,7 @@ var TokenAuthProvider = class {
|
|
|
13743
14534
|
const context = await this.validate(token);
|
|
13744
14535
|
if (!context) return null;
|
|
13745
14536
|
if (context.scopes === void 0 && context.downlinkScopes === void 0 && context.uplinkScopes === void 0) {
|
|
13746
|
-
return { ...context, scopes: (0,
|
|
14537
|
+
return { ...context, scopes: (0, import_core36.claimScopes)({ userId: context.userId }) };
|
|
13747
14538
|
}
|
|
13748
14539
|
return context;
|
|
13749
14540
|
}
|
|
@@ -13751,7 +14542,7 @@ var TokenAuthProvider = class {
|
|
|
13751
14542
|
|
|
13752
14543
|
// src/auth/kora-auth-provider.ts
|
|
13753
14544
|
init_cjs_shims();
|
|
13754
|
-
var
|
|
14545
|
+
var import_core37 = require("@korajs/core");
|
|
13755
14546
|
var KoraAuthProvider = class {
|
|
13756
14547
|
tokenValidator;
|
|
13757
14548
|
userLookup;
|
|
@@ -13778,7 +14569,7 @@ var KoraAuthProvider = class {
|
|
|
13778
14569
|
if (this.deviceTracker) {
|
|
13779
14570
|
await this.deviceTracker.touchDevice(payload.dev);
|
|
13780
14571
|
}
|
|
13781
|
-
const scopes = this.resolveScopes ? await this.resolveScopes(payload.sub) : (0,
|
|
14572
|
+
const scopes = this.resolveScopes ? await this.resolveScopes(payload.sub) : (0, import_core37.claimScopes)({ userId: payload.sub });
|
|
13782
14573
|
return {
|
|
13783
14574
|
userId: payload.sub,
|
|
13784
14575
|
scopes,
|
|
@@ -13833,7 +14624,7 @@ var MixedAuthProvider = class {
|
|
|
13833
14624
|
};
|
|
13834
14625
|
|
|
13835
14626
|
// src/index.ts
|
|
13836
|
-
var
|
|
14627
|
+
var import_core38 = require("@korajs/core");
|
|
13837
14628
|
|
|
13838
14629
|
// src/server/create-server.ts
|
|
13839
14630
|
init_cjs_shims();
|
|
@@ -13848,9 +14639,9 @@ init_ingest_validation();
|
|
|
13848
14639
|
// src/server/static-files.ts
|
|
13849
14640
|
init_cjs_shims();
|
|
13850
14641
|
var import_node_crypto = require("crypto");
|
|
13851
|
-
var
|
|
14642
|
+
var import_node_fs2 = require("fs");
|
|
13852
14643
|
var import_promises = require("fs/promises");
|
|
13853
|
-
var
|
|
14644
|
+
var import_node_path2 = require("path");
|
|
13854
14645
|
var import_node_zlib = require("zlib");
|
|
13855
14646
|
var STATIC_MIME_TYPES = {
|
|
13856
14647
|
".html": "text/html; charset=utf-8",
|
|
@@ -13906,7 +14697,7 @@ var ASSET_PREFIX = "/assets/";
|
|
|
13906
14697
|
var IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
13907
14698
|
var REVALIDATE_CACHE_CONTROL = "no-cache";
|
|
13908
14699
|
function isContentHashedFileName(fileName) {
|
|
13909
|
-
const ext = (0,
|
|
14700
|
+
const ext = (0, import_node_path2.extname)(fileName);
|
|
13910
14701
|
const stem = ext ? fileName.slice(0, -ext.length) : fileName;
|
|
13911
14702
|
const match = /[.-]([A-Za-z0-9_]{8,})$/.exec(stem);
|
|
13912
14703
|
if (!match) return false;
|
|
@@ -13942,19 +14733,25 @@ function fileVersionKey(stats) {
|
|
|
13942
14733
|
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`;
|
|
13943
14734
|
}
|
|
13944
14735
|
var DIGEST_CACHE_MAX_ENTRIES = 4096;
|
|
13945
|
-
function createStaticFileHandler(staticDir) {
|
|
13946
|
-
const root = (0,
|
|
14736
|
+
function createStaticFileHandler(staticDir, options = {}) {
|
|
14737
|
+
const root = (0, import_node_path2.resolve)(staticDir);
|
|
14738
|
+
const spaFallback = options.spaFallback ?? "navigation";
|
|
14739
|
+
const servesShell = (req, pathname) => {
|
|
14740
|
+
if (pathname.startsWith(ASSET_PREFIX)) return false;
|
|
14741
|
+
if (isNavigation(req)) return true;
|
|
14742
|
+
return spaFallback === "extensionless" && (0, import_node_path2.extname)(pathname) === "";
|
|
14743
|
+
};
|
|
13947
14744
|
const compressed = /* @__PURE__ */ new Map();
|
|
13948
14745
|
let compressedBytes = 0;
|
|
13949
14746
|
const digests = /* @__PURE__ */ new Map();
|
|
13950
14747
|
const verifiedSiblings = /* @__PURE__ */ new Map();
|
|
13951
14748
|
async function contentDigest(path, stats) {
|
|
13952
|
-
const
|
|
14749
|
+
const version2 = fileVersionKey(stats);
|
|
13953
14750
|
const cached = digests.get(path);
|
|
13954
|
-
if (cached && cached.version ===
|
|
14751
|
+
if (cached && cached.version === version2) return cached.digest;
|
|
13955
14752
|
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
13956
14753
|
await new Promise((done, fail) => {
|
|
13957
|
-
const stream = (0,
|
|
14754
|
+
const stream = (0, import_node_fs2.createReadStream)(path);
|
|
13958
14755
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
13959
14756
|
stream.on("end", () => done());
|
|
13960
14757
|
stream.on("error", fail);
|
|
@@ -13965,7 +14762,7 @@ function createStaticFileHandler(staticDir) {
|
|
|
13965
14762
|
const oldest = digests.keys().next().value;
|
|
13966
14763
|
if (oldest !== void 0) digests.delete(oldest);
|
|
13967
14764
|
}
|
|
13968
|
-
digests.set(path, { version, digest });
|
|
14765
|
+
digests.set(path, { version: version2, digest });
|
|
13969
14766
|
return digest;
|
|
13970
14767
|
}
|
|
13971
14768
|
function remember(key, body) {
|
|
@@ -13993,14 +14790,14 @@ function createStaticFileHandler(staticDir) {
|
|
|
13993
14790
|
return null;
|
|
13994
14791
|
}
|
|
13995
14792
|
if (decoded.includes("\0")) return null;
|
|
13996
|
-
const candidate = (0,
|
|
13997
|
-
if (candidate !== root && !candidate.startsWith(root +
|
|
14793
|
+
const candidate = (0, import_node_path2.resolve)((0, import_node_path2.join)(root, decoded));
|
|
14794
|
+
if (candidate !== root && !candidate.startsWith(root + import_node_path2.sep)) return null;
|
|
13998
14795
|
return candidate;
|
|
13999
14796
|
}
|
|
14000
14797
|
async function containedRealPath(path) {
|
|
14001
14798
|
try {
|
|
14002
14799
|
const [realRoot, real] = await Promise.all([(0, import_promises.realpath)(root), (0, import_promises.realpath)(path)]);
|
|
14003
|
-
return real === realRoot || real.startsWith(realRoot +
|
|
14800
|
+
return real === realRoot || real.startsWith(realRoot + import_node_path2.sep) ? real : null;
|
|
14004
14801
|
} catch {
|
|
14005
14802
|
return null;
|
|
14006
14803
|
}
|
|
@@ -14013,12 +14810,12 @@ function createStaticFileHandler(staticDir) {
|
|
|
14013
14810
|
const stats = await fileStat(real);
|
|
14014
14811
|
if (stats?.isFile()) return { path: real, name: filePath, stats };
|
|
14015
14812
|
if (stats?.isDirectory()) {
|
|
14016
|
-
const index3 = (0,
|
|
14813
|
+
const index3 = (0, import_node_path2.join)(real, "index.html");
|
|
14017
14814
|
const realIndex = await containedRealPath(index3);
|
|
14018
14815
|
if (realIndex === null) return null;
|
|
14019
14816
|
const indexStats = await fileStat(realIndex);
|
|
14020
14817
|
if (indexStats?.isFile()) {
|
|
14021
|
-
return { path: realIndex, name: (0,
|
|
14818
|
+
return { path: realIndex, name: (0, import_node_path2.join)(filePath, "index.html"), stats: indexStats };
|
|
14022
14819
|
}
|
|
14023
14820
|
}
|
|
14024
14821
|
return null;
|
|
@@ -14031,7 +14828,7 @@ function createStaticFileHandler(staticDir) {
|
|
|
14031
14828
|
return;
|
|
14032
14829
|
}
|
|
14033
14830
|
let found = await locate(pathname);
|
|
14034
|
-
if (!found &&
|
|
14831
|
+
if (!found && servesShell(req, pathname)) {
|
|
14035
14832
|
found = await locate("/index.html");
|
|
14036
14833
|
}
|
|
14037
14834
|
if (!found) {
|
|
@@ -14043,8 +14840,8 @@ function createStaticFileHandler(staticDir) {
|
|
|
14043
14840
|
return;
|
|
14044
14841
|
}
|
|
14045
14842
|
const { path, name, stats } = found;
|
|
14046
|
-
const ext = (0,
|
|
14047
|
-
const fileName = (0,
|
|
14843
|
+
const ext = (0, import_node_path2.extname)(name).toLowerCase();
|
|
14844
|
+
const fileName = (0, import_node_path2.basename)(name);
|
|
14048
14845
|
const cacheControl = cacheControlFor(fileName);
|
|
14049
14846
|
const revalidated = cacheControl === REVALIDATE_CACHE_CONTROL;
|
|
14050
14847
|
const headers = {
|
|
@@ -14088,7 +14885,7 @@ function createStaticFileHandler(staticDir) {
|
|
|
14088
14885
|
res.end();
|
|
14089
14886
|
return;
|
|
14090
14887
|
}
|
|
14091
|
-
(0,
|
|
14888
|
+
(0, import_node_fs2.createReadStream)(path).pipe(res);
|
|
14092
14889
|
};
|
|
14093
14890
|
async function compressedBody(path, name, digest, encoding) {
|
|
14094
14891
|
const sibling = await containedRealPath(`${name}${encoding === "br" ? ".br" : ".gz"}`);
|
|
@@ -14248,6 +15045,11 @@ function parseIpv6(address) {
|
|
|
14248
15045
|
|
|
14249
15046
|
// src/server/production-server.ts
|
|
14250
15047
|
var DEFAULT_MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
15048
|
+
var OPERATIONAL_PATHS = {
|
|
15049
|
+
admin: "/__kora, /__kora/status, /__kora/events",
|
|
15050
|
+
metrics: "/__kora/metrics",
|
|
15051
|
+
backup: "/__kora/backup/*"
|
|
15052
|
+
};
|
|
14251
15053
|
var DEFAULT_MAX_BACKUP_BYTES = 256 * 1024 * 1024;
|
|
14252
15054
|
function createProductionServer(config) {
|
|
14253
15055
|
const port = config.port ?? (Number(process.env.PORT) || 3001);
|
|
@@ -14283,10 +15085,27 @@ function createProductionServer(config) {
|
|
|
14283
15085
|
}
|
|
14284
15086
|
return null;
|
|
14285
15087
|
}
|
|
14286
|
-
|
|
15088
|
+
const production = process.env.NODE_ENV === "production";
|
|
15089
|
+
const allowPublic = config.operationalAuth?.allowPublic === true;
|
|
15090
|
+
const OPERATIONAL_GROUPS = ["admin", "metrics", "backup"];
|
|
15091
|
+
const unprotectedGroups = OPERATIONAL_GROUPS.filter((kind) => !getOperationalToken(kind));
|
|
15092
|
+
function isOperationalRequestAllowed(req, res, kind) {
|
|
14287
15093
|
const expected = getOperationalToken(kind);
|
|
14288
|
-
if (!expected)
|
|
14289
|
-
|
|
15094
|
+
if (!expected) {
|
|
15095
|
+
if (!production || allowPublic) return true;
|
|
15096
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
15097
|
+
res.end(
|
|
15098
|
+
JSON.stringify({
|
|
15099
|
+
error: `This operational endpoint is disabled: no ${kind === "admin" ? "adminToken" : `${kind}Token or adminToken`} is configured and NODE_ENV is production.`,
|
|
15100
|
+
code: "OPERATIONAL_ENDPOINT_DISABLED",
|
|
15101
|
+
fix: "Set operationalAuth tokens in createProductionServer, or operationalAuth.allowPublic: true to serve it without one."
|
|
15102
|
+
})
|
|
15103
|
+
);
|
|
15104
|
+
return false;
|
|
15105
|
+
}
|
|
15106
|
+
if (extractRequestToken(req) === expected) return true;
|
|
15107
|
+
rejectUnauthorized(res);
|
|
15108
|
+
return false;
|
|
14290
15109
|
}
|
|
14291
15110
|
function rejectUnauthorized(res) {
|
|
14292
15111
|
res.writeHead(401, {
|
|
@@ -14445,10 +15264,31 @@ function createProductionServer(config) {
|
|
|
14445
15264
|
getLiveBlobRefs() {
|
|
14446
15265
|
return syncServer.getLiveBlobRefs();
|
|
14447
15266
|
},
|
|
15267
|
+
refreshScopes(userId) {
|
|
15268
|
+
return syncServer.refreshScopes(userId);
|
|
15269
|
+
},
|
|
15270
|
+
revalidateSessions() {
|
|
15271
|
+
return syncServer.revalidateSessions();
|
|
15272
|
+
},
|
|
14448
15273
|
async start() {
|
|
15274
|
+
if (unprotectedGroups.length > 0) {
|
|
15275
|
+
const paths = unprotectedGroups.map((kind) => OPERATIONAL_PATHS[kind]).join(", ");
|
|
15276
|
+
syncServer.getLogger().log({
|
|
15277
|
+
timestamp: Date.now(),
|
|
15278
|
+
level: "warn",
|
|
15279
|
+
event: "server.operational_endpoints_unprotected",
|
|
15280
|
+
details: {
|
|
15281
|
+
groups: unprotectedGroups,
|
|
15282
|
+
disabled: production && !allowPublic,
|
|
15283
|
+
message: production && !allowPublic ? `Operational endpoints without a token are disabled (${paths}). Set operationalAuth.adminToken and backupToken to use them.` : `Operational endpoints without a token are PUBLIC (${paths}). Set operationalAuth.adminToken and backupToken before exposing this server.`
|
|
15284
|
+
}
|
|
15285
|
+
});
|
|
15286
|
+
}
|
|
14449
15287
|
const { createServer } = await import("http");
|
|
14450
15288
|
const { WebSocketServer } = await import("ws");
|
|
14451
|
-
const serveStatic = createStaticFileHandler(staticDir
|
|
15289
|
+
const serveStatic = createStaticFileHandler(staticDir, {
|
|
15290
|
+
...config.spaFallback ? { spaFallback: config.spaFallback } : {}
|
|
15291
|
+
});
|
|
14452
15292
|
httpServer = createServer(async (req, res) => {
|
|
14453
15293
|
try {
|
|
14454
15294
|
await handleRequest(req, res);
|
|
@@ -14482,29 +15322,20 @@ function createProductionServer(config) {
|
|
|
14482
15322
|
return;
|
|
14483
15323
|
}
|
|
14484
15324
|
if (url.pathname === "/__kora/status") {
|
|
14485
|
-
if (!isOperationalRequestAllowed(req, "admin"))
|
|
14486
|
-
rejectUnauthorized(res);
|
|
14487
|
-
return;
|
|
14488
|
-
}
|
|
15325
|
+
if (!isOperationalRequestAllowed(req, res, "admin")) return;
|
|
14489
15326
|
const status = await syncServer.getStatus();
|
|
14490
15327
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
14491
15328
|
res.end(JSON.stringify(status, null, 2));
|
|
14492
15329
|
return;
|
|
14493
15330
|
}
|
|
14494
15331
|
if (url.pathname === "/__kora/metrics") {
|
|
14495
|
-
if (!isOperationalRequestAllowed(req, "metrics"))
|
|
14496
|
-
rejectUnauthorized(res);
|
|
14497
|
-
return;
|
|
14498
|
-
}
|
|
15332
|
+
if (!isOperationalRequestAllowed(req, res, "metrics")) return;
|
|
14499
15333
|
res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
|
|
14500
15334
|
res.end(formatPrometheusMetrics());
|
|
14501
15335
|
return;
|
|
14502
15336
|
}
|
|
14503
15337
|
if (url.pathname === "/__kora/events") {
|
|
14504
|
-
if (!isOperationalRequestAllowed(req, "admin"))
|
|
14505
|
-
rejectUnauthorized(res);
|
|
14506
|
-
return;
|
|
14507
|
-
}
|
|
15338
|
+
if (!isOperationalRequestAllowed(req, res, "admin")) return;
|
|
14508
15339
|
res.writeHead(200, {
|
|
14509
15340
|
"Content-Type": "text/event-stream",
|
|
14510
15341
|
"Cache-Control": "no-cache",
|
|
@@ -14532,20 +15363,14 @@ data: ${JSON.stringify(s)}
|
|
|
14532
15363
|
return;
|
|
14533
15364
|
}
|
|
14534
15365
|
if (url.pathname === "/__kora" || url.pathname === "/__kora/") {
|
|
14535
|
-
if (!isOperationalRequestAllowed(req, "admin"))
|
|
14536
|
-
rejectUnauthorized(res);
|
|
14537
|
-
return;
|
|
14538
|
-
}
|
|
15366
|
+
if (!isOperationalRequestAllowed(req, res, "admin")) return;
|
|
14539
15367
|
const status = await syncServer.getStatus();
|
|
14540
15368
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
14541
15369
|
res.end(renderDashboardHtml(status));
|
|
14542
15370
|
return;
|
|
14543
15371
|
}
|
|
14544
15372
|
if (url.pathname === "/__kora/backup/export" && req.method === "POST") {
|
|
14545
|
-
if (!isOperationalRequestAllowed(req, "backup"))
|
|
14546
|
-
rejectUnauthorized(res);
|
|
14547
|
-
return;
|
|
14548
|
-
}
|
|
15373
|
+
if (!isOperationalRequestAllowed(req, res, "backup")) return;
|
|
14549
15374
|
try {
|
|
14550
15375
|
const backup = await config.store.exportBackup();
|
|
14551
15376
|
res.writeHead(200, {
|
|
@@ -14561,10 +15386,7 @@ data: ${JSON.stringify(s)}
|
|
|
14561
15386
|
return;
|
|
14562
15387
|
}
|
|
14563
15388
|
if (url.pathname === "/__kora/backup/import" && req.method === "POST") {
|
|
14564
|
-
if (!isOperationalRequestAllowed(req, "backup"))
|
|
14565
|
-
rejectUnauthorized(res);
|
|
14566
|
-
return;
|
|
14567
|
-
}
|
|
15389
|
+
if (!isOperationalRequestAllowed(req, res, "backup")) return;
|
|
14568
15390
|
try {
|
|
14569
15391
|
const body = await readBodyBuffer(req, maxBackupBytes);
|
|
14570
15392
|
if (body === null) {
|
|
@@ -14657,7 +15479,7 @@ data: ${JSON.stringify(s)}
|
|
|
14657
15479
|
};
|
|
14658
15480
|
}
|
|
14659
15481
|
function renderDashboardHtml(status) {
|
|
14660
|
-
const
|
|
15482
|
+
const version2 = status.version;
|
|
14661
15483
|
const uptime = formatUptime(status.uptime);
|
|
14662
15484
|
return `<!DOCTYPE html>
|
|
14663
15485
|
<html lang="en">
|
|
@@ -14688,7 +15510,7 @@ function renderDashboardHtml(status) {
|
|
|
14688
15510
|
</head>
|
|
14689
15511
|
<body>
|
|
14690
15512
|
<h1>Kora Sync Server</h1>
|
|
14691
|
-
<p class="subtitle">v${
|
|
15513
|
+
<p class="subtitle">v${version2} · <span class="status-dot running"></span>Running</p>
|
|
14692
15514
|
<div class="grid">
|
|
14693
15515
|
<div class="card"><div class="label">Uptime</div><div class="value">${uptime}</div></div>
|
|
14694
15516
|
<div class="card"><div class="label">Connected Clients</div><div class="value" id="connectedClients">${status.connectedClients}</div></div>
|
|
@@ -14738,6 +15560,7 @@ function formatUptime(ms) {
|
|
|
14738
15560
|
ClientSession,
|
|
14739
15561
|
DEFAULT_MAX_SCOPE_PREDICATE_VALUES,
|
|
14740
15562
|
HttpServerTransport,
|
|
15563
|
+
InvalidDiagnosticOptionsError,
|
|
14741
15564
|
InvalidScopePredicateError,
|
|
14742
15565
|
KoraAuthProvider,
|
|
14743
15566
|
KoraSyncServer,
|
|
@@ -14766,6 +15589,7 @@ function formatUptime(ms) {
|
|
|
14766
15589
|
createProductionServer,
|
|
14767
15590
|
createSilentLogger,
|
|
14768
15591
|
createSqliteServerStore,
|
|
15592
|
+
findJsonStringValues,
|
|
14769
15593
|
isRetriableRejection,
|
|
14770
15594
|
isServerNodeId,
|
|
14771
15595
|
normalizeScopeMap,
|