@degenerate/sharedata 0.2.7 → 0.4.0

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.
Files changed (80) hide show
  1. package/README.md +31 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +20 -0
  5. package/dist/manager/share-data-manager-events.d.ts +27 -0
  6. package/dist/manager/share-data-manager-events.d.ts.map +1 -0
  7. package/dist/manager/share-data-manager-events.js +2 -0
  8. package/dist/manager/share-data-manager.d.ts +84 -0
  9. package/dist/manager/share-data-manager.d.ts.map +1 -0
  10. package/dist/manager/share-data-manager.js +210 -0
  11. package/dist/persistence/block-factory-registry.d.ts +57 -0
  12. package/dist/persistence/block-factory-registry.d.ts.map +1 -0
  13. package/dist/persistence/block-factory-registry.js +36 -0
  14. package/dist/persistence/deserialize-share-data.d.ts +18 -0
  15. package/dist/persistence/deserialize-share-data.d.ts.map +1 -0
  16. package/dist/persistence/deserialize-share-data.js +149 -0
  17. package/dist/persistence/file-share-data-repository.d.ts +30 -0
  18. package/dist/persistence/file-share-data-repository.d.ts.map +1 -0
  19. package/dist/persistence/file-share-data-repository.js +147 -0
  20. package/dist/persistence/firestore-share-data-repository.d.ts +38 -0
  21. package/dist/persistence/firestore-share-data-repository.d.ts.map +1 -0
  22. package/dist/persistence/firestore-share-data-repository.js +78 -0
  23. package/dist/persistence/in-memory-share-data-repository.d.ts +19 -0
  24. package/dist/persistence/in-memory-share-data-repository.d.ts.map +1 -0
  25. package/dist/persistence/in-memory-share-data-repository.js +48 -0
  26. package/dist/persistence/serialize-share-data.d.ts +22 -0
  27. package/dist/persistence/serialize-share-data.d.ts.map +1 -0
  28. package/dist/persistence/serialize-share-data.js +66 -0
  29. package/dist/persistence/share-data-repository.d.ts +49 -0
  30. package/dist/persistence/share-data-repository.d.ts.map +1 -0
  31. package/dist/persistence/share-data-repository.js +2 -0
  32. package/dist/share-data/creation/create-share-data-initial-crypto.d.ts +11 -0
  33. package/dist/share-data/creation/create-share-data-initial-crypto.d.ts.map +1 -0
  34. package/dist/share-data/creation/create-share-data-initial-crypto.js +26 -0
  35. package/dist/share-data/helpers/calculate-share-data-string.d.ts +21 -0
  36. package/dist/share-data/helpers/calculate-share-data-string.d.ts.map +1 -0
  37. package/dist/share-data/helpers/calculate-share-data-string.js +71 -0
  38. package/dist/share-data/helpers/config/default-share-data-string.config.d.ts +7 -0
  39. package/dist/share-data/helpers/config/default-share-data-string.config.d.ts.map +1 -0
  40. package/dist/share-data/helpers/config/default-share-data-string.config.js +30 -0
  41. package/dist/share-data/helpers/config/stringify.d.ts +5 -0
  42. package/dist/share-data/helpers/config/stringify.d.ts.map +1 -0
  43. package/dist/share-data/helpers/config/stringify.js +10 -0
  44. package/dist/share-data/helpers/get-array-string.d.ts +6 -0
  45. package/dist/share-data/helpers/get-array-string.d.ts.map +1 -0
  46. package/dist/share-data/helpers/get-array-string.js +17 -0
  47. package/dist/share-data/proofs/creation/build-transaction-proposal-signer.d.ts +57 -0
  48. package/dist/share-data/proofs/creation/build-transaction-proposal-signer.d.ts.map +1 -0
  49. package/dist/share-data/proofs/creation/build-transaction-proposal-signer.js +36 -0
  50. package/dist/share-data/proofs/creation/create-share-data-proposal-proof.d.ts +12 -0
  51. package/dist/share-data/proofs/creation/create-share-data-proposal-proof.d.ts.map +1 -0
  52. package/dist/share-data/proofs/creation/create-share-data-proposal-proof.js +41 -0
  53. package/dist/share-data/proofs/creation/create-transaction-proposal-proof.d.ts +27 -0
  54. package/dist/share-data/proofs/creation/create-transaction-proposal-proof.d.ts.map +1 -0
  55. package/dist/share-data/proofs/creation/create-transaction-proposal-proof.js +58 -0
  56. package/dist/share-data/proofs/helpers/create-share-data-string-to-sign.d.ts +12 -0
  57. package/dist/share-data/proofs/helpers/create-share-data-string-to-sign.d.ts.map +1 -0
  58. package/dist/share-data/proofs/helpers/create-share-data-string-to-sign.js +17 -0
  59. package/dist/share-data/proofs/helpers/create-transaction-proposal-string-to-sign.d.ts +24 -0
  60. package/dist/share-data/proofs/helpers/create-transaction-proposal-string-to-sign.d.ts.map +1 -0
  61. package/dist/share-data/proofs/helpers/create-transaction-proposal-string-to-sign.js +29 -0
  62. package/dist/share-data/proofs/helpers/get-previous-hash-from-share-data.d.ts +9 -0
  63. package/dist/share-data/proofs/helpers/get-previous-hash-from-share-data.d.ts.map +1 -0
  64. package/dist/share-data/proofs/helpers/get-previous-hash-from-share-data.js +28 -0
  65. package/dist/share-data/proofs/helpers/hash-proposed-state.d.ts +12 -0
  66. package/dist/share-data/proofs/helpers/hash-proposed-state.d.ts.map +1 -0
  67. package/dist/share-data/proofs/helpers/hash-proposed-state.js +18 -0
  68. package/dist/share-data/proofs/testing/test-proof-signer.d.ts +53 -0
  69. package/dist/share-data/proofs/testing/test-proof-signer.d.ts.map +1 -0
  70. package/dist/share-data/proofs/testing/test-proof-signer.js +113 -0
  71. package/dist/share-data/proofs/verification/verify-share-data-proposal-proof.d.ts +40 -0
  72. package/dist/share-data/proofs/verification/verify-share-data-proposal-proof.d.ts.map +1 -0
  73. package/dist/share-data/proofs/verification/verify-share-data-proposal-proof.js +126 -0
  74. package/dist/share-data/proofs/verification/verify-transaction-proposal-proof.d.ts +37 -0
  75. package/dist/share-data/proofs/verification/verify-transaction-proposal-proof.d.ts.map +1 -0
  76. package/dist/share-data/proofs/verification/verify-transaction-proposal-proof.js +125 -0
  77. package/dist/share-data/share-data.d.ts +232 -27
  78. package/dist/share-data/share-data.d.ts.map +1 -1
  79. package/dist/share-data/share-data.js +390 -60
  80. package/package.json +31 -13
@@ -0,0 +1,149 @@
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
+ authorizedSigners: parsed.authorizedSigners,
59
+ });
60
+ // Replay the transaction log in recorded order.
61
+ for (const entry of parsed.transactionLog) {
62
+ const transactionFunction = block.functions.find((fn) => fn.id === entry.functionId);
63
+ if (!transactionFunction) {
64
+ throw new generic_1.ShareDataPersistenceError(parsed.id, `transaction log references unknown functionId "${entry.functionId}"`);
65
+ }
66
+ // Re-authorize the signer before re-verifying the signature. A tampered
67
+ // snapshot could swap in a proof validly signed by a *different* key
68
+ // over the same proposed state — that would pass signature
69
+ // verification, so the proposer-identity gate (the same one
70
+ // addTransaction enforces live) must run on replay too. Checked first
71
+ // so a swapped key fails as a not-authorized error rather than a
72
+ // signature mismatch. Uses the same widened predicate as the live gate:
73
+ // the creation key for any function, or a delegate for its granted
74
+ // function (`entry.proof.purpose` is the function id). The signer set was
75
+ // re-derived from re-verified registration proofs during construction
76
+ // above, so an injected grant with no valid proof never reaches here.
77
+ if (!block.isAuthorizedSigner(entry.proof.publicKey, entry.proof.purpose)) {
78
+ throw new generic_1.ShareDataTransactionNotAuthorizedError(parsed.id, entry.proof.publicKey);
79
+ }
80
+ // Re-verify the transaction proof against the block at its pre-entry
81
+ // state (where it now sits, having replayed every earlier entry) and
82
+ // the proposed state about to be applied. This is the runtime arm of
83
+ // the trust gate: a snapshot whose proposed state or proof was edited
84
+ // throws ShareDataTransactionProofFailedError here rather than being
85
+ // loaded. Verifying before the apply keeps the recomputed current hash
86
+ // aligned with what the signer signed over.
87
+ (0, verify_transaction_proposal_proof_1.verifyTransactionProposalProof)(entry.proof, block, entry.proposedState, block.signingCurve);
88
+ // addTransactionAlreadyValidated derives its accept/reject outcome
89
+ // from (proposedResult && currentResult). Setting both to the stored
90
+ // boolean replays the original outcome exactly: currentState is
91
+ // replaced only when the recorded transaction was accepted.
92
+ const rulesEvaluation = {
93
+ ruleId: '',
94
+ currentResult: entry.ruleOutcome,
95
+ currentConditionFailure: [],
96
+ proposedResult: entry.ruleOutcome,
97
+ proposedConditionFailure: [],
98
+ };
99
+ // Pass the proof so the rebuilt history entry matches the live one;
100
+ // the canonical hash folds history in, so a later transaction's proof
101
+ // verifies only if earlier entries reproduce their proofs.
102
+ block.addTransactionAlreadyValidated(entry.proposedState, rulesEvaluation, transactionFunction, entry.transactionTime, entry.proof);
103
+ }
104
+ // Locking only gates future addFunction/addRule, so applying it after
105
+ // transaction replay reproduces the snapshot's lock state without
106
+ // affecting the rebuilt data.
107
+ if (parsed.expectedIsLocked) {
108
+ block.initialize();
109
+ }
110
+ assertIntegrity(block, parsed);
111
+ return block;
112
+ };
113
+ exports.deserializeShareData = deserializeShareData;
114
+ /**
115
+ * Cross-checks the independently rebuilt block against the snapshot's
116
+ * `expected*` integrity values. A divergence means the snapshot was altered
117
+ * after signing (or the build that wrote it disagrees with this one), so we
118
+ * refuse the load with {@link ShareDataIntegrityError}.
119
+ */
120
+ const assertIntegrity = (block, parsed) => {
121
+ const mismatch = block.creationHash !== parsed.expectedCreationHash ||
122
+ block.creationSignature !== parsed.expectedCreationSignature ||
123
+ block.isLocked !== parsed.expectedIsLocked ||
124
+ block.version !== parsed.expectedVersion ||
125
+ canonicalize(block.currentState) !== canonicalize(parsed.expectedCurrentState);
126
+ if (mismatch) {
127
+ throw new generic_1.ShareDataIntegrityError(parsed.id);
128
+ }
129
+ };
130
+ /**
131
+ * Order-independent JSON canonicalization so `currentState` equality does not
132
+ * hinge on object key insertion order.
133
+ */
134
+ const canonicalize = (value) => JSON.stringify(sortKeys(value));
135
+ const sortKeys = (value) => {
136
+ if (Array.isArray(value)) {
137
+ return value.map(sortKeys);
138
+ }
139
+ if (value && typeof value === 'object') {
140
+ const source = value;
141
+ return Object.keys(source)
142
+ .sort()
143
+ .reduce((acc, key) => {
144
+ acc[key] = sortKeys(source[key]);
145
+ return acc;
146
+ }, {});
147
+ }
148
+ return value;
149
+ };
@@ -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,kBAoCF,CAAC"}
@@ -0,0 +1,66 @@
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
+ // The grant tuples re-applied via `addAuthorizedSigner` on replay; the
55
+ // matching registration proofs are already carried in `structuralProofs`
56
+ // (keyed by their `delegate:` purpose). Empty for non-delegating blocks.
57
+ authorizedSigners: block.authorizedSigners,
58
+ transactionLog,
59
+ expectedCurrentState: block.currentState,
60
+ expectedCreationHash: block.creationHash,
61
+ expectedCreationSignature: block.creationSignature,
62
+ expectedIsLocked: block.isLocked,
63
+ expectedVersion: block.version,
64
+ };
65
+ };
66
+ exports.serializeShareData = serializeShareData;