@interop/wallet-core 0.1.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.
- package/LICENSE.md +20 -0
- package/README.md +83 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/space/activity.d.ts +175 -0
- package/dist/space/activity.d.ts.map +1 -0
- package/dist/space/activity.js +209 -0
- package/dist/space/activity.js.map +1 -0
- package/dist/space/collections.d.ts +65 -0
- package/dist/space/collections.d.ts.map +1 -0
- package/dist/space/collections.js +65 -0
- package/dist/space/collections.js.map +1 -0
- package/dist/space/errors.d.ts +14 -0
- package/dist/space/errors.d.ts.map +1 -0
- package/dist/space/errors.js +18 -0
- package/dist/space/errors.js.map +1 -0
- package/dist/space/index.d.ts +26 -0
- package/dist/space/index.d.ts.map +1 -0
- package/dist/space/index.js +23 -0
- package/dist/space/index.js.map +1 -0
- package/dist/space/publicLink.d.ts +17 -0
- package/dist/space/publicLink.d.ts.map +1 -0
- package/dist/space/publicLink.js +27 -0
- package/dist/space/publicLink.js.map +1 -0
- package/dist/space/wasLink.d.ts +37 -0
- package/dist/space/wasLink.d.ts.map +1 -0
- package/dist/space/wasLink.js +123 -0
- package/dist/space/wasLink.js.map +1 -0
- package/dist/sync/collections.d.ts +61 -0
- package/dist/sync/collections.d.ts.map +1 -0
- package/dist/sync/collections.js +2 -0
- package/dist/sync/collections.js.map +1 -0
- package/dist/sync/engine.d.ts +112 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +188 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/index.d.ts +33 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +29 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/pull.d.ts +68 -0
- package/dist/sync/pull.d.ts.map +1 -0
- package/dist/sync/pull.js +92 -0
- package/dist/sync/pull.js.map +1 -0
- package/dist/sync/push.d.ts +55 -0
- package/dist/sync/push.d.ts.map +1 -0
- package/dist/sync/push.js +191 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/types.d.ts +123 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +25 -0
- package/dist/sync/types.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The push side of the WAS replication engine core: fan each dirty local row out
|
|
6
|
+
* to a conditional WAS write, then reconcile per the content-addressed conflict
|
|
7
|
+
* table.
|
|
8
|
+
*
|
|
9
|
+
* On a content-addressed collection an id's `data` never mutates, so a live row
|
|
10
|
+
* only ever pushes as a create (`If-None-Match: *`) and a tombstone as a delete;
|
|
11
|
+
* there is no update path. A mutable (last-write-wins) collection pushes a live
|
|
12
|
+
* row as a create while never-acked (`version 0`) and as an in-place update
|
|
13
|
+
* (`If-Match`) once acked, and settles a `412` through its injected
|
|
14
|
+
* {@link ResolveConflict} policy.
|
|
15
|
+
*
|
|
16
|
+
* This loop covers the CONTENT sub-resource only (`data` / `version`, at
|
|
17
|
+
* `PUT/DELETE /:id`). It does not drive the independently-versioned METADATA
|
|
18
|
+
* sub-resource (`custom` / `metaVersion`, at `PUT /:id/meta`): a replica that
|
|
19
|
+
* syncs user-writable metadata (the web wallet's RxDB driver, via
|
|
20
|
+
* `WasSyncPort.putMeta`) keeps that half in its own push handler. It is left out
|
|
21
|
+
* of this core deliberately -- none of the wallet Space collections
|
|
22
|
+
* (`private-credentials`, `public-credentials`, `wallet-activity`, `contacts`,
|
|
23
|
+
* `contacts-history`) versions its metadata independently of its content, so
|
|
24
|
+
* folding a `putMeta` diff into this loop would add an untested code path with
|
|
25
|
+
* no collection to exercise it. The `WasSyncPort.putMeta` capability stays
|
|
26
|
+
* optional on the port for the driver that needs it.
|
|
27
|
+
*/
|
|
28
|
+
import { formatEtag } from '@interop/was-client/sync';
|
|
29
|
+
import { WasSyncConflictError, WasSyncNotFoundError } from './types.js';
|
|
30
|
+
// Formats a master revision as the quoted strong ETag the server compares
|
|
31
|
+
// `If-Match` against (revision `3` becomes `"3"`). Re-exported so callers keep
|
|
32
|
+
// importing it from here.
|
|
33
|
+
export { formatEtag };
|
|
34
|
+
/**
|
|
35
|
+
* Pushes a dirty live row. A never-acked row (`version 0`) is a create
|
|
36
|
+
* (`PUT /:id` with `If-None-Match: *`); an acked row (`version > 0`) is an
|
|
37
|
+
* in-place update (`If-Match` over its version) -- reachable only on a mutable
|
|
38
|
+
* collection, since a content-addressed row never mutates in place. On success
|
|
39
|
+
* the acked version is recorded and the row goes clean.
|
|
40
|
+
*
|
|
41
|
+
* A `412` is settled by the collection's policy:
|
|
42
|
+
* - A mutable collection defers to its {@link ResolveConflict} (re-read master,
|
|
43
|
+
* pick the deterministic winner, apply-remote or re-encrypt-local).
|
|
44
|
+
* - An insert-only content-addressed collection (no resolver) applies the
|
|
45
|
+
* built-in settlement: master live -> the identical envelope already exists
|
|
46
|
+
* (same content hash), adopt its version, projection untouched; master
|
|
47
|
+
* absent/tombstone -> deletion wins, adopt the tombstone and delete the
|
|
48
|
+
* projection (a later re-add re-encrypts to a fresh id, so nothing is blocked).
|
|
49
|
+
*/
|
|
50
|
+
async function pushUpsert({ port, store, row, resolveConflict }) {
|
|
51
|
+
try {
|
|
52
|
+
const version = await port.putContent({
|
|
53
|
+
id: row.id,
|
|
54
|
+
data: row.data ?? null,
|
|
55
|
+
...(row.version > 0
|
|
56
|
+
? { ifMatch: formatEtag(row.version) }
|
|
57
|
+
: { ifNoneMatch: true })
|
|
58
|
+
});
|
|
59
|
+
await store.markPushed({ id: row.id, version });
|
|
60
|
+
return { conflictResolved: false };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (!(err instanceof WasSyncConflictError)) {
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
if (resolveConflict) {
|
|
67
|
+
await resolveConflict({
|
|
68
|
+
id: row.id,
|
|
69
|
+
version: row.version,
|
|
70
|
+
data: row.data
|
|
71
|
+
});
|
|
72
|
+
// The resolver may have left the row dirty (local-wins re-encrypt); the
|
|
73
|
+
// caller reruns so the re-push settles within the same sync run.
|
|
74
|
+
return { conflictResolved: true };
|
|
75
|
+
}
|
|
76
|
+
const master = await port.get({ id: row.id });
|
|
77
|
+
if (master === null || master.deleted) {
|
|
78
|
+
await store.adoptMaster({
|
|
79
|
+
id: row.id,
|
|
80
|
+
master: null,
|
|
81
|
+
projection: { kind: 'delete' }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await store.adoptMaster({
|
|
86
|
+
id: row.id,
|
|
87
|
+
master,
|
|
88
|
+
projection: { kind: 'none' }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { conflictResolved: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Attempts one conditional delete. Returns `true` when the delete is settled
|
|
96
|
+
* (`204` acked, or `404` -- already gone / never reached the server), `false`
|
|
97
|
+
* on a `412` so the caller can re-read and retry. Any other error propagates to
|
|
98
|
+
* the engine's backoff.
|
|
99
|
+
*/
|
|
100
|
+
async function tryDelete({ port, store, id, ifMatch }) {
|
|
101
|
+
try {
|
|
102
|
+
const version = await port.deleteContent({
|
|
103
|
+
id,
|
|
104
|
+
...(ifMatch !== undefined && { ifMatch })
|
|
105
|
+
});
|
|
106
|
+
await store.markDeletedPushed({ id, version });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (err instanceof WasSyncNotFoundError) {
|
|
111
|
+
await store.markDeletedPushed({ id });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (err instanceof WasSyncConflictError) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Pushes a dirty tombstone. `DELETE /:id` with `If-Match` when the row was ever
|
|
122
|
+
* acked (`version > 0`), unconditional otherwise:
|
|
123
|
+
* - `204` / `404` -> settled (clean).
|
|
124
|
+
* - `412` then master absent/tombstone -> delete/delete race, settled.
|
|
125
|
+
* - `412` then master live -> retry once with a fresh `If-Match`; a second
|
|
126
|
+
* `412` leaves the row dirty for the next cycle (the next pull refreshes its
|
|
127
|
+
* `version` via the dirty-deleted-vs-live rule, so the retry's `If-Match`
|
|
128
|
+
* becomes current).
|
|
129
|
+
*/
|
|
130
|
+
async function pushDelete({ port, store, row }) {
|
|
131
|
+
const firstIfMatch = row.version > 0 ? formatEtag(row.version) : undefined;
|
|
132
|
+
if (await tryDelete({ port, store, id: row.id, ifMatch: firstIfMatch })) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const master = await port.get({ id: row.id });
|
|
136
|
+
if (master === null || master.deleted) {
|
|
137
|
+
// delete/delete race -- the resource is already a tombstone / absent.
|
|
138
|
+
await store.markDeletedPushed({ id: row.id });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Second attempt with the current master version. If it too hits 412 we simply
|
|
142
|
+
// leave the row dirty (tryDelete returned false and made no store write).
|
|
143
|
+
await tryDelete({
|
|
144
|
+
port,
|
|
145
|
+
store,
|
|
146
|
+
id: row.id,
|
|
147
|
+
ifMatch: formatEtag(master.version)
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Pushes every dirty row for one feed, sequentially (bounds sockets/CPU, and
|
|
152
|
+
* keeps conflict reconciliation deterministic). Honors `signal` between rows.
|
|
153
|
+
* A non-conflict error from any row propagates so the engine aborts the cycle
|
|
154
|
+
* and backs off; already-pushed rows in the batch stay settled.
|
|
155
|
+
*
|
|
156
|
+
* @param options {object}
|
|
157
|
+
* @param options.port {WasSyncPort}
|
|
158
|
+
* @param options.store {SyncStore}
|
|
159
|
+
* @param [options.resolveConflict] {ResolveConflict} mutable-collection policy
|
|
160
|
+
* @param [options.signal] {AbortSignal}
|
|
161
|
+
* @returns {Promise<{ pushed: number; conflictsResolved: number }>} dirty rows
|
|
162
|
+
* processed this cycle, and how many invoked the LWW resolver (a positive
|
|
163
|
+
* count means the caller should rerun so a local-wins re-push settles)
|
|
164
|
+
*/
|
|
165
|
+
export async function runPush({ port, store, resolveConflict, signal }) {
|
|
166
|
+
const rows = await store.getDirtyRows();
|
|
167
|
+
let pushed = 0;
|
|
168
|
+
let conflictsResolved = 0;
|
|
169
|
+
for (const row of rows) {
|
|
170
|
+
if (signal?.aborted) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (row.deleted) {
|
|
174
|
+
await pushDelete({ port, store, row });
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const { conflictResolved } = await pushUpsert({
|
|
178
|
+
port,
|
|
179
|
+
store,
|
|
180
|
+
row,
|
|
181
|
+
resolveConflict
|
|
182
|
+
});
|
|
183
|
+
if (conflictResolved) {
|
|
184
|
+
conflictsResolved += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
pushed += 1;
|
|
188
|
+
}
|
|
189
|
+
return { pushed, conflictsResolved };
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/sync/push.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Types for the WAS replication engine core.
|
|
6
|
+
*
|
|
7
|
+
* The wire contract and port seam (`Json`, `SyncCheckpoint`, `WireDoc`,
|
|
8
|
+
* `MasterState`, `WasSyncPort`, `DocCipher`, and the `WasSyncConflictError` /
|
|
9
|
+
* `WasSyncNotFoundError` signals) come from `@interop/was-client/sync` and are
|
|
10
|
+
* re-exported here so a single import gives a consumer both the wire types and
|
|
11
|
+
* the replica-side seams.
|
|
12
|
+
*
|
|
13
|
+
* The local-persistence seam (`SyncStore`, `SyncedRow`, `ProjectionAction`,
|
|
14
|
+
* `ResolveConflict`) is the replica's side of the contract: it stands in for a
|
|
15
|
+
* concrete store (an RxDB collection, a SQLite `synced_docs` / `sync_checkpoints`
|
|
16
|
+
* table pair, or an in-memory test double). The engine owns the `DocCipher` and
|
|
17
|
+
* decrypts OUTSIDE the store transaction, so these methods never see key
|
|
18
|
+
* material.
|
|
19
|
+
*
|
|
20
|
+
* This module (and `pull.ts` / `push.ts` / `engine.ts`) has no runtime imports
|
|
21
|
+
* beyond the wire contract, so the engine runs anywhere: browser, Node, or React
|
|
22
|
+
* Native, against a fake port and an in-memory store.
|
|
23
|
+
*/
|
|
24
|
+
export { WasSyncConflictError, WasSyncNotFoundError } from '@interop/was-client/sync';
|
|
25
|
+
export type { Json, SyncCheckpoint, WireDoc, MasterState, WasSyncPort, DocCipher } from '@interop/was-client/sync';
|
|
26
|
+
import type { Json, MasterState, SyncCheckpoint, WireDoc } from '@interop/was-client/sync';
|
|
27
|
+
/**
|
|
28
|
+
* A dirty local synced-docs row awaiting push. `data` is the stored body (the
|
|
29
|
+
* EDV envelope on an encrypted collection, or the plaintext JSON on a plaintext
|
|
30
|
+
* one), `null` for a tombstone. `version` is the last server-acked content
|
|
31
|
+
* revision (`0` = never acked, so a create).
|
|
32
|
+
*/
|
|
33
|
+
export interface SyncedRow {
|
|
34
|
+
id: string;
|
|
35
|
+
version: number;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
deleted: boolean;
|
|
38
|
+
data: Json | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What a pulled / reconciled document does to the decrypted read-model (the
|
|
42
|
+
* plaintext projection for the collection). Computed by the engine (which owns
|
|
43
|
+
* the DocCipher) and handed to the store to apply inside the same transaction as
|
|
44
|
+
* the envelope write. The collection-specific meaning of "upsert" / "delete" is
|
|
45
|
+
* supplied to the store as transactional writer functions, so this action stays
|
|
46
|
+
* collection-agnostic: `payload` is the decrypted document body. `none` = leave
|
|
47
|
+
* the projection untouched (e.g. adopting a live master for a document already
|
|
48
|
+
* present locally).
|
|
49
|
+
*/
|
|
50
|
+
export type ProjectionAction = {
|
|
51
|
+
kind: 'upsert';
|
|
52
|
+
payload: Json;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'delete';
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'none';
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The per-row 412-conflict policy for a mutable (last-write-wins) collection,
|
|
60
|
+
* injected into the push loop. Insert-only content-addressed collections leave
|
|
61
|
+
* it undefined: their settlement rules -- identical-envelope adoption and
|
|
62
|
+
* tombstone-wins -- already cover every 412. For a mutable head document the
|
|
63
|
+
* resolver re-reads the master, decides the winner deterministically, and either
|
|
64
|
+
* applies the remote payload or re-encrypts the local one for the next push.
|
|
65
|
+
* Bound to a profile's cipher + store by the caller; opaque to the push loop.
|
|
66
|
+
*/
|
|
67
|
+
export type ResolveConflict = (row: {
|
|
68
|
+
id: string;
|
|
69
|
+
version: number;
|
|
70
|
+
data: Json | null;
|
|
71
|
+
}) => Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* The local-persistence seam, pre-bound to one `(replica, collection)` feed. A
|
|
74
|
+
* concrete store implements it; the engine and the pull/push loops depend only
|
|
75
|
+
* on this interface. Every method that mutates more than one table does so in
|
|
76
|
+
* ONE exclusive transaction (see the per-method notes). Decryption happens in
|
|
77
|
+
* the engine, outside these calls.
|
|
78
|
+
*/
|
|
79
|
+
export interface SyncStore {
|
|
80
|
+
/** The last persisted pull checkpoint, or `undefined` before the first pull. */
|
|
81
|
+
getCheckpoint(): Promise<SyncCheckpoint | undefined>;
|
|
82
|
+
/** All rows awaiting push (dirty). */
|
|
83
|
+
getDirtyRows(): Promise<SyncedRow[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Applies one pulled page in a single exclusive transaction: reconcile each
|
|
86
|
+
* document against the local row (per the pull-apply conflict table), write
|
|
87
|
+
* the matching projection action, and advance the checkpoint. `projections`
|
|
88
|
+
* is keyed by document id.
|
|
89
|
+
*/
|
|
90
|
+
applyPulledPage(options: {
|
|
91
|
+
documents: WireDoc[];
|
|
92
|
+
checkpoint: SyncCheckpoint;
|
|
93
|
+
projections: Map<string, ProjectionAction>;
|
|
94
|
+
}): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Marks a pushed create as acked: clear dirty, and record the server `version`
|
|
97
|
+
* when provided (the `204` ETag).
|
|
98
|
+
*/
|
|
99
|
+
markPushed(options: {
|
|
100
|
+
id: string;
|
|
101
|
+
version?: number;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Marks a pushed delete as settled: keep the tombstone, clear dirty, and
|
|
105
|
+
* record the server `version` when provided.
|
|
106
|
+
*/
|
|
107
|
+
markDeletedPushed(options: {
|
|
108
|
+
id: string;
|
|
109
|
+
version?: number;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Adopts the server's master state for a row whose push hit a `412`, applying
|
|
113
|
+
* `projection` in the same transaction. `master === null` means the server has
|
|
114
|
+
* a tombstone (or the resource is absent): record the tombstone and delete the
|
|
115
|
+
* projection.
|
|
116
|
+
*/
|
|
117
|
+
adoptMaster(options: {
|
|
118
|
+
id: string;
|
|
119
|
+
master: MasterState | null;
|
|
120
|
+
projection: ProjectionAction;
|
|
121
|
+
}): Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EACV,IAAI,EACJ,cAAc,EACd,OAAO,EACP,WAAW,EACX,WAAW,EACX,SAAS,EACV,MAAM,0BAA0B,CAAA;AAEjC,OAAO,KAAK,EACV,IAAI,EACJ,WAAW,EACX,cAAc,EACd,OAAO,EACR,MAAM,0BAA0B,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpB;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;CAClB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEnB;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,gFAAgF;IAChF,aAAa,IAAI,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAA;IAEpD,sCAAsC;IACtC,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;IAEpC;;;;;OAKG;IACH,eAAe,CAAC,OAAO,EAAE;QACvB,SAAS,EAAE,OAAO,EAAE,CAAA;QACpB,UAAU,EAAE,cAAc,CAAA;QAC1B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;KAC3C,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjB;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpE;;;OAGG;IACH,iBAAiB,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE3E;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE;QACnB,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;QAC1B,UAAU,EAAE,gBAAgB,CAAA;KAC7B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Types for the WAS replication engine core.
|
|
6
|
+
*
|
|
7
|
+
* The wire contract and port seam (`Json`, `SyncCheckpoint`, `WireDoc`,
|
|
8
|
+
* `MasterState`, `WasSyncPort`, `DocCipher`, and the `WasSyncConflictError` /
|
|
9
|
+
* `WasSyncNotFoundError` signals) come from `@interop/was-client/sync` and are
|
|
10
|
+
* re-exported here so a single import gives a consumer both the wire types and
|
|
11
|
+
* the replica-side seams.
|
|
12
|
+
*
|
|
13
|
+
* The local-persistence seam (`SyncStore`, `SyncedRow`, `ProjectionAction`,
|
|
14
|
+
* `ResolveConflict`) is the replica's side of the contract: it stands in for a
|
|
15
|
+
* concrete store (an RxDB collection, a SQLite `synced_docs` / `sync_checkpoints`
|
|
16
|
+
* table pair, or an in-memory test double). The engine owns the `DocCipher` and
|
|
17
|
+
* decrypts OUTSIDE the store transaction, so these methods never see key
|
|
18
|
+
* material.
|
|
19
|
+
*
|
|
20
|
+
* This module (and `pull.ts` / `push.ts` / `engine.ts`) has no runtime imports
|
|
21
|
+
* beyond the wire contract, so the engine runs anywhere: browser, Node, or React
|
|
22
|
+
* Native, against a fake port and an in-memory store.
|
|
23
|
+
*/
|
|
24
|
+
export { WasSyncConflictError, WasSyncNotFoundError } from '@interop/was-client/sync';
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,0BAA0B,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@interop/wallet-core",
|
|
3
|
+
"description": "Shared wallet-domain logic (WAS sync engine core and wallet Space layout contracts) for Interop wallet apps.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "pnpm run clear && tsc",
|
|
8
|
+
"clear": "rimraf dist/*",
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"fix": "eslint --fix src test && pnpm run format",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.md\"",
|
|
12
|
+
"lint": "eslint src test",
|
|
13
|
+
"prepare": "pnpm run build",
|
|
14
|
+
"rebuild": "pnpm run clear && pnpm run build",
|
|
15
|
+
"typecheck": "tsc -p tsconfig.dev.json",
|
|
16
|
+
"test": "pnpm run fix && pnpm run lint && pnpm run typecheck && pnpm run test:node",
|
|
17
|
+
"test:browser": "playwright test",
|
|
18
|
+
"test:node": "vitest run",
|
|
19
|
+
"test:coverage": "vitest run --coverage"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE.md"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"react-native": "./dist/index.js",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./sync": {
|
|
34
|
+
"types": "./dist/sync/index.d.ts",
|
|
35
|
+
"react-native": "./dist/sync/index.js",
|
|
36
|
+
"import": "./dist/sync/index.js",
|
|
37
|
+
"default": "./dist/sync/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./space": {
|
|
40
|
+
"types": "./dist/space/index.d.ts",
|
|
41
|
+
"react-native": "./dist/space/index.js",
|
|
42
|
+
"import": "./dist/space/index.js",
|
|
43
|
+
"default": "./dist/space/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"module": "dist/index.js",
|
|
47
|
+
"browser": "dist/index.js",
|
|
48
|
+
"types": "dist/index.d.ts",
|
|
49
|
+
"sideEffects": false,
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@interop/was-client": "^0.19.0",
|
|
52
|
+
"@scure/base": "^1.2.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@eslint/js": "^10.0.1",
|
|
56
|
+
"@playwright/test": "^1.60.0",
|
|
57
|
+
"@types/node": "^26.0.1",
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
59
|
+
"eslint": "^10.4.0",
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"globals": "^17.6.0",
|
|
62
|
+
"prettier": "^3.8.3",
|
|
63
|
+
"rimraf": "^6.1.3",
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"typescript-eslint": "^8.59.4",
|
|
66
|
+
"vite": "^8.0.14",
|
|
67
|
+
"vitest": "^4.1.7"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
},
|
|
72
|
+
"keywords": [
|
|
73
|
+
"wallet",
|
|
74
|
+
"verifiable-credentials",
|
|
75
|
+
"sync",
|
|
76
|
+
"wallet-attached-storage",
|
|
77
|
+
"isomorphic"
|
|
78
|
+
],
|
|
79
|
+
"packageManager": "pnpm@11.5.0",
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">=24.0"
|
|
82
|
+
},
|
|
83
|
+
"author": {
|
|
84
|
+
"name": "Interop Alliance",
|
|
85
|
+
"url": "https://github.com/interop-alliance/"
|
|
86
|
+
},
|
|
87
|
+
"license": "MIT",
|
|
88
|
+
"repository": {
|
|
89
|
+
"type": "git",
|
|
90
|
+
"url": "git+https://github.com/interop-alliance/wallet-core.git"
|
|
91
|
+
},
|
|
92
|
+
"homepage": "https://github.com/interop-alliance/wallet-core",
|
|
93
|
+
"bugs": "https://github.com/interop-alliance/wallet-core/issues"
|
|
94
|
+
}
|