@degenerate/sharedata 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -0
- package/dist/manager/share-data-manager-events.d.ts +27 -0
- package/dist/manager/share-data-manager-events.d.ts.map +1 -0
- package/dist/manager/share-data-manager-events.js +2 -0
- package/dist/manager/share-data-manager.d.ts +84 -0
- package/dist/manager/share-data-manager.d.ts.map +1 -0
- package/dist/manager/share-data-manager.js +210 -0
- package/dist/persistence/block-factory-registry.d.ts +51 -0
- package/dist/persistence/block-factory-registry.d.ts.map +1 -0
- package/dist/persistence/block-factory-registry.js +36 -0
- package/dist/persistence/deserialize-share-data.d.ts +18 -0
- package/dist/persistence/deserialize-share-data.d.ts.map +1 -0
- package/dist/persistence/deserialize-share-data.js +144 -0
- package/dist/persistence/file-share-data-repository.d.ts +30 -0
- package/dist/persistence/file-share-data-repository.d.ts.map +1 -0
- package/dist/persistence/file-share-data-repository.js +147 -0
- package/dist/persistence/firestore-share-data-repository.d.ts +38 -0
- package/dist/persistence/firestore-share-data-repository.d.ts.map +1 -0
- package/dist/persistence/firestore-share-data-repository.js +78 -0
- package/dist/persistence/in-memory-share-data-repository.d.ts +19 -0
- package/dist/persistence/in-memory-share-data-repository.d.ts.map +1 -0
- package/dist/persistence/in-memory-share-data-repository.js +48 -0
- package/dist/persistence/serialize-share-data.d.ts +22 -0
- package/dist/persistence/serialize-share-data.d.ts.map +1 -0
- package/dist/persistence/serialize-share-data.js +62 -0
- package/dist/persistence/share-data-repository.d.ts +49 -0
- package/dist/persistence/share-data-repository.d.ts.map +1 -0
- package/dist/persistence/share-data-repository.js +2 -0
- package/dist/share-data/creation/create-share-data-initial-crypto.d.ts +11 -0
- package/dist/share-data/creation/create-share-data-initial-crypto.d.ts.map +1 -0
- package/dist/share-data/creation/create-share-data-initial-crypto.js +26 -0
- package/dist/share-data/helpers/calculate-share-data-string.d.ts +21 -0
- package/dist/share-data/helpers/calculate-share-data-string.d.ts.map +1 -0
- package/dist/share-data/helpers/calculate-share-data-string.js +71 -0
- package/dist/share-data/helpers/config/default-share-data-string.config.d.ts +7 -0
- package/dist/share-data/helpers/config/default-share-data-string.config.d.ts.map +1 -0
- package/dist/share-data/helpers/config/default-share-data-string.config.js +25 -0
- package/dist/share-data/helpers/config/stringify.d.ts +5 -0
- package/dist/share-data/helpers/config/stringify.d.ts.map +1 -0
- package/dist/share-data/helpers/config/stringify.js +10 -0
- package/dist/share-data/helpers/get-array-string.d.ts +6 -0
- package/dist/share-data/helpers/get-array-string.d.ts.map +1 -0
- package/dist/share-data/helpers/get-array-string.js +17 -0
- package/dist/share-data/proofs/creation/build-transaction-proposal-signer.d.ts +57 -0
- package/dist/share-data/proofs/creation/build-transaction-proposal-signer.d.ts.map +1 -0
- package/dist/share-data/proofs/creation/build-transaction-proposal-signer.js +36 -0
- package/dist/share-data/proofs/creation/create-share-data-proposal-proof.d.ts +12 -0
- package/dist/share-data/proofs/creation/create-share-data-proposal-proof.d.ts.map +1 -0
- package/dist/share-data/proofs/creation/create-share-data-proposal-proof.js +41 -0
- package/dist/share-data/proofs/creation/create-transaction-proposal-proof.d.ts +27 -0
- package/dist/share-data/proofs/creation/create-transaction-proposal-proof.d.ts.map +1 -0
- package/dist/share-data/proofs/creation/create-transaction-proposal-proof.js +58 -0
- package/dist/share-data/proofs/helpers/create-share-data-string-to-sign.d.ts +12 -0
- package/dist/share-data/proofs/helpers/create-share-data-string-to-sign.d.ts.map +1 -0
- package/dist/share-data/proofs/helpers/create-share-data-string-to-sign.js +17 -0
- package/dist/share-data/proofs/helpers/create-transaction-proposal-string-to-sign.d.ts +24 -0
- package/dist/share-data/proofs/helpers/create-transaction-proposal-string-to-sign.d.ts.map +1 -0
- package/dist/share-data/proofs/helpers/create-transaction-proposal-string-to-sign.js +29 -0
- package/dist/share-data/proofs/helpers/get-previous-hash-from-share-data.d.ts +9 -0
- package/dist/share-data/proofs/helpers/get-previous-hash-from-share-data.d.ts.map +1 -0
- package/dist/share-data/proofs/helpers/get-previous-hash-from-share-data.js +28 -0
- package/dist/share-data/proofs/helpers/hash-proposed-state.d.ts +12 -0
- package/dist/share-data/proofs/helpers/hash-proposed-state.d.ts.map +1 -0
- package/dist/share-data/proofs/helpers/hash-proposed-state.js +18 -0
- package/dist/share-data/proofs/testing/test-proof-signer.d.ts +53 -0
- package/dist/share-data/proofs/testing/test-proof-signer.d.ts.map +1 -0
- package/dist/share-data/proofs/testing/test-proof-signer.js +113 -0
- package/dist/share-data/proofs/verification/verify-share-data-proposal-proof.d.ts +40 -0
- package/dist/share-data/proofs/verification/verify-share-data-proposal-proof.d.ts.map +1 -0
- package/dist/share-data/proofs/verification/verify-share-data-proposal-proof.js +126 -0
- package/dist/share-data/proofs/verification/verify-transaction-proposal-proof.d.ts +37 -0
- package/dist/share-data/proofs/verification/verify-transaction-proposal-proof.d.ts.map +1 -0
- package/dist/share-data/proofs/verification/verify-transaction-proposal-proof.js +125 -0
- package/dist/share-data/share-data.d.ts +180 -27
- package/dist/share-data/share-data.d.ts.map +1 -1
- package/dist/share-data/share-data.js +303 -60
- package/package.json +33 -12
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deserializeShareData = void 0;
|
|
4
|
+
const generic_1 = require("@degenerate/generic");
|
|
5
|
+
const verify_transaction_proposal_proof_1 = require("../share-data/proofs/verification/verify-transaction-proposal-proof");
|
|
6
|
+
const block_factory_registry_1 = require("./block-factory-registry");
|
|
7
|
+
// Reconstructs a ShareData from its persisted snapshot. This is the trust
|
|
8
|
+
// gate: the snapshot's `expected*` values are NEVER taken as source of truth.
|
|
9
|
+
// The block is rebuilt from scratch by replaying construction (which
|
|
10
|
+
// re-verifies the structural-proof chain) and replaying the transaction log,
|
|
11
|
+
// then the independently rebuilt result is cross-checked against the
|
|
12
|
+
// snapshot's `expected*` fields. Any tampering — a flipped proof, a doctored
|
|
13
|
+
// final state — surfaces as a thrown error rather than a silently trusted load.
|
|
14
|
+
/**
|
|
15
|
+
* Replays a {@link PersistedShareData} snapshot back into a live, executable
|
|
16
|
+
* ShareData. The rebuilt block re-binds real function closures (via the
|
|
17
|
+
* registered builder for its block type), so it accepts new proposals just
|
|
18
|
+
* like a freshly-created block.
|
|
19
|
+
*
|
|
20
|
+
* Throws:
|
|
21
|
+
* - {@link ShareDataPersistenceError} for an unsupported schema version, an
|
|
22
|
+
* unregistered block type, or a transaction referencing an unknown
|
|
23
|
+
* `functionId`.
|
|
24
|
+
* - {@link ShareDataStructuralProofFailedError} (from the replayed factory)
|
|
25
|
+
* if the structural-proof chain does not verify.
|
|
26
|
+
* - {@link ShareDataIntegrityError} if the replayed block diverges from the
|
|
27
|
+
* snapshot's `expected*` cross-checks.
|
|
28
|
+
*/
|
|
29
|
+
const deserializeShareData = (snapshot) => {
|
|
30
|
+
// Read id + schemaVersion defensively *before* full validation so an
|
|
31
|
+
// unsupported version yields a precise persistence error (naming the id)
|
|
32
|
+
// rather than a raw Zod failure on the `schemaVersion` literal.
|
|
33
|
+
const rawId = typeof snapshot?.id === 'string'
|
|
34
|
+
? snapshot.id
|
|
35
|
+
: 'unknown';
|
|
36
|
+
const rawVersion = snapshot?.schemaVersion;
|
|
37
|
+
if (rawVersion !== generic_1.PERSISTED_SHARE_DATA_SCHEMA_VERSION) {
|
|
38
|
+
throw new generic_1.ShareDataPersistenceError(rawId, `unsupported schema version ${String(rawVersion)} ` +
|
|
39
|
+
`(this build understands version ${generic_1.PERSISTED_SHARE_DATA_SCHEMA_VERSION})`);
|
|
40
|
+
}
|
|
41
|
+
const parsed = generic_1.PersistedShareDataSchema.parse(snapshot);
|
|
42
|
+
// `ShareBlockTypeSchema` infers a string-literal union, while the registry
|
|
43
|
+
// keys on the `ShareBlockType` enum. The two carry identical runtime
|
|
44
|
+
// values, so this reconstitutes the validated wire string into the domain
|
|
45
|
+
// enum at the parse boundary.
|
|
46
|
+
const blockType = parsed.blockType;
|
|
47
|
+
const builder = (0, block_factory_registry_1.getShareBlockBuilder)(blockType);
|
|
48
|
+
if (!builder) {
|
|
49
|
+
throw new generic_1.ShareDataPersistenceError(parsed.id, `no block builder registered for block type "${parsed.blockType}"`);
|
|
50
|
+
}
|
|
51
|
+
// Replay construction. The factory re-runs addFunction/addRule, each of
|
|
52
|
+
// which re-verifies the matching structural proof against the rebuilt
|
|
53
|
+
// state; a tampered proof or initialCrypto throws here.
|
|
54
|
+
const block = builder({
|
|
55
|
+
creationConfig: parsed.creationConfig,
|
|
56
|
+
initialCrypto: parsed.initialCrypto,
|
|
57
|
+
structuralProofs: parsed.structuralProofs,
|
|
58
|
+
});
|
|
59
|
+
// Replay the transaction log in recorded order.
|
|
60
|
+
for (const entry of parsed.transactionLog) {
|
|
61
|
+
const transactionFunction = block.functions.find((fn) => fn.id === entry.functionId);
|
|
62
|
+
if (!transactionFunction) {
|
|
63
|
+
throw new generic_1.ShareDataPersistenceError(parsed.id, `transaction log references unknown functionId "${entry.functionId}"`);
|
|
64
|
+
}
|
|
65
|
+
// Re-authorize the signer before re-verifying the signature. A tampered
|
|
66
|
+
// snapshot could swap in a proof validly signed by a *different* key
|
|
67
|
+
// over the same proposed state — that would pass signature
|
|
68
|
+
// verification, so the proposer-identity gate (the same one
|
|
69
|
+
// addTransaction enforces live) must run on replay too. Checked first
|
|
70
|
+
// so a swapped key fails as a not-authorized error rather than a
|
|
71
|
+
// signature mismatch.
|
|
72
|
+
if (entry.proof.publicKey !== block.creationPublicKey) {
|
|
73
|
+
throw new generic_1.ShareDataTransactionNotAuthorizedError(parsed.id, entry.proof.publicKey);
|
|
74
|
+
}
|
|
75
|
+
// Re-verify the transaction proof against the block at its pre-entry
|
|
76
|
+
// state (where it now sits, having replayed every earlier entry) and
|
|
77
|
+
// the proposed state about to be applied. This is the runtime arm of
|
|
78
|
+
// the trust gate: a snapshot whose proposed state or proof was edited
|
|
79
|
+
// throws ShareDataTransactionProofFailedError here rather than being
|
|
80
|
+
// loaded. Verifying before the apply keeps the recomputed current hash
|
|
81
|
+
// aligned with what the signer signed over.
|
|
82
|
+
(0, verify_transaction_proposal_proof_1.verifyTransactionProposalProof)(entry.proof, block, entry.proposedState, block.signingCurve);
|
|
83
|
+
// addTransactionAlreadyValidated derives its accept/reject outcome
|
|
84
|
+
// from (proposedResult && currentResult). Setting both to the stored
|
|
85
|
+
// boolean replays the original outcome exactly: currentState is
|
|
86
|
+
// replaced only when the recorded transaction was accepted.
|
|
87
|
+
const rulesEvaluation = {
|
|
88
|
+
ruleId: '',
|
|
89
|
+
currentResult: entry.ruleOutcome,
|
|
90
|
+
currentConditionFailure: [],
|
|
91
|
+
proposedResult: entry.ruleOutcome,
|
|
92
|
+
proposedConditionFailure: [],
|
|
93
|
+
};
|
|
94
|
+
// Pass the proof so the rebuilt history entry matches the live one;
|
|
95
|
+
// the canonical hash folds history in, so a later transaction's proof
|
|
96
|
+
// verifies only if earlier entries reproduce their proofs.
|
|
97
|
+
block.addTransactionAlreadyValidated(entry.proposedState, rulesEvaluation, transactionFunction, entry.transactionTime, entry.proof);
|
|
98
|
+
}
|
|
99
|
+
// Locking only gates future addFunction/addRule, so applying it after
|
|
100
|
+
// transaction replay reproduces the snapshot's lock state without
|
|
101
|
+
// affecting the rebuilt data.
|
|
102
|
+
if (parsed.expectedIsLocked) {
|
|
103
|
+
block.initialize();
|
|
104
|
+
}
|
|
105
|
+
assertIntegrity(block, parsed);
|
|
106
|
+
return block;
|
|
107
|
+
};
|
|
108
|
+
exports.deserializeShareData = deserializeShareData;
|
|
109
|
+
/**
|
|
110
|
+
* Cross-checks the independently rebuilt block against the snapshot's
|
|
111
|
+
* `expected*` integrity values. A divergence means the snapshot was altered
|
|
112
|
+
* after signing (or the build that wrote it disagrees with this one), so we
|
|
113
|
+
* refuse the load with {@link ShareDataIntegrityError}.
|
|
114
|
+
*/
|
|
115
|
+
const assertIntegrity = (block, parsed) => {
|
|
116
|
+
const mismatch = block.creationHash !== parsed.expectedCreationHash ||
|
|
117
|
+
block.creationSignature !== parsed.expectedCreationSignature ||
|
|
118
|
+
block.isLocked !== parsed.expectedIsLocked ||
|
|
119
|
+
block.version !== parsed.expectedVersion ||
|
|
120
|
+
canonicalize(block.currentState) !== canonicalize(parsed.expectedCurrentState);
|
|
121
|
+
if (mismatch) {
|
|
122
|
+
throw new generic_1.ShareDataIntegrityError(parsed.id);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Order-independent JSON canonicalization so `currentState` equality does not
|
|
127
|
+
* hinge on object key insertion order.
|
|
128
|
+
*/
|
|
129
|
+
const canonicalize = (value) => JSON.stringify(sortKeys(value));
|
|
130
|
+
const sortKeys = (value) => {
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
return value.map(sortKeys);
|
|
133
|
+
}
|
|
134
|
+
if (value && typeof value === 'object') {
|
|
135
|
+
const source = value;
|
|
136
|
+
return Object.keys(source)
|
|
137
|
+
.sort()
|
|
138
|
+
.reduce((acc, key) => {
|
|
139
|
+
acc[key] = sortKeys(source[key]);
|
|
140
|
+
return acc;
|
|
141
|
+
}, {});
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PersistedShareData } from '@degenerate/generic';
|
|
2
|
+
import { ShareData } from '../share-data/share-data';
|
|
3
|
+
import { ShareDataRepository } from './share-data-repository';
|
|
4
|
+
/**
|
|
5
|
+
* Persists each ShareData snapshot as a pretty-printed JSON file named after
|
|
6
|
+
* its id. Writes are atomic (temp file + rename) so a crash mid-write can
|
|
7
|
+
* never leave a half-written, unverifiable snapshot behind. Loads route
|
|
8
|
+
* through {@link deserializeShareData}, so a file edited on disk fails
|
|
9
|
+
* verification instead of loading silently.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FileShareDataRepository implements ShareDataRepository {
|
|
12
|
+
private readonly directory;
|
|
13
|
+
constructor(directory: string);
|
|
14
|
+
save(snapshot: PersistedShareData): Promise<void>;
|
|
15
|
+
load(id: string): Promise<ShareData | undefined>;
|
|
16
|
+
loadAll(): Promise<ShareData[]>;
|
|
17
|
+
loadAllSnapshots(): Promise<PersistedShareData[]>;
|
|
18
|
+
remove(id: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Maps a block id to its on-disk path, guarding against path traversal.
|
|
21
|
+
* A block id can originate from an untrusted client — the HTTP create
|
|
22
|
+
* route takes `metadata.shareBlockId` straight from the request body — so
|
|
23
|
+
* an id like `../../etc/cron.d/x` must never escape the storage directory.
|
|
24
|
+
* `encodeURIComponent` neutralizes path separators (`/`, `\`) and any `..`
|
|
25
|
+
* collapses to a literal filename segment; the resolved path is then
|
|
26
|
+
* re-asserted to be a direct child of the directory as defense in depth.
|
|
27
|
+
*/
|
|
28
|
+
private resolveFilePath;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=file-share-data-repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-share-data-repository.d.ts","sourceRoot":"","sources":["../../src/persistence/file-share-data-repository.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,kBAAkB,EAGrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAS9D;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,mBAAmB;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM;IAExC,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBjD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAchD,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAK/B,gBAAgB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAuBjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;CAY1B"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileShareDataRepository = void 0;
|
|
37
|
+
const fs_1 = require("fs");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const generic_1 = require("@degenerate/generic");
|
|
40
|
+
const deserialize_share_data_1 = require("./deserialize-share-data");
|
|
41
|
+
// File/JSON implementation of {@link ShareDataRepository}: one snapshot per
|
|
42
|
+
// `<directory>/<id>.json` file. The first of the three planned stores — chosen
|
|
43
|
+
// first because it needs no external service yet exercises the full
|
|
44
|
+
// serialize → durable bytes → reload-and-re-verify path, including survival
|
|
45
|
+
// across a process restart (a fresh instance pointed at the same directory
|
|
46
|
+
// reloads everything).
|
|
47
|
+
/**
|
|
48
|
+
* Persists each ShareData snapshot as a pretty-printed JSON file named after
|
|
49
|
+
* its id. Writes are atomic (temp file + rename) so a crash mid-write can
|
|
50
|
+
* never leave a half-written, unverifiable snapshot behind. Loads route
|
|
51
|
+
* through {@link deserializeShareData}, so a file edited on disk fails
|
|
52
|
+
* verification instead of loading silently.
|
|
53
|
+
*/
|
|
54
|
+
class FileShareDataRepository {
|
|
55
|
+
constructor(directory) {
|
|
56
|
+
this.directory = directory;
|
|
57
|
+
}
|
|
58
|
+
async save(snapshot) {
|
|
59
|
+
await fs_1.promises.mkdir(this.directory, { recursive: true });
|
|
60
|
+
const filePath = this.resolveFilePath(snapshot.id);
|
|
61
|
+
// Write to a unique temp file, then rename into place. rename(2) is
|
|
62
|
+
// atomic within a directory, so a reader (or `loadAll`) never observes
|
|
63
|
+
// a partially-written file, and a crash leaves either the old snapshot
|
|
64
|
+
// or the new one — never a corrupt blend.
|
|
65
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
66
|
+
await fs_1.promises.writeFile(tmpPath, JSON.stringify(snapshot, null, 2), 'utf8');
|
|
67
|
+
try {
|
|
68
|
+
await fs_1.promises.rename(tmpPath, filePath);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
await fs_1.promises.rm(tmpPath, { force: true });
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async load(id) {
|
|
76
|
+
const filePath = this.resolveFilePath(id);
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = await fs_1.promises.readFile(filePath, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err.code === 'ENOENT') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
return (0, deserialize_share_data_1.deserializeShareData)(JSON.parse(raw));
|
|
88
|
+
}
|
|
89
|
+
async loadAll() {
|
|
90
|
+
const snapshots = await this.loadAllSnapshots();
|
|
91
|
+
return snapshots.map((snapshot) => (0, deserialize_share_data_1.deserializeShareData)(snapshot));
|
|
92
|
+
}
|
|
93
|
+
async loadAllSnapshots() {
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = await fs_1.promises.readdir(this.directory);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (err.code === 'ENOENT') {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
// Only finished snapshots end in `.json`; in-flight `.tmp` writes are
|
|
105
|
+
// skipped. Sorted for a deterministic load order.
|
|
106
|
+
const files = entries.filter((file) => file.endsWith('.json')).sort();
|
|
107
|
+
const snapshots = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const raw = await fs_1.promises.readFile(path.join(this.directory, file), 'utf8');
|
|
110
|
+
// Schema-validate (not trust-verify) so callers receive a well-typed
|
|
111
|
+
// snapshot; `deserializeShareData` re-runs the full trust gate.
|
|
112
|
+
snapshots.push(generic_1.PersistedShareDataSchema.parse(JSON.parse(raw)));
|
|
113
|
+
}
|
|
114
|
+
return snapshots;
|
|
115
|
+
}
|
|
116
|
+
async remove(id) {
|
|
117
|
+
const filePath = this.resolveFilePath(id);
|
|
118
|
+
try {
|
|
119
|
+
await fs_1.promises.unlink(filePath);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (err.code === 'ENOENT') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Maps a block id to its on-disk path, guarding against path traversal.
|
|
130
|
+
* A block id can originate from an untrusted client — the HTTP create
|
|
131
|
+
* route takes `metadata.shareBlockId` straight from the request body — so
|
|
132
|
+
* an id like `../../etc/cron.d/x` must never escape the storage directory.
|
|
133
|
+
* `encodeURIComponent` neutralizes path separators (`/`, `\`) and any `..`
|
|
134
|
+
* collapses to a literal filename segment; the resolved path is then
|
|
135
|
+
* re-asserted to be a direct child of the directory as defense in depth.
|
|
136
|
+
*/
|
|
137
|
+
resolveFilePath(id) {
|
|
138
|
+
const directory = path.resolve(this.directory);
|
|
139
|
+
const fileName = `${encodeURIComponent(id)}.json`;
|
|
140
|
+
const resolved = path.resolve(directory, fileName);
|
|
141
|
+
if (path.dirname(resolved) !== directory) {
|
|
142
|
+
throw new generic_1.ShareDataPersistenceError(id, 'refusing to resolve a snapshot path outside the storage directory');
|
|
143
|
+
}
|
|
144
|
+
return resolved;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.FileShareDataRepository = FileShareDataRepository;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { firestore } from 'firebase-admin';
|
|
2
|
+
import { PersistedShareData } from '@degenerate/generic';
|
|
3
|
+
import { ShareData } from '../share-data/share-data';
|
|
4
|
+
import { ShareDataRepository } from './share-data-repository';
|
|
5
|
+
/**
|
|
6
|
+
* Persists each ShareData snapshot as a Firestore document named after its id,
|
|
7
|
+
* in {@link DatabaseSettingsFirestore.ShareData.Collection} by default. `save`
|
|
8
|
+
* round-trips the snapshot through JSON to strip `undefined` (Firestore
|
|
9
|
+
* rejects `undefined` field values) — parity with the file store, which writes
|
|
10
|
+
* `JSON.stringify`. Loads route through {@link deserializeShareData}, so a
|
|
11
|
+
* document edited out-of-band fails verification instead of loading silently.
|
|
12
|
+
*/
|
|
13
|
+
export declare class FirestoreShareDataRepository implements ShareDataRepository {
|
|
14
|
+
private readonly firestore;
|
|
15
|
+
private readonly collectionName;
|
|
16
|
+
constructor(firestore: firestore.Firestore, collectionName?: string);
|
|
17
|
+
save(snapshot: PersistedShareData): Promise<void>;
|
|
18
|
+
load(id: string): Promise<ShareData | undefined>;
|
|
19
|
+
loadAll(): Promise<ShareData[]>;
|
|
20
|
+
loadAllSnapshots(): Promise<PersistedShareData[]>;
|
|
21
|
+
remove(id: string): Promise<void>;
|
|
22
|
+
private docRef;
|
|
23
|
+
/**
|
|
24
|
+
* Strip `undefined` field values, which Firestore rejects, by round-tripping
|
|
25
|
+
* through JSON. Matches the file store's `JSON.stringify` semantics so both
|
|
26
|
+
* stores persist byte-identical snapshots.
|
|
27
|
+
*/
|
|
28
|
+
private toFirestoreData;
|
|
29
|
+
/**
|
|
30
|
+
* Validate a block id against Firestore's document-id rules before it is
|
|
31
|
+
* used as a path segment. A block id can originate from an untrusted client
|
|
32
|
+
* (the HTTP create route reads `metadata.shareBlockId` from the request
|
|
33
|
+
* body), so an id containing `/` — or one Firestore reserves — must be
|
|
34
|
+
* rejected rather than silently writing to an unexpected document or path.
|
|
35
|
+
*/
|
|
36
|
+
private docId;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=firestore-share-data-repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firestore-share-data-repository.d.ts","sourceRoot":"","sources":["../../src/persistence/firestore-share-data-repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAEH,kBAAkB,EAGrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAU9D;;;;;;;GAOG;AACH,qBAAa,4BAA6B,YAAW,mBAAmB;IAEhE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;gBADd,SAAS,EAAE,SAAS,CAAC,SAAS,EAC9B,cAAc,GAAE,MAAuD;IAGtF,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAQhD,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAK/B,gBAAgB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAOjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,OAAO,CAAC,MAAM;IAId;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;;;OAMG;IACH,OAAO,CAAC,KAAK;CAYhB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FirestoreShareDataRepository = void 0;
|
|
4
|
+
const generic_1 = require("@degenerate/generic");
|
|
5
|
+
const deserialize_share_data_1 = require("./deserialize-share-data");
|
|
6
|
+
// Firestore implementation of {@link ShareDataRepository}: one snapshot per
|
|
7
|
+
// document, keyed by block id, in a single collection. The second of the three
|
|
8
|
+
// planned stores. `firebase-admin` is referenced by TYPE ONLY — the live
|
|
9
|
+
// `Firestore` instance is injected by the caller — so `@degenerate/sharedata`
|
|
10
|
+
// takes no runtime dependency on firebase-admin and never initializes an app
|
|
11
|
+
// itself. A host that wants Firestore-backed persistence constructs its own
|
|
12
|
+
// `admin.firestore()` (or `getFirestore(app)`) and hands it in.
|
|
13
|
+
/**
|
|
14
|
+
* Persists each ShareData snapshot as a Firestore document named after its id,
|
|
15
|
+
* in {@link DatabaseSettingsFirestore.ShareData.Collection} by default. `save`
|
|
16
|
+
* round-trips the snapshot through JSON to strip `undefined` (Firestore
|
|
17
|
+
* rejects `undefined` field values) — parity with the file store, which writes
|
|
18
|
+
* `JSON.stringify`. Loads route through {@link deserializeShareData}, so a
|
|
19
|
+
* document edited out-of-band fails verification instead of loading silently.
|
|
20
|
+
*/
|
|
21
|
+
class FirestoreShareDataRepository {
|
|
22
|
+
constructor(firestore, collectionName = generic_1.DatabaseSettingsFirestore.ShareData.Collection) {
|
|
23
|
+
this.firestore = firestore;
|
|
24
|
+
this.collectionName = collectionName;
|
|
25
|
+
}
|
|
26
|
+
async save(snapshot) {
|
|
27
|
+
await this.docRef(snapshot.id).set(this.toFirestoreData(snapshot));
|
|
28
|
+
}
|
|
29
|
+
async load(id) {
|
|
30
|
+
const doc = await this.docRef(id).get();
|
|
31
|
+
if (!doc.exists) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return (0, deserialize_share_data_1.deserializeShareData)(generic_1.PersistedShareDataSchema.parse(doc.data()));
|
|
35
|
+
}
|
|
36
|
+
async loadAll() {
|
|
37
|
+
const snapshots = await this.loadAllSnapshots();
|
|
38
|
+
return snapshots.map((snapshot) => (0, deserialize_share_data_1.deserializeShareData)(snapshot));
|
|
39
|
+
}
|
|
40
|
+
async loadAllSnapshots() {
|
|
41
|
+
const query = await this.firestore.collection(this.collectionName).get();
|
|
42
|
+
// Schema-validate (not trust-verify) so callers receive well-typed
|
|
43
|
+
// snapshots; `deserializeShareData` re-runs the full trust gate.
|
|
44
|
+
return query.docs.map((doc) => generic_1.PersistedShareDataSchema.parse(doc.data()));
|
|
45
|
+
}
|
|
46
|
+
async remove(id) {
|
|
47
|
+
await this.docRef(id).delete();
|
|
48
|
+
}
|
|
49
|
+
docRef(id) {
|
|
50
|
+
return this.firestore.collection(this.collectionName).doc(this.docId(id));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Strip `undefined` field values, which Firestore rejects, by round-tripping
|
|
54
|
+
* through JSON. Matches the file store's `JSON.stringify` semantics so both
|
|
55
|
+
* stores persist byte-identical snapshots.
|
|
56
|
+
*/
|
|
57
|
+
toFirestoreData(snapshot) {
|
|
58
|
+
return JSON.parse(JSON.stringify(snapshot));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate a block id against Firestore's document-id rules before it is
|
|
62
|
+
* used as a path segment. A block id can originate from an untrusted client
|
|
63
|
+
* (the HTTP create route reads `metadata.shareBlockId` from the request
|
|
64
|
+
* body), so an id containing `/` — or one Firestore reserves — must be
|
|
65
|
+
* rejected rather than silently writing to an unexpected document or path.
|
|
66
|
+
*/
|
|
67
|
+
docId(id) {
|
|
68
|
+
if (id.length === 0 ||
|
|
69
|
+
id.includes('/') ||
|
|
70
|
+
id === '.' ||
|
|
71
|
+
id === '..' ||
|
|
72
|
+
/^__.*__$/.test(id)) {
|
|
73
|
+
throw new generic_1.ShareDataPersistenceError(id, 'invalid Firestore document id');
|
|
74
|
+
}
|
|
75
|
+
return id;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.FirestoreShareDataRepository = FirestoreShareDataRepository;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PersistedShareData } from '@degenerate/generic';
|
|
2
|
+
import { ShareData } from '../share-data/share-data';
|
|
3
|
+
import { ShareDataRepository } from './share-data-repository';
|
|
4
|
+
/**
|
|
5
|
+
* Keeps each ShareData snapshot in an in-process `Map`, keyed by block id. To
|
|
6
|
+
* mirror a durable store faithfully — and to avoid handing out a reference a
|
|
7
|
+
* caller could mutate behind the manager's back — `save` deep-clones the
|
|
8
|
+
* snapshot via JSON round-trip on the way in, and `load`/`loadAll` re-verify
|
|
9
|
+
* through {@link deserializeShareData} on the way out.
|
|
10
|
+
*/
|
|
11
|
+
export declare class InMemoryShareDataRepository implements ShareDataRepository {
|
|
12
|
+
private readonly snapshots;
|
|
13
|
+
save(snapshot: PersistedShareData): Promise<void>;
|
|
14
|
+
load(id: string): Promise<ShareData | undefined>;
|
|
15
|
+
loadAll(): Promise<ShareData[]>;
|
|
16
|
+
loadAllSnapshots(): Promise<PersistedShareData[]>;
|
|
17
|
+
remove(id: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=in-memory-share-data-repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-share-data-repository.d.ts","sourceRoot":"","sources":["../../src/persistence/in-memory-share-data-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA4B,MAAM,qBAAqB,CAAC;AAEnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAU9D;;;;;;GAMG;AACH,qBAAa,2BAA4B,YAAW,mBAAmB;IACnE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IAE7D,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAQhD,OAAO,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAK/B,gBAAgB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAMjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG1C"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryShareDataRepository = void 0;
|
|
4
|
+
const generic_1 = require("@degenerate/generic");
|
|
5
|
+
const deserialize_share_data_1 = require("./deserialize-share-data");
|
|
6
|
+
// In-memory implementation of {@link ShareDataRepository}: snapshots live in a
|
|
7
|
+
// `Map`, nothing touches disk or the network. The third of the three planned
|
|
8
|
+
// stores. Useful for tests and for a process that wants the manager's
|
|
9
|
+
// hydrate/write-through plumbing without durable storage. It still round-trips
|
|
10
|
+
// through serialize/deserialize, so it exercises the same trust gate as the
|
|
11
|
+
// durable stores — a snapshot is re-verified on load, never handed back as the
|
|
12
|
+
// live object that was saved.
|
|
13
|
+
/**
|
|
14
|
+
* Keeps each ShareData snapshot in an in-process `Map`, keyed by block id. To
|
|
15
|
+
* mirror a durable store faithfully — and to avoid handing out a reference a
|
|
16
|
+
* caller could mutate behind the manager's back — `save` deep-clones the
|
|
17
|
+
* snapshot via JSON round-trip on the way in, and `load`/`loadAll` re-verify
|
|
18
|
+
* through {@link deserializeShareData} on the way out.
|
|
19
|
+
*/
|
|
20
|
+
class InMemoryShareDataRepository {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.snapshots = new Map();
|
|
23
|
+
}
|
|
24
|
+
async save(snapshot) {
|
|
25
|
+
// Clone so a later mutation of the caller's object cannot reach into
|
|
26
|
+
// stored state — the durable stores serialize to bytes, so this keeps
|
|
27
|
+
// behavior identical.
|
|
28
|
+
this.snapshots.set(snapshot.id, JSON.parse(JSON.stringify(snapshot)));
|
|
29
|
+
}
|
|
30
|
+
async load(id) {
|
|
31
|
+
const snapshot = this.snapshots.get(id);
|
|
32
|
+
if (!snapshot) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return (0, deserialize_share_data_1.deserializeShareData)(generic_1.PersistedShareDataSchema.parse(snapshot));
|
|
36
|
+
}
|
|
37
|
+
async loadAll() {
|
|
38
|
+
const snapshots = await this.loadAllSnapshots();
|
|
39
|
+
return snapshots.map((snapshot) => (0, deserialize_share_data_1.deserializeShareData)(snapshot));
|
|
40
|
+
}
|
|
41
|
+
async loadAllSnapshots() {
|
|
42
|
+
return [...this.snapshots.values()].map((snapshot) => generic_1.PersistedShareDataSchema.parse(snapshot));
|
|
43
|
+
}
|
|
44
|
+
async remove(id) {
|
|
45
|
+
this.snapshots.delete(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.InMemoryShareDataRepository = InMemoryShareDataRepository;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PersistedCreationConfig, PersistedShareData, PersistedTransaction, ShareBlockType } from '@degenerate/generic';
|
|
2
|
+
import { ShareData } from '../share-data/share-data';
|
|
3
|
+
/**
|
|
4
|
+
* Serializes a constructed ShareData into a {@link PersistedShareData}
|
|
5
|
+
* snapshot.
|
|
6
|
+
*
|
|
7
|
+
* @param blockType which factory built the block — drives builder
|
|
8
|
+
* resolution on reload.
|
|
9
|
+
* @param creationConfig the factory options minus `proofFor`
|
|
10
|
+
* (`metadata`, `creationPublicKey`, `initialState`),
|
|
11
|
+
* captured at creation time.
|
|
12
|
+
* @param block the live, fully-constructed block.
|
|
13
|
+
* @param transactionLog post-creation transactions captured at apply time,
|
|
14
|
+
* in the order they were applied.
|
|
15
|
+
*
|
|
16
|
+
* The `expected*` fields snapshot the block's finished state. They are NOT
|
|
17
|
+
* trusted on reload — `deserializeShareData` cross-checks them against an
|
|
18
|
+
* independently replayed block, so a snapshot whose state was edited without
|
|
19
|
+
* re-signing fails loudly.
|
|
20
|
+
*/
|
|
21
|
+
export declare const serializeShareData: (blockType: ShareBlockType, creationConfig: PersistedCreationConfig, block: ShareData, transactionLog: PersistedTransaction[]) => PersistedShareData;
|
|
22
|
+
//# sourceMappingURL=serialize-share-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize-share-data.d.ts","sourceRoot":"","sources":["../../src/persistence/serialize-share-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EAEjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAUrD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAC3B,WAAW,cAAc,EACzB,gBAAgB,uBAAuB,EACvC,OAAO,SAAS,EAChB,gBAAgB,oBAAoB,EAAE,KACvC,kBAgCF,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serializeShareData = void 0;
|
|
4
|
+
const generic_1 = require("@degenerate/generic");
|
|
5
|
+
// Produces the durable wire-format snapshot of a live ShareData. The snapshot
|
|
6
|
+
// is event-sourced: it captures the *creation recipe* (block type + creation
|
|
7
|
+
// config + structural proofs) plus an ordered transaction log, NOT a field
|
|
8
|
+
// dump of the finished object. The live block cannot supply the original
|
|
9
|
+
// partial `initialState` or per-transaction inputs/proposed-states (the class
|
|
10
|
+
// does not retain them), so those are passed in by the caller, which holds
|
|
11
|
+
// them at creation/apply time. See `deserializeShareData` for the inverse.
|
|
12
|
+
/**
|
|
13
|
+
* Serializes a constructed ShareData into a {@link PersistedShareData}
|
|
14
|
+
* snapshot.
|
|
15
|
+
*
|
|
16
|
+
* @param blockType which factory built the block — drives builder
|
|
17
|
+
* resolution on reload.
|
|
18
|
+
* @param creationConfig the factory options minus `proofFor`
|
|
19
|
+
* (`metadata`, `creationPublicKey`, `initialState`),
|
|
20
|
+
* captured at creation time.
|
|
21
|
+
* @param block the live, fully-constructed block.
|
|
22
|
+
* @param transactionLog post-creation transactions captured at apply time,
|
|
23
|
+
* in the order they were applied.
|
|
24
|
+
*
|
|
25
|
+
* The `expected*` fields snapshot the block's finished state. They are NOT
|
|
26
|
+
* trusted on reload — `deserializeShareData` cross-checks them against an
|
|
27
|
+
* independently replayed block, so a snapshot whose state was edited without
|
|
28
|
+
* re-signing fails loudly.
|
|
29
|
+
*/
|
|
30
|
+
const serializeShareData = (blockType, creationConfig, block, transactionLog) => {
|
|
31
|
+
// Re-key the structural proofs by `purpose` to match the wire format the
|
|
32
|
+
// lookup-based `proofFor` consumes on reload. The live block stores them
|
|
33
|
+
// as an ordered array; one proof per addFunction/addRule call.
|
|
34
|
+
const structuralProofs = {};
|
|
35
|
+
for (const proof of block.structuralProofs) {
|
|
36
|
+
structuralProofs[proof.purpose] = proof;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: generic_1.PERSISTED_SHARE_DATA_SCHEMA_VERSION,
|
|
40
|
+
id: block.id,
|
|
41
|
+
blockType,
|
|
42
|
+
signingCurve: block.signingCurve,
|
|
43
|
+
// `lastUpdated` is only ever set by `initializeCrypto`, so it still
|
|
44
|
+
// holds the creation timestamp — exactly the `timestamp` the client's
|
|
45
|
+
// initial-crypto payload carried.
|
|
46
|
+
initialCrypto: {
|
|
47
|
+
publicKey: block.creationPublicKey,
|
|
48
|
+
shareDataHash: block.creationHash,
|
|
49
|
+
shareDataSignature: block.creationSignature,
|
|
50
|
+
timestamp: block.lastUpdated,
|
|
51
|
+
},
|
|
52
|
+
creationConfig,
|
|
53
|
+
structuralProofs,
|
|
54
|
+
transactionLog,
|
|
55
|
+
expectedCurrentState: block.currentState,
|
|
56
|
+
expectedCreationHash: block.creationHash,
|
|
57
|
+
expectedCreationSignature: block.creationSignature,
|
|
58
|
+
expectedIsLocked: block.isLocked,
|
|
59
|
+
expectedVersion: block.version,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
exports.serializeShareData = serializeShareData;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PersistedShareData } from '@degenerate/generic';
|
|
2
|
+
import { ShareData } from '../share-data/share-data';
|
|
3
|
+
/**
|
|
4
|
+
* Storage abstraction for persisted ShareData snapshots.
|
|
5
|
+
*
|
|
6
|
+
* `save` takes a fully-built {@link PersistedShareData} rather than a live
|
|
7
|
+
* `ShareData`, because a constructed block does NOT retain the two pieces
|
|
8
|
+
* `serializeShareData` needs — the factory's original creation config and the
|
|
9
|
+
* per-transaction inputs/proposed-states. The caller (the `ShareDataManager`,
|
|
10
|
+
* which holds those at create/apply time) is responsible for serializing
|
|
11
|
+
* before it persists.
|
|
12
|
+
*
|
|
13
|
+
* `load` / `loadAll` are a TRUST GATE, not a passthrough: every reconstructed
|
|
14
|
+
* block is re-verified by {@link deserializeShareData} (creation signature +
|
|
15
|
+
* the full structural-proof chain + integrity cross-check against the stored
|
|
16
|
+
* `expected*` values). A snapshot that was edited on disk without re-signing
|
|
17
|
+
* throws rather than loading silently. Reconstruction requires the relevant
|
|
18
|
+
* block-type builders to be registered first (see `registerShareBlockFactories`
|
|
19
|
+
* in `@degenerate/shareblock`).
|
|
20
|
+
*/
|
|
21
|
+
export interface ShareDataRepository {
|
|
22
|
+
/** Durably store a snapshot, replacing any existing one with the same id. */
|
|
23
|
+
save(snapshot: PersistedShareData): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Reconstruct and re-verify the block stored under `id`, or resolve to
|
|
26
|
+
* `undefined` if none is stored. Throws if the stored snapshot fails
|
|
27
|
+
* verification.
|
|
28
|
+
*/
|
|
29
|
+
load(id: string): Promise<ShareData | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Reconstruct and re-verify every stored block. Throws on the first
|
|
32
|
+
* snapshot that fails verification — a corrupt store is surfaced, never
|
|
33
|
+
* partially trusted.
|
|
34
|
+
*/
|
|
35
|
+
loadAll(): Promise<ShareData[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Read every stored snapshot in its raw `PersistedShareData` form, schema-
|
|
38
|
+
* validated but NOT trust-verified. The trust gate (`deserializeShareData`)
|
|
39
|
+
* runs over the snapshot, not over a hand-built object, so a caller that
|
|
40
|
+
* needs to keep appending durable transactions — the `ShareDataManager`
|
|
41
|
+
* during `hydrate()` — gets both the verifiable snapshot and, from it, the
|
|
42
|
+
* creation recipe + existing transaction log a live block does not retain.
|
|
43
|
+
* Callers that only need executable blocks should use {@link loadAll}.
|
|
44
|
+
*/
|
|
45
|
+
loadAllSnapshots(): Promise<PersistedShareData[]>;
|
|
46
|
+
/** Remove the snapshot stored under `id`. A no-op if none is stored. */
|
|
47
|
+
remove(id: string): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=share-data-repository.d.ts.map
|