@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.
Files changed (55) hide show
  1. package/LICENSE.md +20 -0
  2. package/README.md +83 -0
  3. package/dist/index.d.ts +15 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +15 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/space/activity.d.ts +175 -0
  8. package/dist/space/activity.d.ts.map +1 -0
  9. package/dist/space/activity.js +209 -0
  10. package/dist/space/activity.js.map +1 -0
  11. package/dist/space/collections.d.ts +65 -0
  12. package/dist/space/collections.d.ts.map +1 -0
  13. package/dist/space/collections.js +65 -0
  14. package/dist/space/collections.js.map +1 -0
  15. package/dist/space/errors.d.ts +14 -0
  16. package/dist/space/errors.d.ts.map +1 -0
  17. package/dist/space/errors.js +18 -0
  18. package/dist/space/errors.js.map +1 -0
  19. package/dist/space/index.d.ts +26 -0
  20. package/dist/space/index.d.ts.map +1 -0
  21. package/dist/space/index.js +23 -0
  22. package/dist/space/index.js.map +1 -0
  23. package/dist/space/publicLink.d.ts +17 -0
  24. package/dist/space/publicLink.d.ts.map +1 -0
  25. package/dist/space/publicLink.js +27 -0
  26. package/dist/space/publicLink.js.map +1 -0
  27. package/dist/space/wasLink.d.ts +37 -0
  28. package/dist/space/wasLink.d.ts.map +1 -0
  29. package/dist/space/wasLink.js +123 -0
  30. package/dist/space/wasLink.js.map +1 -0
  31. package/dist/sync/collections.d.ts +61 -0
  32. package/dist/sync/collections.d.ts.map +1 -0
  33. package/dist/sync/collections.js +2 -0
  34. package/dist/sync/collections.js.map +1 -0
  35. package/dist/sync/engine.d.ts +112 -0
  36. package/dist/sync/engine.d.ts.map +1 -0
  37. package/dist/sync/engine.js +188 -0
  38. package/dist/sync/engine.js.map +1 -0
  39. package/dist/sync/index.d.ts +33 -0
  40. package/dist/sync/index.d.ts.map +1 -0
  41. package/dist/sync/index.js +29 -0
  42. package/dist/sync/index.js.map +1 -0
  43. package/dist/sync/pull.d.ts +68 -0
  44. package/dist/sync/pull.d.ts.map +1 -0
  45. package/dist/sync/pull.js +92 -0
  46. package/dist/sync/pull.js.map +1 -0
  47. package/dist/sync/push.d.ts +55 -0
  48. package/dist/sync/push.d.ts.map +1 -0
  49. package/dist/sync/push.js +191 -0
  50. package/dist/sync/push.js.map +1 -0
  51. package/dist/sync/types.d.ts +123 -0
  52. package/dist/sync/types.d.ts.map +1 -0
  53. package/dist/sync/types.js +25 -0
  54. package/dist/sync/types.js.map +1 -0
  55. 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
+ }