@korajs/core 1.0.0-beta.0 → 1.0.0-beta.2
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/bindings/index.d.cts +2 -2
- package/dist/bindings/index.d.ts +2 -2
- package/dist/{build-scope-map-DOf4JLTh.d.ts → build-scope-map-BvdTzmm4.d.ts} +1 -1
- package/dist/{build-scope-map-BIeawJzC.d.cts → build-scope-map-K25AyQXb.d.cts} +1 -1
- package/dist/{events-BynBOsO3.d.cts → events-DhblJN4r.d.cts} +144 -2
- package/dist/{events-BynBOsO3.d.ts → events-DhblJN4r.d.ts} +144 -2
- package/dist/index.cjs +566 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -7
- package/dist/index.d.ts +174 -7
- package/dist/index.js +553 -22
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/{operation-D5WOZYvy.d.ts → operation-CC7O3-wf.d.ts} +1 -1
- package/dist/{operation-BpZlYSpe.d.cts → operation-D-pemjcf.d.cts} +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -109,6 +109,226 @@ function formatUUID(bytes) {
|
|
|
109
109
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// src/blob/blob-ref.ts
|
|
113
|
+
async function hashBlob(bytes) {
|
|
114
|
+
const view = new Uint8Array(bytes.byteLength);
|
|
115
|
+
view.set(bytes);
|
|
116
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", view);
|
|
117
|
+
const out = new Uint8Array(digest);
|
|
118
|
+
return Array.from(out, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
119
|
+
}
|
|
120
|
+
async function createBlobRef(bytes, metadata = {}) {
|
|
121
|
+
const hash = await hashBlob(bytes);
|
|
122
|
+
const ref = { hash, size: bytes.byteLength };
|
|
123
|
+
if (metadata.mimeType !== void 0) {
|
|
124
|
+
ref.mimeType = metadata.mimeType;
|
|
125
|
+
}
|
|
126
|
+
if (metadata.filename !== void 0) {
|
|
127
|
+
ref.filename = metadata.filename;
|
|
128
|
+
}
|
|
129
|
+
return ref;
|
|
130
|
+
}
|
|
131
|
+
function isBlobRef(value) {
|
|
132
|
+
if (typeof value !== "object" || value === null) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const ref = value;
|
|
136
|
+
if (typeof ref.hash !== "string" || !/^[0-9a-f]{64}$/.test(ref.hash)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (typeof ref.size !== "number" || !Number.isInteger(ref.size) || ref.size < 0) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (ref.mimeType !== void 0 && typeof ref.mimeType !== "string") {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (ref.filename !== void 0 && typeof ref.filename !== "string") {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (ref.manifestHash !== void 0 && (typeof ref.manifestHash !== "string" || !/^[0-9a-f]{64}$/.test(ref.manifestHash))) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/secret/secret-crypto.ts
|
|
155
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
156
|
+
var SALT_BYTES = 16;
|
|
157
|
+
var IV_BYTES = 12;
|
|
158
|
+
var HASH_BYTES = 32;
|
|
159
|
+
function toHex(bytes) {
|
|
160
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
161
|
+
}
|
|
162
|
+
function fromHex(hex) {
|
|
163
|
+
if (hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
|
|
164
|
+
throw new Error("Invalid hex string");
|
|
165
|
+
}
|
|
166
|
+
const out = new Uint8Array(hex.length / 2);
|
|
167
|
+
for (let i = 0; i < out.length; i++) {
|
|
168
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function randomBytes(n) {
|
|
173
|
+
const out = new Uint8Array(n);
|
|
174
|
+
globalThis.crypto.getRandomValues(out);
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function timingSafeEqual(a, b) {
|
|
178
|
+
if (a.length !== b.length) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
let diff = 0;
|
|
182
|
+
for (let i = 0; i < a.length; i++) {
|
|
183
|
+
diff |= a[i] ^ b[i];
|
|
184
|
+
}
|
|
185
|
+
return diff === 0;
|
|
186
|
+
}
|
|
187
|
+
async function deriveAesKey(secretKey, salt) {
|
|
188
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
189
|
+
"raw",
|
|
190
|
+
new TextEncoder().encode(secretKey),
|
|
191
|
+
"PBKDF2",
|
|
192
|
+
false,
|
|
193
|
+
["deriveKey"]
|
|
194
|
+
);
|
|
195
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
196
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
197
|
+
baseKey,
|
|
198
|
+
{ name: "AES-GCM", length: 256 },
|
|
199
|
+
false,
|
|
200
|
+
["encrypt", "decrypt"]
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
async function encryptSecret(plaintext, secretKey) {
|
|
204
|
+
const salt = randomBytes(SALT_BYTES);
|
|
205
|
+
const iv = randomBytes(IV_BYTES);
|
|
206
|
+
const key = await deriveAesKey(secretKey, salt);
|
|
207
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
208
|
+
{ name: "AES-GCM", iv },
|
|
209
|
+
key,
|
|
210
|
+
new TextEncoder().encode(plaintext)
|
|
211
|
+
);
|
|
212
|
+
return `v1.${toHex(salt)}.${toHex(iv)}.${toHex(new Uint8Array(ciphertext))}`;
|
|
213
|
+
}
|
|
214
|
+
async function decryptSecret(encrypted, secretKey) {
|
|
215
|
+
const parts = encrypted.split(".");
|
|
216
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
217
|
+
throw new Error("Invalid encrypted secret format");
|
|
218
|
+
}
|
|
219
|
+
const salt = fromHex(parts[1]);
|
|
220
|
+
const iv = fromHex(parts[2]);
|
|
221
|
+
const ciphertext = fromHex(parts[3]);
|
|
222
|
+
const key = await deriveAesKey(secretKey, salt);
|
|
223
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
224
|
+
{ name: "AES-GCM", iv },
|
|
225
|
+
key,
|
|
226
|
+
ciphertext
|
|
227
|
+
);
|
|
228
|
+
return new TextDecoder().decode(plaintext);
|
|
229
|
+
}
|
|
230
|
+
async function hashSecret(plaintext) {
|
|
231
|
+
const salt = randomBytes(SALT_BYTES);
|
|
232
|
+
const hash = await pbkdf2(plaintext, salt);
|
|
233
|
+
return `v1.${toHex(salt)}.${toHex(hash)}`;
|
|
234
|
+
}
|
|
235
|
+
async function verifySecret(plaintext, stored) {
|
|
236
|
+
const parts = stored.split(".");
|
|
237
|
+
if (parts.length !== 3 || parts[0] !== "v1") {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
let salt;
|
|
241
|
+
let expected;
|
|
242
|
+
try {
|
|
243
|
+
salt = fromHex(parts[1]);
|
|
244
|
+
expected = fromHex(parts[2]);
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const actual = await pbkdf2(plaintext, salt);
|
|
249
|
+
return timingSafeEqual(actual, expected);
|
|
250
|
+
}
|
|
251
|
+
async function pbkdf2(plaintext, salt) {
|
|
252
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
253
|
+
"raw",
|
|
254
|
+
new TextEncoder().encode(plaintext),
|
|
255
|
+
"PBKDF2",
|
|
256
|
+
false,
|
|
257
|
+
["deriveBits"]
|
|
258
|
+
);
|
|
259
|
+
const bits = await globalThis.crypto.subtle.deriveBits(
|
|
260
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
261
|
+
baseKey,
|
|
262
|
+
HASH_BYTES * 8
|
|
263
|
+
);
|
|
264
|
+
return new Uint8Array(bits);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/secret/secret-transform.ts
|
|
268
|
+
var MissingSecretKeyError = class extends Error {
|
|
269
|
+
constructor(field) {
|
|
270
|
+
super(
|
|
271
|
+
`Secret field "${field}" is encrypted but no encryption key is configured. Provide sync.encryption.key (or a key provider) to createApp.`
|
|
272
|
+
);
|
|
273
|
+
this.field = field;
|
|
274
|
+
this.name = "MissingSecretKeyError";
|
|
275
|
+
}
|
|
276
|
+
field;
|
|
277
|
+
};
|
|
278
|
+
async function resolveKey(provider) {
|
|
279
|
+
if (!provider) {
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
return provider();
|
|
283
|
+
}
|
|
284
|
+
async function transformSecretFieldsForWrite(data, collectionDef, keyProvider) {
|
|
285
|
+
const hasSecret = Object.values(collectionDef.fields).some((f) => f.kind === "secret");
|
|
286
|
+
if (!hasSecret) {
|
|
287
|
+
return data;
|
|
288
|
+
}
|
|
289
|
+
const result = { ...data };
|
|
290
|
+
let key;
|
|
291
|
+
let keyResolved = false;
|
|
292
|
+
for (const [field, value] of Object.entries(data)) {
|
|
293
|
+
const descriptor = collectionDef.fields[field];
|
|
294
|
+
if (descriptor?.kind !== "secret") {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (typeof value !== "string") {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const mode = descriptor.secretMode ?? "encrypted";
|
|
301
|
+
if (mode === "hashed") {
|
|
302
|
+
result[field] = await hashSecret(value);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!keyResolved) {
|
|
306
|
+
key = await resolveKey(keyProvider);
|
|
307
|
+
keyResolved = true;
|
|
308
|
+
}
|
|
309
|
+
if (key === void 0) {
|
|
310
|
+
throw new MissingSecretKeyError(field);
|
|
311
|
+
}
|
|
312
|
+
result[field] = await encryptSecret(value, key);
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
async function revealSecret(storedValue, mode, keyProvider, field = "secret") {
|
|
317
|
+
if (mode === "hashed") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Secret field "${field}" is hashed (one-way) and cannot be revealed. Use verifySecretValue to check a candidate.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const key = await resolveKey(keyProvider);
|
|
323
|
+
if (key === void 0) {
|
|
324
|
+
throw new MissingSecretKeyError(field);
|
|
325
|
+
}
|
|
326
|
+
return decryptSecret(storedValue, key);
|
|
327
|
+
}
|
|
328
|
+
async function verifySecretValue(candidate, storedValue) {
|
|
329
|
+
return verifySecret(candidate, storedValue);
|
|
330
|
+
}
|
|
331
|
+
|
|
112
332
|
// src/operations/apply-result.ts
|
|
113
333
|
var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
|
|
114
334
|
var APPLY_FAILURE_CODES = {
|
|
@@ -488,7 +708,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
|
|
|
488
708
|
}
|
|
489
709
|
|
|
490
710
|
// src/schema/define.ts
|
|
491
|
-
var COLLECTION_NAME_RE = /^[
|
|
711
|
+
var COLLECTION_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
492
712
|
var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
493
713
|
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
|
|
494
714
|
function defineSchema(input) {
|
|
@@ -553,7 +773,7 @@ function validateVersion(version) {
|
|
|
553
773
|
function validateCollectionName(name) {
|
|
554
774
|
if (!COLLECTION_NAME_RE.test(name)) {
|
|
555
775
|
throw new SchemaValidationError(
|
|
556
|
-
`Collection name "${name}" is invalid. Must
|
|
776
|
+
`Collection name "${name}" is invalid. Must start with a letter and contain only letters, numbers, and underscores.`,
|
|
557
777
|
{ collection: name }
|
|
558
778
|
);
|
|
559
779
|
}
|
|
@@ -745,6 +965,11 @@ function validateRelation(name, relation, collections) {
|
|
|
745
965
|
}
|
|
746
966
|
}
|
|
747
967
|
|
|
968
|
+
// src/schema/quote-ident.ts
|
|
969
|
+
function quoteIdent(name) {
|
|
970
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
971
|
+
}
|
|
972
|
+
|
|
748
973
|
// src/schema/sql-gen.ts
|
|
749
974
|
function generateSQL(collectionName, collection, relations) {
|
|
750
975
|
const statements = [];
|
|
@@ -756,7 +981,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
756
981
|
if (relations) {
|
|
757
982
|
for (const rel of Object.values(relations)) {
|
|
758
983
|
if (rel.from === collectionName && rel.field === fieldName) {
|
|
759
|
-
colDef += ` REFERENCES ${rel.to}(id)`;
|
|
984
|
+
colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
|
|
760
985
|
fkFields.push(fieldName);
|
|
761
986
|
break;
|
|
762
987
|
}
|
|
@@ -769,36 +994,40 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
769
994
|
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
770
995
|
columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
|
|
771
996
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
772
|
-
statements.push(
|
|
997
|
+
statements.push(
|
|
998
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
|
|
773
999
|
${columns.join(",\n ")}
|
|
774
|
-
)`
|
|
1000
|
+
)`
|
|
1001
|
+
);
|
|
775
1002
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
776
1003
|
const colDef = columnDefinition(fieldName, descriptor);
|
|
777
|
-
statements.push(
|
|
778
|
-
|
|
1004
|
+
statements.push(
|
|
1005
|
+
`--kora:safe-alter
|
|
1006
|
+
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
|
|
1007
|
+
);
|
|
779
1008
|
}
|
|
780
1009
|
statements.push(
|
|
781
1010
|
`--kora:safe-alter
|
|
782
|
-
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1011
|
+
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
783
1012
|
);
|
|
784
1013
|
statements.push(
|
|
785
1014
|
`--kora:safe-alter
|
|
786
|
-
ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
1015
|
+
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
787
1016
|
);
|
|
788
1017
|
for (const indexField of collection.indexes) {
|
|
789
1018
|
statements.push(
|
|
790
|
-
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
|
|
1019
|
+
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
|
|
791
1020
|
);
|
|
792
1021
|
}
|
|
793
1022
|
for (const fkField of fkFields) {
|
|
794
1023
|
if (!indexedFields.has(fkField)) {
|
|
795
1024
|
statements.push(
|
|
796
|
-
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${collectionName} (${fkField})`
|
|
1025
|
+
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
|
|
797
1026
|
);
|
|
798
1027
|
}
|
|
799
1028
|
}
|
|
800
1029
|
statements.push(
|
|
801
|
-
`CREATE TABLE IF NOT EXISTS _kora_ops_${collectionName} (
|
|
1030
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
|
|
802
1031
|
id TEXT PRIMARY KEY NOT NULL,
|
|
803
1032
|
node_id TEXT NOT NULL,
|
|
804
1033
|
type TEXT NOT NULL,
|
|
@@ -812,7 +1041,7 @@ ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '
|
|
|
812
1041
|
)`
|
|
813
1042
|
);
|
|
814
1043
|
statements.push(
|
|
815
|
-
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
|
|
1044
|
+
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
|
|
816
1045
|
);
|
|
817
1046
|
return statements;
|
|
818
1047
|
}
|
|
@@ -830,6 +1059,9 @@ function generateFullDDL(schema) {
|
|
|
830
1059
|
statements.push(
|
|
831
1060
|
"CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
|
|
832
1061
|
);
|
|
1062
|
+
statements.push(
|
|
1063
|
+
"CREATE TABLE IF NOT EXISTS _kora_sync_rejected (\n operation_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n retriable INTEGER NOT NULL,\n rejected_at INTEGER NOT NULL\n)"
|
|
1064
|
+
);
|
|
833
1065
|
statements.push(
|
|
834
1066
|
"CREATE TABLE IF NOT EXISTS _kora_audit_traces (\n id TEXT PRIMARY KEY NOT NULL,\n recorded_at INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n field TEXT NOT NULL,\n strategy TEXT NOT NULL,\n tier INTEGER NOT NULL,\n constraint_name TEXT,\n trace_json TEXT NOT NULL\n)"
|
|
835
1067
|
);
|
|
@@ -846,7 +1078,7 @@ function generateFullDDL(schema) {
|
|
|
846
1078
|
}
|
|
847
1079
|
function columnDefinition(fieldName, descriptor) {
|
|
848
1080
|
const sqlType = mapFieldType(descriptor);
|
|
849
|
-
const parts = [fieldName, sqlType];
|
|
1081
|
+
const parts = [quoteIdent(fieldName), sqlType];
|
|
850
1082
|
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
851
1083
|
parts.push("NOT NULL");
|
|
852
1084
|
}
|
|
@@ -855,7 +1087,7 @@ function columnDefinition(fieldName, descriptor) {
|
|
|
855
1087
|
}
|
|
856
1088
|
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
857
1089
|
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
858
|
-
parts.push(`CHECK (${fieldName} IN (${values}))`);
|
|
1090
|
+
parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
|
|
859
1091
|
}
|
|
860
1092
|
return parts.join(" ");
|
|
861
1093
|
}
|
|
@@ -874,6 +1106,17 @@ function mapFieldType(descriptor) {
|
|
|
874
1106
|
case "array":
|
|
875
1107
|
return "TEXT";
|
|
876
1108
|
// JSON-serialized
|
|
1109
|
+
case "object":
|
|
1110
|
+
return "TEXT";
|
|
1111
|
+
// JSON-serialized structured object
|
|
1112
|
+
case "json":
|
|
1113
|
+
return "TEXT";
|
|
1114
|
+
// JSON-serialized dynamic-key value
|
|
1115
|
+
case "blob":
|
|
1116
|
+
return "TEXT";
|
|
1117
|
+
// JSON-serialized content-addressed BlobRef (bytes stored out of band)
|
|
1118
|
+
case "secret":
|
|
1119
|
+
return "TEXT";
|
|
877
1120
|
case "richtext":
|
|
878
1121
|
return "BLOB";
|
|
879
1122
|
}
|
|
@@ -1107,6 +1350,160 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1107
1350
|
};
|
|
1108
1351
|
}
|
|
1109
1352
|
};
|
|
1353
|
+
var ObjectFieldBuilder = class _ObjectFieldBuilder extends FieldBuilder {
|
|
1354
|
+
_fields;
|
|
1355
|
+
constructor(fields, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
1356
|
+
super("object", required, defaultValue, auto, mergeStrategy);
|
|
1357
|
+
this._fields = fields;
|
|
1358
|
+
}
|
|
1359
|
+
optional() {
|
|
1360
|
+
return new _ObjectFieldBuilder(
|
|
1361
|
+
this._fields,
|
|
1362
|
+
false,
|
|
1363
|
+
this._defaultValue,
|
|
1364
|
+
this._auto,
|
|
1365
|
+
this._mergeStrategy
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
default(value) {
|
|
1369
|
+
return new _ObjectFieldBuilder(this._fields, false, value, this._auto, this._mergeStrategy);
|
|
1370
|
+
}
|
|
1371
|
+
auto() {
|
|
1372
|
+
return new _ObjectFieldBuilder(this._fields, false, void 0, true, this._mergeStrategy);
|
|
1373
|
+
}
|
|
1374
|
+
merge(strategy) {
|
|
1375
|
+
return new _ObjectFieldBuilder(
|
|
1376
|
+
this._fields,
|
|
1377
|
+
this._required,
|
|
1378
|
+
this._defaultValue,
|
|
1379
|
+
this._auto,
|
|
1380
|
+
strategy
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
_build() {
|
|
1384
|
+
const nestedFields = {};
|
|
1385
|
+
for (const [key, builder] of Object.entries(this._fields)) {
|
|
1386
|
+
nestedFields[key] = builder._build();
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
kind: "object",
|
|
1390
|
+
required: this._required,
|
|
1391
|
+
defaultValue: this._defaultValue,
|
|
1392
|
+
auto: this._auto,
|
|
1393
|
+
enumValues: null,
|
|
1394
|
+
itemKind: null,
|
|
1395
|
+
mergeStrategy: this._mergeStrategy,
|
|
1396
|
+
transitions: null,
|
|
1397
|
+
nestedFields
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var JsonFieldBuilder = class _JsonFieldBuilder extends FieldBuilder {
|
|
1402
|
+
optional() {
|
|
1403
|
+
return new _JsonFieldBuilder(
|
|
1404
|
+
"json",
|
|
1405
|
+
false,
|
|
1406
|
+
this._defaultValue,
|
|
1407
|
+
this._auto,
|
|
1408
|
+
this._mergeStrategy
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
default(value) {
|
|
1412
|
+
return new _JsonFieldBuilder(
|
|
1413
|
+
"json",
|
|
1414
|
+
false,
|
|
1415
|
+
value,
|
|
1416
|
+
this._auto,
|
|
1417
|
+
this._mergeStrategy
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
auto() {
|
|
1421
|
+
return new _JsonFieldBuilder("json", false, void 0, true, this._mergeStrategy);
|
|
1422
|
+
}
|
|
1423
|
+
merge(strategy) {
|
|
1424
|
+
return new _JsonFieldBuilder(
|
|
1425
|
+
"json",
|
|
1426
|
+
this._required,
|
|
1427
|
+
this._defaultValue,
|
|
1428
|
+
this._auto,
|
|
1429
|
+
strategy
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
_build() {
|
|
1433
|
+
return {
|
|
1434
|
+
kind: "json",
|
|
1435
|
+
required: this._required,
|
|
1436
|
+
defaultValue: this._defaultValue,
|
|
1437
|
+
auto: this._auto,
|
|
1438
|
+
enumValues: null,
|
|
1439
|
+
itemKind: null,
|
|
1440
|
+
mergeStrategy: this._mergeStrategy,
|
|
1441
|
+
transitions: null,
|
|
1442
|
+
nestedFields: null
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
1447
|
+
_secretMode;
|
|
1448
|
+
constructor(secretMode = "encrypted", required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
1449
|
+
super("secret", required, defaultValue, auto, mergeStrategy);
|
|
1450
|
+
this._secretMode = secretMode;
|
|
1451
|
+
}
|
|
1452
|
+
/** Store this secret as a one-way salted hash (passwords: verify, never read back). */
|
|
1453
|
+
hashed() {
|
|
1454
|
+
return new _SecretFieldBuilder(
|
|
1455
|
+
"hashed",
|
|
1456
|
+
this._required,
|
|
1457
|
+
this._defaultValue,
|
|
1458
|
+
this._auto,
|
|
1459
|
+
this._mergeStrategy
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
/** Store this secret as reversible ciphertext (tokens/keys: decrypt to use). */
|
|
1463
|
+
encrypted() {
|
|
1464
|
+
return new _SecretFieldBuilder(
|
|
1465
|
+
"encrypted",
|
|
1466
|
+
this._required,
|
|
1467
|
+
this._defaultValue,
|
|
1468
|
+
this._auto,
|
|
1469
|
+
this._mergeStrategy
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
optional() {
|
|
1473
|
+
return new _SecretFieldBuilder(
|
|
1474
|
+
this._secretMode,
|
|
1475
|
+
false,
|
|
1476
|
+
this._defaultValue,
|
|
1477
|
+
this._auto,
|
|
1478
|
+
this._mergeStrategy
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
auto() {
|
|
1482
|
+
return new _SecretFieldBuilder(this._secretMode, false, void 0, true, this._mergeStrategy);
|
|
1483
|
+
}
|
|
1484
|
+
merge(strategy) {
|
|
1485
|
+
return new _SecretFieldBuilder(
|
|
1486
|
+
this._secretMode,
|
|
1487
|
+
this._required,
|
|
1488
|
+
this._defaultValue,
|
|
1489
|
+
this._auto,
|
|
1490
|
+
strategy
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
_build() {
|
|
1494
|
+
return {
|
|
1495
|
+
kind: "secret",
|
|
1496
|
+
required: this._required,
|
|
1497
|
+
defaultValue: this._defaultValue,
|
|
1498
|
+
auto: this._auto,
|
|
1499
|
+
enumValues: null,
|
|
1500
|
+
itemKind: null,
|
|
1501
|
+
mergeStrategy: this._mergeStrategy,
|
|
1502
|
+
transitions: null,
|
|
1503
|
+
secretMode: this._secretMode
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1110
1507
|
var t = {
|
|
1111
1508
|
string() {
|
|
1112
1509
|
return new FieldBuilder("string", true, void 0, false);
|
|
@@ -1128,6 +1525,18 @@ var t = {
|
|
|
1128
1525
|
},
|
|
1129
1526
|
array(itemBuilder) {
|
|
1130
1527
|
return new ArrayFieldBuilder(itemBuilder, true, void 0, false);
|
|
1528
|
+
},
|
|
1529
|
+
object(fields) {
|
|
1530
|
+
return new ObjectFieldBuilder(fields, true, void 0, false);
|
|
1531
|
+
},
|
|
1532
|
+
json() {
|
|
1533
|
+
return new JsonFieldBuilder("json", true, void 0, false);
|
|
1534
|
+
},
|
|
1535
|
+
blob() {
|
|
1536
|
+
return new FieldBuilder("blob", true, void 0, false);
|
|
1537
|
+
},
|
|
1538
|
+
secret() {
|
|
1539
|
+
return new SecretFieldBuilder("encrypted", true, void 0, false);
|
|
1131
1540
|
}
|
|
1132
1541
|
};
|
|
1133
1542
|
|
|
@@ -1292,7 +1701,98 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1292
1701
|
}
|
|
1293
1702
|
break;
|
|
1294
1703
|
}
|
|
1704
|
+
case "object": {
|
|
1705
|
+
if (!isPlainObject(value)) {
|
|
1706
|
+
throw new SchemaValidationError(
|
|
1707
|
+
`Field "${fieldName}" in collection "${collection}" must be a plain object, got ${describeType(value)}`,
|
|
1708
|
+
{
|
|
1709
|
+
collection,
|
|
1710
|
+
field: fieldName,
|
|
1711
|
+
expectedType: "object",
|
|
1712
|
+
receivedType: describeType(value)
|
|
1713
|
+
}
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
if (descriptor.nestedFields) {
|
|
1717
|
+
for (const [nestedName, nestedDescriptor] of Object.entries(descriptor.nestedFields)) {
|
|
1718
|
+
const nestedValue = value[nestedName];
|
|
1719
|
+
if (nestedValue !== void 0 && nestedValue !== null) {
|
|
1720
|
+
validateFieldValue(
|
|
1721
|
+
collection,
|
|
1722
|
+
`${fieldName}.${nestedName}`,
|
|
1723
|
+
nestedDescriptor,
|
|
1724
|
+
nestedValue
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
case "json": {
|
|
1732
|
+
if (!isJsonSerializable(value)) {
|
|
1733
|
+
throw new SchemaValidationError(
|
|
1734
|
+
`Field "${fieldName}" in collection "${collection}" must be JSON-serializable, got ${describeType(value)}`,
|
|
1735
|
+
{ collection, field: fieldName, expectedType: "json", receivedType: describeType(value) }
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
case "blob": {
|
|
1741
|
+
if (!isBlobRef(value)) {
|
|
1742
|
+
throw new SchemaValidationError(
|
|
1743
|
+
`Field "${fieldName}" in collection "${collection}" must be a BlobRef (from the blob store), got ${describeType(value)}`,
|
|
1744
|
+
{ collection, field: fieldName, expectedType: "blob", receivedType: describeType(value) }
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
case "secret": {
|
|
1750
|
+
if (typeof value !== "string") {
|
|
1751
|
+
throw new SchemaValidationError(
|
|
1752
|
+
`Field "${fieldName}" in collection "${collection}" must be a string, got ${describeType(value)}`,
|
|
1753
|
+
{
|
|
1754
|
+
collection,
|
|
1755
|
+
field: fieldName,
|
|
1756
|
+
expectedType: "secret",
|
|
1757
|
+
receivedType: describeType(value)
|
|
1758
|
+
}
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function isPlainObject(value) {
|
|
1766
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
const proto = Object.getPrototypeOf(value);
|
|
1770
|
+
return proto === null || proto === Object.prototype;
|
|
1771
|
+
}
|
|
1772
|
+
function isJsonSerializable(value) {
|
|
1773
|
+
if (value === null) {
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
const type = typeof value;
|
|
1777
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
1778
|
+
return Number.isFinite(value) || type !== "number";
|
|
1295
1779
|
}
|
|
1780
|
+
if (Array.isArray(value)) {
|
|
1781
|
+
return value.every(isJsonSerializable);
|
|
1782
|
+
}
|
|
1783
|
+
if (type === "object") {
|
|
1784
|
+
return Object.values(value).every(isJsonSerializable);
|
|
1785
|
+
}
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1788
|
+
function describeType(value) {
|
|
1789
|
+
if (value === null) {
|
|
1790
|
+
return "null";
|
|
1791
|
+
}
|
|
1792
|
+
if (Array.isArray(value)) {
|
|
1793
|
+
return "array";
|
|
1794
|
+
}
|
|
1795
|
+
return typeof value;
|
|
1296
1796
|
}
|
|
1297
1797
|
function jsTypeForKind(kind) {
|
|
1298
1798
|
switch (kind) {
|
|
@@ -1724,14 +2224,18 @@ function migrationStepsToSQL(steps) {
|
|
|
1724
2224
|
statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
|
|
1725
2225
|
break;
|
|
1726
2226
|
case "removeField":
|
|
1727
|
-
statements.push(
|
|
2227
|
+
statements.push(
|
|
2228
|
+
`ALTER TABLE ${quoteIdent(step.collection)} DROP COLUMN ${quoteIdent(step.field)}`
|
|
2229
|
+
);
|
|
1728
2230
|
break;
|
|
1729
2231
|
case "renameField":
|
|
1730
|
-
statements.push(
|
|
2232
|
+
statements.push(
|
|
2233
|
+
`ALTER TABLE ${quoteIdent(step.collection)} RENAME COLUMN ${quoteIdent(step.from)} TO ${quoteIdent(step.to)}`
|
|
2234
|
+
);
|
|
1731
2235
|
break;
|
|
1732
2236
|
case "addIndex":
|
|
1733
2237
|
statements.push(
|
|
1734
|
-
`CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
|
|
2238
|
+
`CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
|
|
1735
2239
|
);
|
|
1736
2240
|
break;
|
|
1737
2241
|
case "removeIndex":
|
|
@@ -1749,13 +2253,13 @@ function rollbackStepsToSQL(migration) {
|
|
|
1749
2253
|
}
|
|
1750
2254
|
function addFieldSQL(collection, field, descriptor) {
|
|
1751
2255
|
const sqlType = mapFieldType2(descriptor);
|
|
1752
|
-
const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
|
|
2256
|
+
const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
|
|
1753
2257
|
if (descriptor.defaultValue !== void 0) {
|
|
1754
2258
|
parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
|
|
1755
2259
|
}
|
|
1756
2260
|
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1757
2261
|
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
1758
|
-
parts.push(`CHECK (${field} IN (${values}))`);
|
|
2262
|
+
parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
|
|
1759
2263
|
}
|
|
1760
2264
|
return parts.join(" ");
|
|
1761
2265
|
}
|
|
@@ -1773,6 +2277,14 @@ function mapFieldType2(descriptor) {
|
|
|
1773
2277
|
return "INTEGER";
|
|
1774
2278
|
case "array":
|
|
1775
2279
|
return "TEXT";
|
|
2280
|
+
case "object":
|
|
2281
|
+
return "TEXT";
|
|
2282
|
+
case "json":
|
|
2283
|
+
return "TEXT";
|
|
2284
|
+
case "blob":
|
|
2285
|
+
return "TEXT";
|
|
2286
|
+
case "secret":
|
|
2287
|
+
return "TEXT";
|
|
1776
2288
|
case "richtext":
|
|
1777
2289
|
return "BLOB";
|
|
1778
2290
|
}
|
|
@@ -1791,7 +2303,14 @@ var SCALAR_TYPE_MAP = {
|
|
|
1791
2303
|
number: "double",
|
|
1792
2304
|
boolean: "bool",
|
|
1793
2305
|
timestamp: "int64",
|
|
1794
|
-
richtext: "bytes"
|
|
2306
|
+
richtext: "bytes",
|
|
2307
|
+
// object/json values travel as JSON-serialized strings on the proto wire.
|
|
2308
|
+
object: "string",
|
|
2309
|
+
json: "string",
|
|
2310
|
+
// A blob field carries a JSON-serialized content-addressed reference, not bytes.
|
|
2311
|
+
blob: "string",
|
|
2312
|
+
// A secret field's stored value is a JSON string (ciphertext or hash).
|
|
2313
|
+
secret: "string"
|
|
1795
2314
|
};
|
|
1796
2315
|
var ARRAY_ITEM_TYPE_MAP = {
|
|
1797
2316
|
string: "string",
|
|
@@ -2034,6 +2553,7 @@ export {
|
|
|
2034
2553
|
MergeConflictError,
|
|
2035
2554
|
MigrationBuilder,
|
|
2036
2555
|
MigrationRollbackError,
|
|
2556
|
+
MissingSecretKeyError,
|
|
2037
2557
|
OperationError,
|
|
2038
2558
|
RemoteClockDriftError,
|
|
2039
2559
|
RollbackBuilder,
|
|
@@ -2050,16 +2570,19 @@ export {
|
|
|
2050
2570
|
collectSchemaScopeFields,
|
|
2051
2571
|
collectSchemaScopeValueKeys,
|
|
2052
2572
|
computeDelta,
|
|
2573
|
+
createBlobRef,
|
|
2053
2574
|
createOperation,
|
|
2054
2575
|
createReversibleMigration,
|
|
2055
2576
|
createVersionVector,
|
|
2056
2577
|
decodeBytesFromOpData,
|
|
2578
|
+
decryptSecret,
|
|
2057
2579
|
defaultApplyFailureReason,
|
|
2058
2580
|
defaultSequenceFormat,
|
|
2059
2581
|
defineSchema,
|
|
2060
2582
|
deserializeVector,
|
|
2061
2583
|
dominates,
|
|
2062
2584
|
encodeBytesForOpData,
|
|
2585
|
+
encryptSecret,
|
|
2063
2586
|
extractScopeValuesFromClaims,
|
|
2064
2587
|
extractTimestamp,
|
|
2065
2588
|
formatSequenceValue,
|
|
@@ -2072,8 +2595,11 @@ export {
|
|
|
2072
2595
|
getKoraErrorFix,
|
|
2073
2596
|
getTransitionMap,
|
|
2074
2597
|
hasSchemaSyncRules,
|
|
2598
|
+
hashBlob,
|
|
2599
|
+
hashSecret,
|
|
2075
2600
|
isApplyFailure,
|
|
2076
2601
|
isAtomicOp,
|
|
2602
|
+
isBlobRef,
|
|
2077
2603
|
isCollectionSyncScoped,
|
|
2078
2604
|
isKoraBytesValue,
|
|
2079
2605
|
isLegacyNumericByteObject,
|
|
@@ -2083,14 +2609,19 @@ export {
|
|
|
2083
2609
|
migrate,
|
|
2084
2610
|
migrationStepsToSQL,
|
|
2085
2611
|
op,
|
|
2612
|
+
quoteIdent,
|
|
2086
2613
|
resolveAtomicOp,
|
|
2614
|
+
revealSecret,
|
|
2087
2615
|
rollbackStepsToSQL,
|
|
2088
2616
|
serializeVector,
|
|
2089
2617
|
t,
|
|
2090
2618
|
toAtomicOp,
|
|
2619
|
+
transformSecretFieldsForWrite,
|
|
2091
2620
|
validateRecord,
|
|
2092
2621
|
validateTransition,
|
|
2093
2622
|
vectorsEqual,
|
|
2094
|
-
verifyOperationIntegrity
|
|
2623
|
+
verifyOperationIntegrity,
|
|
2624
|
+
verifySecret,
|
|
2625
|
+
verifySecretValue
|
|
2095
2626
|
};
|
|
2096
2627
|
//# sourceMappingURL=index.js.map
|