@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,65 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The shared wallet Space layout: the collection ids and descriptive specs both
|
|
6
|
+
* wallet replicas provision, so a credential written by one is found and read by
|
|
7
|
+
* the other. Both replicas MUST agree on every field here -- `collectionId`
|
|
8
|
+
* decides where a document lands on the server, `idDerivation` and `mutable`
|
|
9
|
+
* decide whether it is overwritten in place or only appended, and `encryption` /
|
|
10
|
+
* `isPublic` decide how it is stored and who can read it. A disagreement splits
|
|
11
|
+
* the feed into separate or incompatibly-shaped collections that never converge.
|
|
12
|
+
*
|
|
13
|
+
* The contacts collections (`contacts`, `contacts-history`) are NOT declared
|
|
14
|
+
* here: their ids and specs live in `@interop/social-core`
|
|
15
|
+
* (`CONTACTS_COLLECTION_SPEC` / `CONTACTS_HISTORY_COLLECTION_SPEC`), which apps
|
|
16
|
+
* import directly. The field vocabulary below mirrors that spec.
|
|
17
|
+
*/
|
|
18
|
+
/** The immutable, content-addressed, EDV-encrypted credential replica. */
|
|
19
|
+
export const PRIVATE_CREDENTIALS_COLLECTION = 'private-credentials';
|
|
20
|
+
/** The plaintext, world-readable copies of publicly shared credentials. */
|
|
21
|
+
export const PUBLIC_CREDENTIALS_COLLECTION = 'public-credentials';
|
|
22
|
+
/** The append-only, EDV-encrypted wallet activity log. */
|
|
23
|
+
export const WALLET_ACTIVITY_COLLECTION = 'wallet-activity';
|
|
24
|
+
/**
|
|
25
|
+
* The immutable credential replica: each credential stored as an EDV envelope,
|
|
26
|
+
* addressed by the envelope's content hash, never overwritten, never public.
|
|
27
|
+
*/
|
|
28
|
+
export const PRIVATE_CREDENTIALS_COLLECTION_SPEC = {
|
|
29
|
+
collectionId: PRIVATE_CREDENTIALS_COLLECTION,
|
|
30
|
+
idDerivation: 'content',
|
|
31
|
+
mutable: false,
|
|
32
|
+
encryption: 'edv',
|
|
33
|
+
isPublic: false
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The plaintext, world-readable copies of shared credentials, keyed by the
|
|
37
|
+
* credential's content cid (the same id every replica mints, so a share
|
|
38
|
+
* converges), granted collection-level world read on the server.
|
|
39
|
+
*/
|
|
40
|
+
export const PUBLIC_CREDENTIALS_COLLECTION_SPEC = {
|
|
41
|
+
collectionId: PUBLIC_CREDENTIALS_COLLECTION,
|
|
42
|
+
idDerivation: 'content',
|
|
43
|
+
mutable: false,
|
|
44
|
+
encryption: 'plaintext',
|
|
45
|
+
isPublic: true
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The append-only activity log: each entry stored as an EDV envelope, addressed
|
|
49
|
+
* by content hash, never overwritten, never public. Shared with the web wallet's
|
|
50
|
+
* `wallet-activity` collection, so each replica reads the other's entries.
|
|
51
|
+
*/
|
|
52
|
+
export const WALLET_ACTIVITY_COLLECTION_SPEC = {
|
|
53
|
+
collectionId: WALLET_ACTIVITY_COLLECTION,
|
|
54
|
+
idDerivation: 'content',
|
|
55
|
+
mutable: false,
|
|
56
|
+
encryption: 'edv',
|
|
57
|
+
isPublic: false
|
|
58
|
+
};
|
|
59
|
+
/** The wallet Space's own (non-contacts) collection specs, in provision order. */
|
|
60
|
+
export const WALLET_SPACE_COLLECTION_SPECS = [
|
|
61
|
+
PRIVATE_CREDENTIALS_COLLECTION_SPEC,
|
|
62
|
+
PUBLIC_CREDENTIALS_COLLECTION_SPEC,
|
|
63
|
+
WALLET_ACTIVITY_COLLECTION_SPEC
|
|
64
|
+
];
|
|
65
|
+
//# sourceMappingURL=collections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.js","sourceRoot":"","sources":["../../src/space/collections.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;GAaG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,8BAA8B,GAAG,qBAAqB,CAAA;AACnE,2EAA2E;AAC3E,MAAM,CAAC,MAAM,6BAA6B,GAAG,oBAAoB,CAAA;AACjE,0DAA0D;AAC1D,MAAM,CAAC,MAAM,0BAA0B,GAAG,iBAAiB,CAAA;AAyB3D;;;GAGG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAwB;IACtE,YAAY,EAAE,8BAA8B;IAC5C,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,KAAK;IACjB,QAAQ,EAAE,KAAK;CAChB,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAwB;IACrE,YAAY,EAAE,6BAA6B;IAC3C,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,QAAQ,EAAE,IAAI;CACf,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAwB;IAClE,YAAY,EAAE,0BAA0B;IACxC,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,KAAK;IACjB,QAAQ,EAAE,KAAK;CAChB,CAAA;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,6BAA6B,GAA0B;IAClE,mCAAmC;IACnC,kCAAkC;IAClC,+BAA+B;CAChC,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A user-facing error whose `message` is safe to show verbatim. Carries the name
|
|
6
|
+
* `'HumanReadableError'` so an app that classifies errors by `name` (rather than
|
|
7
|
+
* by `instanceof`, which fails across module realms) treats a wallet-core error
|
|
8
|
+
* the same as its own. Thrown by `parseWasLinkPayload` for every malformed /
|
|
9
|
+
* wrong-version / non-link input.
|
|
10
|
+
*/
|
|
11
|
+
export declare class HumanReadableError extends Error {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/space/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAK5B"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A user-facing error whose `message` is safe to show verbatim. Carries the name
|
|
6
|
+
* `'HumanReadableError'` so an app that classifies errors by `name` (rather than
|
|
7
|
+
* by `instanceof`, which fails across module realms) treats a wallet-core error
|
|
8
|
+
* the same as its own. Thrown by `parseWasLinkPayload` for every malformed /
|
|
9
|
+
* wrong-version / non-link input.
|
|
10
|
+
*/
|
|
11
|
+
export class HumanReadableError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.message = message;
|
|
15
|
+
this.name = 'HumanReadableError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/space/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `@interop/wallet-core/space` subpath: the wallet Space layout contract two
|
|
6
|
+
* WAS-enabled wallet apps share.
|
|
7
|
+
*
|
|
8
|
+
* - The shared collection ids and descriptive specs (`private-credentials`,
|
|
9
|
+
* `public-credentials`, `wallet-activity`). The contacts collections stay in
|
|
10
|
+
* `@interop/social-core`.
|
|
11
|
+
* - The `wallet-activity` wire shape (`WalletActivity`) and the pure
|
|
12
|
+
* `addHistory*` payload builders.
|
|
13
|
+
* - `publicCredentialUrl`, the world-readable shared-credential URL both
|
|
14
|
+
* replicas derive identically.
|
|
15
|
+
* - The `was-link` QR hand-off contract (`buildWasLinkPayload` /
|
|
16
|
+
* `parseWasLinkPayload` / `encodeWasLinkSecret`).
|
|
17
|
+
*/
|
|
18
|
+
export { PRIVATE_CREDENTIALS_COLLECTION, PUBLIC_CREDENTIALS_COLLECTION, WALLET_ACTIVITY_COLLECTION, PRIVATE_CREDENTIALS_COLLECTION_SPEC, PUBLIC_CREDENTIALS_COLLECTION_SPEC, WALLET_ACTIVITY_COLLECTION_SPEC, WALLET_SPACE_COLLECTION_SPECS } from './collections.js';
|
|
19
|
+
export type { SpaceCollectionSpec } from './collections.js';
|
|
20
|
+
export { ACTIVITY_TYPE, addHistoryNewAccount, addHistorySpaceCreated, addHistoryCredentialCreated, addHistoryCredentialDeleted, addHistoryCredentialShared, addHistoryCredentialUnshared, addHistoryLogin, addHistoryAppRevoke } from './activity.js';
|
|
21
|
+
export type { WalletActivity, ActivityGrant } from './activity.js';
|
|
22
|
+
export { publicCredentialUrl } from './publicLink.js';
|
|
23
|
+
export { encodeWasLinkSecret, buildWasLinkPayload, parseWasLinkPayload } from './wasLink.js';
|
|
24
|
+
export type { WasLinkPayload } from './wasLink.js';
|
|
25
|
+
export { HumanReadableError } from './errors.js';
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/space/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,8BAA8B,EAC9B,6BAA6B,EAC7B,0BAA0B,EAC1B,mCAAmC,EACnC,kCAAkC,EAClC,+BAA+B,EAC/B,6BAA6B,EAC9B,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,4BAA4B,EAC5B,eAAe,EACf,mBAAmB,EACpB,MAAM,eAAe,CAAA;AACtB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `@interop/wallet-core/space` subpath: the wallet Space layout contract two
|
|
6
|
+
* WAS-enabled wallet apps share.
|
|
7
|
+
*
|
|
8
|
+
* - The shared collection ids and descriptive specs (`private-credentials`,
|
|
9
|
+
* `public-credentials`, `wallet-activity`). The contacts collections stay in
|
|
10
|
+
* `@interop/social-core`.
|
|
11
|
+
* - The `wallet-activity` wire shape (`WalletActivity`) and the pure
|
|
12
|
+
* `addHistory*` payload builders.
|
|
13
|
+
* - `publicCredentialUrl`, the world-readable shared-credential URL both
|
|
14
|
+
* replicas derive identically.
|
|
15
|
+
* - The `was-link` QR hand-off contract (`buildWasLinkPayload` /
|
|
16
|
+
* `parseWasLinkPayload` / `encodeWasLinkSecret`).
|
|
17
|
+
*/
|
|
18
|
+
export { PRIVATE_CREDENTIALS_COLLECTION, PUBLIC_CREDENTIALS_COLLECTION, WALLET_ACTIVITY_COLLECTION, PRIVATE_CREDENTIALS_COLLECTION_SPEC, PUBLIC_CREDENTIALS_COLLECTION_SPEC, WALLET_ACTIVITY_COLLECTION_SPEC, WALLET_SPACE_COLLECTION_SPECS } from './collections.js';
|
|
19
|
+
export { ACTIVITY_TYPE, addHistoryNewAccount, addHistorySpaceCreated, addHistoryCredentialCreated, addHistoryCredentialDeleted, addHistoryCredentialShared, addHistoryCredentialUnshared, addHistoryLogin, addHistoryAppRevoke } from './activity.js';
|
|
20
|
+
export { publicCredentialUrl } from './publicLink.js';
|
|
21
|
+
export { encodeWasLinkSecret, buildWasLinkPayload, parseWasLinkPayload } from './wasLink.js';
|
|
22
|
+
export { HumanReadableError } from './errors.js';
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/space/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,8BAA8B,EAC9B,6BAA6B,EAC7B,0BAA0B,EAC1B,mCAAmC,EACnC,kCAAkC,EAClC,+BAA+B,EAC/B,6BAA6B,EAC9B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,0BAA0B,EAC1B,4BAA4B,EAC5B,eAAe,EACf,mBAAmB,EACpB,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The absolute, world-readable URL of a credential's shared copy:
|
|
3
|
+
* `{serverUrl}/space/{spaceId}/public-credentials/{cid}`. Takes the server URL,
|
|
4
|
+
* Space id, and credential cid as arguments, with no dependency on app config.
|
|
5
|
+
*
|
|
6
|
+
* @param options {object}
|
|
7
|
+
* @param options.serverUrl {string} the WAS storage server base URL
|
|
8
|
+
* @param options.spaceId {string} the profile's WAS Space id
|
|
9
|
+
* @param options.cid {string} the credential's content cid
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export declare function publicCredentialUrl({ serverUrl, spaceId, cid }: {
|
|
13
|
+
serverUrl: string;
|
|
14
|
+
spaceId: string;
|
|
15
|
+
cid: string;
|
|
16
|
+
}): string;
|
|
17
|
+
//# sourceMappingURL=publicLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicLink.d.ts","sourceRoot":"","sources":["../../src/space/publicLink.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,OAAO,EACP,GAAG,EACJ,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,MAAM,CAKT"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Public-credential URL derivation. Sharing a credential writes a plaintext copy
|
|
6
|
+
* into the profile's `public-credentials` collection, keyed by the credential's
|
|
7
|
+
* content cid; once replication mirrors it to the profile's WAS Space it
|
|
8
|
+
* resolves at the URL below. Both wallet replicas build the identical URL for a
|
|
9
|
+
* given `(serverUrl, spaceId, cid)`, so a link handed out by one resolves the
|
|
10
|
+
* copy replicated by the other.
|
|
11
|
+
*/
|
|
12
|
+
import { PUBLIC_CREDENTIALS_COLLECTION } from './collections.js';
|
|
13
|
+
/**
|
|
14
|
+
* The absolute, world-readable URL of a credential's shared copy:
|
|
15
|
+
* `{serverUrl}/space/{spaceId}/public-credentials/{cid}`. Takes the server URL,
|
|
16
|
+
* Space id, and credential cid as arguments, with no dependency on app config.
|
|
17
|
+
*
|
|
18
|
+
* @param options {object}
|
|
19
|
+
* @param options.serverUrl {string} the WAS storage server base URL
|
|
20
|
+
* @param options.spaceId {string} the profile's WAS Space id
|
|
21
|
+
* @param options.cid {string} the credential's content cid
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function publicCredentialUrl({ serverUrl, spaceId, cid }) {
|
|
25
|
+
return new URL(`/space/${spaceId}/${PUBLIC_CREDENTIALS_COLLECTION}/${cid}`, serverUrl).toString();
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=publicLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicLink.js","sourceRoot":"","sources":["../../src/space/publicLink.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;GAOG;AACH,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAA;AAEhE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,SAAS,EACT,OAAO,EACP,GAAG,EAKJ;IACC,OAAO,IAAI,GAAG,CACZ,UAAU,OAAO,IAAI,6BAA6B,IAAI,GAAG,EAAE,EAC3D,SAAS,CACV,CAAC,QAAQ,EAAE,CAAA;AACd,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The decoded link: the WAS server plus the controller secret AS A STRING. */
|
|
2
|
+
export interface WasLinkPayload {
|
|
3
|
+
serverUrl: string;
|
|
4
|
+
/** The controller secret (already decoded back to the passphrase string). */
|
|
5
|
+
secret: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Encodes a passphrase into the payload's `secret` field: `base64url(utf8(...))`,
|
|
9
|
+
* no padding.
|
|
10
|
+
*
|
|
11
|
+
* @param passphrase {string}
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export declare function encodeWasLinkSecret(passphrase: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Builds the full `was-link` payload string a QR encodes.
|
|
17
|
+
*
|
|
18
|
+
* @param options {object}
|
|
19
|
+
* @param options.serverUrl {string}
|
|
20
|
+
* @param options.passphrase {string}
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildWasLinkPayload({ serverUrl, passphrase }: {
|
|
24
|
+
serverUrl: string;
|
|
25
|
+
passphrase: string;
|
|
26
|
+
}): string;
|
|
27
|
+
/**
|
|
28
|
+
* Parses and validates a scanned payload, returning the server URL and the
|
|
29
|
+
* decoded string secret. Throws {@link HumanReadableError} for anything that is
|
|
30
|
+
* not a valid current-version `was-link` payload (so the scanner shows a clean
|
|
31
|
+
* message rather than mis-handling an unrelated QR).
|
|
32
|
+
*
|
|
33
|
+
* @param raw {string} the raw scanned code value
|
|
34
|
+
* @returns {WasLinkPayload}
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseWasLinkPayload(raw: string): WasLinkPayload;
|
|
37
|
+
//# sourceMappingURL=wasLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasLink.d.ts","sourceRoot":"","sources":["../../src/space/wasLink.ts"],"names":[],"mappings":"AAgBA,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;CACf;AAyCD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,UAAU,EACX,EAAE;IACD,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,MAAM,CAOT;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAyC/D"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `was-link` QR hand-off payload -- the "Connect mobile wallet" flow where
|
|
6
|
+
* the web wallet displays a QR that carries an account's controller secret and
|
|
7
|
+
* the mobile wallet scans it.
|
|
8
|
+
*
|
|
9
|
+
* The payload is a NON-URL JSON blob (deliberately not a link, so the OS camera
|
|
10
|
+
* app / deep-link handling never routes it and it cannot leak into browser
|
|
11
|
+
* history or link-preview fetchers). It is accepted only by the in-app scanner.
|
|
12
|
+
* Shape: `{ v: 1, t: 'was-link', serverUrl, secret: base64url(utf8(passphrase)) }`.
|
|
13
|
+
*/
|
|
14
|
+
import { base64urlnopad } from '@scure/base';
|
|
15
|
+
import { HumanReadableError } from './errors.js';
|
|
16
|
+
const NOT_A_LINK = 'This QR code is not a wallet connection code.';
|
|
17
|
+
const BAD_SERVER = 'Connection code points at an insecure or invalid server and was rejected.';
|
|
18
|
+
/** Strips optional base64 padding so the no-pad decoder accepts either form. */
|
|
19
|
+
function stripPadding(value) {
|
|
20
|
+
return value.replace(/=+$/, '');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validates the scanned `serverUrl` before the wallet ever signs a request to
|
|
24
|
+
* it. The code carries the controller secret in the clear, so a bad `serverUrl`
|
|
25
|
+
* would let a crafted QR aim capability-signed requests (and the secret) at an
|
|
26
|
+
* attacker-controlled or cleartext host. Requires a parseable `https:` URL;
|
|
27
|
+
* plain `http:` is allowed ONLY for loopback dev hosts.
|
|
28
|
+
*
|
|
29
|
+
* This is a scheme/host rule, not an allowlist: the payload carries its own
|
|
30
|
+
* `serverUrl`, and there is no app-configured set of permitted servers to inject
|
|
31
|
+
* -- a connection code names the storage server the user is joining. If a
|
|
32
|
+
* consuming app later needs to constrain the host further, that check belongs at
|
|
33
|
+
* the call site, on the returned {@link WasLinkPayload.serverUrl}.
|
|
34
|
+
*/
|
|
35
|
+
function assertValidServerUrl(serverUrl) {
|
|
36
|
+
let url;
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(serverUrl);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new HumanReadableError(BAD_SERVER);
|
|
42
|
+
}
|
|
43
|
+
const isLoopback = url.hostname === 'localhost' ||
|
|
44
|
+
url.hostname === '127.0.0.1' ||
|
|
45
|
+
url.hostname === '::1';
|
|
46
|
+
if (url.protocol === 'https:' || (url.protocol === 'http:' && isLoopback)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
throw new HumanReadableError(BAD_SERVER);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Encodes a passphrase into the payload's `secret` field: `base64url(utf8(...))`,
|
|
53
|
+
* no padding.
|
|
54
|
+
*
|
|
55
|
+
* @param passphrase {string}
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function encodeWasLinkSecret(passphrase) {
|
|
59
|
+
return base64urlnopad.encode(new TextEncoder().encode(passphrase));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds the full `was-link` payload string a QR encodes.
|
|
63
|
+
*
|
|
64
|
+
* @param options {object}
|
|
65
|
+
* @param options.serverUrl {string}
|
|
66
|
+
* @param options.passphrase {string}
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export function buildWasLinkPayload({ serverUrl, passphrase }) {
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
v: 1,
|
|
72
|
+
t: 'was-link',
|
|
73
|
+
serverUrl,
|
|
74
|
+
secret: encodeWasLinkSecret(passphrase)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses and validates a scanned payload, returning the server URL and the
|
|
79
|
+
* decoded string secret. Throws {@link HumanReadableError} for anything that is
|
|
80
|
+
* not a valid current-version `was-link` payload (so the scanner shows a clean
|
|
81
|
+
* message rather than mis-handling an unrelated QR).
|
|
82
|
+
*
|
|
83
|
+
* @param raw {string} the raw scanned code value
|
|
84
|
+
* @returns {WasLinkPayload}
|
|
85
|
+
*/
|
|
86
|
+
export function parseWasLinkPayload(raw) {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(raw);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new HumanReadableError(NOT_A_LINK);
|
|
93
|
+
}
|
|
94
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
95
|
+
throw new HumanReadableError(NOT_A_LINK);
|
|
96
|
+
}
|
|
97
|
+
const { v, t, serverUrl, secret } = parsed;
|
|
98
|
+
if (t !== 'was-link') {
|
|
99
|
+
throw new HumanReadableError(NOT_A_LINK);
|
|
100
|
+
}
|
|
101
|
+
if (v !== 1) {
|
|
102
|
+
throw new HumanReadableError('This connection code is from a newer app version. Please update.');
|
|
103
|
+
}
|
|
104
|
+
if (typeof serverUrl !== 'string' || serverUrl.length === 0) {
|
|
105
|
+
throw new HumanReadableError('Connection code is missing its server.');
|
|
106
|
+
}
|
|
107
|
+
assertValidServerUrl(serverUrl);
|
|
108
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
109
|
+
throw new HumanReadableError('Connection code is missing its secret.');
|
|
110
|
+
}
|
|
111
|
+
let secretText;
|
|
112
|
+
try {
|
|
113
|
+
secretText = new TextDecoder().decode(base64urlnopad.decode(stripPadding(secret)));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new HumanReadableError('Connection code has a malformed secret.');
|
|
117
|
+
}
|
|
118
|
+
if (secretText.length === 0) {
|
|
119
|
+
throw new HumanReadableError('Connection code has an empty secret.');
|
|
120
|
+
}
|
|
121
|
+
return { serverUrl, secret: secretText };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=wasLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasLink.js","sourceRoot":"","sources":["../../src/space/wasLink.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;GASG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAShD,MAAM,UAAU,GAAG,+CAA+C,CAAA;AAClE,MAAM,UAAU,GACd,2EAA2E,CAAA;AAE7E,gFAAgF;AAChF,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACjC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,oBAAoB,CAAC,SAAiB;IAC7C,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAC1C,CAAC;IACD,MAAM,UAAU,GACd,GAAG,CAAC,QAAQ,KAAK,WAAW;QAC5B,GAAG,CAAC,QAAQ,KAAK,WAAW;QAC5B,GAAG,CAAC,QAAQ,KAAK,KAAK,CAAA;IACxB,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,EAAE,CAAC;QAC1E,OAAM;IACR,CAAC;IACD,MAAM,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAA;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,OAAO,cAAc,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;AACpE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,SAAS,EACT,UAAU,EAIX;IACC,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,CAAC,EAAE,CAAC;QACJ,CAAC,EAAE,UAAU;QACb,SAAS;QACT,MAAM,EAAE,mBAAmB,CAAC,UAAU,CAAC;KACxC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAC1C,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,MAAM,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAC1C,CAAC;IAED,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAiC,CAAA;IACrE,IAAI,CAAC,KAAK,UAAU,EAAE,CAAC;QACrB,MAAM,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAC1C,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACZ,MAAM,IAAI,kBAAkB,CAC1B,kEAAkE,CACnE,CAAA;IACH,CAAC;IACD,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,kBAAkB,CAAC,wCAAwC,CAAC,CAAA;IACxE,CAAC;IACD,oBAAoB,CAAC,SAAS,CAAC,CAAA;IAC/B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,kBAAkB,CAAC,wCAAwC,CAAC,CAAA;IACxE,CAAC;IAED,IAAI,UAAkB,CAAA;IACtB,IAAI,CAAC;QACH,UAAU,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CACnC,cAAc,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAC5C,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,kBAAkB,CAAC,yCAAyC,CAAC,CAAA;IACzE,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,kBAAkB,CAAC,sCAAsC,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AAC1C,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The generic per-collection spec SHAPE the replication engine + store drive one
|
|
6
|
+
* feed with. It bundles everything a feed needs: its id and id-derivation model,
|
|
7
|
+
* its plaintext-projection transactional writers, its lazy-migration sweep, the
|
|
8
|
+
* read-model refresh to fire after a pull, and (for a mutable collection) its
|
|
9
|
+
* 412 last-write-wins policy.
|
|
10
|
+
*
|
|
11
|
+
* Only the SHAPE lives here. The concrete registry -- which collections a given
|
|
12
|
+
* app replicates and how each writer touches the app's own store -- stays
|
|
13
|
+
* app-side, because the writers bind to the app's transaction handle and its
|
|
14
|
+
* read-model refresh mechanism. The spec is parameterized over those two so a
|
|
15
|
+
* SQLite-backed replica and a future RxDB adapter can both implement it without
|
|
16
|
+
* this module importing either store:
|
|
17
|
+
*
|
|
18
|
+
* - `Tx` is the store's transaction handle passed to `applyUpsertTx` /
|
|
19
|
+
* `applyDeleteTx` (a SQLite database instance in the mobile store; whatever an
|
|
20
|
+
* RxDB writer needs in the browser store). It replaces the mobile store's
|
|
21
|
+
* concrete `SQLiteDatabase` leak with a type parameter.
|
|
22
|
+
* - `RefreshContext` is whatever `onPullApplied` needs to trigger a read-model
|
|
23
|
+
* refresh (a Redux dispatch in one app, a store handle or `void` in another).
|
|
24
|
+
*/
|
|
25
|
+
import type { DocCipher, Json, ResolveConflict, SyncStore, WasSyncPort } from './types.js';
|
|
26
|
+
/**
|
|
27
|
+
* One synced collection's full behavior. `applyUpsertTx` / `applyDeleteTx` are
|
|
28
|
+
* the store's plaintext-projection writers, invoked inside the store's pull
|
|
29
|
+
* transaction with the decrypted payload; `migrate` is the unlinked-row sweep;
|
|
30
|
+
* `makeResolveConflict` is present only for a mutable (LWW) collection, absent
|
|
31
|
+
* for insert-only content-addressed collections whose push settlement covers
|
|
32
|
+
* every 412.
|
|
33
|
+
*
|
|
34
|
+
* `encryption` selects the doc cipher and the server-side collection marker:
|
|
35
|
+
* `'edv'` (the default) envelopes every doc, `'plaintext'` ships it verbatim;
|
|
36
|
+
* `isPublic` grants collection-level world read on the server. Both replicas
|
|
37
|
+
* MUST agree on `collectionId` / `idDerivation` / `encryption` / `isPublic` for
|
|
38
|
+
* a collection, or their writes land in separate or differently-shaped
|
|
39
|
+
* collections and never converge.
|
|
40
|
+
*/
|
|
41
|
+
export interface SyncedCollectionSpec<Tx = unknown, RefreshContext = void> {
|
|
42
|
+
collectionId: string;
|
|
43
|
+
idDerivation: 'content' | 'random';
|
|
44
|
+
encryption?: 'edv' | 'plaintext';
|
|
45
|
+
isPublic?: boolean;
|
|
46
|
+
applyUpsertTx: (tx: Tx, profileRecordId: string, syncId: string, payload: Json) => Promise<void>;
|
|
47
|
+
applyDeleteTx: (tx: Tx, profileRecordId: string, syncId: string) => Promise<void>;
|
|
48
|
+
onPullApplied: (context: RefreshContext) => void;
|
|
49
|
+
migrate: (options: {
|
|
50
|
+
profileRecordId: string;
|
|
51
|
+
cipher: DocCipher;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
}) => Promise<void>;
|
|
54
|
+
makeResolveConflict?: (options: {
|
|
55
|
+
profileRecordId: string;
|
|
56
|
+
cipher: DocCipher;
|
|
57
|
+
port: WasSyncPort;
|
|
58
|
+
store: SyncStore;
|
|
59
|
+
}) => ResolveConflict;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/sync/collections.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,SAAS,EACT,IAAI,EACJ,eAAe,EACf,SAAS,EACT,WAAW,EACZ,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,oBAAoB,CAAC,EAAE,GAAG,OAAO,EAAE,cAAc,GAAG,IAAI;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,GAAG,QAAQ,CAAA;IAClC,UAAU,CAAC,EAAE,KAAK,GAAG,WAAW,CAAA;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,CACb,EAAE,EAAE,EAAE,EACN,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,IAAI,KACV,OAAO,CAAC,IAAI,CAAC,CAAA;IAClB,aAAa,EAAE,CACb,EAAE,EAAE,EAAE,EACN,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,IAAI,CAAC,CAAA;IAClB,aAAa,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IAChD,OAAO,EAAE,CAAC,OAAO,EAAE;QACjB,eAAe,EAAE,MAAM,CAAA;QACvB,MAAM,EAAE,SAAS,CAAA;QACjB,MAAM,CAAC,EAAE,WAAW,CAAA;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnB,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC9B,eAAe,EAAE,MAAM,CAAA;QACvB,MAAM,EAAE,SAAS,CAAA;QACjB,IAAI,EAAE,WAAW,CAAA;QACjB,KAAK,EAAE,SAAS,CAAA;KACjB,KAAK,eAAe,CAAA;CACtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.js","sourceRoot":"","sources":["../../src/sync/collections.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* `SyncEngine` -- drives one `(replica, collection)` feed through the pull/push
|
|
6
|
+
* cycle. Single-flight (concurrent `sync()` calls coalesce and set a rerun
|
|
7
|
+
* flag), migrate-once (the initial pull-before-migrate ordering that keeps
|
|
8
|
+
* envelope minting from creating server-side duplicates), and self-healing via
|
|
9
|
+
* exponential backoff + jitter on failure.
|
|
10
|
+
*
|
|
11
|
+
* All side effects are injected ({@link SyncEngineDeps}) so the engine runs
|
|
12
|
+
* anywhere -- browser, Node, or React Native -- with a fake port, an in-memory
|
|
13
|
+
* store, and a non-firing scheduler. The consuming app wires the real port,
|
|
14
|
+
* DocCipher, provisioning, and lazy migration.
|
|
15
|
+
*/
|
|
16
|
+
import type { Json, ResolveConflict, SyncStore, WasSyncPort } from './types.js';
|
|
17
|
+
/** Per-feed replication status, surfaced to the app's state layer. */
|
|
18
|
+
export type SyncStatus = 'idle' | 'syncing' | 'synced' | 'error';
|
|
19
|
+
/**
|
|
20
|
+
* Everything the engine needs, injected. The pure protocol ({@link WasSyncPort},
|
|
21
|
+
* {@link SyncStore}) plus the app-supplied seams: provisioning, the
|
|
22
|
+
* migrated/last-synced stamps, decryption, and status/refetch callbacks. The
|
|
23
|
+
* `schedule` / `random` seams make backoff deterministic under test.
|
|
24
|
+
*/
|
|
25
|
+
export interface SyncEngineDeps {
|
|
26
|
+
port: WasSyncPort;
|
|
27
|
+
store: SyncStore;
|
|
28
|
+
/** Decrypts a pulled body to its plaintext payload (DocCipher). */
|
|
29
|
+
decryptDoc: (envelope: Json) => Promise<Json>;
|
|
30
|
+
/**
|
|
31
|
+
* The 412 policy for a mutable (LWW) collection. Absent for insert-only
|
|
32
|
+
* content-addressed collections, whose built-in push settlement covers every
|
|
33
|
+
* conflict.
|
|
34
|
+
*/
|
|
35
|
+
resolveConflict?: ResolveConflict;
|
|
36
|
+
batchSize?: number;
|
|
37
|
+
/** Idempotent space + collection provisioning. */
|
|
38
|
+
ensureProvisioned: () => Promise<void>;
|
|
39
|
+
/** Has this feed's lazy migration already run (per-collection milestone)? */
|
|
40
|
+
isMigrated: () => Promise<boolean>;
|
|
41
|
+
/** Mint bodies for this feed's still-unlinked local rows. */
|
|
42
|
+
runLazyMigration: (signal: AbortSignal) => Promise<void>;
|
|
43
|
+
/** Record this feed's migrated milestone after a successful first migration. */
|
|
44
|
+
stampMigrated: () => Promise<void>;
|
|
45
|
+
/** Stamp the replica's last-synced time after a successful cycle. */
|
|
46
|
+
stampLastSynced: () => Promise<void>;
|
|
47
|
+
/** Called on every status transition (drives the app's state layer). */
|
|
48
|
+
onStatusChange?: (status: SyncStatus) => void;
|
|
49
|
+
/** Called after a pull that applied >= 1 document (triggers the refetch). */
|
|
50
|
+
onPullApplied?: () => void;
|
|
51
|
+
backoff?: {
|
|
52
|
+
baseDelayMs?: number;
|
|
53
|
+
maxDelayMs?: number;
|
|
54
|
+
};
|
|
55
|
+
/** Schedules a retry; returns a canceller. Defaults to setTimeout. */
|
|
56
|
+
schedule?: (fn: () => void, delayMs: number) => () => void;
|
|
57
|
+
/** Jitter source in [0, 1). Defaults to Math.random. */
|
|
58
|
+
random?: () => number;
|
|
59
|
+
}
|
|
60
|
+
export declare class SyncEngine {
|
|
61
|
+
private readonly deps;
|
|
62
|
+
status: SyncStatus;
|
|
63
|
+
private readonly batchSize;
|
|
64
|
+
private readonly baseDelayMs;
|
|
65
|
+
private readonly maxDelayMs;
|
|
66
|
+
private readonly schedule;
|
|
67
|
+
private readonly random;
|
|
68
|
+
private running;
|
|
69
|
+
private rerunRequested;
|
|
70
|
+
private stopped;
|
|
71
|
+
private failureCount;
|
|
72
|
+
private currentRun;
|
|
73
|
+
private abortController;
|
|
74
|
+
private cancelRetry;
|
|
75
|
+
constructor(deps: SyncEngineDeps);
|
|
76
|
+
/**
|
|
77
|
+
* Requests a sync. Single-flight: if a cycle is in flight this only flags a
|
|
78
|
+
* rerun (so writes that land mid-cycle are not lost) and resolves with the
|
|
79
|
+
* in-flight run; otherwise it starts a fresh run. Never rejects -- failures
|
|
80
|
+
* settle into `status = 'error'` plus a scheduled backoff retry, per the
|
|
81
|
+
* local-first invariant (sync must never surface as a rejected write).
|
|
82
|
+
*
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
sync(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Stops the engine: aborts any in-flight cycle (the injected signal unwinds
|
|
88
|
+
* pull/push between pages/rows), cancels a pending retry, and resets to idle.
|
|
89
|
+
* The caller drops the cached agents/ciphers so key material does not outlive
|
|
90
|
+
* the unlocked session.
|
|
91
|
+
*/
|
|
92
|
+
stop(): void;
|
|
93
|
+
private run;
|
|
94
|
+
/**
|
|
95
|
+
* One full replication cycle. On the very first run (never migrated) it pulls
|
|
96
|
+
* before the sweep so existing local rows hash-link to any bodies already on
|
|
97
|
+
* the server -- the sweep then only encrypts genuinely-new records
|
|
98
|
+
* (re-encrypting an existing one would mint a different content id and leave a
|
|
99
|
+
* permanent server duplicate). The unlinked-record sweep (`runLazyMigration`)
|
|
100
|
+
* runs on EVERY cycle, not just the first, so records that enter the replica
|
|
101
|
+
* outside the synced write path -- an import, or a write whose minting failed
|
|
102
|
+
* and fell back to a plain insert -- are still picked up and pushed (it is a
|
|
103
|
+
* cheap no-op when there are none). Steady state is sweep-then-push-then-pull:
|
|
104
|
+
* our own writes echo back in the same cycle's pull, idempotently.
|
|
105
|
+
*/
|
|
106
|
+
private runCycle;
|
|
107
|
+
private pull;
|
|
108
|
+
private scheduleRetry;
|
|
109
|
+
private clearRetry;
|
|
110
|
+
private setStatus;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/sync/engine.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAI/E,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAA;AAMhE;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAA;IACjB,KAAK,EAAE,SAAS,CAAA;IAChB,mEAAmE;IACnE,UAAU,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,6EAA6E;IAC7E,UAAU,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAClC,6DAA6D;IAC7D,gBAAgB,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxD,gFAAgF;IAChF,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,qEAAqE;IACrE,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpC,wEAAwE;IACxE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAA;IAC7C,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAE1B,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvD,sEAAsE;IACtE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC1D,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;CACtB;AAOD,qBAAa,UAAU;IAiBT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhBjC,MAAM,EAAE,UAAU,CAAS;IAE3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiD;IAC1E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,WAAW,CAA4B;gBAElB,IAAI,EAAE,cAAc;IAQjD;;;;;;;;OAQG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrB;;;;;OAKG;IACH,IAAI,IAAI,IAAI;YAOE,GAAG;IA8BjB;;;;;;;;;;;OAWG;YACW,QAAQ;YAoDR,IAAI;IAWlB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,SAAS;CAOlB"}
|