orez-lite 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/browser/generated/sqlite3-browser.wasm +0 -0
- package/dist/browser/generated/sync_wasm.js +922 -0
- package/dist/browser/generated/sync_wasm_bg.wasm +0 -0
- package/dist/browser/host.d.ts +10 -0
- package/dist/browser/host.d.ts.map +1 -0
- package/dist/browser/host.js +496 -0
- package/dist/browser/host.js.map +1 -0
- package/dist/browser/idb-snapshot.d.ts +11 -0
- package/dist/browser/idb-snapshot.d.ts.map +1 -0
- package/dist/browser/idb-snapshot.js +122 -0
- package/dist/browser/idb-snapshot.js.map +1 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +5 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/message-port.d.ts +7 -0
- package/dist/browser/message-port.d.ts.map +1 -0
- package/dist/browser/message-port.js +188 -0
- package/dist/browser/message-port.js.map +1 -0
- package/dist/browser/sqlite-adapter.d.ts +53 -0
- package/dist/browser/sqlite-adapter.d.ts.map +1 -0
- package/dist/browser/sqlite-adapter.js +126 -0
- package/dist/browser/sqlite-adapter.js.map +1 -0
- package/dist/browser/types.d.ts +52 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cf-deploy/bundle.d.ts +18 -0
- package/dist/cf-deploy/bundle.d.ts.map +1 -0
- package/dist/cf-deploy/bundle.js +273 -0
- package/dist/cf-deploy/bundle.js.map +1 -0
- package/dist/cf-deploy/config.d.ts +12 -0
- package/dist/cf-deploy/config.d.ts.map +1 -0
- package/dist/cf-deploy/config.js +11 -0
- package/dist/cf-deploy/config.js.map +1 -0
- package/dist/cf-deploy/index.d.ts +10 -0
- package/dist/cf-deploy/index.d.ts.map +1 -0
- package/dist/cf-deploy/index.js +12 -0
- package/dist/cf-deploy/index.js.map +1 -0
- package/dist/cf-deploy/leaves.d.ts +8 -0
- package/dist/cf-deploy/leaves.d.ts.map +1 -0
- package/dist/cf-deploy/leaves.js +66 -0
- package/dist/cf-deploy/leaves.js.map +1 -0
- package/dist/cf-deploy/migration.d.ts +27 -0
- package/dist/cf-deploy/migration.d.ts.map +1 -0
- package/dist/cf-deploy/migration.js +734 -0
- package/dist/cf-deploy/migration.js.map +1 -0
- package/dist/cf-deploy/nativeMigrations.d.ts +24 -0
- package/dist/cf-deploy/nativeMigrations.d.ts.map +1 -0
- package/dist/cf-deploy/nativeMigrations.js +185 -0
- package/dist/cf-deploy/nativeMigrations.js.map +1 -0
- package/dist/cf-deploy/prune.d.ts +14 -0
- package/dist/cf-deploy/prune.d.ts.map +1 -0
- package/dist/cf-deploy/prune.js +235 -0
- package/dist/cf-deploy/prune.js.map +1 -0
- package/dist/cf-deploy/readiness.d.ts +11 -0
- package/dist/cf-deploy/readiness.d.ts.map +1 -0
- package/dist/cf-deploy/readiness.js +56 -0
- package/dist/cf-deploy/readiness.js.map +1 -0
- package/dist/cf-deploy/sources.d.ts +3 -0
- package/dist/cf-deploy/sources.d.ts.map +1 -0
- package/dist/cf-deploy/sources.js +24 -0
- package/dist/cf-deploy/sources.js.map +1 -0
- package/dist/cf-deploy/wrangler.d.ts +9 -0
- package/dist/cf-deploy/wrangler.d.ts.map +1 -0
- package/dist/cf-deploy/wrangler.js +48 -0
- package/dist/cf-deploy/wrangler.js.map +1 -0
- package/dist/cf-do/application-sql.d.ts +49 -0
- package/dist/cf-do/application-sql.d.ts.map +1 -0
- package/dist/cf-do/application-sql.js +124 -0
- package/dist/cf-do/application-sql.js.map +1 -0
- package/dist/cf-do/cdc.d.ts +155 -0
- package/dist/cf-do/cdc.d.ts.map +1 -0
- package/dist/cf-do/cdc.js +723 -0
- package/dist/cf-do/cdc.js.map +1 -0
- package/dist/cf-do/cursor-pull.d.ts +27 -0
- package/dist/cf-do/cursor-pull.d.ts.map +1 -0
- package/dist/cf-do/cursor-pull.js +78 -0
- package/dist/cf-do/cursor-pull.js.map +1 -0
- package/dist/cf-do/lite-data-worker.d.ts +171 -0
- package/dist/cf-do/lite-data-worker.d.ts.map +1 -0
- package/dist/cf-do/lite-data-worker.js +685 -0
- package/dist/cf-do/lite-data-worker.js.map +1 -0
- package/dist/cf-do/namespace-backup.d.ts +85 -0
- package/dist/cf-do/namespace-backup.d.ts.map +1 -0
- package/dist/cf-do/namespace-backup.js +470 -0
- package/dist/cf-do/namespace-backup.js.map +1 -0
- package/dist/cf-do/row-undo.d.ts +36 -0
- package/dist/cf-do/row-undo.d.ts.map +1 -0
- package/dist/cf-do/row-undo.js +221 -0
- package/dist/cf-do/row-undo.js.map +1 -0
- package/dist/cf-do/tx-journal.d.ts +118 -0
- package/dist/cf-do/tx-journal.d.ts.map +1 -0
- package/dist/cf-do/tx-journal.js +717 -0
- package/dist/cf-do/tx-journal.js.map +1 -0
- package/dist/cf-do/watermark.d.ts +29 -0
- package/dist/cf-do/watermark.d.ts.map +1 -0
- package/dist/cf-do/watermark.js +105 -0
- package/dist/cf-do/watermark.js.map +1 -0
- package/dist/cf-do/worker.d.ts +282 -0
- package/dist/cf-do/worker.d.ts.map +1 -0
- package/dist/cf-do/worker.js +1999 -0
- package/dist/cf-do/worker.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/cloudflare-build.d.ts +9 -0
- package/dist/cloudflare-build.d.ts.map +1 -0
- package/dist/cloudflare-build.js +9 -0
- package/dist/cloudflare-build.js.map +1 -0
- package/dist/cloudflare-bun-wasm-loader.d.ts +2 -0
- package/dist/cloudflare-bun-wasm-loader.d.ts.map +1 -0
- package/dist/cloudflare-bun-wasm-loader.js +2 -0
- package/dist/cloudflare-bun-wasm-loader.js.map +1 -0
- package/dist/cloudflare-query-compiler.d.ts +2 -0
- package/dist/cloudflare-query-compiler.d.ts.map +1 -0
- package/dist/cloudflare-query-compiler.js +2 -0
- package/dist/cloudflare-query-compiler.js.map +1 -0
- package/dist/cloudflare-runtime.d.ts +15 -0
- package/dist/cloudflare-runtime.d.ts.map +1 -0
- package/dist/cloudflare-runtime.js +11 -0
- package/dist/cloudflare-runtime.js.map +1 -0
- package/dist/cloudflare-sync.d.ts +3 -0
- package/dist/cloudflare-sync.d.ts.map +1 -0
- package/dist/cloudflare-sync.js +3 -0
- package/dist/cloudflare-sync.js.map +1 -0
- package/dist/cloudflare-transaction-query.d.ts +2 -0
- package/dist/cloudflare-transaction-query.d.ts.map +1 -0
- package/dist/cloudflare-transaction-query.js +2 -0
- package/dist/cloudflare-transaction-query.js.map +1 -0
- package/dist/cloudflare-vite-wasm-loader.d.ts +2 -0
- package/dist/cloudflare-vite-wasm-loader.d.ts.map +1 -0
- package/dist/cloudflare-vite-wasm-loader.js +2 -0
- package/dist/cloudflare-vite-wasm-loader.js.map +1 -0
- package/dist/cloudflare.d.ts +3 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +3 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/do-sql-tracking.d.ts +114 -0
- package/dist/do-sql-tracking.d.ts.map +1 -0
- package/dist/do-sql-tracking.js +280 -0
- package/dist/do-sql-tracking.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/wake.d.ts +22 -0
- package/dist/wake.d.ts.map +1 -0
- package/dist/wake.js +114 -0
- package/dist/wake.js.map +1 -0
- package/dist/worker/cf-do-shim.d.ts +53 -0
- package/dist/worker/cf-do-shim.d.ts.map +1 -0
- package/dist/worker/cf-do-shim.js +66 -0
- package/dist/worker/cf-do-shim.js.map +1 -0
- package/dist/zero-http/encrypted-column-codec.d.ts +46 -0
- package/dist/zero-http/encrypted-column-codec.d.ts.map +1 -0
- package/dist/zero-http/encrypted-column-codec.js +676 -0
- package/dist/zero-http/encrypted-column-codec.js.map +1 -0
- package/dist/zero-http/encrypted-column-conformance.d.ts +7 -0
- package/dist/zero-http/encrypted-column-conformance.d.ts.map +1 -0
- package/dist/zero-http/encrypted-column-conformance.js +161 -0
- package/dist/zero-http/encrypted-column-conformance.js.map +1 -0
- package/dist/zero-http/fixture-schema.d.ts +127 -0
- package/dist/zero-http/fixture-schema.d.ts.map +1 -0
- package/dist/zero-http/fixture-schema.js +56 -0
- package/dist/zero-http/fixture-schema.js.map +1 -0
- package/dist/zero-http/mount.d.ts +71 -0
- package/dist/zero-http/mount.d.ts.map +1 -0
- package/dist/zero-http/mount.js +594 -0
- package/dist/zero-http/mount.js.map +1 -0
- package/dist/zero-http/payload-codec.d.ts +91 -0
- package/dist/zero-http/payload-codec.d.ts.map +1 -0
- package/dist/zero-http/payload-codec.js +10 -0
- package/dist/zero-http/payload-codec.js.map +1 -0
- package/dist/zero-http/server.d.ts +16 -0
- package/dist/zero-http/server.d.ts.map +1 -0
- package/dist/zero-http/server.js +270 -0
- package/dist/zero-http/server.js.map +1 -0
- package/dist/zero-http/test-harness.d.ts +32 -0
- package/dist/zero-http/test-harness.d.ts.map +1 -0
- package/dist/zero-http/test-harness.js +77 -0
- package/dist/zero-http/test-harness.js.map +1 -0
- package/dist/zero-http/transport.d.ts +71 -0
- package/dist/zero-http/transport.d.ts.map +1 -0
- package/dist/zero-http/transport.js +1106 -0
- package/dist/zero-http/transport.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
2
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
3
|
+
import { expand, extract, hkdf } from '@noble/hashes/hkdf.js';
|
|
4
|
+
import { hmac } from '@noble/hashes/hmac.js';
|
|
5
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
6
|
+
const textEncoder = new TextEncoder();
|
|
7
|
+
const textDecoder = new TextDecoder();
|
|
8
|
+
const emptyBytes = new Uint8Array(0);
|
|
9
|
+
const envelopePrefix = 'orez-e1.';
|
|
10
|
+
const base64URLAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
11
|
+
const hpkeKEMID = 0x0020;
|
|
12
|
+
const hpkeKDFID = 0x0001;
|
|
13
|
+
const hpkeAEADID = 0x0003;
|
|
14
|
+
const hpkeKEMSuiteID = concatBytes(text('KEM'), integerToBytes(hpkeKEMID, 2));
|
|
15
|
+
const hpkeSuiteID = concatBytes(text('HPKE'), integerToBytes(hpkeKEMID, 2), integerToBytes(hpkeKDFID, 2), integerToBytes(hpkeAEADID, 2));
|
|
16
|
+
export function createEncryptedColumnCodec(options) {
|
|
17
|
+
if (!options.keyring ||
|
|
18
|
+
typeof options.keyring.current !== 'function' ||
|
|
19
|
+
typeof options.keyring.get !== 'function') {
|
|
20
|
+
throw new Error('orez-e1 requires an encryption keyring');
|
|
21
|
+
}
|
|
22
|
+
const manifest = normalizeManifest(options.manifest);
|
|
23
|
+
const manifestHash = base64URLEncode(sha256(text(canonicalJSON(options.manifest))));
|
|
24
|
+
const id = `orez-e1:${manifest.networkID}:${manifest.schemaID}:${manifestHash}`;
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
id,
|
|
27
|
+
async encodePush(body) {
|
|
28
|
+
assertPushRequest(body);
|
|
29
|
+
rejectConflictingDuplicateMutations(body.mutations);
|
|
30
|
+
let mutations;
|
|
31
|
+
let currentKeyPromise;
|
|
32
|
+
const currentKey = () => {
|
|
33
|
+
currentKeyPromise ??= options.keyring.current().then(validateCurrentKey);
|
|
34
|
+
return currentKeyPromise;
|
|
35
|
+
};
|
|
36
|
+
const epochKeys = new Map();
|
|
37
|
+
const getEpochKey = (epoch) => {
|
|
38
|
+
let promise = epochKeys.get(epoch);
|
|
39
|
+
if (!promise) {
|
|
40
|
+
promise = options.keyring.get(epoch).then((key) => validateEpochKey(epoch, key));
|
|
41
|
+
epochKeys.set(epoch, promise);
|
|
42
|
+
}
|
|
43
|
+
return promise;
|
|
44
|
+
};
|
|
45
|
+
for (let index = 0; index < body.mutations.length; index++) {
|
|
46
|
+
const mutation = body.mutations[index];
|
|
47
|
+
if (mutation.type !== 'custom' || typeof mutation.name !== 'string')
|
|
48
|
+
continue;
|
|
49
|
+
const rowMutation = manifest.rowMutations.get(mutation.name);
|
|
50
|
+
if (!rowMutation)
|
|
51
|
+
continue;
|
|
52
|
+
const encoded = await encodeRowMutation(mutation, rowMutation.argumentIndex, manifest, currentKey, getEpochKey);
|
|
53
|
+
if (encoded === mutation)
|
|
54
|
+
continue;
|
|
55
|
+
mutations ??= [...body.mutations];
|
|
56
|
+
mutations[index] = encoded;
|
|
57
|
+
}
|
|
58
|
+
return mutations ? { ...body, mutations } : body;
|
|
59
|
+
},
|
|
60
|
+
async decodePull(response) {
|
|
61
|
+
assertPullResponse(response);
|
|
62
|
+
if (response.unchanged)
|
|
63
|
+
return response;
|
|
64
|
+
const epochKeys = new Map();
|
|
65
|
+
const getEpochKey = (epoch) => {
|
|
66
|
+
let promise = epochKeys.get(epoch);
|
|
67
|
+
if (!promise) {
|
|
68
|
+
promise = options.keyring.get(epoch).then((key) => validateEpochKey(epoch, key));
|
|
69
|
+
epochKeys.set(epoch, promise);
|
|
70
|
+
}
|
|
71
|
+
return promise;
|
|
72
|
+
};
|
|
73
|
+
let rowsPatch;
|
|
74
|
+
for (let index = 0; index < response.rowsPatch.length; index++) {
|
|
75
|
+
const patch = response.rowsPatch[index];
|
|
76
|
+
if (!isJSONObject(patch) || patch.op !== 'put')
|
|
77
|
+
continue;
|
|
78
|
+
if (typeof patch.tableName !== 'string')
|
|
79
|
+
continue;
|
|
80
|
+
const table = manifest.tablesByPhysicalName.get(patch.tableName);
|
|
81
|
+
if (!table || !isJSONObject(patch.value))
|
|
82
|
+
continue;
|
|
83
|
+
const decodedValue = await decodePullValue(patch.value, table, manifest, getEpochKey);
|
|
84
|
+
if (decodedValue === patch.value)
|
|
85
|
+
continue;
|
|
86
|
+
rowsPatch ??= [...response.rowsPatch];
|
|
87
|
+
rowsPatch[index] = { ...patch, value: decodedValue };
|
|
88
|
+
}
|
|
89
|
+
return rowsPatch ? { ...response, rowsPatch } : response;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function deriveX25519KeyPair(inputKeyMaterial) {
|
|
94
|
+
assertBytes('X25519 input key material', inputKeyMaterial, 32);
|
|
95
|
+
const dkpPRK = hpkeLabeledExtract(hpkeKEMSuiteID, emptyBytes, 'dkp_prk', inputKeyMaterial);
|
|
96
|
+
const privateKey = hpkeLabeledExpand(hpkeKEMSuiteID, dkpPRK, 'sk', emptyBytes, 32);
|
|
97
|
+
return { privateKey, publicKey: x25519.getPublicKey(privateKey) };
|
|
98
|
+
}
|
|
99
|
+
export function generateX25519KeyPair(randomBytes) {
|
|
100
|
+
return deriveX25519KeyPair(readSecureRandom(randomBytes, 32));
|
|
101
|
+
}
|
|
102
|
+
export function hpkeSeal(options) {
|
|
103
|
+
assertBytes('HPKE recipient public key', options.recipientPublicKey, 32);
|
|
104
|
+
assertBytes('HPKE plaintext', options.plaintext);
|
|
105
|
+
const info = options.info ?? emptyBytes;
|
|
106
|
+
const associatedData = options.associatedData ?? emptyBytes;
|
|
107
|
+
assertBytes('HPKE info', info);
|
|
108
|
+
assertBytes('HPKE associated data', associatedData);
|
|
109
|
+
const ephemeral = deriveX25519KeyPair(readSecureRandom(options.randomBytes, 32));
|
|
110
|
+
const context = hpkeSenderContext(ephemeral.privateKey, ephemeral.publicKey, options.recipientPublicKey, info);
|
|
111
|
+
return {
|
|
112
|
+
encapsulatedKey: ephemeral.publicKey,
|
|
113
|
+
ciphertext: chacha20poly1305(context.key, context.baseNonce, associatedData).encrypt(options.plaintext),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function hpkeOpen(options) {
|
|
117
|
+
assertBytes('HPKE recipient private key', options.recipientPrivateKey, 32);
|
|
118
|
+
assertBytes('HPKE encapsulated key', options.encapsulatedKey, 32);
|
|
119
|
+
assertBytes('HPKE ciphertext', options.ciphertext);
|
|
120
|
+
const info = options.info ?? emptyBytes;
|
|
121
|
+
const associatedData = options.associatedData ?? emptyBytes;
|
|
122
|
+
assertBytes('HPKE info', info);
|
|
123
|
+
assertBytes('HPKE associated data', associatedData);
|
|
124
|
+
const context = hpkeRecipientContext(options.recipientPrivateKey, options.encapsulatedKey, info);
|
|
125
|
+
return chacha20poly1305(context.key, context.baseNonce, associatedData).decrypt(options.ciphertext);
|
|
126
|
+
}
|
|
127
|
+
export function wrapContentKey(options) {
|
|
128
|
+
assertBytes('network content key', options.contentKey, 32);
|
|
129
|
+
return hpkeSeal({
|
|
130
|
+
recipientPublicKey: options.recipientPublicKey,
|
|
131
|
+
plaintext: options.contentKey,
|
|
132
|
+
info: options.info,
|
|
133
|
+
associatedData: options.associatedData,
|
|
134
|
+
randomBytes: options.randomBytes,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export function unwrapContentKey(options) {
|
|
138
|
+
const contentKey = hpkeOpen(options);
|
|
139
|
+
assertBytes('unwrapped network content key', contentKey, 32);
|
|
140
|
+
return contentKey;
|
|
141
|
+
}
|
|
142
|
+
async function encodeRowMutation(mutation, argumentIndex, manifest, currentKey, getEpochKey) {
|
|
143
|
+
if (!Array.isArray(mutation.args) || argumentIndex >= mutation.args.length) {
|
|
144
|
+
throw codecError(manifest, `row mutation ${mutation.name} is missing its row batch`);
|
|
145
|
+
}
|
|
146
|
+
const batch = validateRowBatch(mutation.args[argumentIndex], manifest, mutation.name ?? '');
|
|
147
|
+
const mutationTag = createMutationTag(mutation.clientID, mutation.id);
|
|
148
|
+
let rows;
|
|
149
|
+
let changed = false;
|
|
150
|
+
for (let index = 0; index < batch.rows.length; index++) {
|
|
151
|
+
const row = batch.rows[index];
|
|
152
|
+
if (row.op !== 'put')
|
|
153
|
+
continue;
|
|
154
|
+
const table = manifest.tablesByLogicalName.get(row.table);
|
|
155
|
+
if (!table)
|
|
156
|
+
continue;
|
|
157
|
+
const primaryKeyTuple = readPrimaryKeyTuple(row.value, table, manifest, 'logical');
|
|
158
|
+
let value = row.value;
|
|
159
|
+
for (const column of table.columns) {
|
|
160
|
+
if (!Object.hasOwn(value, column.logicalName))
|
|
161
|
+
continue;
|
|
162
|
+
const originalValue = value[column.logicalName];
|
|
163
|
+
if (typeof originalValue === 'string' && originalValue.startsWith(envelopePrefix)) {
|
|
164
|
+
const existing = parseEnvelope(originalValue);
|
|
165
|
+
if (existing.mutationTag !== mutationTag) {
|
|
166
|
+
throw cellError(manifest, table, column, existing.epoch, mutationTag, 'encoded value has a different mutation tag');
|
|
167
|
+
}
|
|
168
|
+
const key = await getEpochKey(existing.epoch);
|
|
169
|
+
if (!key) {
|
|
170
|
+
throw cellError(manifest, table, column, existing.epoch, mutationTag, 'no key is available for encoded retry');
|
|
171
|
+
}
|
|
172
|
+
decryptEnvelopeValue(existing, key, manifest, table, column, primaryKeyTuple);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const writable = await currentKey();
|
|
176
|
+
if (!writable) {
|
|
177
|
+
throw cellError(manifest, table, column, undefined, mutationTag, 'no current encryption key is available');
|
|
178
|
+
}
|
|
179
|
+
const plaintext = text(canonicalJSON(originalValue));
|
|
180
|
+
const associatedData = createAssociatedData(manifest, table, column, primaryKeyTuple, writable.epoch, mutationTag);
|
|
181
|
+
const { dataKey, nonceKey } = deriveColumnKeys(writable.key, manifest.networkID);
|
|
182
|
+
const nonce = deriveColumnNonce(nonceKey, associatedData, plaintext);
|
|
183
|
+
const ciphertext = xchacha20poly1305(dataKey, nonce, associatedData).encrypt(plaintext);
|
|
184
|
+
// the derived nonce stays inside the binary payload so a reader can open
|
|
185
|
+
// the aead, then re-derive and verify it against the authenticated plaintext.
|
|
186
|
+
const envelope = `orez-e1.${writable.epoch}.${mutationTag}.${base64URLEncode(concatBytes(nonce, ciphertext))}`;
|
|
187
|
+
value = { ...value, [column.logicalName]: envelope };
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
if (value !== row.value) {
|
|
191
|
+
rows ??= [...batch.rows];
|
|
192
|
+
rows[index] = { ...row, value };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!changed)
|
|
196
|
+
return mutation;
|
|
197
|
+
const nextBatch = { ...batch, rows: rows ?? batch.rows };
|
|
198
|
+
const args = [...mutation.args];
|
|
199
|
+
args[argumentIndex] = nextBatch;
|
|
200
|
+
return { ...mutation, args };
|
|
201
|
+
}
|
|
202
|
+
async function decodePullValue(originalValue, table, manifest, getEpochKey) {
|
|
203
|
+
let value = originalValue;
|
|
204
|
+
let primaryKeyTuple;
|
|
205
|
+
for (const column of table.columns) {
|
|
206
|
+
if (!Object.hasOwn(originalValue, column.physicalName))
|
|
207
|
+
continue;
|
|
208
|
+
const encoded = originalValue[column.physicalName];
|
|
209
|
+
if (typeof encoded !== 'string' || !encoded.startsWith(envelopePrefix)) {
|
|
210
|
+
throw codecError(manifest, `table ${table.logicalName} has plaintext in encrypted column ${column.logicalName}`);
|
|
211
|
+
}
|
|
212
|
+
const envelope = parseEnvelope(encoded);
|
|
213
|
+
const key = await getEpochKey(envelope.epoch);
|
|
214
|
+
if (!key)
|
|
215
|
+
continue;
|
|
216
|
+
primaryKeyTuple ??= readPrimaryKeyTuple(originalValue, table, manifest, 'physical');
|
|
217
|
+
const decoded = decryptEnvelopeValue(envelope, key, manifest, table, column, primaryKeyTuple);
|
|
218
|
+
value = { ...value, [column.physicalName]: decoded };
|
|
219
|
+
}
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
function normalizeManifest(input) {
|
|
223
|
+
if (!isJSONObject(input) || input.version !== 1) {
|
|
224
|
+
throw new Error('orez-e1 manifest version must be 1');
|
|
225
|
+
}
|
|
226
|
+
if (typeof input.networkID !== 'string' ||
|
|
227
|
+
!input.networkID ||
|
|
228
|
+
typeof input.schemaID !== 'string' ||
|
|
229
|
+
!input.schemaID) {
|
|
230
|
+
throw new Error('orez-e1 manifest requires networkID and schemaID');
|
|
231
|
+
}
|
|
232
|
+
canonicalJSON(input);
|
|
233
|
+
if (!isJSONObject(input.rowMutations) || !isJSONObject(input.tables)) {
|
|
234
|
+
throw new Error('orez-e1 manifest requires rowMutations and tables');
|
|
235
|
+
}
|
|
236
|
+
const rowMutations = new Map();
|
|
237
|
+
for (const [name, config] of Object.entries(input.rowMutations)) {
|
|
238
|
+
if (!name ||
|
|
239
|
+
!isJSONObject(config) ||
|
|
240
|
+
!Number.isSafeInteger(config.argumentIndex) ||
|
|
241
|
+
config.argumentIndex < 0 ||
|
|
242
|
+
config.format !== 'orez-row-batch-v1') {
|
|
243
|
+
throw new Error(`orez-e1 manifest has an invalid row mutation ${name}`);
|
|
244
|
+
}
|
|
245
|
+
rowMutations.set(name, {
|
|
246
|
+
argumentIndex: config.argumentIndex,
|
|
247
|
+
format: 'orez-row-batch-v1',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const tablesByLogicalName = new Map();
|
|
251
|
+
const tablesByPhysicalName = new Map();
|
|
252
|
+
for (const [logicalName, spec] of Object.entries(input.tables)) {
|
|
253
|
+
if (!logicalName || !isJSONObject(spec)) {
|
|
254
|
+
throw new Error('orez-e1 manifest has an invalid table');
|
|
255
|
+
}
|
|
256
|
+
const physicalName = spec.serverName === undefined ? logicalName : spec.serverName;
|
|
257
|
+
if (typeof physicalName !== 'string' || !physicalName) {
|
|
258
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has an invalid serverName`);
|
|
259
|
+
}
|
|
260
|
+
if (!Array.isArray(spec.primaryKey) || spec.primaryKey.length === 0) {
|
|
261
|
+
throw new Error(`orez-e1 manifest table ${logicalName} requires a primary key`);
|
|
262
|
+
}
|
|
263
|
+
const primaryKeyNames = spec.primaryKey.map((column) => {
|
|
264
|
+
if (typeof column !== 'string' || !column) {
|
|
265
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has an invalid primary key`);
|
|
266
|
+
}
|
|
267
|
+
return column;
|
|
268
|
+
});
|
|
269
|
+
if (new Set(primaryKeyNames).size !== primaryKeyNames.length) {
|
|
270
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has duplicate primary keys`);
|
|
271
|
+
}
|
|
272
|
+
const primaryKeyServerNames = spec.primaryKeyServerNames;
|
|
273
|
+
if (primaryKeyServerNames !== undefined && !isJSONObject(primaryKeyServerNames)) {
|
|
274
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has invalid primary key server names`);
|
|
275
|
+
}
|
|
276
|
+
for (const name of Object.keys(primaryKeyServerNames ?? {})) {
|
|
277
|
+
if (!primaryKeyNames.includes(name)) {
|
|
278
|
+
throw new Error(`orez-e1 manifest table ${logicalName} maps unknown primary key ${name}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const primaryKey = primaryKeyNames.map((primaryKeyName) => {
|
|
282
|
+
const mapped = primaryKeyServerNames?.[primaryKeyName];
|
|
283
|
+
const physicalPrimaryKey = mapped === undefined ? primaryKeyName : mapped;
|
|
284
|
+
if (typeof physicalPrimaryKey !== 'string' || !physicalPrimaryKey) {
|
|
285
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has an invalid primary key serverName`);
|
|
286
|
+
}
|
|
287
|
+
return { logicalName: primaryKeyName, physicalName: physicalPrimaryKey };
|
|
288
|
+
});
|
|
289
|
+
const physicalPrimaryKeys = primaryKey.map(({ physicalName }) => physicalName);
|
|
290
|
+
if (new Set(physicalPrimaryKeys).size !== physicalPrimaryKeys.length) {
|
|
291
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has ambiguous physical primary keys`);
|
|
292
|
+
}
|
|
293
|
+
if (!isJSONObject(spec.columns)) {
|
|
294
|
+
throw new Error(`orez-e1 manifest table ${logicalName} requires columns`);
|
|
295
|
+
}
|
|
296
|
+
const physicalColumns = new Set(physicalPrimaryKeys);
|
|
297
|
+
const columns = [];
|
|
298
|
+
for (const [columnName, columnSpec] of Object.entries(spec.columns)) {
|
|
299
|
+
if (!columnName || !isJSONObject(columnSpec)) {
|
|
300
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has an invalid column`);
|
|
301
|
+
}
|
|
302
|
+
if (primaryKeyNames.includes(columnName)) {
|
|
303
|
+
throw new Error(`orez-e1 manifest cannot encrypt primary key ${logicalName}.${columnName}`);
|
|
304
|
+
}
|
|
305
|
+
const physicalColumn = columnSpec.serverName === undefined ? columnName : columnSpec.serverName;
|
|
306
|
+
if (typeof physicalColumn !== 'string' || !physicalColumn) {
|
|
307
|
+
throw new Error(`orez-e1 manifest column ${logicalName}.${columnName} has an invalid serverName`);
|
|
308
|
+
}
|
|
309
|
+
if (physicalColumns.has(physicalColumn)) {
|
|
310
|
+
throw new Error(`orez-e1 manifest has ambiguous physical column ${physicalName}.${physicalColumn}`);
|
|
311
|
+
}
|
|
312
|
+
physicalColumns.add(physicalColumn);
|
|
313
|
+
columns.push({ logicalName: columnName, physicalName: physicalColumn });
|
|
314
|
+
}
|
|
315
|
+
if (columns.length === 0) {
|
|
316
|
+
throw new Error(`orez-e1 manifest table ${logicalName} has no encrypted columns`);
|
|
317
|
+
}
|
|
318
|
+
const table = { logicalName, physicalName, primaryKey, columns };
|
|
319
|
+
if (tablesByPhysicalName.has(physicalName)) {
|
|
320
|
+
throw new Error(`orez-e1 manifest has ambiguous physical table ${physicalName}`);
|
|
321
|
+
}
|
|
322
|
+
tablesByLogicalName.set(logicalName, table);
|
|
323
|
+
tablesByPhysicalName.set(physicalName, table);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
networkID: input.networkID,
|
|
327
|
+
schemaID: input.schemaID,
|
|
328
|
+
rowMutations,
|
|
329
|
+
tablesByLogicalName,
|
|
330
|
+
tablesByPhysicalName,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function validateRowBatch(value, manifest, mutationName) {
|
|
334
|
+
if (!isJSONObject(value)) {
|
|
335
|
+
throw codecError(manifest, `row mutation ${mutationName} has an invalid row batch`);
|
|
336
|
+
}
|
|
337
|
+
canonicalJSON(value);
|
|
338
|
+
if (typeof value.sourceID !== 'string' ||
|
|
339
|
+
!isSequence(value.fromSeq) ||
|
|
340
|
+
!isSequence(value.throughSeq) ||
|
|
341
|
+
!Array.isArray(value.rows)) {
|
|
342
|
+
throw codecError(manifest, `row mutation ${mutationName} has an invalid row batch`);
|
|
343
|
+
}
|
|
344
|
+
for (const row of value.rows) {
|
|
345
|
+
if (!isJSONObject(row) ||
|
|
346
|
+
!isSequence(row.seq) ||
|
|
347
|
+
typeof row.table !== 'string' ||
|
|
348
|
+
(row.op !== 'put' && row.op !== 'del') ||
|
|
349
|
+
(row.op === 'put' && !isJSONObject(row.value)) ||
|
|
350
|
+
(row.op === 'del' && !isJSONObject(row.key))) {
|
|
351
|
+
throw codecError(manifest, `row mutation ${mutationName} has an invalid row batch`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function rejectConflictingDuplicateMutations(mutations) {
|
|
357
|
+
const canonicalByIdentity = new Map();
|
|
358
|
+
for (const mutation of mutations) {
|
|
359
|
+
if (mutation.type !== 'custom')
|
|
360
|
+
continue;
|
|
361
|
+
assertMutationIdentity(mutation);
|
|
362
|
+
const identity = `${mutation.clientID}\u0000${mutation.id}`;
|
|
363
|
+
const canonical = canonicalJSON(mutation);
|
|
364
|
+
const previous = canonicalByIdentity.get(identity);
|
|
365
|
+
if (previous !== undefined && previous !== canonical) {
|
|
366
|
+
throw new Error('orez-e1 push contains conflicting duplicate custom mutations');
|
|
367
|
+
}
|
|
368
|
+
canonicalByIdentity.set(identity, canonical);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function assertPushRequest(value) {
|
|
372
|
+
if (!isJSONObject(value) || !Array.isArray(value.mutations)) {
|
|
373
|
+
throw new Error('orez-e1 received an invalid push request');
|
|
374
|
+
}
|
|
375
|
+
for (const mutation of value.mutations) {
|
|
376
|
+
if (!isJSONObject(mutation)) {
|
|
377
|
+
throw new Error('orez-e1 received an invalid push mutation');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function assertPullResponse(value) {
|
|
382
|
+
if (!isJSONObject(value)) {
|
|
383
|
+
throw new Error('orez-e1 received an invalid pull response');
|
|
384
|
+
}
|
|
385
|
+
if (value.unchanged === true) {
|
|
386
|
+
if (value.cookie === null || typeof value.cookie === 'number')
|
|
387
|
+
return;
|
|
388
|
+
throw new Error('orez-e1 received an invalid pull response');
|
|
389
|
+
}
|
|
390
|
+
if ((value.unchanged !== undefined && value.unchanged !== false) ||
|
|
391
|
+
typeof value.cookie !== 'number' ||
|
|
392
|
+
!isJSONObject(value.lastMutationIDChanges) ||
|
|
393
|
+
!Array.isArray(value.rowsPatch)) {
|
|
394
|
+
throw new Error('orez-e1 received an invalid pull response');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function assertMutationIdentity(mutation) {
|
|
398
|
+
if (typeof mutation.clientID !== 'string' ||
|
|
399
|
+
!Number.isSafeInteger(mutation.id) ||
|
|
400
|
+
mutation.id < 0) {
|
|
401
|
+
throw new Error('orez-e1 custom mutation has an invalid identity');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function readPrimaryKeyTuple(value, table, manifest, names) {
|
|
405
|
+
return table.primaryKey.map((column) => {
|
|
406
|
+
const name = names === 'logical' ? column.logicalName : column.physicalName;
|
|
407
|
+
if (!Object.hasOwn(value, name)) {
|
|
408
|
+
throw codecError(manifest, `table ${table.logicalName} is missing primary key ${column.logicalName}`);
|
|
409
|
+
}
|
|
410
|
+
return value[name];
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function decryptEnvelopeValue(envelope, key, manifest, table, column, primaryKeyTuple) {
|
|
414
|
+
const associatedData = createAssociatedData(manifest, table, column, primaryKeyTuple, envelope.epoch, envelope.mutationTag);
|
|
415
|
+
const { dataKey, nonceKey } = deriveColumnKeys(key, manifest.networkID);
|
|
416
|
+
const nonce = envelope.payload.subarray(0, 24);
|
|
417
|
+
const ciphertext = envelope.payload.subarray(24);
|
|
418
|
+
try {
|
|
419
|
+
const plaintext = xchacha20poly1305(dataKey, nonce, associatedData).decrypt(ciphertext);
|
|
420
|
+
const decodedText = decodeCanonicalUTF8(plaintext);
|
|
421
|
+
const decoded = JSON.parse(decodedText);
|
|
422
|
+
if (canonicalJSON(decoded) !== decodedText) {
|
|
423
|
+
throw new Error('plaintext is not canonical JSON');
|
|
424
|
+
}
|
|
425
|
+
const expectedNonce = deriveColumnNonce(nonceKey, associatedData, plaintext);
|
|
426
|
+
if (!equalBytes(nonce, expectedNonce))
|
|
427
|
+
throw new Error('derived nonce mismatch');
|
|
428
|
+
return decoded;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
throw cellError(manifest, table, column, envelope.epoch, envelope.mutationTag, 'authentication failed');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function createMutationTag(clientID, mutationID) {
|
|
435
|
+
if (!clientID || !Number.isSafeInteger(mutationID) || mutationID < 0) {
|
|
436
|
+
throw new Error('orez-e1 custom mutation has an invalid identity');
|
|
437
|
+
}
|
|
438
|
+
return base64URLEncode(sha256(concatBytes(lengthPrefix(text(clientID)), integerToBytes(mutationID, 8)))).slice(0, 16);
|
|
439
|
+
}
|
|
440
|
+
function createAssociatedData(manifest, table, column, primaryKeyTuple, epoch, mutationTag) {
|
|
441
|
+
return concatLengthPrefixed([
|
|
442
|
+
text('orez-e1'),
|
|
443
|
+
text(manifest.networkID),
|
|
444
|
+
text(manifest.schemaID),
|
|
445
|
+
integerToBytes(epoch, 8),
|
|
446
|
+
text(mutationTag),
|
|
447
|
+
text(table.logicalName),
|
|
448
|
+
text(canonicalJSON(primaryKeyTuple)),
|
|
449
|
+
text(column.logicalName),
|
|
450
|
+
]);
|
|
451
|
+
}
|
|
452
|
+
function deriveColumnKeys(contentKey, networkID) {
|
|
453
|
+
assertBytes('network content key', contentKey, 32);
|
|
454
|
+
const salt = text(networkID);
|
|
455
|
+
return {
|
|
456
|
+
dataKey: hkdf(sha256, contentKey, salt, text('orez-e1/data'), 32),
|
|
457
|
+
nonceKey: hkdf(sha256, contentKey, salt, text('orez-e1/nonce'), 32),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function deriveColumnNonce(nonceKey, associatedData, plaintext) {
|
|
461
|
+
return hmac(sha256, nonceKey, concatBytes(associatedData, sha256(plaintext))).slice(0, 24);
|
|
462
|
+
}
|
|
463
|
+
function parseEnvelope(value) {
|
|
464
|
+
const match = /^orez-e1\.(0|[1-9][0-9]*)\.([A-Za-z0-9_-]{16})\.([A-Za-z0-9_-]+)$/.exec(value);
|
|
465
|
+
if (!match)
|
|
466
|
+
throw new Error('orez-e1 envelope is malformed');
|
|
467
|
+
const epoch = Number(match[1]);
|
|
468
|
+
if (!Number.isSafeInteger(epoch))
|
|
469
|
+
throw new Error('orez-e1 envelope epoch is invalid');
|
|
470
|
+
const payload = base64URLDecode(match[3]);
|
|
471
|
+
if (payload.length < 40)
|
|
472
|
+
throw new Error('orez-e1 envelope ciphertext is truncated');
|
|
473
|
+
return { epoch, mutationTag: match[2], payload };
|
|
474
|
+
}
|
|
475
|
+
function hpkeSenderContext(ephemeralPrivateKey, encapsulatedKey, recipientPublicKey, info) {
|
|
476
|
+
const dh = validatedX25519(ephemeralPrivateKey, recipientPublicKey);
|
|
477
|
+
const sharedSecret = hpkeExtractAndExpand(dh, concatBytes(encapsulatedKey, recipientPublicKey));
|
|
478
|
+
return hpkeKeySchedule(sharedSecret, info);
|
|
479
|
+
}
|
|
480
|
+
function hpkeRecipientContext(recipientPrivateKey, encapsulatedKey, info) {
|
|
481
|
+
const recipientPublicKey = x25519.getPublicKey(recipientPrivateKey);
|
|
482
|
+
const dh = validatedX25519(recipientPrivateKey, encapsulatedKey);
|
|
483
|
+
const sharedSecret = hpkeExtractAndExpand(dh, concatBytes(encapsulatedKey, recipientPublicKey));
|
|
484
|
+
return hpkeKeySchedule(sharedSecret, info);
|
|
485
|
+
}
|
|
486
|
+
function hpkeExtractAndExpand(dh, kemContext) {
|
|
487
|
+
const eaePRK = hpkeLabeledExtract(hpkeKEMSuiteID, emptyBytes, 'eae_prk', dh);
|
|
488
|
+
return hpkeLabeledExpand(hpkeKEMSuiteID, eaePRK, 'shared_secret', kemContext, 32);
|
|
489
|
+
}
|
|
490
|
+
function hpkeKeySchedule(sharedSecret, info) {
|
|
491
|
+
const pskIDHash = hpkeLabeledExtract(hpkeSuiteID, emptyBytes, 'psk_id_hash', emptyBytes);
|
|
492
|
+
const infoHash = hpkeLabeledExtract(hpkeSuiteID, emptyBytes, 'info_hash', info);
|
|
493
|
+
const keyScheduleContext = concatBytes(new Uint8Array([0]), pskIDHash, infoHash);
|
|
494
|
+
const secret = hpkeLabeledExtract(hpkeSuiteID, sharedSecret, 'secret', emptyBytes);
|
|
495
|
+
return {
|
|
496
|
+
key: hpkeLabeledExpand(hpkeSuiteID, secret, 'key', keyScheduleContext, 32),
|
|
497
|
+
baseNonce: hpkeLabeledExpand(hpkeSuiteID, secret, 'base_nonce', keyScheduleContext, 12),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function hpkeLabeledExtract(suiteID, salt, label, inputKeyMaterial) {
|
|
501
|
+
return extract(sha256, concatBytes(text('HPKE-v1'), suiteID, text(label), inputKeyMaterial), salt);
|
|
502
|
+
}
|
|
503
|
+
function hpkeLabeledExpand(suiteID, prk, label, info, length) {
|
|
504
|
+
return expand(sha256, prk, concatBytes(integerToBytes(length, 2), text('HPKE-v1'), suiteID, text(label), info), length);
|
|
505
|
+
}
|
|
506
|
+
function validatedX25519(privateKey, publicKey) {
|
|
507
|
+
let shared;
|
|
508
|
+
try {
|
|
509
|
+
shared = x25519.getSharedSecret(privateKey, publicKey);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
throw new Error('HPKE X25519 validation failed');
|
|
513
|
+
}
|
|
514
|
+
if (shared.every((byte) => byte === 0)) {
|
|
515
|
+
throw new Error('HPKE X25519 validation failed');
|
|
516
|
+
}
|
|
517
|
+
return shared;
|
|
518
|
+
}
|
|
519
|
+
function readSecureRandom(randomBytes, length) {
|
|
520
|
+
if (typeof randomBytes !== 'function') {
|
|
521
|
+
throw new Error('secure randomBytes adapter is required');
|
|
522
|
+
}
|
|
523
|
+
const bytes = randomBytes(length);
|
|
524
|
+
assertBytes('secure randomBytes result', bytes, length);
|
|
525
|
+
return new Uint8Array(bytes);
|
|
526
|
+
}
|
|
527
|
+
function validateCurrentKey(value) {
|
|
528
|
+
if (value === undefined)
|
|
529
|
+
return undefined;
|
|
530
|
+
if (!isSequence(value.epoch))
|
|
531
|
+
throw new Error('orez-e1 current epoch is invalid');
|
|
532
|
+
assertBytes('network content key', value.key, 32);
|
|
533
|
+
return { epoch: value.epoch, key: new Uint8Array(value.key) };
|
|
534
|
+
}
|
|
535
|
+
function validateEpochKey(epoch, key) {
|
|
536
|
+
if (key === undefined)
|
|
537
|
+
return undefined;
|
|
538
|
+
assertBytes(`network content key for epoch ${epoch}`, key, 32);
|
|
539
|
+
return new Uint8Array(key);
|
|
540
|
+
}
|
|
541
|
+
function assertBytes(name, value, length) {
|
|
542
|
+
if (!(value instanceof Uint8Array) ||
|
|
543
|
+
(length !== undefined && value.length !== length)) {
|
|
544
|
+
throw new Error(length === undefined ? `${name} must be bytes` : `${name} must be ${length} bytes`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function isSequence(value) {
|
|
548
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
549
|
+
}
|
|
550
|
+
function isJSONObject(value) {
|
|
551
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
552
|
+
}
|
|
553
|
+
function canonicalJSON(value) {
|
|
554
|
+
return canonicalJSONInner(value, new Set());
|
|
555
|
+
}
|
|
556
|
+
function canonicalJSONInner(value, ancestors) {
|
|
557
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
558
|
+
return JSON.stringify(value);
|
|
559
|
+
}
|
|
560
|
+
if (typeof value === 'number') {
|
|
561
|
+
if (!Number.isFinite(value))
|
|
562
|
+
throw new Error('orez-e1 only accepts finite JSON numbers');
|
|
563
|
+
return JSON.stringify(value);
|
|
564
|
+
}
|
|
565
|
+
if (typeof value !== 'object')
|
|
566
|
+
throw new Error('orez-e1 only accepts JSON values');
|
|
567
|
+
if (ancestors.has(value))
|
|
568
|
+
throw new Error('orez-e1 JSON value is cyclic');
|
|
569
|
+
ancestors.add(value);
|
|
570
|
+
let encoded;
|
|
571
|
+
if (Array.isArray(value)) {
|
|
572
|
+
for (let index = 0; index < value.length; index++) {
|
|
573
|
+
if (!Object.hasOwn(value, index))
|
|
574
|
+
throw new Error('orez-e1 JSON array is sparse');
|
|
575
|
+
}
|
|
576
|
+
encoded = `[${value.map((item) => canonicalJSONInner(item, ancestors)).join(',')}]`;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
encoded = `{${Object.keys(value)
|
|
580
|
+
.sort()
|
|
581
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJSONInner(value[key], ancestors)}`)
|
|
582
|
+
.join(',')}}`;
|
|
583
|
+
}
|
|
584
|
+
ancestors.delete(value);
|
|
585
|
+
return encoded;
|
|
586
|
+
}
|
|
587
|
+
function decodeCanonicalUTF8(bytes) {
|
|
588
|
+
const decoded = textDecoder.decode(bytes);
|
|
589
|
+
if (!equalBytes(text(decoded), bytes))
|
|
590
|
+
throw new Error('plaintext is not UTF-8');
|
|
591
|
+
return decoded;
|
|
592
|
+
}
|
|
593
|
+
function text(value) {
|
|
594
|
+
return textEncoder.encode(value);
|
|
595
|
+
}
|
|
596
|
+
function concatLengthPrefixed(parts) {
|
|
597
|
+
return concatBytes(...parts.map(lengthPrefix));
|
|
598
|
+
}
|
|
599
|
+
function lengthPrefix(value) {
|
|
600
|
+
return concatBytes(integerToBytes(value.length, 4), value);
|
|
601
|
+
}
|
|
602
|
+
function integerToBytes(value, width) {
|
|
603
|
+
let remaining = typeof value === 'bigint' ? value : BigInt(value);
|
|
604
|
+
if (remaining < 0)
|
|
605
|
+
throw new Error('cannot encode a negative integer');
|
|
606
|
+
const bytes = new Uint8Array(width);
|
|
607
|
+
for (let index = width - 1; index >= 0; index--) {
|
|
608
|
+
bytes[index] = Number(remaining & 0xffn);
|
|
609
|
+
remaining >>= 8n;
|
|
610
|
+
}
|
|
611
|
+
if (remaining !== 0n)
|
|
612
|
+
throw new Error('integer does not fit in requested width');
|
|
613
|
+
return bytes;
|
|
614
|
+
}
|
|
615
|
+
function concatBytes(...arrays) {
|
|
616
|
+
const result = new Uint8Array(arrays.reduce((sum, array) => sum + array.length, 0));
|
|
617
|
+
let offset = 0;
|
|
618
|
+
for (const array of arrays) {
|
|
619
|
+
result.set(array, offset);
|
|
620
|
+
offset += array.length;
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
function equalBytes(left, right) {
|
|
625
|
+
if (left.length !== right.length)
|
|
626
|
+
return false;
|
|
627
|
+
let difference = 0;
|
|
628
|
+
for (let index = 0; index < left.length; index++) {
|
|
629
|
+
difference |= left[index] ^ right[index];
|
|
630
|
+
}
|
|
631
|
+
return difference === 0;
|
|
632
|
+
}
|
|
633
|
+
function base64URLEncode(bytes) {
|
|
634
|
+
let result = '';
|
|
635
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
636
|
+
const first = bytes[index];
|
|
637
|
+
const second = index + 1 < bytes.length ? bytes[index + 1] : 0;
|
|
638
|
+
const third = index + 2 < bytes.length ? bytes[index + 2] : 0;
|
|
639
|
+
const combined = (first << 16) | (second << 8) | third;
|
|
640
|
+
result += base64URLAlphabet[(combined >>> 18) & 63];
|
|
641
|
+
result += base64URLAlphabet[(combined >>> 12) & 63];
|
|
642
|
+
if (index + 1 < bytes.length)
|
|
643
|
+
result += base64URLAlphabet[(combined >>> 6) & 63];
|
|
644
|
+
if (index + 2 < bytes.length)
|
|
645
|
+
result += base64URLAlphabet[combined & 63];
|
|
646
|
+
}
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
function base64URLDecode(value) {
|
|
650
|
+
if (!value || value.length % 4 === 1 || /[^A-Za-z0-9_-]/.test(value)) {
|
|
651
|
+
throw new Error('invalid base64url');
|
|
652
|
+
}
|
|
653
|
+
const result = new Uint8Array(Math.floor((value.length * 6) / 8));
|
|
654
|
+
let accumulator = 0;
|
|
655
|
+
let bits = 0;
|
|
656
|
+
let outputIndex = 0;
|
|
657
|
+
for (const character of value) {
|
|
658
|
+
accumulator = (accumulator << 6) | base64URLAlphabet.indexOf(character);
|
|
659
|
+
bits += 6;
|
|
660
|
+
if (bits >= 8) {
|
|
661
|
+
bits -= 8;
|
|
662
|
+
result[outputIndex++] = (accumulator >>> bits) & 0xff;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (base64URLEncode(result) !== value)
|
|
666
|
+
throw new Error('non-canonical base64url');
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
function codecError(manifest, message) {
|
|
670
|
+
return new Error(`orez-e1 schema ${manifest.schemaID}: ${message}`);
|
|
671
|
+
}
|
|
672
|
+
function cellError(manifest, table, column, epoch, mutationTag, message) {
|
|
673
|
+
const epochDetail = epoch === undefined ? '' : ` epoch ${epoch}`;
|
|
674
|
+
return codecError(manifest, `${table.logicalName}.${column.logicalName}${epochDetail} mutation ${mutationTag.slice(0, 8)}: ${message}`);
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=encrypted-column-codec.js.map
|