@noy-db/hub 0.1.0-pre.9 → 0.2.0-pre.10
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/aggregate/index.cjs +100 -36
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +2 -2
- package/dist/aggregate/index.d.ts +2 -2
- package/dist/aggregate/index.js +16 -9
- package/dist/aggregate/index.js.map +1 -1
- package/dist/attestation/index.cjs +305 -0
- package/dist/attestation/index.cjs.map +1 -0
- package/dist/attestation/index.d.cts +52 -0
- package/dist/attestation/index.d.ts +52 -0
- package/dist/attestation/index.js +36 -0
- package/dist/attestation/index.js.map +1 -0
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +7 -6
- package/dist/blobs/index.d.ts +7 -6
- package/dist/blobs/index.js +10 -8
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.cjs +19121 -60
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +175 -6
- package/dist/bundle/index.d.ts +175 -6
- package/dist/bundle/index.js +543 -4
- package/dist/bundle/index.js.map +1 -1
- package/dist/chunk-26NK23DZ.js +296 -0
- package/dist/chunk-26NK23DZ.js.map +1 -0
- package/dist/{chunk-TDR6T5CJ.js → chunk-2LPPNWF6.js} +91 -132
- package/dist/chunk-2LPPNWF6.js.map +1 -0
- package/dist/{chunk-PTVMYYON.js → chunk-2N62W5YP.js} +3 -3
- package/dist/{chunk-QGZRWRSL.js → chunk-3LPV6BXR.js} +4 -4
- package/dist/{chunk-QAVUREFT.js → chunk-4CLICFEY.js} +12 -6
- package/dist/chunk-4CLICFEY.js.map +1 -0
- package/dist/chunk-4USCAEDT.js +10529 -0
- package/dist/chunk-4USCAEDT.js.map +1 -0
- package/dist/chunk-5IXJGFF2.js +83 -0
- package/dist/chunk-5IXJGFF2.js.map +1 -0
- package/dist/chunk-5OEJ6GOT.js +124 -0
- package/dist/chunk-5OEJ6GOT.js.map +1 -0
- package/dist/{chunk-4PWAI7Q4.js → chunk-5OX6XVNS.js} +5 -5
- package/dist/{chunk-2CSJGFCB.js → chunk-6EOXTJS2.js} +6 -229
- package/dist/chunk-6EOXTJS2.js.map +1 -0
- package/dist/chunk-6T2UDBKG.js +53 -0
- package/dist/chunk-6T2UDBKG.js.map +1 -0
- package/dist/{chunk-GOUT6DND.js → chunk-6YLPHBKR.js} +382 -95
- package/dist/chunk-6YLPHBKR.js.map +1 -0
- package/dist/chunk-7CEGU63S.js +179 -0
- package/dist/chunk-7CEGU63S.js.map +1 -0
- package/dist/chunk-A3JMGXPG.js +125 -0
- package/dist/chunk-A3JMGXPG.js.map +1 -0
- package/dist/chunk-BB27JMWB.js +795 -0
- package/dist/chunk-BB27JMWB.js.map +1 -0
- package/dist/{chunk-SCZXXXU4.js → chunk-BDV7INMP.js} +7 -32
- package/dist/chunk-BDV7INMP.js.map +1 -0
- package/dist/chunk-C3WE6UJY.js +19 -0
- package/dist/chunk-C3WE6UJY.js.map +1 -0
- package/dist/chunk-CH22FZHT.js +96 -0
- package/dist/chunk-CH22FZHT.js.map +1 -0
- package/dist/chunk-CXFOITNS.js +34 -0
- package/dist/chunk-CXFOITNS.js.map +1 -0
- package/dist/chunk-CXJG63MA.js +109 -0
- package/dist/chunk-CXJG63MA.js.map +1 -0
- package/dist/chunk-DAP2XL7Q.js +51 -0
- package/dist/chunk-DAP2XL7Q.js.map +1 -0
- package/dist/{chunk-AVVPZ4BC.js → chunk-DJRWA3Q5.js} +4 -4
- package/dist/chunk-DRXIZOFV.js +233 -0
- package/dist/chunk-DRXIZOFV.js.map +1 -0
- package/dist/chunk-FO3UEG4S.js +313 -0
- package/dist/chunk-FO3UEG4S.js.map +1 -0
- package/dist/chunk-GAUEWM7D.js +147 -0
- package/dist/chunk-GAUEWM7D.js.map +1 -0
- package/dist/{chunk-MDDTIZUO.js → chunk-GNHAC43Q.js} +218 -119
- package/dist/chunk-GNHAC43Q.js.map +1 -0
- package/dist/chunk-HHOO7HGH.js +57 -0
- package/dist/chunk-HHOO7HGH.js.map +1 -0
- package/dist/{chunk-WDM5XGGS.js → chunk-HQSQC2XL.js} +182 -12
- package/dist/chunk-HQSQC2XL.js.map +1 -0
- package/dist/chunk-IMYKDWB4.js +139 -0
- package/dist/chunk-IMYKDWB4.js.map +1 -0
- package/dist/{chunk-M62XNWRA.js → chunk-LSTBFLL2.js} +2 -2
- package/dist/{chunk-ACLDOTNQ.js → chunk-O6EJ6WTI.js} +436 -3
- package/dist/chunk-O6EJ6WTI.js.map +1 -0
- package/dist/chunk-PC6ZEDRL.js +71 -0
- package/dist/chunk-PC6ZEDRL.js.map +1 -0
- package/dist/chunk-PM3QYWUU.js +251 -0
- package/dist/chunk-PM3QYWUU.js.map +1 -0
- package/dist/chunk-PVUUIWHY.js +73 -0
- package/dist/chunk-PVUUIWHY.js.map +1 -0
- package/dist/chunk-PXTQPZO4.js +830 -0
- package/dist/chunk-PXTQPZO4.js.map +1 -0
- package/dist/{chunk-ZFKD4QMV.js → chunk-QSOYKKMD.js} +4 -4
- package/dist/chunk-QSOYKKMD.js.map +1 -0
- package/dist/{chunk-MR4424N3.js → chunk-R233SLY3.js} +2 -2
- package/dist/chunk-RC6SU5NO.js +36 -0
- package/dist/chunk-RC6SU5NO.js.map +1 -0
- package/dist/{chunk-USKYUS74.js → chunk-RRNA5GKT.js} +2 -2
- package/dist/{chunk-R36SIKES.js → chunk-RYIL3PI2.js} +2 -2
- package/dist/chunk-STNPB3UM.js +9 -0
- package/dist/chunk-STNPB3UM.js.map +1 -0
- package/dist/{chunk-M5INGEFC.js → chunk-TV3YZ35S.js} +7 -1
- package/dist/chunk-TV3YZ35S.js.map +1 -0
- package/dist/chunk-TY32C732.js +59 -0
- package/dist/chunk-TY32C732.js.map +1 -0
- package/dist/chunk-UMLVJTYV.js +20 -0
- package/dist/chunk-UMLVJTYV.js.map +1 -0
- package/dist/{chunk-NPC4LFV5.js → chunk-WIBHRONM.js} +2 -2
- package/dist/chunk-WIBHRONM.js.map +1 -0
- package/dist/{chunk-RKJ6OL7K.js → chunk-WIRRPTFH.js} +1 -1
- package/dist/chunk-WIRRPTFH.js.map +1 -0
- package/dist/{chunk-VQBTTTUN.js → chunk-Y26YV5R3.js} +4 -4
- package/dist/{chunk-VQBTTTUN.js.map → chunk-Y26YV5R3.js.map} +1 -1
- package/dist/{chunk-NXFEYLVG.js → chunk-YM7LFCG7.js} +5 -4
- package/dist/{chunk-NXFEYLVG.js.map → chunk-YM7LFCG7.js.map} +1 -1
- package/dist/{chunk-CIMZBAZB.js → chunk-Z6FNBOTC.js} +1 -1
- package/dist/chunk-Z6FNBOTC.js.map +1 -0
- package/dist/chunk-ZROPXHJY.js +82 -0
- package/dist/chunk-ZROPXHJY.js.map +1 -0
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +7 -6
- package/dist/consent/index.d.ts +7 -6
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-IVKU7YTT.js → crypto-2CRLG4F4.js} +3 -3
- package/dist/{delegation-2DBS2EOH.js → delegation-ZTRT2PRV.js} +5 -4
- package/dist/derivations/index.cjs +368 -0
- package/dist/derivations/index.cjs.map +1 -0
- package/dist/derivations/index.d.cts +72 -0
- package/dist/derivations/index.d.ts +72 -0
- package/dist/derivations/index.js +27 -0
- package/dist/{dev-unlock-Da1B0TIK.d.cts → dev-unlock-AglVnkPY.d.cts} +1 -1
- package/dist/{dev-unlock-BdPp68qn.d.ts → dev-unlock-BOEYl1xl.d.ts} +1 -1
- package/dist/discriminant-BN9REW3o.d.cts +60 -0
- package/dist/discriminant-BN9REW3o.d.ts +60 -0
- package/dist/executor-S76VN45G.js +8 -0
- package/dist/executor-UCXLIGLW.js +11 -0
- package/dist/executor-UCXLIGLW.js.map +1 -0
- package/dist/executor-ZCNZJMGR.js +8 -0
- package/dist/executor-ZCNZJMGR.js.map +1 -0
- package/dist/fanout-sidecar-OKPMMPLG.js +51 -0
- package/dist/fanout-sidecar-OKPMMPLG.js.map +1 -0
- package/dist/guards/index.cjs +322 -0
- package/dist/guards/index.cjs.map +1 -0
- package/dist/guards/index.d.cts +31 -0
- package/dist/guards/index.d.ts +31 -0
- package/dist/guards/index.js +29 -0
- package/dist/guards/index.js.map +1 -0
- package/dist/{hash-lsoL3eEW.d.ts → hash-B9m3_fhj.d.ts} +1 -1
- package/dist/{hash-BEfzPKwo.d.cts → hash-RVqz2zi8.d.cts} +1 -1
- package/dist/history/index.cjs +9 -2
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +8 -7
- package/dist/history/index.d.ts +8 -7
- package/dist/history/index.js +6 -6
- package/dist/i18n/index.cjs +368 -27
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +7 -6
- package/dist/i18n/index.d.ts +7 -6
- package/dist/i18n/index.js +34 -6
- package/dist/i18n/index.js.map +1 -1
- package/dist/{index-DJTf9yxn.d.ts → index-B8bjExET.d.cts} +508 -14
- package/dist/{index-6xNpPsxR.d.cts → index-DfUbNad8.d.ts} +508 -14
- package/dist/index.cjs +8779 -1260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -19
- package/dist/index.d.ts +231 -19
- package/dist/index.js +311 -7370
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs +7 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.d.cts +3 -3
- package/dist/indexing/index.d.ts +3 -3
- package/dist/indexing/index.js +4 -4
- package/dist/issue-3W6IVLKH.js +12 -0
- package/dist/issue-3W6IVLKH.js.map +1 -0
- package/dist/{lazy-builder-BwEoBQZ9.d.ts → lazy-builder-Ci5_YG73.d.cts} +2 -2
- package/dist/{lazy-builder-CZVLKh0Z.d.cts → lazy-builder-D5GU14TS.d.ts} +2 -2
- package/dist/{ledger-QZTTHQAQ.js → ledger-O7FXOG3D.js} +6 -6
- package/dist/ledger-O7FXOG3D.js.map +1 -0
- package/dist/materialized-views/index.cjs +856 -0
- package/dist/materialized-views/index.cjs.map +1 -0
- package/dist/materialized-views/index.d.cts +186 -0
- package/dist/materialized-views/index.d.ts +186 -0
- package/dist/materialized-views/index.js +45 -0
- package/dist/materialized-views/index.js.map +1 -0
- package/dist/noydb-YAZNH5TI.js +34 -0
- package/dist/noydb-YAZNH5TI.js.map +1 -0
- package/dist/overlay-views/index.cjs +369 -0
- package/dist/overlay-views/index.cjs.map +1 -0
- package/dist/overlay-views/index.d.cts +82 -0
- package/dist/overlay-views/index.d.ts +82 -0
- package/dist/overlay-views/index.js +25 -0
- package/dist/overlay-views/index.js.map +1 -0
- package/dist/periods/index.cjs +7 -1
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +7 -6
- package/dist/periods/index.d.ts +7 -6
- package/dist/periods/index.js +6 -6
- package/dist/{predicate-SBHmi6D0.d.cts → predicate-Bt5ft-9c.d.cts} +51 -2
- package/dist/{predicate-SBHmi6D0.d.ts → predicate-Bt5ft-9c.d.ts} +51 -2
- package/dist/{public-envelope-6JTACYJV.js → public-envelope-HMYHZIRH.js} +4 -4
- package/dist/public-envelope-HMYHZIRH.js.map +1 -0
- package/dist/query/index.cjs +555 -128
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +3 -3
- package/dist/query/index.d.ts +3 -3
- package/dist/query/index.js +32 -11
- package/dist/read-only-facade-ITU6L7BL.js +7 -0
- package/dist/read-only-facade-ITU6L7BL.js.map +1 -0
- package/dist/registry-DKEXOJVO.js +7 -0
- package/dist/registry-DKEXOJVO.js.map +1 -0
- package/dist/registry-ST2VNFZC.js +10 -0
- package/dist/registry-ST2VNFZC.js.map +1 -0
- package/dist/registry-UFIK7CSR.js +8 -0
- package/dist/registry-UFIK7CSR.js.map +1 -0
- package/dist/registry-ZGYYSM5I.js +8 -0
- package/dist/registry-ZGYYSM5I.js.map +1 -0
- package/dist/revoke-S6JMSLUN.js +17 -0
- package/dist/revoke-S6JMSLUN.js.map +1 -0
- package/dist/session/index.cjs +7 -1
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +8 -7
- package/dist/session/index.d.ts +8 -7
- package/dist/session/index.js +10 -3
- package/dist/session/index.js.map +1 -1
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +7 -6
- package/dist/shadow/index.d.ts +7 -6
- package/dist/shadow/index.js +2 -2
- package/dist/signer-7NPTB3SQ.js +18 -0
- package/dist/signer-7NPTB3SQ.js.map +1 -0
- package/dist/snapshots/index.cjs +937 -0
- package/dist/snapshots/index.cjs.map +1 -0
- package/dist/snapshots/index.d.cts +28 -0
- package/dist/snapshots/index.d.ts +28 -0
- package/dist/snapshots/index.js +152 -0
- package/dist/snapshots/index.js.map +1 -0
- package/dist/stale-VKXSXJF4.js +13 -0
- package/dist/stale-VKXSXJF4.js.map +1 -0
- package/dist/store/index.cjs +14 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +7 -6
- package/dist/store/index.d.ts +7 -6
- package/dist/store/index.js +5 -2
- package/dist/{strategy-D-SrOLCl.d.ts → strategy-CT2LCKAX.d.cts} +84 -19
- package/dist/{strategy-D-SrOLCl.d.cts → strategy-CT2LCKAX.d.ts} +84 -19
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +6 -5
- package/dist/sync/index.d.ts +6 -5
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs +1554 -2
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +7 -6
- package/dist/team/index.d.ts +7 -6
- package/dist/team/index.js +77 -8
- package/dist/tx/index.cjs +375 -43
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +8 -7
- package/dist/tx/index.d.ts +8 -7
- package/dist/tx/index.js +56 -3
- package/dist/tx/index.js.map +1 -1
- package/dist/{types-Bo7NSXJr.d.ts → types-CaNQm4i8.d.ts} +3902 -614
- package/dist/{types-Bnb82f5R.d.cts → types-n2_IfwlQ.d.cts} +3902 -614
- package/dist/{index-CywCC1qZ.d.cts → ulid-B9SMWj5i.d.ts} +216 -27
- package/dist/{index-8QDuznDr.d.ts → ulid-CLMjmyhG.d.cts} +216 -27
- package/dist/util/index.cjs +7 -0
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.d.cts +2 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/index.js +5 -1
- package/dist/util/index.js.map +1 -1
- package/dist/with-derivation-CVIOPTUf.d.ts +13 -0
- package/dist/with-derivation-aKrtS7Jj.d.cts +13 -0
- package/dist/with-guard-DZQbPzoP.d.cts +18 -0
- package/dist/with-guard-DseETUrF.d.ts +18 -0
- package/dist/with-materialized-view-C1eA1_T_.d.cts +27 -0
- package/dist/with-materialized-view-DaYaE8-Q.d.ts +27 -0
- package/dist/with-overlayed-view-DQsh2p8H.d.ts +13 -0
- package/dist/with-overlayed-view-DleJfKcV.d.cts +13 -0
- package/package.json +77 -3
- package/dist/chunk-2CSJGFCB.js.map +0 -1
- package/dist/chunk-ACLDOTNQ.js.map +0 -1
- package/dist/chunk-BTDCBVJW.js +0 -160
- package/dist/chunk-BTDCBVJW.js.map +0 -1
- package/dist/chunk-CIMZBAZB.js.map +0 -1
- package/dist/chunk-EXHNQEV4.js +0 -392
- package/dist/chunk-EXHNQEV4.js.map +0 -1
- package/dist/chunk-GOUT6DND.js.map +0 -1
- package/dist/chunk-M5INGEFC.js.map +0 -1
- package/dist/chunk-MDDTIZUO.js.map +0 -1
- package/dist/chunk-NPC4LFV5.js.map +0 -1
- package/dist/chunk-QAVUREFT.js.map +0 -1
- package/dist/chunk-RKJ6OL7K.js.map +0 -1
- package/dist/chunk-SCZXXXU4.js.map +0 -1
- package/dist/chunk-TDR6T5CJ.js.map +0 -1
- package/dist/chunk-WDM5XGGS.js.map +0 -1
- package/dist/chunk-ZFKD4QMV.js.map +0 -1
- /package/dist/{chunk-PTVMYYON.js.map → chunk-2N62W5YP.js.map} +0 -0
- /package/dist/{chunk-QGZRWRSL.js.map → chunk-3LPV6BXR.js.map} +0 -0
- /package/dist/{chunk-4PWAI7Q4.js.map → chunk-5OX6XVNS.js.map} +0 -0
- /package/dist/{chunk-AVVPZ4BC.js.map → chunk-DJRWA3Q5.js.map} +0 -0
- /package/dist/{chunk-M62XNWRA.js.map → chunk-LSTBFLL2.js.map} +0 -0
- /package/dist/{chunk-MR4424N3.js.map → chunk-R233SLY3.js.map} +0 -0
- /package/dist/{chunk-USKYUS74.js.map → chunk-RRNA5GKT.js.map} +0 -0
- /package/dist/{chunk-R36SIKES.js.map → chunk-RYIL3PI2.js.map} +0 -0
- /package/dist/{crypto-IVKU7YTT.js.map → crypto-2CRLG4F4.js.map} +0 -0
- /package/dist/{delegation-2DBS2EOH.js.map → delegation-ZTRT2PRV.js.map} +0 -0
- /package/dist/{ledger-QZTTHQAQ.js.map → derivations/index.js.map} +0 -0
- /package/dist/{public-envelope-6JTACYJV.js.map → executor-S76VN45G.js.map} +0 -0
package/dist/chunk-BTDCBVJW.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ConflictError
|
|
3
|
-
} from "./chunk-ACLDOTNQ.js";
|
|
4
|
-
|
|
5
|
-
// src/tx/transaction.ts
|
|
6
|
-
var TxContext = class {
|
|
7
|
-
/** @internal */
|
|
8
|
-
_ops = [];
|
|
9
|
-
/** @internal */
|
|
10
|
-
_db;
|
|
11
|
-
/** @internal */
|
|
12
|
-
constructor(db) {
|
|
13
|
-
this._db = db;
|
|
14
|
-
}
|
|
15
|
-
/** Scope subsequent `collection()` calls to the named vault. */
|
|
16
|
-
vault(name) {
|
|
17
|
-
const v = this._db.vault(name);
|
|
18
|
-
return new TxVault(this, v);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
var TxVault = class {
|
|
22
|
-
/** @internal */
|
|
23
|
-
_ctx;
|
|
24
|
-
/** @internal */
|
|
25
|
-
_vault;
|
|
26
|
-
/** @internal */
|
|
27
|
-
constructor(ctx, vault) {
|
|
28
|
-
this._ctx = ctx;
|
|
29
|
-
this._vault = vault;
|
|
30
|
-
}
|
|
31
|
-
/** Scope subsequent op calls to the named collection. */
|
|
32
|
-
collection(name) {
|
|
33
|
-
const c = this._vault.collection(name);
|
|
34
|
-
return new TxCollection(this._ctx, this._vault, c, name);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
var TxCollection = class {
|
|
38
|
-
/** @internal */
|
|
39
|
-
_ctx;
|
|
40
|
-
/** @internal */
|
|
41
|
-
_vault;
|
|
42
|
-
/** @internal */
|
|
43
|
-
_coll;
|
|
44
|
-
/** @internal */
|
|
45
|
-
_name;
|
|
46
|
-
/** @internal */
|
|
47
|
-
constructor(ctx, vault, coll, name) {
|
|
48
|
-
this._ctx = ctx;
|
|
49
|
-
this._vault = vault;
|
|
50
|
-
this._coll = coll;
|
|
51
|
-
this._name = name;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Read the current committed value, or the most-recently-staged
|
|
55
|
-
* value from the same transaction if one exists.
|
|
56
|
-
*/
|
|
57
|
-
async get(id) {
|
|
58
|
-
for (let i = this._ctx._ops.length - 1; i >= 0; i--) {
|
|
59
|
-
const op = this._ctx._ops[i];
|
|
60
|
-
if (op.vaultName === this._vault.name && op.collectionName === this._name && op.id === id) {
|
|
61
|
-
if (op.type === "delete") return null;
|
|
62
|
-
return op.record;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return this._coll.get(id);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Stage a put. Does not write until the transaction body returns.
|
|
69
|
-
* Supply `{ expectedVersion }` to enforce optimistic concurrency
|
|
70
|
-
* during the commit pre-flight.
|
|
71
|
-
*/
|
|
72
|
-
put(id, record, options) {
|
|
73
|
-
const op = {
|
|
74
|
-
type: "put",
|
|
75
|
-
vaultName: this._vault.name,
|
|
76
|
-
collectionName: this._name,
|
|
77
|
-
id,
|
|
78
|
-
record
|
|
79
|
-
};
|
|
80
|
-
if (options?.expectedVersion !== void 0) op.expectedVersion = options.expectedVersion;
|
|
81
|
-
this._ctx._ops.push(op);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Stage a delete. Does not write until the transaction body returns.
|
|
85
|
-
* Supply `{ expectedVersion }` to enforce optimistic concurrency
|
|
86
|
-
* during the commit pre-flight.
|
|
87
|
-
*/
|
|
88
|
-
delete(id, options) {
|
|
89
|
-
const op = {
|
|
90
|
-
type: "delete",
|
|
91
|
-
vaultName: this._vault.name,
|
|
92
|
-
collectionName: this._name,
|
|
93
|
-
id
|
|
94
|
-
};
|
|
95
|
-
if (options?.expectedVersion !== void 0) op.expectedVersion = options.expectedVersion;
|
|
96
|
-
this._ctx._ops.push(op);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
async function runTransaction(db, fn) {
|
|
100
|
-
const ctx = new TxContext(db);
|
|
101
|
-
const bodyResult = await fn(ctx);
|
|
102
|
-
if (ctx._ops.length === 0) return bodyResult;
|
|
103
|
-
const priorEnvelopes = /* @__PURE__ */ new Map();
|
|
104
|
-
const store = db._store;
|
|
105
|
-
for (const op of ctx._ops) {
|
|
106
|
-
const key = keyOf(op);
|
|
107
|
-
if (!priorEnvelopes.has(key)) {
|
|
108
|
-
const env = await store.get(op.vaultName, op.collectionName, op.id);
|
|
109
|
-
priorEnvelopes.set(key, env);
|
|
110
|
-
}
|
|
111
|
-
if (op.expectedVersion !== void 0) {
|
|
112
|
-
const env = priorEnvelopes.get(key) ?? null;
|
|
113
|
-
const actual = env?._v ?? 0;
|
|
114
|
-
if (actual !== op.expectedVersion) {
|
|
115
|
-
throw new ConflictError(
|
|
116
|
-
actual,
|
|
117
|
-
`Transaction pre-flight: ${op.vaultName}/${op.collectionName}/${op.id} expected v${op.expectedVersion}, found v${actual}`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const executed = [];
|
|
123
|
-
try {
|
|
124
|
-
for (const op of ctx._ops) {
|
|
125
|
-
const coll = db.vault(op.vaultName).collection(op.collectionName);
|
|
126
|
-
const key = keyOf(op);
|
|
127
|
-
const prior = priorEnvelopes.get(key) ?? null;
|
|
128
|
-
if (op.type === "put") {
|
|
129
|
-
await coll.put(op.id, op.record);
|
|
130
|
-
} else {
|
|
131
|
-
await coll.delete(op.id);
|
|
132
|
-
}
|
|
133
|
-
executed.push({ op, priorEnvelope: prior });
|
|
134
|
-
}
|
|
135
|
-
return bodyResult;
|
|
136
|
-
} catch (err) {
|
|
137
|
-
for (const { op, priorEnvelope } of executed.slice().reverse()) {
|
|
138
|
-
try {
|
|
139
|
-
if (priorEnvelope) {
|
|
140
|
-
await store.put(op.vaultName, op.collectionName, op.id, priorEnvelope);
|
|
141
|
-
} else {
|
|
142
|
-
await store.delete(op.vaultName, op.collectionName, op.id);
|
|
143
|
-
}
|
|
144
|
-
} catch {
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function keyOf(op) {
|
|
151
|
-
return `${op.vaultName}\0${op.collectionName}\0${op.id}`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export {
|
|
155
|
-
TxContext,
|
|
156
|
-
TxVault,
|
|
157
|
-
TxCollection,
|
|
158
|
-
runTransaction
|
|
159
|
-
};
|
|
160
|
-
//# sourceMappingURL=chunk-BTDCBVJW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tx/transaction.ts"],"sourcesContent":["/**\n * Multi-record atomic transactions.\n *\n * Lets an application stage writes across two or more collections (or\n * vaults) and commit them all-or-nothing.\n *\n * ```ts\n * await db.transaction(async (tx) => {\n * const inv = tx.vault('acme').collection<Invoice>('invoices')\n * const pay = tx.vault('acme').collection<Payment>('payments')\n * await inv.put(invoiceId, { ...invoice, status: 'paid' })\n * await pay.put(paymentId, { invoiceId, amount, paidAt })\n * })\n * // If the body throws before returning: nothing persisted.\n * // If the body returns: all puts committed; any CAS mismatch rolls\n * // the batch back and surfaces as ConflictError.\n * ```\n *\n * ## Atomicity semantics\n *\n * Ops are buffered during the body. On body-return the hub:\n *\n * 1. **Pre-flight** — re-reads every touched envelope and enforces\n * any caller-supplied `expectedVersion`. A mismatch throws\n * `ConflictError` with *no* writes performed.\n * 2. **Execute** — calls `Collection.put()` / `.delete()` for each\n * staged op in declaration order. History snapshots, ledger\n * appends, and change events fire as normal per op.\n * 3. **Unwind on failure** — if step 2 throws mid-batch, each\n * already-committed op is reverted via the raw store (restoring\n * the captured prior envelope, or deleting if none existed). The\n * ledger is NOT rewritten — audit history preserves the partial\n * commit and the revert.\n *\n * **Crash window.** Steps 2–3 are not a storage-layer transaction —\n * if the process dies between two executed ops, the on-disk state is\n * partial. True all-or-nothing atomicity requires a store that\n * implements `NoydbStore.tx()` (DynamoDB `TransactWriteItems`,\n * IndexedDB `readwrite` transaction, …). This executor declares\n * that future integration point via the `tx?()` method + the\n * `StoreCapabilities.txAtomic` bit, but does not yet delegate\n * to it — the cascade into `Fork · Stores` tracks the per-adapter\n * wire-up.\n *\n * ## Not covered\n *\n * - Cross-sync-peer atomicity. Transactions commit against the\n * primary store only; the sync engine pushes on its normal\n * schedule. For cross-peer two-phase commit use `SyncTransaction`\n * via `db.transaction(vaultName)`.\n * - Read-your-writes within the body. `tx.collection().get(id)`\n * returns the most-recently-staged value for that id when one\n * exists; if no staged op has touched the id, it reads the current\n * committed state. Version numbers returned by `get` reflect the\n * pre-transaction state (staged puts have no version yet).\n *\n * @module\n */\n\nimport type { Noydb } from '../noydb.js'\nimport type { Vault } from '../vault.js'\nimport type { Collection } from '../collection.js'\nimport type { EncryptedEnvelope } from '../types.js'\nimport { ConflictError } from '../errors.js'\n\n/** One op buffered inside a running `TxContext`. @internal */\ninterface StagedOp {\n type: 'put' | 'delete'\n vaultName: string\n collectionName: string\n id: string\n record?: unknown\n expectedVersion?: number\n}\n\n/**\n * Transaction handle passed to the user's body. Use\n * `tx.vault(name).collection<T>(name)` to get a per-collection\n * facade; its `put`/`delete`/`get` calls stage ops against the tx.\n */\nexport class TxContext {\n /** @internal */\n readonly _ops: StagedOp[] = []\n /** @internal */\n readonly _db: Noydb\n\n /** @internal */\n constructor(db: Noydb) {\n this._db = db\n }\n\n /** Scope subsequent `collection()` calls to the named vault. */\n vault(name: string): TxVault {\n const v = this._db.vault(name)\n return new TxVault(this, v)\n }\n}\n\n/** Per-vault facade inside a running transaction. */\nexport class TxVault {\n /** @internal */\n readonly _ctx: TxContext\n /** @internal */\n readonly _vault: Vault\n\n /** @internal */\n constructor(ctx: TxContext, vault: Vault) {\n this._ctx = ctx\n this._vault = vault\n }\n\n /** Scope subsequent op calls to the named collection. */\n collection<T>(name: string): TxCollection<T> {\n const c = this._vault.collection<T>(name)\n return new TxCollection<T>(this._ctx, this._vault, c, name)\n }\n}\n\n/** Per-collection facade inside a running transaction. */\nexport class TxCollection<T> {\n /** @internal */\n readonly _ctx: TxContext\n /** @internal */\n readonly _vault: Vault\n /** @internal */\n readonly _coll: Collection<T>\n /** @internal */\n readonly _name: string\n\n /** @internal */\n constructor(ctx: TxContext, vault: Vault, coll: Collection<T>, name: string) {\n this._ctx = ctx\n this._vault = vault\n this._coll = coll\n this._name = name\n }\n\n /**\n * Read the current committed value, or the most-recently-staged\n * value from the same transaction if one exists.\n */\n async get(id: string): Promise<T | null> {\n for (let i = this._ctx._ops.length - 1; i >= 0; i--) {\n const op = this._ctx._ops[i]!\n if (\n op.vaultName === this._vault.name &&\n op.collectionName === this._name &&\n op.id === id\n ) {\n if (op.type === 'delete') return null\n return op.record as T\n }\n }\n return this._coll.get(id)\n }\n\n /**\n * Stage a put. Does not write until the transaction body returns.\n * Supply `{ expectedVersion }` to enforce optimistic concurrency\n * during the commit pre-flight.\n */\n put(id: string, record: T, options?: { expectedVersion?: number }): void {\n const op: StagedOp = {\n type: 'put',\n vaultName: this._vault.name,\n collectionName: this._name,\n id,\n record,\n }\n if (options?.expectedVersion !== undefined) op.expectedVersion = options.expectedVersion\n this._ctx._ops.push(op)\n }\n\n /**\n * Stage a delete. Does not write until the transaction body returns.\n * Supply `{ expectedVersion }` to enforce optimistic concurrency\n * during the commit pre-flight.\n */\n delete(id: string, options?: { expectedVersion?: number }): void {\n const op: StagedOp = {\n type: 'delete',\n vaultName: this._vault.name,\n collectionName: this._name,\n id,\n }\n if (options?.expectedVersion !== undefined) op.expectedVersion = options.expectedVersion\n this._ctx._ops.push(op)\n }\n}\n\n/**\n * Commit plan: pre-flight check + execution + revert plan. Returned\n * from `runTransaction`.\n *\n * @internal — exposed only for the `Collection.putMany({atomic:true})`\n * wire-up so the bulk path can share the executor without creating\n * an outer TxContext.\n */\nexport async function runTransaction<T>(\n db: Noydb,\n fn: (tx: TxContext) => Promise<T> | T,\n): Promise<T> {\n const ctx = new TxContext(db)\n const bodyResult = await fn(ctx)\n\n if (ctx._ops.length === 0) return bodyResult\n\n // Phase 1 — pre-flight: snapshot every touched envelope and enforce\n // any caller-supplied expectedVersion. Same (vault, coll, id) touched\n // more than once in one tx snapshots only the *initial* committed\n // state; the in-order replay in Phase 2 takes care of successor ops.\n const priorEnvelopes = new Map<string, EncryptedEnvelope | null>()\n const store = db._store\n for (const op of ctx._ops) {\n const key = keyOf(op)\n if (!priorEnvelopes.has(key)) {\n const env = await store.get(op.vaultName, op.collectionName, op.id)\n priorEnvelopes.set(key, env)\n }\n if (op.expectedVersion !== undefined) {\n const env = priorEnvelopes.get(key) ?? null\n const actual = env?._v ?? 0\n if (actual !== op.expectedVersion) {\n throw new ConflictError(\n actual,\n `Transaction pre-flight: ${op.vaultName}/${op.collectionName}/${op.id} ` +\n `expected v${op.expectedVersion}, found v${actual}`,\n )\n }\n }\n }\n\n // Phase 2 — execute via the Collection layer so history snapshots,\n // ledger entries, and change events fire normally. We capture each\n // successful op so a mid-batch throw can revert in Phase 3.\n const executed: Array<{ op: StagedOp; priorEnvelope: EncryptedEnvelope | null }> = []\n try {\n for (const op of ctx._ops) {\n const coll = db.vault(op.vaultName).collection(op.collectionName)\n const key = keyOf(op)\n const prior = priorEnvelopes.get(key) ?? null\n if (op.type === 'put') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await coll.put(op.id, op.record as any)\n } else {\n await coll.delete(op.id)\n }\n executed.push({ op, priorEnvelope: prior })\n }\n return bodyResult\n } catch (err) {\n // Phase 3 — best-effort revert. Restore captured prior envelopes\n // via the raw store to avoid re-firing Collection-level side\n // effects (we don't want a cascade of change events undoing\n // themselves). The ledger is left as-is: each committed op\n // appended an entry; the revert is deliberately not recorded as a\n // compensating entry because 's contract is \"atomic or not at\n // all\" from the caller's view, not \"every write visible in the\n // audit trail.\" Auditors who need the intermediate state can still\n // reconstruct it by walking the ledger through the failed-tx\n // timestamp.\n for (const { op, priorEnvelope } of executed.slice().reverse()) {\n try {\n if (priorEnvelope) {\n await store.put(op.vaultName, op.collectionName, op.id, priorEnvelope)\n } else {\n await store.delete(op.vaultName, op.collectionName, op.id)\n }\n } catch {\n // swallow — best-effort. Surfacing the revert error would\n // mask the original one that triggered the rollback.\n }\n }\n throw err\n }\n}\n\nfunction keyOf(op: StagedOp): string {\n return `${op.vaultName}\\x00${op.collectionName}\\x00${op.id}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/history/ledger/entry.ts","../src/history/ledger/hash.ts"],"sourcesContent":["/**\n * Ledger entry shape + canonical JSON + sha256 helpers.\n *\n * This file holds the PURE primitives used by the hash-chained ledger:\n * the entry type, the deterministic (sort-stable) JSON encoder, and\n * the sha256 hasher that produces `prevHash` and `ledger.head()`.\n *\n * Everything here is validator-free and side-effect free — the only\n * runtime dep is Web Crypto's `subtle.digest` for the sha256 call,\n * which we already use for every other hashing operation in the core.\n *\n * The hash chain property works like this:\n *\n * hash(entry[i]) = sha256(canonicalJSON(entry[i]))\n * entry[i+1].prevHash = hash(entry[i])\n *\n * Any modification to `entry[i]` (field values, field order, whitespace)\n * produces a different `hash(entry[i])`, which means `entry[i+1]`'s\n * stored `prevHash` no longer matches the recomputed hash, which means\n * `verify()` returns `{ ok: false, divergedAt: i + 1 }`. The chain is\n * append-only and tamper-evident without external anchoring.\n */\n\n/**\n * A single ledger entry in its plaintext form — what gets serialized,\n * hashed, and then encrypted with the ledger DEK before being written\n * to the `_ledger/` adapter collection.\n *\n * ## Why hash the ciphertext, not the plaintext?\n *\n * `payloadHash` is the sha256 of the record's ENCRYPTED envelope bytes,\n * not its plaintext. This matters:\n *\n * 1. **Zero-knowledge preserved.** A user (or a third party) can\n * verify the ledger against the stored envelopes without any\n * decryption keys. The adapter layer already holds only\n * ciphertext, so hashing the ciphertext keeps the ledger at the\n * same privacy level as the adapter.\n *\n * 2. **Determinism.** Plaintext → ciphertext is randomized by the\n * fresh per-write IV, so `hash(plaintext)` would need extra\n * normalization. `hash(ciphertext)` is already deterministic and\n * unique per write.\n *\n * 3. **Detection property.** If an attacker modifies even one byte of\n * the stored ciphertext (trying to flip a record), the hash\n * changes, the ledger's recorded `payloadHash` no longer matches,\n * and a data-integrity check fails. We don't do that check in\n * `verify()` today, but the\n * hook is there for a future `verifyIntegrity()` follow-up.\n *\n * Fields marked `op`, `collection`, `id`, `version`, `ts`, `actor` are\n * plaintext METADATA about the operation — NOT the record itself. The\n * entry is still encrypted at rest via the ledger DEK, but adapters\n * could theoretically infer operation patterns from the sizes and\n * timestamps. This is an accepted trade-off for the tamper-evidence\n * property; full ORAM-level privacy is out of scope for noy-db.\n */\nexport interface LedgerEntry {\n /**\n * Zero-based sequential position of this entry in the chain. The\n * canonical adapter key is this number zero-padded to 10 digits\n * (`\"0000000001\"`) so lexicographic ordering matches numeric order.\n */\n readonly index: number\n\n /**\n * Hex-encoded sha256 of the canonical JSON of the PREVIOUS entry.\n * The genesis entry (index 0) has `prevHash === ''` — the first\n * entry in a fresh vault has nothing to point back to.\n */\n readonly prevHash: string\n\n /**\n * Which kind of mutation this entry records. only supports\n * data operations (`put`, `delete`). Access-control operations\n * (`grant`, `revoke`, `rotate`) will be added in a follow-up once\n * the keyring write path is instrumented — that's tracked in the\n * epic issue.\n */\n readonly op: 'put' | 'delete'\n\n /** The collection the mutation targeted. */\n readonly collection: string\n\n /** The record id the mutation targeted. */\n readonly id: string\n\n /**\n * The record version AFTER this mutation. For `put` this is the\n * newly assigned version; for `delete` this is the version that\n * was deleted (the last version visible to reads).\n */\n readonly version: number\n\n /** ISO timestamp of the mutation. */\n readonly ts: string\n\n /** User id of the actor who performed the mutation. */\n readonly actor: string\n\n /**\n * Hex-encoded sha256 of the encrypted envelope's `_data` field.\n * For `put`, this is the hash of the new ciphertext. For `delete`,\n * it's the hash of the last visible ciphertext at deletion time,\n * or the empty string if nothing was there to delete. Hashing the\n * ciphertext (not the plaintext) preserves zero-knowledge — see\n * the file docstring.\n */\n readonly payloadHash: string\n\n /**\n * Optional hex-encoded sha256 of the encrypted JSON Patch delta\n * blob stored alongside this entry in `_ledger_deltas/`. Present\n * only for `put` operations that had a previous version — the\n * genesis put of a new record, and every `delete`, leave this\n * field undefined.\n *\n * The delta payload itself lives in a sibling internal collection\n * (`_ledger_deltas/<paddedIndex>`) and is encrypted with the\n * ledger DEK. Callers use `ledger.loadDelta(index)` to decrypt and\n * deserialize it when reconstructing a historical version.\n *\n * Why optional instead of always-present: the first put of a\n * record has no previous version to diff against, so storing an\n * empty patch would be noise. For deletes there's no \"next\" state\n * to describe with a delta. Both cases set this field to undefined.\n *\n * Note: the canonical-JSON hasher treats `undefined` as invalid\n * (it's one of the guard rails), so on the wire this field is\n * either `{ deltaHash: '<hex>' }` or absent from the JSON\n * entirely — never `{ deltaHash: undefined }`.\n */\n readonly deltaHash?: string\n}\n\n/**\n * Canonical (sort-stable) JSON encoder.\n *\n * This function is the load-bearing primitive of the hash chain:\n * `sha256(canonicalJSON(entry))` must produce the same hex string\n * every time, on every machine, for the same logical entry — otherwise\n * `verify()` would return `{ ok: false }` on cross-platform reads.\n *\n * JavaScript's `JSON.stringify` is almost canonical, but NOT quite:\n * it preserves the insertion order of object keys, which means\n * `{a:1,b:2}` and `{b:2,a:1}` serialize differently. We fix this by\n * recursively walking objects and sorting their keys before\n * concatenation.\n *\n * Arrays keep their original order (reordering them would change\n * semantics). Numbers, strings, booleans, and `null` use the default\n * JSON encoding. `undefined` and functions are rejected — ledger\n * entries are plain data, and silently dropping `undefined` would\n * break the \"same input → same hash\" property if a caller forgot to\n * omit a field.\n *\n * Performance: one pass per nesting level; O(n log n) for key sorting\n * at each object. Entries are small (< 1 KB) so this is negligible\n * compared to the sha256 call.\n */\nexport function canonicalJson(value: unknown): string {\n if (value === null) return 'null'\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(\n `canonicalJson: refusing to encode non-finite number ${String(value)}`,\n )\n }\n return JSON.stringify(value)\n }\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'bigint') {\n throw new Error('canonicalJson: BigInt is not JSON-serializable')\n }\n if (typeof value === 'undefined' || typeof value === 'function') {\n throw new Error(\n `canonicalJson: refusing to encode ${typeof value} — include all fields explicitly`,\n )\n }\n if (Array.isArray(value)) {\n return '[' + value.map((v) => canonicalJson(v)).join(',') + ']'\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n const parts: string[] = []\n for (const key of keys) {\n parts.push(JSON.stringify(key) + ':' + canonicalJson(obj[key]))\n }\n return '{' + parts.join(',') + '}'\n }\n throw new Error(`canonicalJson: unexpected value type: ${typeof value}`)\n}\n\n/**\n * Compute a hex-encoded sha256 of a string via Web Crypto's subtle API.\n *\n * We use hex (not base64) for hashes because hex is case-insensitive,\n * fixed-length (64 chars), and easier to compare visually in debug\n * output. Base64 would save a few bytes in storage but every encrypted\n * ledger entry is already much larger than the hash itself.\n */\nexport async function sha256Hex(input: string): Promise<string> {\n const bytes = new TextEncoder().encode(input)\n const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)\n return bytesToHex(new Uint8Array(digest))\n}\n\n/**\n * Compute the canonical hash of a ledger entry. Short wrapper around\n * `canonicalJson` + `sha256Hex`; callers use this instead of composing\n * the two functions every time, so any future change to the hashing\n * pipeline (e.g., adding a domain-separation prefix) lives in one place.\n */\nexport async function hashEntry(entry: LedgerEntry): Promise<string> {\n return sha256Hex(canonicalJson(entry))\n}\n\n/** Convert a Uint8Array to a lowercase hex string. */\nfunction bytesToHex(bytes: Uint8Array): string {\n const hex = new Array<string>(bytes.length)\n for (let i = 0; i < bytes.length; i++) {\n // Non-null assertion: indexing a Uint8Array within bounds always\n // returns a number, but the compiler's noUncheckedIndexedAccess\n // flag widens it to `number | undefined`. Safe here by construction.\n hex[i] = (bytes[i] ?? 0).toString(16).padStart(2, '0')\n }\n return hex.join('')\n}\n\n/**\n * Pad an index to the canonical 10-digit form used as the adapter key.\n * Ten digits is enough for ~10 billion ledger entries per vault\n * — far beyond any realistic use case, but cheap enough that the extra\n * digits don't hurt storage.\n */\nexport function paddedIndex(index: number): string {\n return String(index).padStart(10, '0')\n}\n\n/** Parse a padded adapter key back into a number. Returns NaN on malformed input. */\nexport function parseIndex(key: string): number {\n return Number.parseInt(key, 10)\n}\n","/**\n * Envelope payload hash — pinned in its own leaf module so consumers\n * (DictionaryHandle, the active history strategy) can import it\n * without dragging in the `LedgerStore` class.\n *\n * see `constants.ts` for the broader rationale.\n *\n * @internal\n */\n\nimport type { EncryptedEnvelope } from '../../types.js'\nimport { sha256Hex } from './entry.js'\n\n/**\n * Compute the `payloadHash` value for an encrypted envelope. Used by\n * `LedgerStore.append` for both put (hash the new envelope) and\n * delete (hash the previous envelope) paths, and by\n * `DictionaryHandle` so its ledger entries match the same contract.\n *\n * Returns the empty string when there is no envelope (delete of a\n * never-existed record). The empty string tolerated by the ledger\n * entry's `payloadHash` field as the canonical \"nothing here\" value.\n */\nexport async function envelopePayloadHash(\n envelope: EncryptedEnvelope | null,\n): Promise<string> {\n if (!envelope) return ''\n // `_data` is a base64 string for encrypted envelopes and the raw\n // JSON for plaintext ones. Both are strings, so a single sha256Hex\n // call works for both modes — the hash value differs between\n // encrypted/plaintext compartments because the bytes on disk\n // differ.\n return sha256Hex(envelope._data)\n}\n"],"mappings":";AAiKO,SAAS,cAAc,OAAwB;AACpD,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,SAAS;AACxD,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,uDAAuD,OAAO,KAAK,CAAC;AAAA,MACtE;AAAA,IACF;AACA,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AACA,MAAI,OAAO,UAAU,SAAU,QAAO,KAAK,UAAU,KAAK;AAC1D,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,MAAI,OAAO,UAAU,eAAe,OAAO,UAAU,YAAY;AAC/D,UAAM,IAAI;AAAA,MACR,qCAAqC,OAAO,KAAK;AAAA,IACnD;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,MAAM,IAAI,CAAC,MAAM,cAAc,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI;AAAA,EAC9D;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,MAAM;AACZ,UAAM,OAAO,OAAO,KAAK,GAAG,EAAE,KAAK;AACnC,UAAM,QAAkB,CAAC;AACzB,eAAW,OAAO,MAAM;AACtB,YAAM,KAAK,KAAK,UAAU,GAAG,IAAI,MAAM,cAAc,IAAI,GAAG,CAAC,CAAC;AAAA,IAChE;AACA,WAAO,MAAM,MAAM,KAAK,GAAG,IAAI;AAAA,EACjC;AACA,QAAM,IAAI,MAAM,yCAAyC,OAAO,KAAK,EAAE;AACzE;AAUA,eAAsB,UAAU,OAAgC;AAC9D,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK;AAC5C,QAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,KAAK;AACrE,SAAO,WAAW,IAAI,WAAW,MAAM,CAAC;AAC1C;AAQA,eAAsB,UAAU,OAAqC;AACnE,SAAO,UAAU,cAAc,KAAK,CAAC;AACvC;AAGA,SAAS,WAAW,OAA2B;AAC7C,QAAM,MAAM,IAAI,MAAc,MAAM,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AAIrC,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EACvD;AACA,SAAO,IAAI,KAAK,EAAE;AACpB;AAQO,SAAS,YAAY,OAAuB;AACjD,SAAO,OAAO,KAAK,EAAE,SAAS,IAAI,GAAG;AACvC;AAGO,SAAS,WAAW,KAAqB;AAC9C,SAAO,OAAO,SAAS,KAAK,EAAE;AAChC;;;AC9NA,eAAsB,oBACpB,UACiB;AACjB,MAAI,CAAC,SAAU,QAAO;AAMtB,SAAO,UAAU,SAAS,KAAK;AACjC;","names":[]}
|
package/dist/chunk-EXHNQEV4.js
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
pickLocale
|
|
3
|
-
} from "./chunk-PTVMYYON.js";
|
|
4
|
-
import {
|
|
5
|
-
BundleIntegrityError
|
|
6
|
-
} from "./chunk-ACLDOTNQ.js";
|
|
7
|
-
|
|
8
|
-
// src/bundle/format.ts
|
|
9
|
-
var NOYDB_BUNDLE_MAGIC = new Uint8Array([78, 68, 66, 49]);
|
|
10
|
-
var NOYDB_BUNDLE_PREFIX_BYTES = 10;
|
|
11
|
-
var NOYDB_BUNDLE_FORMAT_VERSION = 1;
|
|
12
|
-
var FLAG_COMPRESSED = 1;
|
|
13
|
-
var FLAG_HAS_INTEGRITY_HASH = 2;
|
|
14
|
-
var COMPRESSION_NONE = 0;
|
|
15
|
-
var COMPRESSION_GZIP = 1;
|
|
16
|
-
var COMPRESSION_BROTLI = 2;
|
|
17
|
-
var ALLOWED_HEADER_KEYS = /* @__PURE__ */ new Set([
|
|
18
|
-
"formatVersion",
|
|
19
|
-
"handle",
|
|
20
|
-
"bodyBytes",
|
|
21
|
-
"bodySha256",
|
|
22
|
-
"publicEnvelope"
|
|
23
|
-
]);
|
|
24
|
-
function validateBundleHeader(parsed) {
|
|
25
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`.noydb bundle header must be a JSON object, got ${parsed === null ? "null" : typeof parsed}`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
for (const key of Object.keys(parsed)) {
|
|
31
|
-
if (!ALLOWED_HEADER_KEYS.has(key)) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`.noydb bundle header contains forbidden key "${key}". Only minimum-disclosure fields are allowed: ${[...ALLOWED_HEADER_KEYS].join(", ")}.`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const h = parsed;
|
|
38
|
-
if (typeof h["formatVersion"] !== "number" || h["formatVersion"] !== NOYDB_BUNDLE_FORMAT_VERSION) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`.noydb bundle header.formatVersion must be ${NOYDB_BUNDLE_FORMAT_VERSION}, got ${String(h["formatVersion"])}. The reader does not support forward-compat versions; upgrade the reader to handle newer bundles.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
if (typeof h["handle"] !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/.test(h["handle"])) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
`.noydb bundle header.handle must be a 26-character Crockford base32 ULID, got ${typeof h["handle"] === "string" ? `"${h["handle"]}"` : String(h["handle"])}.`
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
if (typeof h["bodyBytes"] !== "number" || !Number.isInteger(h["bodyBytes"]) || h["bodyBytes"] < 0) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`.noydb bundle header.bodyBytes must be a non-negative integer, got ${String(h["bodyBytes"])}.`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
if (typeof h["bodySha256"] !== "string" || !/^[0-9a-f]{64}$/.test(h["bodySha256"])) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`.noydb bundle header.bodySha256 must be a 64-character lowercase hex string, got ${typeof h["bodySha256"] === "string" ? `"${h["bodySha256"]}"` : String(h["bodySha256"])}.`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
if (h["publicEnvelope"] !== void 0) {
|
|
59
|
-
const env = h["publicEnvelope"];
|
|
60
|
-
if (env === null || typeof env !== "object" || Array.isArray(env)) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`.noydb bundle header.publicEnvelope must be a JSON object when present, got ${typeof env}.`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const e = env;
|
|
66
|
-
if (e["_noydb_public"] !== 1) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`.noydb bundle header.publicEnvelope._noydb_public must be 1, got ${String(e["_noydb_public"])}.`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
if (typeof e["version"] !== "number" || !Number.isInteger(e["version"]) || e["version"] < 1) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`.noydb bundle header.publicEnvelope.version must be a positive integer, got ${String(e["version"])}.`
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function encodeBundleHeader(header) {
|
|
79
|
-
validateBundleHeader(header);
|
|
80
|
-
const json = JSON.stringify({
|
|
81
|
-
formatVersion: header.formatVersion,
|
|
82
|
-
handle: header.handle,
|
|
83
|
-
bodyBytes: header.bodyBytes,
|
|
84
|
-
bodySha256: header.bodySha256,
|
|
85
|
-
...header.publicEnvelope !== void 0 ? { publicEnvelope: header.publicEnvelope } : {}
|
|
86
|
-
});
|
|
87
|
-
return new TextEncoder().encode(json);
|
|
88
|
-
}
|
|
89
|
-
function decodeBundleHeader(bytes) {
|
|
90
|
-
const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
91
|
-
let parsed;
|
|
92
|
-
try {
|
|
93
|
-
parsed = JSON.parse(json);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`.noydb bundle header is not valid JSON: ${err.message}`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
validateBundleHeader(parsed);
|
|
100
|
-
return parsed;
|
|
101
|
-
}
|
|
102
|
-
function readUint32BE(bytes, offset) {
|
|
103
|
-
return (bytes[offset] << 24 >>> 0) + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3];
|
|
104
|
-
}
|
|
105
|
-
function writeUint32BE(bytes, offset, value) {
|
|
106
|
-
bytes[offset] = value >>> 24 & 255;
|
|
107
|
-
bytes[offset + 1] = value >>> 16 & 255;
|
|
108
|
-
bytes[offset + 2] = value >>> 8 & 255;
|
|
109
|
-
bytes[offset + 3] = value & 255;
|
|
110
|
-
}
|
|
111
|
-
function hasNoydbBundleMagic(bytes) {
|
|
112
|
-
if (bytes.length < NOYDB_BUNDLE_MAGIC.length) return false;
|
|
113
|
-
for (let i = 0; i < NOYDB_BUNDLE_MAGIC.length; i++) {
|
|
114
|
-
if (bytes[i] !== NOYDB_BUNDLE_MAGIC[i]) return false;
|
|
115
|
-
}
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// src/bundle/bundle.ts
|
|
120
|
-
var cachedBrotliSupport = null;
|
|
121
|
-
function supportsBrotliCompression() {
|
|
122
|
-
if (cachedBrotliSupport !== null) return cachedBrotliSupport;
|
|
123
|
-
try {
|
|
124
|
-
new CompressionStream("br");
|
|
125
|
-
cachedBrotliSupport = true;
|
|
126
|
-
} catch {
|
|
127
|
-
cachedBrotliSupport = false;
|
|
128
|
-
}
|
|
129
|
-
return cachedBrotliSupport;
|
|
130
|
-
}
|
|
131
|
-
function resetBrotliSupportCache() {
|
|
132
|
-
cachedBrotliSupport = null;
|
|
133
|
-
}
|
|
134
|
-
function selectCompression(option) {
|
|
135
|
-
const choice = option ?? "auto";
|
|
136
|
-
if (choice === "none") return { format: COMPRESSION_NONE, streamFormat: null };
|
|
137
|
-
if (choice === "gzip") return { format: COMPRESSION_GZIP, streamFormat: "gzip" };
|
|
138
|
-
if (choice === "brotli") {
|
|
139
|
-
if (!supportsBrotliCompression()) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`writeNoydbBundle({ compression: 'brotli' }) is not supported on this runtime. Brotli requires Node 22+, Chrome 124+, or Firefox 122+. Use { compression: 'auto' } to fall back to gzip silently, or { compression: 'gzip' } to be explicit.`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return { format: COMPRESSION_BROTLI, streamFormat: "br" };
|
|
145
|
-
}
|
|
146
|
-
if (supportsBrotliCompression()) {
|
|
147
|
-
return { format: COMPRESSION_BROTLI, streamFormat: "br" };
|
|
148
|
-
}
|
|
149
|
-
return { format: COMPRESSION_GZIP, streamFormat: "gzip" };
|
|
150
|
-
}
|
|
151
|
-
async function pumpThroughStream(input, stream) {
|
|
152
|
-
const readable = new Blob([input]).stream().pipeThrough(stream);
|
|
153
|
-
const reader = readable.getReader();
|
|
154
|
-
const chunks = [];
|
|
155
|
-
let total = 0;
|
|
156
|
-
for (; ; ) {
|
|
157
|
-
const { value, done } = await reader.read();
|
|
158
|
-
if (done) break;
|
|
159
|
-
if (value) {
|
|
160
|
-
chunks.push(value);
|
|
161
|
-
total += value.length;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const out = new Uint8Array(total);
|
|
165
|
-
let offset = 0;
|
|
166
|
-
for (const chunk of chunks) {
|
|
167
|
-
out.set(chunk, offset);
|
|
168
|
-
offset += chunk.length;
|
|
169
|
-
}
|
|
170
|
-
return out;
|
|
171
|
-
}
|
|
172
|
-
async function sha256Hex(bytes) {
|
|
173
|
-
const copy = new Uint8Array(bytes.length);
|
|
174
|
-
copy.set(bytes);
|
|
175
|
-
const digest = await crypto.subtle.digest("SHA-256", copy);
|
|
176
|
-
const view = new Uint8Array(digest);
|
|
177
|
-
let hex = "";
|
|
178
|
-
for (let i = 0; i < view.length; i++) {
|
|
179
|
-
hex += view[i].toString(16).padStart(2, "0");
|
|
180
|
-
}
|
|
181
|
-
return hex;
|
|
182
|
-
}
|
|
183
|
-
function concatBytes(parts) {
|
|
184
|
-
let total = 0;
|
|
185
|
-
for (const p of parts) total += p.length;
|
|
186
|
-
const out = new Uint8Array(total);
|
|
187
|
-
let offset = 0;
|
|
188
|
-
for (const p of parts) {
|
|
189
|
-
out.set(p, offset);
|
|
190
|
-
offset += p.length;
|
|
191
|
-
}
|
|
192
|
-
return out;
|
|
193
|
-
}
|
|
194
|
-
async function applyRecipientRewrap(vault, dumpJson, opts) {
|
|
195
|
-
if (opts.exportPassphrase === void 0 && opts.recipients === void 0) {
|
|
196
|
-
return dumpJson;
|
|
197
|
-
}
|
|
198
|
-
const recipients = opts.recipients ?? [
|
|
199
|
-
{
|
|
200
|
-
id: vault.userId,
|
|
201
|
-
passphrase: opts.exportPassphrase,
|
|
202
|
-
role: vault.role
|
|
203
|
-
}
|
|
204
|
-
];
|
|
205
|
-
const recipientKeyrings = await vault.buildBundleRecipientKeyrings(recipients);
|
|
206
|
-
const backup = JSON.parse(dumpJson);
|
|
207
|
-
backup.keyrings = recipientKeyrings;
|
|
208
|
-
return JSON.stringify(backup);
|
|
209
|
-
}
|
|
210
|
-
function applySliceFilters(dumpJson, opts) {
|
|
211
|
-
const collectionsFilter = opts.collections ? new Set(opts.collections) : null;
|
|
212
|
-
const sinceMs = opts.since !== void 0 ? new Date(opts.since).getTime() : null;
|
|
213
|
-
if (collectionsFilter === null && sinceMs === null) return dumpJson;
|
|
214
|
-
const backup = JSON.parse(dumpJson);
|
|
215
|
-
if (backup.collections && typeof backup.collections === "object") {
|
|
216
|
-
const next = {};
|
|
217
|
-
for (const [name, records] of Object.entries(backup.collections)) {
|
|
218
|
-
if (collectionsFilter && !collectionsFilter.has(name)) continue;
|
|
219
|
-
if (sinceMs === null) {
|
|
220
|
-
next[name] = records;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
const kept = {};
|
|
224
|
-
for (const [id, env] of Object.entries(records)) {
|
|
225
|
-
const envTs = env._ts ? new Date(env._ts).getTime() : NaN;
|
|
226
|
-
if (Number.isFinite(envTs) && envTs >= sinceMs) {
|
|
227
|
-
kept[id] = env;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
next[name] = kept;
|
|
231
|
-
}
|
|
232
|
-
backup.collections = next;
|
|
233
|
-
}
|
|
234
|
-
return JSON.stringify(backup);
|
|
235
|
-
}
|
|
236
|
-
async function applyPlaintextFilters(vault, dumpJson, opts) {
|
|
237
|
-
if (opts.where === void 0 && opts.tierAtMost === void 0) {
|
|
238
|
-
return dumpJson;
|
|
239
|
-
}
|
|
240
|
-
const backup = JSON.parse(dumpJson);
|
|
241
|
-
if (!backup.collections || typeof backup.collections !== "object") {
|
|
242
|
-
return dumpJson;
|
|
243
|
-
}
|
|
244
|
-
const tierCeiling = opts.tierAtMost;
|
|
245
|
-
const where = opts.where;
|
|
246
|
-
const next = {};
|
|
247
|
-
for (const [collName, records] of Object.entries(backup.collections)) {
|
|
248
|
-
const kept = {};
|
|
249
|
-
for (const [id, env] of Object.entries(records)) {
|
|
250
|
-
if (tierCeiling !== void 0) {
|
|
251
|
-
const tier = env._tier ?? 0;
|
|
252
|
-
if (tier > tierCeiling) continue;
|
|
253
|
-
}
|
|
254
|
-
if (where !== void 0) {
|
|
255
|
-
const record = await vault._decryptEnvelopeForBundleFilter(
|
|
256
|
-
env,
|
|
257
|
-
collName
|
|
258
|
-
);
|
|
259
|
-
const ok = await where(record, { collection: collName, id });
|
|
260
|
-
if (!ok) continue;
|
|
261
|
-
}
|
|
262
|
-
kept[id] = env;
|
|
263
|
-
}
|
|
264
|
-
next[collName] = kept;
|
|
265
|
-
}
|
|
266
|
-
backup.collections = next;
|
|
267
|
-
return JSON.stringify(backup);
|
|
268
|
-
}
|
|
269
|
-
async function writeNoydbBundle(vault, opts = {}) {
|
|
270
|
-
if (opts.exportPassphrase !== void 0 && opts.recipients !== void 0) {
|
|
271
|
-
throw new Error(
|
|
272
|
-
"writeNoydbBundle: pass either exportPassphrase or recipients, not both"
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
const handle = await vault.getBundleHandle();
|
|
276
|
-
const dumpJson = await vault.dump();
|
|
277
|
-
const rekeyed = await applyRecipientRewrap(vault, dumpJson, opts);
|
|
278
|
-
const plainFiltered = await applyPlaintextFilters(vault, rekeyed, opts);
|
|
279
|
-
const filtered = applySliceFilters(plainFiltered, opts);
|
|
280
|
-
const dumpBytes = new TextEncoder().encode(filtered);
|
|
281
|
-
const { format, streamFormat } = selectCompression(opts.compression);
|
|
282
|
-
const body = streamFormat === null ? dumpBytes : await pumpThroughStream(dumpBytes, new CompressionStream(streamFormat));
|
|
283
|
-
const bodySha256 = await sha256Hex(body);
|
|
284
|
-
const publicEnvelope = await vault.getPublicEnvelope();
|
|
285
|
-
const header = {
|
|
286
|
-
formatVersion: NOYDB_BUNDLE_FORMAT_VERSION,
|
|
287
|
-
handle,
|
|
288
|
-
bodyBytes: body.length,
|
|
289
|
-
bodySha256,
|
|
290
|
-
...publicEnvelope !== void 0 ? { publicEnvelope } : {}
|
|
291
|
-
};
|
|
292
|
-
const headerBytes = encodeBundleHeader(header);
|
|
293
|
-
const prefix = new Uint8Array(NOYDB_BUNDLE_PREFIX_BYTES);
|
|
294
|
-
prefix.set(NOYDB_BUNDLE_MAGIC, 0);
|
|
295
|
-
prefix[4] = (streamFormat === null ? 0 : FLAG_COMPRESSED) | FLAG_HAS_INTEGRITY_HASH;
|
|
296
|
-
prefix[5] = format;
|
|
297
|
-
writeUint32BE(prefix, 6, headerBytes.length);
|
|
298
|
-
return concatBytes([prefix, headerBytes, body]);
|
|
299
|
-
}
|
|
300
|
-
function parsePrefixAndHeader(bytes) {
|
|
301
|
-
if (!hasNoydbBundleMagic(bytes)) {
|
|
302
|
-
throw new Error(
|
|
303
|
-
`Not a .noydb bundle: missing 'NDB1' magic prefix. The first 4 bytes are ${[...bytes.slice(0, 4)].map((b) => b.toString(16).padStart(2, "0")).join(" ")}.`
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
if (bytes.length < NOYDB_BUNDLE_PREFIX_BYTES) {
|
|
307
|
-
throw new Error(
|
|
308
|
-
`Truncated .noydb bundle: file is only ${bytes.length} bytes, which is less than the ${NOYDB_BUNDLE_PREFIX_BYTES}-byte fixed prefix.`
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
const flags = bytes[4];
|
|
312
|
-
const algo = bytes[5];
|
|
313
|
-
if (algo !== COMPRESSION_NONE && algo !== COMPRESSION_GZIP && algo !== COMPRESSION_BROTLI) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`.noydb bundle declares unknown compression algorithm ${algo}. Known values: 0 (none), 1 (gzip), 2 (brotli).`
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
const headerLength = readUint32BE(bytes, 6);
|
|
319
|
-
const bodyOffset = NOYDB_BUNDLE_PREFIX_BYTES + headerLength;
|
|
320
|
-
if (bodyOffset > bytes.length) {
|
|
321
|
-
throw new Error(
|
|
322
|
-
`Truncated .noydb bundle: declared header length ${headerLength} would extend past end of file (${bytes.length} bytes).`
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const headerBytes = bytes.slice(NOYDB_BUNDLE_PREFIX_BYTES, bodyOffset);
|
|
326
|
-
const header = decodeBundleHeader(headerBytes);
|
|
327
|
-
return { header, bodyOffset, algo, flags };
|
|
328
|
-
}
|
|
329
|
-
function readNoydbBundleHeader(bytes) {
|
|
330
|
-
return parsePrefixAndHeader(bytes).header;
|
|
331
|
-
}
|
|
332
|
-
function readNoydbBundlePublicEnvelope(bytes, opts = {}) {
|
|
333
|
-
const header = parsePrefixAndHeader(bytes).header;
|
|
334
|
-
const env = header.publicEnvelope;
|
|
335
|
-
if (!env) return void 0;
|
|
336
|
-
if (opts.locale === void 0) return env;
|
|
337
|
-
return {
|
|
338
|
-
...env,
|
|
339
|
-
...env.name !== void 0 ? { name: pickLocale(env.name, opts.locale, env.defaultLocale) } : {},
|
|
340
|
-
...env.description !== void 0 ? { description: pickLocale(env.description, opts.locale, env.defaultLocale) } : {}
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
async function readNoydbBundle(bytes) {
|
|
344
|
-
const { header, bodyOffset, algo } = parsePrefixAndHeader(bytes);
|
|
345
|
-
const body = bytes.slice(bodyOffset);
|
|
346
|
-
if (body.length !== header.bodyBytes) {
|
|
347
|
-
throw new BundleIntegrityError(
|
|
348
|
-
`body length ${body.length} does not match header.bodyBytes ${header.bodyBytes}. The bundle was truncated or padded between write and read.`
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
const actualSha = await sha256Hex(body);
|
|
352
|
-
if (actualSha !== header.bodySha256) {
|
|
353
|
-
throw new BundleIntegrityError(
|
|
354
|
-
`body sha256 ${actualSha} does not match header.bodySha256 ${header.bodySha256}. The bundle bytes were modified between write and read \u2014 refuse to decompress.`
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
let dumpBytes;
|
|
358
|
-
if (algo === COMPRESSION_NONE) {
|
|
359
|
-
dumpBytes = body;
|
|
360
|
-
} else {
|
|
361
|
-
const streamFormat = algo === COMPRESSION_BROTLI ? "br" : "gzip";
|
|
362
|
-
try {
|
|
363
|
-
dumpBytes = await pumpThroughStream(body, new DecompressionStream(streamFormat));
|
|
364
|
-
} catch (err) {
|
|
365
|
-
throw new BundleIntegrityError(
|
|
366
|
-
`decompression failed: ${err.message}. The bundle passed the integrity hash but the body is not valid ${streamFormat} data \u2014 likely a producer bug.`
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
const dumpJson = new TextDecoder("utf-8", { fatal: true }).decode(dumpBytes);
|
|
371
|
-
return { header, dumpJson };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export {
|
|
375
|
-
NOYDB_BUNDLE_MAGIC,
|
|
376
|
-
NOYDB_BUNDLE_PREFIX_BYTES,
|
|
377
|
-
NOYDB_BUNDLE_FORMAT_VERSION,
|
|
378
|
-
FLAG_COMPRESSED,
|
|
379
|
-
FLAG_HAS_INTEGRITY_HASH,
|
|
380
|
-
COMPRESSION_NONE,
|
|
381
|
-
COMPRESSION_GZIP,
|
|
382
|
-
COMPRESSION_BROTLI,
|
|
383
|
-
validateBundleHeader,
|
|
384
|
-
encodeBundleHeader,
|
|
385
|
-
hasNoydbBundleMagic,
|
|
386
|
-
resetBrotliSupportCache,
|
|
387
|
-
writeNoydbBundle,
|
|
388
|
-
readNoydbBundleHeader,
|
|
389
|
-
readNoydbBundlePublicEnvelope,
|
|
390
|
-
readNoydbBundle
|
|
391
|
-
};
|
|
392
|
-
//# sourceMappingURL=chunk-EXHNQEV4.js.map
|