@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
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ The `@degenerate/sharedata` package is a core component of the SAFE system, resp
|
|
|
8
8
|
## Contents
|
|
9
9
|
|
|
10
10
|
1. [Installation](#installation)
|
|
11
|
+
2. [Persistence](#persistence)
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
@@ -17,3 +18,33 @@ To install the `@degenerate/sharedata` package, use the following command:
|
|
|
17
18
|
```bash
|
|
18
19
|
npm install @degenerate/sharedata
|
|
19
20
|
```
|
|
21
|
+
|
|
22
|
+
## Persistence
|
|
23
|
+
|
|
24
|
+
`ShareData` state is **durable** — it survives a process restart. A
|
|
25
|
+
`ShareDataManager` constructed with a `ShareDataRepository` hydrates its blocks
|
|
26
|
+
on `await manager.hydrate()` and write-through-persists every `addShareData` /
|
|
27
|
+
`applyTransaction` / `removeShareDataById`. Constructed with no repository it
|
|
28
|
+
behaves exactly as before (in-memory only), so existing callers are untouched.
|
|
29
|
+
|
|
30
|
+
Loads are a **trust gate**, never a passthrough: each snapshot is replayed
|
|
31
|
+
through `deserializeShareData`, which re-verifies the creation signature, the
|
|
32
|
+
full structural-proof chain, and an integrity cross-check against the stored
|
|
33
|
+
`expected*` values. A snapshot edited out-of-band fails loudly instead of
|
|
34
|
+
loading silently. Reconstruction needs the block-type builders registered
|
|
35
|
+
first — call `registerShareBlockFactories()` from `@degenerate/shareblock`
|
|
36
|
+
during start-up.
|
|
37
|
+
|
|
38
|
+
Three interchangeable implementations of the `ShareDataRepository` interface
|
|
39
|
+
ship here:
|
|
40
|
+
|
|
41
|
+
| Repository | Backing store | Typical use |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `FileShareDataRepository` | one JSON file per block (atomic temp-file + rename writes) | single-node durability with no external service |
|
|
44
|
+
| `FirestoreShareDataRepository` | one Firestore document per block | hosted/multi-node deployments |
|
|
45
|
+
| `InMemoryShareDataRepository` | an in-process `Map` | tests and parity (still round-trips the trust gate) |
|
|
46
|
+
|
|
47
|
+
`firebase-admin` is an **optional peer dependency** — only install it if you
|
|
48
|
+
use `FirestoreShareDataRepository`. The repository takes an injected
|
|
49
|
+
`firestore.Firestore` instance (the host initializes its own app); the package
|
|
50
|
+
never references `firebase-admin` at runtime.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
|
+
export * from './manager/share-data-manager';
|
|
2
|
+
export * from './manager/share-data-manager-events';
|
|
3
|
+
export * from './persistence/block-factory-registry';
|
|
4
|
+
export * from './persistence/serialize-share-data';
|
|
5
|
+
export * from './persistence/deserialize-share-data';
|
|
6
|
+
export * from './persistence/share-data-repository';
|
|
7
|
+
export * from './persistence/file-share-data-repository';
|
|
8
|
+
export * from './persistence/firestore-share-data-repository';
|
|
9
|
+
export * from './persistence/in-memory-share-data-repository';
|
|
1
10
|
export * from './share-data/share-data';
|
|
11
|
+
export * from './share-data/creation/create-share-data-initial-crypto';
|
|
12
|
+
export * from './share-data/proofs/creation/create-share-data-proposal-proof';
|
|
13
|
+
export * from './share-data/proofs/creation/create-transaction-proposal-proof';
|
|
14
|
+
export * from './share-data/proofs/creation/build-transaction-proposal-signer';
|
|
15
|
+
export * from './share-data/proofs/verification/verify-share-data-proposal-proof';
|
|
16
|
+
export * from './share-data/proofs/verification/verify-transaction-proposal-proof';
|
|
17
|
+
export * from './share-data/proofs/testing/test-proof-signer';
|
|
2
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qCAAqC,CAAC;AACpD,cAAc,sCAAsC,CAAC;AACrD,cAAc,oCAAoC,CAAC;AACnD,cAAc,sCAAsC,CAAC;AACrD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yBAAyB,CAAC;AACxC,cAAc,wDAAwD,CAAC;AACvE,cAAc,+DAA+D,CAAC;AAC9E,cAAc,gEAAgE,CAAC;AAC/E,cAAc,gEAAgE,CAAC;AAC/E,cAAc,mEAAmE,CAAC;AAClF,cAAc,oEAAoE,CAAC;AAKnF,cAAc,+CAA+C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,24 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./manager/share-data-manager"), exports);
|
|
18
|
+
__exportStar(require("./manager/share-data-manager-events"), exports);
|
|
19
|
+
__exportStar(require("./persistence/block-factory-registry"), exports);
|
|
20
|
+
__exportStar(require("./persistence/serialize-share-data"), exports);
|
|
21
|
+
__exportStar(require("./persistence/deserialize-share-data"), exports);
|
|
22
|
+
__exportStar(require("./persistence/share-data-repository"), exports);
|
|
23
|
+
__exportStar(require("./persistence/file-share-data-repository"), exports);
|
|
24
|
+
__exportStar(require("./persistence/firestore-share-data-repository"), exports);
|
|
25
|
+
__exportStar(require("./persistence/in-memory-share-data-repository"), exports);
|
|
17
26
|
__exportStar(require("./share-data/share-data"), exports);
|
|
27
|
+
__exportStar(require("./share-data/creation/create-share-data-initial-crypto"), exports);
|
|
28
|
+
__exportStar(require("./share-data/proofs/creation/create-share-data-proposal-proof"), exports);
|
|
29
|
+
__exportStar(require("./share-data/proofs/creation/create-transaction-proposal-proof"), exports);
|
|
30
|
+
__exportStar(require("./share-data/proofs/creation/build-transaction-proposal-signer"), exports);
|
|
31
|
+
__exportStar(require("./share-data/proofs/verification/verify-share-data-proposal-proof"), exports);
|
|
32
|
+
__exportStar(require("./share-data/proofs/verification/verify-transaction-proposal-proof"), exports);
|
|
33
|
+
// Test-only signing helpers. Naming convention (`generateTest*`,
|
|
34
|
+
// `createTestProofFor`) is the documentation — these are not for
|
|
35
|
+
// production use. Production wiring lives in
|
|
36
|
+
// `buildEllipticalCurvesProofFor` from `@degenerate/shareblock`.
|
|
37
|
+
__exportStar(require("./share-data/proofs/testing/test-proof-signer"), exports);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IShareData, IShareFunction } from '@degenerate/generic';
|
|
2
|
+
export type ShareDataManagerEventType = 'share-data:added' | 'share-data:updated' | 'share-data:removed' | 'share-data:transaction';
|
|
3
|
+
export interface ShareDataAddedEvent {
|
|
4
|
+
type: 'share-data:added';
|
|
5
|
+
shareDataId: string;
|
|
6
|
+
currentState: IShareData;
|
|
7
|
+
}
|
|
8
|
+
export interface ShareDataUpdatedEvent {
|
|
9
|
+
type: 'share-data:updated';
|
|
10
|
+
shareDataId: string;
|
|
11
|
+
currentState: IShareData;
|
|
12
|
+
}
|
|
13
|
+
export interface ShareDataRemovedEvent {
|
|
14
|
+
type: 'share-data:removed';
|
|
15
|
+
shareDataId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ShareDataTransactionEvent {
|
|
18
|
+
type: 'share-data:transaction';
|
|
19
|
+
shareDataId: string;
|
|
20
|
+
transactionId: string;
|
|
21
|
+
accepted: boolean;
|
|
22
|
+
currentState: IShareData;
|
|
23
|
+
transactionFunction: IShareFunction<any, (...args: any[]) => any, any>;
|
|
24
|
+
}
|
|
25
|
+
export type ShareDataManagerEvent = ShareDataAddedEvent | ShareDataUpdatedEvent | ShareDataRemovedEvent | ShareDataTransactionEvent;
|
|
26
|
+
export type ShareDataManagerListener<E extends ShareDataManagerEvent = ShareDataManagerEvent> = (event: E) => void;
|
|
27
|
+
//# sourceMappingURL=share-data-manager-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share-data-manager-events.d.ts","sourceRoot":"","sources":["../../src/manager/share-data-manager-events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEjE,MAAM,MAAM,yBAAyB,GAC/B,kBAAkB,GAClB,oBAAoB,GACpB,oBAAoB,GACpB,wBAAwB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,UAAU,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IAClC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACtC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,UAAU,CAAC;IACzB,mBAAmB,EAAE,cAAc,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,GAAG,CAAC,CAAC;CAC1E;AAED,MAAM,MAAM,qBAAqB,GAC3B,mBAAmB,GACnB,qBAAqB,GACrB,qBAAqB,GACrB,yBAAyB,CAAC;AAEhC,MAAM,MAAM,wBAAwB,CAAC,CAAC,SAAS,qBAAqB,GAAG,qBAAqB,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { IShareData, IShareDataCryptoProof, IShareFunction, ShareDataCreationRecord } from '@degenerate/generic';
|
|
2
|
+
import { ShareData } from '../share-data/share-data';
|
|
3
|
+
import { ShareDataRepository } from '../persistence/share-data-repository';
|
|
4
|
+
import { ShareDataManagerEventType, ShareDataManagerListener } from './share-data-manager-events';
|
|
5
|
+
export declare class ShareDataManager {
|
|
6
|
+
private _shareDataObjects;
|
|
7
|
+
private _emitter;
|
|
8
|
+
private readonly _repository?;
|
|
9
|
+
private _persistence;
|
|
10
|
+
/**
|
|
11
|
+
* @param repository optional durable store. When supplied, blocks added
|
|
12
|
+
* with a creation record are persisted and every subsequent mutation
|
|
13
|
+
* writes through; call {@link hydrate} once at startup to reload them.
|
|
14
|
+
* The no-arg form stays purely in-memory, so existing callers/tests are
|
|
15
|
+
* untouched.
|
|
16
|
+
*/
|
|
17
|
+
constructor(repository?: ShareDataRepository);
|
|
18
|
+
/**
|
|
19
|
+
* Loads every durably-stored block into memory, re-verifying each through
|
|
20
|
+
* the trust gate, and recovers its per-block persistence state so later
|
|
21
|
+
* transactions keep writing through. A no-op when no repository was
|
|
22
|
+
* supplied. Call once at startup, before serving traffic. Throws if a
|
|
23
|
+
* stored snapshot fails verification or collides with an already-registered
|
|
24
|
+
* id.
|
|
25
|
+
*/
|
|
26
|
+
hydrate(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Registers a block and, when a {@link ShareDataCreationRecord} is supplied
|
|
29
|
+
* and a repository is configured, durably persists it. The record carries
|
|
30
|
+
* the two things a live block cannot reproduce on its own — the block type
|
|
31
|
+
* and the factory creation config — which the manager needs to keep
|
|
32
|
+
* re-serializing the block as transactions accrue. Without a record (or
|
|
33
|
+
* without a repository) the block is in-memory only.
|
|
34
|
+
*
|
|
35
|
+
* Persists before registering: a durable-write failure rejects and the
|
|
36
|
+
* block never enters the in-memory set, so memory and store cannot diverge.
|
|
37
|
+
*/
|
|
38
|
+
addShareData(shareData: ShareData, creationRecord?: ShareDataCreationRecord): Promise<void>;
|
|
39
|
+
getShareDataById(id: string): ShareData | undefined;
|
|
40
|
+
updateShareData(shareData: ShareData): void;
|
|
41
|
+
/**
|
|
42
|
+
* Removes a block and, when it was persisted, deletes its durable snapshot
|
|
43
|
+
* first so a delete that cannot be recorded rejects before memory changes.
|
|
44
|
+
*/
|
|
45
|
+
removeShareDataById(id: string): Promise<void>;
|
|
46
|
+
getShareData(): ShareData[];
|
|
47
|
+
getShareDataData(): IShareData[];
|
|
48
|
+
/**
|
|
49
|
+
* Returns the current state of the ShareData with the given id. Throws
|
|
50
|
+
* {@link ShareDataNotFoundError} when no ShareData is registered under
|
|
51
|
+
* that id — i.e., the return is never `undefined`. Callers that need a
|
|
52
|
+
* "lookup or null" shape should use {@link getShareDataById} instead.
|
|
53
|
+
*/
|
|
54
|
+
getCurrentShareDataState(id: string): IShareData;
|
|
55
|
+
/**
|
|
56
|
+
* Proposes a transaction against the ShareData identified by `id`. The
|
|
57
|
+
* `proof` must cryptographically authorize moving the block to exactly
|
|
58
|
+
* `proposedData` via `transactionFunction` — it is verified by
|
|
59
|
+
* `ShareData.addTransaction` before the rule engine runs, so an unsigned or
|
|
60
|
+
* mis-signed proposal never reaches the rules or mutates state. `proof`
|
|
61
|
+
* is required precisely so nothing gets in without a signature: this is the
|
|
62
|
+
* canonical mutation entry point (the realtime propose handler routes
|
|
63
|
+
* here), and there is deliberately no unsigned variant on it.
|
|
64
|
+
*
|
|
65
|
+
* Once authorized, the proposal is judged by `startRuleEvaluation`;
|
|
66
|
+
* `_currentState` is replaced only if accepted. When the block is
|
|
67
|
+
* persisted, the appended transaction is written through before the event
|
|
68
|
+
* is emitted — a durable-write failure rejects rather than silently
|
|
69
|
+
* advancing only memory. Either way a transaction event is emitted with the
|
|
70
|
+
* outcome so observers can react.
|
|
71
|
+
*
|
|
72
|
+
* Library-internal callers that operate before the signing boundary (replay,
|
|
73
|
+
* seeding) use `ShareData.addTransactionNotValidated` directly; that path
|
|
74
|
+
* bypasses both the proof check and the emitter.
|
|
75
|
+
*/
|
|
76
|
+
applyTransaction(id: string, proposedData: IShareData, transactionFunction: IShareFunction<any, (...args: any[]) => any, any>, proof: IShareDataCryptoProof): Promise<{
|
|
77
|
+
accepted: boolean;
|
|
78
|
+
transactionId: string;
|
|
79
|
+
}>;
|
|
80
|
+
on(type: ShareDataManagerEventType, listener: ShareDataManagerListener): void;
|
|
81
|
+
off(type: ShareDataManagerEventType, listener: ShareDataManagerListener): void;
|
|
82
|
+
private _emit;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=share-data-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share-data-manager.d.ts","sourceRoot":"","sources":["../../src/manager/share-data-manager.ts"],"names":[],"mappings":"AACA,OAAO,EACH,UAAU,EACV,qBAAqB,EACrB,cAAc,EAGd,uBAAuB,EAG1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAEH,yBAAyB,EACzB,wBAAwB,EAC3B,MAAM,6BAA6B,CAAC;AAmBrC,qBAAa,gBAAgB;IACzB,OAAO,CAAC,iBAAiB,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAe;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAsB;IACnD,OAAO,CAAC,YAAY,CAAgC;IAEpD;;;;;;OAMG;gBACS,UAAU,CAAC,EAAE,mBAAmB;IAO5C;;;;;;;OAOG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB9B;;;;;;;;;;OAUG;IACG,YAAY,CACd,SAAS,EAAE,SAAS,EACpB,cAAc,CAAC,EAAE,uBAAuB,GACzC,OAAO,CAAC,IAAI,CAAC;IA+BhB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAInD,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAkB3C;;;OAGG;IACG,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBpD,YAAY,IAAI,SAAS,EAAE;IAI3B,gBAAgB,IAAI,UAAU,EAAE;IAIhC;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU;IAQhD;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,gBAAgB,CAClB,EAAE,EAAE,MAAM,EACV,YAAY,EAAE,UAAU,EACxB,mBAAmB,EAAE,cAAc,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,GAAG,CAAC,EACtE,KAAK,EAAE,qBAAqB,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IAmDxD,EAAE,CAAC,IAAI,EAAE,yBAAyB,EAAE,QAAQ,EAAE,wBAAwB,GAAG,IAAI;IAI7E,GAAG,CAAC,IAAI,EAAE,yBAAyB,EAAE,QAAQ,EAAE,wBAAwB,GAAG,IAAI;IAI9E,OAAO,CAAC,KAAK;CAGhB"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ShareDataManager = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const generic_1 = require("@degenerate/generic");
|
|
6
|
+
const deserialize_share_data_1 = require("../persistence/deserialize-share-data");
|
|
7
|
+
const serialize_share_data_1 = require("../persistence/serialize-share-data");
|
|
8
|
+
class ShareDataManager {
|
|
9
|
+
/**
|
|
10
|
+
* @param repository optional durable store. When supplied, blocks added
|
|
11
|
+
* with a creation record are persisted and every subsequent mutation
|
|
12
|
+
* writes through; call {@link hydrate} once at startup to reload them.
|
|
13
|
+
* The no-arg form stays purely in-memory, so existing callers/tests are
|
|
14
|
+
* untouched.
|
|
15
|
+
*/
|
|
16
|
+
constructor(repository) {
|
|
17
|
+
this._shareDataObjects = [];
|
|
18
|
+
this._emitter = new events_1.EventEmitter();
|
|
19
|
+
this._repository = repository;
|
|
20
|
+
this._persistence = new Map();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Loads every durably-stored block into memory, re-verifying each through
|
|
24
|
+
* the trust gate, and recovers its per-block persistence state so later
|
|
25
|
+
* transactions keep writing through. A no-op when no repository was
|
|
26
|
+
* supplied. Call once at startup, before serving traffic. Throws if a
|
|
27
|
+
* stored snapshot fails verification or collides with an already-registered
|
|
28
|
+
* id.
|
|
29
|
+
*/
|
|
30
|
+
async hydrate() {
|
|
31
|
+
if (!this._repository) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const snapshots = await this._repository.loadAllSnapshots();
|
|
35
|
+
for (const snapshot of snapshots) {
|
|
36
|
+
// The trust gate: a tampered snapshot throws here rather than being
|
|
37
|
+
// loaded. The executable block and the recovered persistence state
|
|
38
|
+
// are rebuilt from the same bytes.
|
|
39
|
+
const block = (0, deserialize_share_data_1.deserializeShareData)(snapshot);
|
|
40
|
+
if (this.getShareDataById(block.id)) {
|
|
41
|
+
throw new generic_1.ShareDataAlreadyExistsError(block.id);
|
|
42
|
+
}
|
|
43
|
+
this._shareDataObjects.push(block);
|
|
44
|
+
this._persistence.set(block.id, {
|
|
45
|
+
blockType: snapshot.blockType,
|
|
46
|
+
creationConfig: snapshot.creationConfig,
|
|
47
|
+
transactionLog: [...snapshot.transactionLog],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Registers a block and, when a {@link ShareDataCreationRecord} is supplied
|
|
53
|
+
* and a repository is configured, durably persists it. The record carries
|
|
54
|
+
* the two things a live block cannot reproduce on its own — the block type
|
|
55
|
+
* and the factory creation config — which the manager needs to keep
|
|
56
|
+
* re-serializing the block as transactions accrue. Without a record (or
|
|
57
|
+
* without a repository) the block is in-memory only.
|
|
58
|
+
*
|
|
59
|
+
* Persists before registering: a durable-write failure rejects and the
|
|
60
|
+
* block never enters the in-memory set, so memory and store cannot diverge.
|
|
61
|
+
*/
|
|
62
|
+
async addShareData(shareData, creationRecord) {
|
|
63
|
+
const existingShareData = this.getShareDataById(shareData.id);
|
|
64
|
+
if (existingShareData) {
|
|
65
|
+
throw new generic_1.ShareDataAlreadyExistsError(shareData.id);
|
|
66
|
+
}
|
|
67
|
+
if (this._repository && creationRecord) {
|
|
68
|
+
const persistence = {
|
|
69
|
+
blockType: creationRecord.blockType,
|
|
70
|
+
creationConfig: creationRecord.creationConfig,
|
|
71
|
+
transactionLog: [],
|
|
72
|
+
};
|
|
73
|
+
await this._repository.save((0, serialize_share_data_1.serializeShareData)(persistence.blockType, persistence.creationConfig, shareData, persistence.transactionLog));
|
|
74
|
+
this._persistence.set(shareData.id, persistence);
|
|
75
|
+
}
|
|
76
|
+
this._shareDataObjects.push(shareData);
|
|
77
|
+
this._emit({
|
|
78
|
+
type: 'share-data:added',
|
|
79
|
+
shareDataId: shareData.id,
|
|
80
|
+
currentState: shareData.currentState,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
getShareDataById(id) {
|
|
84
|
+
return this._shareDataObjects.find((shareData) => shareData.id === id);
|
|
85
|
+
}
|
|
86
|
+
updateShareData(shareData) {
|
|
87
|
+
const existingShareData = this.getShareDataById(shareData.id);
|
|
88
|
+
if (!existingShareData) {
|
|
89
|
+
throw new generic_1.ShareDataNotFoundError(shareData.id);
|
|
90
|
+
}
|
|
91
|
+
this._shareDataObjects = this._shareDataObjects.map((sd) => {
|
|
92
|
+
if (sd.id === shareData.id) {
|
|
93
|
+
return shareData;
|
|
94
|
+
}
|
|
95
|
+
return sd;
|
|
96
|
+
});
|
|
97
|
+
this._emit({
|
|
98
|
+
type: 'share-data:updated',
|
|
99
|
+
shareDataId: shareData.id,
|
|
100
|
+
currentState: shareData.currentState,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Removes a block and, when it was persisted, deletes its durable snapshot
|
|
105
|
+
* first so a delete that cannot be recorded rejects before memory changes.
|
|
106
|
+
*/
|
|
107
|
+
async removeShareDataById(id) {
|
|
108
|
+
const existingShareData = this.getShareDataById(id);
|
|
109
|
+
if (!existingShareData) {
|
|
110
|
+
throw new generic_1.ShareDataNotFoundError(id);
|
|
111
|
+
}
|
|
112
|
+
if (this._repository && this._persistence.has(id)) {
|
|
113
|
+
await this._repository.remove(id);
|
|
114
|
+
}
|
|
115
|
+
this._persistence.delete(id);
|
|
116
|
+
this._shareDataObjects = this._shareDataObjects.filter((shareData) => shareData.id !== id);
|
|
117
|
+
this._emit({
|
|
118
|
+
type: 'share-data:removed',
|
|
119
|
+
shareDataId: id,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
getShareData() {
|
|
123
|
+
return this._shareDataObjects;
|
|
124
|
+
}
|
|
125
|
+
getShareDataData() {
|
|
126
|
+
return this._shareDataObjects.map((shareData) => shareData.currentState);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns the current state of the ShareData with the given id. Throws
|
|
130
|
+
* {@link ShareDataNotFoundError} when no ShareData is registered under
|
|
131
|
+
* that id — i.e., the return is never `undefined`. Callers that need a
|
|
132
|
+
* "lookup or null" shape should use {@link getShareDataById} instead.
|
|
133
|
+
*/
|
|
134
|
+
getCurrentShareDataState(id) {
|
|
135
|
+
const shareData = this.getShareDataById(id);
|
|
136
|
+
if (!shareData) {
|
|
137
|
+
throw new generic_1.ShareDataNotFoundError(id);
|
|
138
|
+
}
|
|
139
|
+
return shareData.currentState;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Proposes a transaction against the ShareData identified by `id`. The
|
|
143
|
+
* `proof` must cryptographically authorize moving the block to exactly
|
|
144
|
+
* `proposedData` via `transactionFunction` — it is verified by
|
|
145
|
+
* `ShareData.addTransaction` before the rule engine runs, so an unsigned or
|
|
146
|
+
* mis-signed proposal never reaches the rules or mutates state. `proof`
|
|
147
|
+
* is required precisely so nothing gets in without a signature: this is the
|
|
148
|
+
* canonical mutation entry point (the realtime propose handler routes
|
|
149
|
+
* here), and there is deliberately no unsigned variant on it.
|
|
150
|
+
*
|
|
151
|
+
* Once authorized, the proposal is judged by `startRuleEvaluation`;
|
|
152
|
+
* `_currentState` is replaced only if accepted. When the block is
|
|
153
|
+
* persisted, the appended transaction is written through before the event
|
|
154
|
+
* is emitted — a durable-write failure rejects rather than silently
|
|
155
|
+
* advancing only memory. Either way a transaction event is emitted with the
|
|
156
|
+
* outcome so observers can react.
|
|
157
|
+
*
|
|
158
|
+
* Library-internal callers that operate before the signing boundary (replay,
|
|
159
|
+
* seeding) use `ShareData.addTransactionNotValidated` directly; that path
|
|
160
|
+
* bypasses both the proof check and the emitter.
|
|
161
|
+
*/
|
|
162
|
+
async applyTransaction(id, proposedData, transactionFunction, proof) {
|
|
163
|
+
const shareData = this.getShareDataById(id);
|
|
164
|
+
if (!shareData) {
|
|
165
|
+
throw new generic_1.ShareDataNotFoundError(id);
|
|
166
|
+
}
|
|
167
|
+
const outcome = shareData.addTransaction(proposedData, transactionFunction, proof);
|
|
168
|
+
const persistence = this._persistence.get(id);
|
|
169
|
+
if (this._repository && persistence) {
|
|
170
|
+
if (transactionFunction.id === undefined) {
|
|
171
|
+
throw new generic_1.ShareDataPersistenceError(id, 'cannot persist a transaction for a function without an id');
|
|
172
|
+
}
|
|
173
|
+
// Read the just-appended history entry for the deterministic time
|
|
174
|
+
// and recorded outcome so a reload replays an identical hash chain.
|
|
175
|
+
const latest = shareData.transactionHistory[shareData.transactionHistory.length - 1];
|
|
176
|
+
const appended = {
|
|
177
|
+
functionId: transactionFunction.id,
|
|
178
|
+
proposedState: proposedData,
|
|
179
|
+
proof,
|
|
180
|
+
transactionTime: latest.transactionTime,
|
|
181
|
+
ruleOutcome: latest.ruleOutcome,
|
|
182
|
+
};
|
|
183
|
+
// Build the next log locally and only commit it after the durable
|
|
184
|
+
// write succeeds, so the manager's record never claims a
|
|
185
|
+
// transaction the store rejected.
|
|
186
|
+
const nextLog = [...persistence.transactionLog, appended];
|
|
187
|
+
await this._repository.save((0, serialize_share_data_1.serializeShareData)(persistence.blockType, persistence.creationConfig, shareData, nextLog));
|
|
188
|
+
persistence.transactionLog = nextLog;
|
|
189
|
+
}
|
|
190
|
+
this._emit({
|
|
191
|
+
type: 'share-data:transaction',
|
|
192
|
+
shareDataId: id,
|
|
193
|
+
transactionId: outcome.transactionId,
|
|
194
|
+
accepted: outcome.accepted,
|
|
195
|
+
currentState: shareData.currentState,
|
|
196
|
+
transactionFunction,
|
|
197
|
+
});
|
|
198
|
+
return outcome;
|
|
199
|
+
}
|
|
200
|
+
on(type, listener) {
|
|
201
|
+
this._emitter.on(type, listener);
|
|
202
|
+
}
|
|
203
|
+
off(type, listener) {
|
|
204
|
+
this._emitter.off(type, listener);
|
|
205
|
+
}
|
|
206
|
+
_emit(event) {
|
|
207
|
+
this._emitter.emit(event.type, event);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.ShareDataManager = ShareDataManager;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IShareDataInitialCryptoData, PersistedCreationConfig, ShareBlockType, ShareDataProofs } from '@degenerate/generic';
|
|
2
|
+
import { ShareData } from '../share-data/share-data';
|
|
3
|
+
/**
|
|
4
|
+
* The inputs a block builder needs to replay construction of a persisted
|
|
5
|
+
* ShareData. These are exactly the fields a factory consumes once the
|
|
6
|
+
* `proofFor` callback is reconstructed from the stored proofs:
|
|
7
|
+
*
|
|
8
|
+
* - `creationConfig` — the factory options minus `proofFor`
|
|
9
|
+
* (`metadata`, `creationPublicKey`, `initialState`).
|
|
10
|
+
* - `initialCrypto` — the client-computed creation crypto, applied to the
|
|
11
|
+
* in-construction block on the first proof lookup.
|
|
12
|
+
* - `structuralProofs` — the per-purpose proof bundle, replayed in place of
|
|
13
|
+
* a live signer so the rebuilt block carries byte-identical proofs.
|
|
14
|
+
*/
|
|
15
|
+
export interface ShareBlockBuilderInput {
|
|
16
|
+
creationConfig: PersistedCreationConfig;
|
|
17
|
+
initialCrypto: IShareDataInitialCryptoData;
|
|
18
|
+
structuralProofs: ShareDataProofs;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Rebuilds a ShareData of a single block type from its persisted recipe.
|
|
22
|
+
* Implementations live in `@degenerate/shareblock` (one per
|
|
23
|
+
* {@link ShareBlockType}) and are wired in via
|
|
24
|
+
* {@link registerShareBlockBuilder}. A builder must replay the *same* factory
|
|
25
|
+
* the block was created with so the structural-proof chain re-verifies; it
|
|
26
|
+
* must not mutate or trust any `expected*` integrity values (those are
|
|
27
|
+
* cross-checked separately after replay).
|
|
28
|
+
*/
|
|
29
|
+
export type ShareBlockBuilder = (input: ShareBlockBuilderInput) => ShareData;
|
|
30
|
+
/**
|
|
31
|
+
* Registers the builder used to rehydrate snapshots of a given block type.
|
|
32
|
+
* Idempotent-by-overwrite: re-registering replaces the prior builder, which
|
|
33
|
+
* keeps test setup and repeated module loads stable.
|
|
34
|
+
*/
|
|
35
|
+
export declare const registerShareBlockBuilder: (blockType: ShareBlockType, builder: ShareBlockBuilder) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the registered builder for a block type, or `undefined` if none has
|
|
38
|
+
* been registered. The deserialize engine treats `undefined` as an
|
|
39
|
+
* unreconstructable snapshot and throws `ShareDataPersistenceError` with the
|
|
40
|
+
* offending id — resolution stays decoupled from error-with-id semantics.
|
|
41
|
+
*/
|
|
42
|
+
export declare const getShareBlockBuilder: (blockType: ShareBlockType) => ShareBlockBuilder | undefined;
|
|
43
|
+
/** Whether a builder is registered for the given block type. */
|
|
44
|
+
export declare const hasShareBlockBuilder: (blockType: ShareBlockType) => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Clears all registered builders. Intended for test isolation so one suite's
|
|
47
|
+
* registrations do not leak into another; production code registers once at
|
|
48
|
+
* import time and never clears.
|
|
49
|
+
*/
|
|
50
|
+
export declare const clearShareBlockBuilders: () => void;
|
|
51
|
+
//# sourceMappingURL=block-factory-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-factory-registry.d.ts","sourceRoot":"","sources":["../../src/persistence/block-factory-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,cAAc,EACd,eAAe,EAClB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAgBrD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,sBAAsB;IACnC,cAAc,EAAE,uBAAuB,CAAC;IACxC,aAAa,EAAE,2BAA2B,CAAC;IAC3C,gBAAgB,EAAE,eAAe,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,sBAAsB,KAAK,SAAS,CAAC;AAQ7E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAClC,WAAW,cAAc,EACzB,SAAS,iBAAiB,KAC3B,IAEF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC7B,WAAW,cAAc,KAC1B,iBAAiB,GAAG,SAAoC,CAAC;AAE5D,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,GAAI,WAAW,cAAc,KAAG,OACtC,CAAC;AAE5B;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAO,IAE1C,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearShareBlockBuilders = exports.hasShareBlockBuilder = exports.getShareBlockBuilder = exports.registerShareBlockBuilder = void 0;
|
|
4
|
+
//#endregion Builder contract
|
|
5
|
+
//#region Registry
|
|
6
|
+
const builders = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Registers the builder used to rehydrate snapshots of a given block type.
|
|
9
|
+
* Idempotent-by-overwrite: re-registering replaces the prior builder, which
|
|
10
|
+
* keeps test setup and repeated module loads stable.
|
|
11
|
+
*/
|
|
12
|
+
const registerShareBlockBuilder = (blockType, builder) => {
|
|
13
|
+
builders.set(blockType, builder);
|
|
14
|
+
};
|
|
15
|
+
exports.registerShareBlockBuilder = registerShareBlockBuilder;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the registered builder for a block type, or `undefined` if none has
|
|
18
|
+
* been registered. The deserialize engine treats `undefined` as an
|
|
19
|
+
* unreconstructable snapshot and throws `ShareDataPersistenceError` with the
|
|
20
|
+
* offending id — resolution stays decoupled from error-with-id semantics.
|
|
21
|
+
*/
|
|
22
|
+
const getShareBlockBuilder = (blockType) => builders.get(blockType);
|
|
23
|
+
exports.getShareBlockBuilder = getShareBlockBuilder;
|
|
24
|
+
/** Whether a builder is registered for the given block type. */
|
|
25
|
+
const hasShareBlockBuilder = (blockType) => builders.has(blockType);
|
|
26
|
+
exports.hasShareBlockBuilder = hasShareBlockBuilder;
|
|
27
|
+
/**
|
|
28
|
+
* Clears all registered builders. Intended for test isolation so one suite's
|
|
29
|
+
* registrations do not leak into another; production code registers once at
|
|
30
|
+
* import time and never clears.
|
|
31
|
+
*/
|
|
32
|
+
const clearShareBlockBuilders = () => {
|
|
33
|
+
builders.clear();
|
|
34
|
+
};
|
|
35
|
+
exports.clearShareBlockBuilders = clearShareBlockBuilders;
|
|
36
|
+
//#endregion Registry
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ShareData } from '../share-data/share-data';
|
|
2
|
+
/**
|
|
3
|
+
* Replays a {@link PersistedShareData} snapshot back into a live, executable
|
|
4
|
+
* ShareData. The rebuilt block re-binds real function closures (via the
|
|
5
|
+
* registered builder for its block type), so it accepts new proposals just
|
|
6
|
+
* like a freshly-created block.
|
|
7
|
+
*
|
|
8
|
+
* Throws:
|
|
9
|
+
* - {@link ShareDataPersistenceError} for an unsupported schema version, an
|
|
10
|
+
* unregistered block type, or a transaction referencing an unknown
|
|
11
|
+
* `functionId`.
|
|
12
|
+
* - {@link ShareDataStructuralProofFailedError} (from the replayed factory)
|
|
13
|
+
* if the structural-proof chain does not verify.
|
|
14
|
+
* - {@link ShareDataIntegrityError} if the replayed block diverges from the
|
|
15
|
+
* snapshot's `expected*` cross-checks.
|
|
16
|
+
*/
|
|
17
|
+
export declare const deserializeShareData: (snapshot: unknown) => ShareData;
|
|
18
|
+
//# sourceMappingURL=deserialize-share-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deserialize-share-data.d.ts","sourceRoot":"","sources":["../../src/persistence/deserialize-share-data.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAYrD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,GAAI,UAAU,OAAO,KAAG,SAgHxD,CAAC"}
|