@interop/was-react 0.2.1 → 0.3.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/README.md +9 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/documentApp.d.ts +2 -2
- package/dist/react/documentApp.d.ts.map +1 -1
- package/dist/react/documentApp.js +8 -8
- package/dist/react/documentApp.js.map +1 -1
- package/dist/storage/adopt.js +7 -7
- package/dist/storage/entityStore.d.ts +1 -1
- package/dist/storage/localStore.d.ts +2 -2
- package/dist/storage/localStore.js +2 -2
- package/dist/storage/storageManager.d.ts +3 -3
- package/dist/storage/storageManager.js +7 -7
- package/dist/sync/lww.d.ts +9 -9
- package/dist/sync/lww.js +10 -10
- package/dist/sync/lwwConflictHandler.d.ts +5 -5
- package/dist/sync/lwwConflictHandler.js +4 -4
- package/package.json +1 -4
- package/dist/dev/cli.d.ts +0 -16
- package/dist/dev/cli.d.ts.map +0 -1
- package/dist/dev/cli.js +0 -119
- package/dist/dev/cli.js.map +0 -1
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ no per-collection key is derived, payloads are stored as-is locally and
|
|
|
104
104
|
remotely, and the stored resource id is the payload's own logical `id`, so a
|
|
105
105
|
public document keeps a stable, shareable resource URL across edits. Be aware
|
|
106
106
|
that everything in a public payload is world-readable -- including the LWW
|
|
107
|
-
bookkeeping fields `updatedAt` and `
|
|
107
|
+
bookkeeping fields `updatedAt` and `clientId` (`clientId` is a random
|
|
108
108
|
per-install identifier, but still a linkability handle across a user's public
|
|
109
109
|
documents). Changing a collection's visibility after first use is a
|
|
110
110
|
data-migration event, not a config tweak: rows written in the other mode stop
|
|
@@ -112,7 +112,7 @@ being readable. A registry that maps one WAS collection id to both visibilities
|
|
|
112
112
|
is rejected at store open. Two payload constraints on public collections: a
|
|
113
113
|
top-level object-valued `jwe` field is reserved (the read path uses it to
|
|
114
114
|
recognize a stray encrypted envelope and refuses the row), and payloads should
|
|
115
|
-
carry the `updatedAt` / `
|
|
115
|
+
carry the `updatedAt` / `clientId` LWW fields like any other collection --
|
|
116
116
|
without them a concurrent multi-device edit falls back to server-wins.
|
|
117
117
|
|
|
118
118
|
At login, a public collection is requested from the wallet with the distinct
|
|
@@ -138,7 +138,7 @@ export interface Note {
|
|
|
138
138
|
body: string
|
|
139
139
|
createdAt: string
|
|
140
140
|
updatedAt: string
|
|
141
|
-
|
|
141
|
+
clientId: string
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export const useNotes = createEntityStore<Note>('notes')
|
|
@@ -201,7 +201,7 @@ export function LoginPage() {
|
|
|
201
201
|
|
|
202
202
|
```tsx
|
|
203
203
|
import { uuidv7 } from 'uuidv7'
|
|
204
|
-
import {
|
|
204
|
+
import { getClientId } from '@interop/was-react'
|
|
205
205
|
import { useNotes } from './stores.js'
|
|
206
206
|
|
|
207
207
|
export function Notes() {
|
|
@@ -216,7 +216,7 @@ export function Notes() {
|
|
|
216
216
|
body: '',
|
|
217
217
|
createdAt: now,
|
|
218
218
|
updatedAt: now,
|
|
219
|
-
|
|
219
|
+
clientId: getClientId()
|
|
220
220
|
})
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -233,7 +233,7 @@ export function Notes() {
|
|
|
233
233
|
}
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
-
Entity payloads MUST carry `updatedAt` and `
|
|
236
|
+
Entity payloads MUST carry `updatedAt` and `clientId` (from `getClientId()`),
|
|
237
237
|
stamped on EVERY insert and update: they are the last-write-wins pair that
|
|
238
238
|
settles concurrent multi-device edits of the same entity. A payload without them
|
|
239
239
|
loses every sync conflict.
|
|
@@ -402,8 +402,8 @@ Hooks:
|
|
|
402
402
|
zcaps). Pull is driven by the WAS `changes` feed; a low-frequency periodic
|
|
403
403
|
re-sync (`sync.pollMs`, default 15s) keeps open sessions converging.
|
|
404
404
|
- **Conflict resolution.** Last-writer-wins on the payload's own
|
|
405
|
-
`(updatedAt,
|
|
406
|
-
`
|
|
405
|
+
`(updatedAt, clientId)` (ISO lexical compare, with a per-install random
|
|
406
|
+
`clientId` tiebreaker). Updates re-encrypt in place under the same envelope id
|
|
407
407
|
with `sequence`+1 (a mutable-head model); deletes are soft-delete tombstones.
|
|
408
408
|
- **Status.** `useSyncStatus()` rolls the per-collection replication states up
|
|
409
409
|
to an aggregate: `offline` (no replication running / local-only), or
|
|
@@ -419,7 +419,7 @@ cost.
|
|
|
419
419
|
| ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
|
|
420
420
|
| `@interop/was-react` | Core: config, identity, auth, sync, storage, session store, React provider and hooks | `react`, `zustand`, `rxdb` |
|
|
421
421
|
| `@interop/was-react/mui` | Optional `ProtectedRoute`, `ReconnectBanner`, `SyncStatusChip` | `@mui/material`, `@mui/icons-material`, `react-router` |
|
|
422
|
-
| `@interop/was-react/dev` | Node-only `provisionDevGrants`
|
|
422
|
+
| `@interop/was-react/dev` | Node-only `provisionDevGrants` | (none; Node only) |
|
|
423
423
|
|
|
424
424
|
MUI usage:
|
|
425
425
|
|
|
@@ -463,19 +463,6 @@ The `./dev` entry provisions a Space, collections, and delegated read/write
|
|
|
463
463
|
grants against a running was-teaching-server, so an app can dev-sync without a
|
|
464
464
|
CHAPI wallet in the loop. It is Node-only (uses `node:fs`).
|
|
465
465
|
|
|
466
|
-
CLI:
|
|
467
|
-
|
|
468
|
-
```
|
|
469
|
-
was-provision-dev-grants \
|
|
470
|
-
--server-url http://localhost:3002 \
|
|
471
|
-
--seed <hex-or-base64url 32-byte seed> \
|
|
472
|
-
--collections notes \
|
|
473
|
-
--space-name "Dev Space" \
|
|
474
|
-
--out ./public/dev-grants.local.json
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Programmatic:
|
|
478
|
-
|
|
479
466
|
```ts
|
|
480
467
|
import { provisionDevGrants } from '@interop/was-react/dev'
|
|
481
468
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { loginWithWallet, requestGrants, LoginCancelledError, WalletUnsupportedE
|
|
|
25
25
|
export type { WalletAPIMessage, IVPOffer, IVPRequest, IVPRDetails, IVPRQuery, IQueryByExample, IDIDAuthenticationQuery, IZcapQuery, IAppConnectQuery, IAppConnectCapabilityQuery, ICapabilityQueryDetail, WalletResponse, WalletRequestProfile, IVerifiableCredential, IVerifiablePresentation, IZcap } from './auth/walletRequestTypes.js';
|
|
26
26
|
export * from './sync/index.js';
|
|
27
27
|
export { LocalStore } from './storage/localStore.js';
|
|
28
|
-
export { setLocalStore, requireStore, hasStore, clearLocalStore, setRemoteStore, requireRemoteStore, hasRemoteStore, clearRemoteStore,
|
|
28
|
+
export { setLocalStore, requireStore, hasStore, clearLocalStore, setRemoteStore, requireRemoteStore, hasRemoteStore, clearRemoteStore, getClientId } from './storage/storageManager.js';
|
|
29
29
|
export { createEntityStore, type EntityStore } from './storage/entityStore.js';
|
|
30
30
|
export { WasRemoteStore, type MarkerResult, type EqualityQueryPage } from './storage/wasRemoteStore.js';
|
|
31
31
|
export { publicUrlFor } from './storage/publicUrl.js';
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ export * from './sync/index.js';
|
|
|
32
32
|
// stores, the delegated remote store, sync status, rehydrate mechanism, and the
|
|
33
33
|
// replication controller.
|
|
34
34
|
export { LocalStore } from './storage/localStore.js';
|
|
35
|
-
export { setLocalStore, requireStore, hasStore, clearLocalStore, setRemoteStore, requireRemoteStore, hasRemoteStore, clearRemoteStore,
|
|
35
|
+
export { setLocalStore, requireStore, hasStore, clearLocalStore, setRemoteStore, requireRemoteStore, hasRemoteStore, clearRemoteStore, getClientId } from './storage/storageManager.js';
|
|
36
36
|
export { createEntityStore } from './storage/entityStore.js';
|
|
37
37
|
export { WasRemoteStore } from './storage/wasRemoteStore.js';
|
|
38
38
|
export { publicUrlFor } from './storage/publicUrl.js';
|
|
@@ -27,7 +27,7 @@ export interface DocumentApp<T extends object> {
|
|
|
27
27
|
/**
|
|
28
28
|
* The tier-1 document hook. Usable in any component below the provider.
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
useAppDocument: () => {
|
|
31
31
|
/**
|
|
32
32
|
* The document: `undefined` during boot, then the stored value or the
|
|
33
33
|
* configured `initial` when nothing has been written yet.
|
|
@@ -87,7 +87,7 @@ export interface DocumentApp<T extends object> {
|
|
|
87
87
|
/**
|
|
88
88
|
* Builds the complete wiring for a one-document app: a `WasAppConfig` with a
|
|
89
89
|
* single sandbox collection and local-first onboarding, the singleton-document
|
|
90
|
-
* store registry, and the typed `
|
|
90
|
+
* store registry, and the typed `useAppDocument` hook bound to both.
|
|
91
91
|
*
|
|
92
92
|
* @param options {object}
|
|
93
93
|
* @param options.appName {string} human-readable name (consent reason lines)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentApp.d.ts","sourceRoot":"","sources":["../../src/react/documentApp.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAG/D;;GAEG;AACH,eAAO,MAAM,uBAAuB,aAAa,CAAA;AAOjD;;GAEG;AACH,eAAO,MAAM,sBAAsB,oBAAoB,CAAA;AAavD;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM;IAC3C;;;OAGG;IACH,MAAM,EAAE,YAAY,CAAA;IACpB;;OAEG;IACH,QAAQ,EAAE,aAAa,CAAA;IACvB;;OAEG;IACH,
|
|
1
|
+
{"version":3,"file":"documentApp.d.ts","sourceRoot":"","sources":["../../src/react/documentApp.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAG/D;;GAEG;AACH,eAAO,MAAM,uBAAuB,aAAa,CAAA;AAOjD;;GAEG;AACH,eAAO,MAAM,sBAAsB,oBAAoB,CAAA;AAavD;;;GAGG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM;IAC3C;;;OAGG;IACH,MAAM,EAAE,YAAY,CAAA;IACpB;;OAEG;IACH,QAAQ,EAAE,aAAa,CAAA;IACvB;;OAEG;IACH,cAAc,EAAE,MAAM;QACpB;;;WAGG;QACH,GAAG,EAAE,CAAC,GAAG,SAAS,CAAA;QAClB;;;;;WAKG;QACH,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC/D;;;WAGG;QACH,MAAM,EAAE,aAAa,CAAA;QACrB;;WAEG;QACH,IAAI,EAAE,UAAU,CAAA;QAChB;;WAEG;QACH,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;QAC/B;;;WAGG;QACH,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACzC;;;;;;;WAOG;QACH,OAAO,EAAE,MAAM,OAAO,CAAC;YAAE,QAAQ,EAAE,OAAO,CAAA;SAAE,GAAG,IAAI,CAAC,CAAA;QACpD;;;WAGG;QACH,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;QAC/B;;WAEG;QACH,UAAU,EAAE,OAAO,CAAA;QACnB;;WAEG;QACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KACrB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,EAClD,OAAO,EACP,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,MAAM,EACN,gBAAgB,EAChB,IAAI,EACJ,MAAM,EACP,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAA;IAC9C,UAAU,EAAE,oBAAoB,CAAA;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,MAAM,CAAC,EAAE,eAAe,CAAA;CACzB,GAAG,WAAW,CAAC,CAAC,CAAC,CAgIjB"}
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
* The tier-1 "one sandbox document" facade: `defineDocumentApp` builds the
|
|
6
6
|
* whole was-react wiring (config, store registry, entity store) for an app
|
|
7
7
|
* whose entire model is a single key-value document -- an Excalidraw-style
|
|
8
|
-
* editor, a browser-game save file -- and returns a typed `
|
|
8
|
+
* editor, a browser-game save file -- and returns a typed `useAppDocument` hook
|
|
9
9
|
* over it. The app never sees `createEntityStore`, grants parsing, or sync
|
|
10
10
|
* internals: it renders `doc`, calls `update`, and optionally offers file
|
|
11
11
|
* export/import and a "Save to Web Spaces" connect button.
|
|
12
12
|
*
|
|
13
13
|
* The facade is a degenerate entity store: one collection (the app-named
|
|
14
14
|
* sandbox collection) holding one logical document under a fixed id. The
|
|
15
|
-
* stored row wraps the app's data (`{ id, updatedAt,
|
|
15
|
+
* stored row wraps the app's data (`{ id, updatedAt, clientId, data }`) so app
|
|
16
16
|
* fields can never collide with the LWW fields the sync layer requires, and
|
|
17
|
-
* the facade stamps `updatedAt`/`
|
|
17
|
+
* the facade stamps `updatedAt`/`clientId` on every write itself. Hydration
|
|
18
18
|
* goes through `LocalStore.hydrateSingleton`, which LWW-reconciles the
|
|
19
19
|
* duplicate envelope rows two devices can mint for the same logical document.
|
|
20
20
|
*
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { useStore } from 'zustand';
|
|
31
31
|
import { createEntityStore } from '../storage/entityStore.js';
|
|
32
|
-
import {
|
|
32
|
+
import { getClientId, requireStore } from '../storage/storageManager.js';
|
|
33
33
|
import { useAuthStore, useSession, useSyncStatus } from './hooks.js';
|
|
34
34
|
/**
|
|
35
35
|
* The localStore / RxDB collection key the facade's one collection uses.
|
|
@@ -46,7 +46,7 @@ export const DOCUMENT_EXPORT_FORMAT = 'was-document/v1';
|
|
|
46
46
|
/**
|
|
47
47
|
* Builds the complete wiring for a one-document app: a `WasAppConfig` with a
|
|
48
48
|
* single sandbox collection and local-first onboarding, the singleton-document
|
|
49
|
-
* store registry, and the typed `
|
|
49
|
+
* store registry, and the typed `useAppDocument` hook bound to both.
|
|
50
50
|
*
|
|
51
51
|
* @param options {object}
|
|
52
52
|
* @param options.appName {string} human-readable name (consent reason lines)
|
|
@@ -103,7 +103,7 @@ export function defineDocumentApp({ appName, appOrigin, mediatorBase, document,
|
|
|
103
103
|
await docStore.getState().upsert({
|
|
104
104
|
id: DOCUMENT_ID,
|
|
105
105
|
updatedAt: new Date().toISOString(),
|
|
106
|
-
|
|
106
|
+
clientId: getClientId({
|
|
107
107
|
...(storageKeyPrefix !== undefined && { storageKeyPrefix })
|
|
108
108
|
}),
|
|
109
109
|
data
|
|
@@ -152,7 +152,7 @@ export function defineDocumentApp({ appName, appOrigin, mediatorBase, document,
|
|
|
152
152
|
const imported = record.document;
|
|
153
153
|
await update(() => imported);
|
|
154
154
|
}
|
|
155
|
-
function
|
|
155
|
+
function useAppDocument() {
|
|
156
156
|
const held = docStore(state => state.byId.get(DOCUMENT_ID));
|
|
157
157
|
const { status, authenticating, error } = useSession();
|
|
158
158
|
const { state: syncState } = useSyncStatus();
|
|
@@ -175,6 +175,6 @@ export function defineDocumentApp({ appName, appOrigin, mediatorBase, document,
|
|
|
175
175
|
error
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
-
return { config, registry,
|
|
178
|
+
return { config, registry, useAppDocument };
|
|
179
179
|
}
|
|
180
180
|
//# sourceMappingURL=documentApp.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentApp.js","sourceRoot":"","sources":["../../src/react/documentApp.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"documentApp.js","sourceRoot":"","sources":["../../src/react/documentApp.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/adopt.js
CHANGED
|
@@ -14,30 +14,30 @@
|
|
|
14
14
|
* the same last-write-wins rule replication runs ({@link remotePayloadWins});
|
|
15
15
|
* a connected doc without LWW fields always loses to a stamped adopted one.
|
|
16
16
|
*
|
|
17
|
-
* Adopted payloads missing `updatedAt`/`
|
|
17
|
+
* Adopted payloads missing `updatedAt`/`clientId` are stamped at adoption time
|
|
18
18
|
* (the sync layer's conflict resolution requires them); payloads that already
|
|
19
19
|
* carry them keep their original values, so a doc edited long ago does not
|
|
20
20
|
* suddenly outrank fresher remote edits.
|
|
21
21
|
*/
|
|
22
22
|
import { uuidv7 } from 'uuidv7';
|
|
23
23
|
import { lwwFields, remotePayloadWins } from '../sync/lww.js';
|
|
24
|
-
import {
|
|
24
|
+
import { getClientId } from './storageManager.js';
|
|
25
25
|
/**
|
|
26
26
|
* The one-per-merge stamp applied to payloads missing their LWW fields.
|
|
27
27
|
*
|
|
28
|
-
* @returns {{ updatedAt: string,
|
|
28
|
+
* @returns {{ updatedAt: string, clientId: string }}
|
|
29
29
|
*/
|
|
30
30
|
function adoptionStamp() {
|
|
31
|
-
let
|
|
31
|
+
let clientId;
|
|
32
32
|
try {
|
|
33
|
-
|
|
33
|
+
clientId = getClientId();
|
|
34
34
|
}
|
|
35
35
|
catch {
|
|
36
36
|
// No localStorage (non-browser environments): an unpersisted id still
|
|
37
37
|
// gives the LWW tiebreak a deterministic value for this merge.
|
|
38
|
-
|
|
38
|
+
clientId = uuidv7();
|
|
39
39
|
}
|
|
40
|
-
return { updatedAt: new Date().toISOString(),
|
|
40
|
+
return { updatedAt: new Date().toISOString(), clientId };
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Merges the collected anonymous-replica payloads into `store` (the already
|
|
@@ -60,7 +60,7 @@ export interface EntityStore<T extends {
|
|
|
60
60
|
* stream owns the persisted row already). Used for per-doc reactive patching
|
|
61
61
|
* of pulled/conflict-resolved remote changes; coalesced into one store update
|
|
62
62
|
* per pull burst (see the reactivity note above). When both the incoming and
|
|
63
|
-
* the held doc carry the LWW fields (`updatedAt` + `
|
|
63
|
+
* the held doc carry the LWW fields (`updatedAt` + `clientId`), a stale
|
|
64
64
|
* incoming doc is discarded rather than clobbering the newer held one.
|
|
65
65
|
*/
|
|
66
66
|
patch: (doc: T) => void;
|
|
@@ -152,7 +152,7 @@ export declare class LocalStore {
|
|
|
152
152
|
* created the singleton before syncing produce distinct envelope rows that all
|
|
153
153
|
* decrypt to the same logical id; because LWW conflict resolution is
|
|
154
154
|
* per-envelope-id, those duplicates never reconcile on their own. This keeps
|
|
155
|
-
* the last-writer-wins winner (payload `updatedAt`, `
|
|
155
|
+
* the last-writer-wins winner (payload `updatedAt`, `clientId` tiebreak) and
|
|
156
156
|
* tombstones the losers so the deletion replicates and the space converges on
|
|
157
157
|
* one row. Returns the winning payload, or `null` when the collection is empty.
|
|
158
158
|
*
|
|
@@ -162,7 +162,7 @@ export declare class LocalStore {
|
|
|
162
162
|
hydrateSingleton<T extends {
|
|
163
163
|
id: string;
|
|
164
164
|
updatedAt: string;
|
|
165
|
-
|
|
165
|
+
clientId: string;
|
|
166
166
|
}>(key: string): Promise<T | null>;
|
|
167
167
|
/**
|
|
168
168
|
* Decrypts a single EDV envelope into its plaintext payload, for per-doc
|
|
@@ -111,7 +111,7 @@ export class LocalStore {
|
|
|
111
111
|
// Each collection gets an LWW conflict handler bound to its own cipher, so
|
|
112
112
|
// a 412 push conflict (concurrent multi-device edit of the same mutable
|
|
113
113
|
// head) is settled by decrypting both sides and comparing payload
|
|
114
|
-
// `updatedAt` (
|
|
114
|
+
// `updatedAt` (clientId tiebreak) rather than RxDB's default master-wins.
|
|
115
115
|
// On a public collection the codec is pass-through, so the handler reads
|
|
116
116
|
// those fields directly off the plaintext payload.
|
|
117
117
|
const collectionsConfig = Object.fromEntries(collections.map(({ key }) => {
|
|
@@ -351,7 +351,7 @@ export class LocalStore {
|
|
|
351
351
|
* created the singleton before syncing produce distinct envelope rows that all
|
|
352
352
|
* decrypt to the same logical id; because LWW conflict resolution is
|
|
353
353
|
* per-envelope-id, those duplicates never reconcile on their own. This keeps
|
|
354
|
-
* the last-writer-wins winner (payload `updatedAt`, `
|
|
354
|
+
* the last-writer-wins winner (payload `updatedAt`, `clientId` tiebreak) and
|
|
355
355
|
* tombstones the losers so the deletion replicates and the space converges on
|
|
356
356
|
* one row. Returns the winning payload, or `null` when the collection is empty.
|
|
357
357
|
*
|
|
@@ -53,15 +53,15 @@ export declare function hasRemoteStore(): boolean;
|
|
|
53
53
|
*/
|
|
54
54
|
export declare function clearRemoteStore(): void;
|
|
55
55
|
/**
|
|
56
|
-
* A stable per-install
|
|
57
|
-
* every payload), persisted in localStorage under `<prefix>
|
|
56
|
+
* A stable per-install client id (the last-write-wins tiebreak stamped into
|
|
57
|
+
* every payload), persisted in localStorage under `<prefix>clientId`.
|
|
58
58
|
*
|
|
59
59
|
* @param [options] {object}
|
|
60
60
|
* @param [options.storageKeyPrefix] {string} the localStorage key prefix
|
|
61
61
|
* (defaults to {@link DEFAULT_STORAGE_KEY_PREFIX})
|
|
62
62
|
* @returns {string}
|
|
63
63
|
*/
|
|
64
|
-
export declare function
|
|
64
|
+
export declare function getClientId({ storageKeyPrefix }?: {
|
|
65
65
|
storageKeyPrefix?: string;
|
|
66
66
|
}): string;
|
|
67
67
|
//# sourceMappingURL=storageManager.d.ts.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
5
|
* The storage manager: a thin process-wide holder for the one {@link LocalStore}
|
|
6
|
-
* instance, the per-session {@link WasRemoteStore}, plus the per-install
|
|
6
|
+
* instance, the per-session {@link WasRemoteStore}, plus the per-install client
|
|
7
7
|
* id. Entity stores reach for the stores through {@link requireStore} /
|
|
8
8
|
* {@link requireRemoteStore} inside their verbs rather than importing them
|
|
9
9
|
* directly, which keeps this module free of store imports (no cycle) and lets
|
|
@@ -88,20 +88,20 @@ export function clearRemoteStore() {
|
|
|
88
88
|
remoteStore = null;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* A stable per-install
|
|
92
|
-
* every payload), persisted in localStorage under `<prefix>
|
|
91
|
+
* A stable per-install client id (the last-write-wins tiebreak stamped into
|
|
92
|
+
* every payload), persisted in localStorage under `<prefix>clientId`.
|
|
93
93
|
*
|
|
94
94
|
* @param [options] {object}
|
|
95
95
|
* @param [options.storageKeyPrefix] {string} the localStorage key prefix
|
|
96
96
|
* (defaults to {@link DEFAULT_STORAGE_KEY_PREFIX})
|
|
97
97
|
* @returns {string}
|
|
98
98
|
*/
|
|
99
|
-
export function
|
|
100
|
-
const
|
|
101
|
-
let id = localStorage.getItem(
|
|
99
|
+
export function getClientId({ storageKeyPrefix = DEFAULT_STORAGE_KEY_PREFIX } = {}) {
|
|
100
|
+
const clientIdKey = `${storageKeyPrefix}clientId`;
|
|
101
|
+
let id = localStorage.getItem(clientIdKey);
|
|
102
102
|
if (!id) {
|
|
103
103
|
id = uuidv7();
|
|
104
|
-
localStorage.setItem(
|
|
104
|
+
localStorage.setItem(clientIdKey, id);
|
|
105
105
|
}
|
|
106
106
|
return id;
|
|
107
107
|
}
|
package/dist/sync/lww.d.ts
CHANGED
|
@@ -7,35 +7,35 @@
|
|
|
7
7
|
* two payloads and converge on the same winner with no coordination.
|
|
8
8
|
*
|
|
9
9
|
* `updatedAt` (an ISO-8601 string, so lexical compare == chronological compare)
|
|
10
|
-
* decides; `
|
|
10
|
+
* decides; `clientId` breaks an exact tie deterministically. Both fields live in
|
|
11
11
|
* the payload (never the envelope-level checkpoint `updatedAt`).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Whether the remote payload wins over the local one under last-write-wins.
|
|
15
15
|
* Later `updatedAt` wins; on an exact `updatedAt` tie the lexically greater
|
|
16
|
-
* `
|
|
16
|
+
* `clientId` wins (an arbitrary but deterministic, replica-independent choice).
|
|
17
17
|
*
|
|
18
|
-
* @param remote {{ updatedAt: string;
|
|
19
|
-
* @param local {{ updatedAt: string;
|
|
18
|
+
* @param remote {{ updatedAt: string; clientId: string }}
|
|
19
|
+
* @param local {{ updatedAt: string; clientId: string }}
|
|
20
20
|
* @returns {boolean} true if the remote payload should replace the local one
|
|
21
21
|
*/
|
|
22
22
|
export declare function remotePayloadWins(remote: {
|
|
23
23
|
updatedAt: string;
|
|
24
|
-
|
|
24
|
+
clientId: string;
|
|
25
25
|
}, local: {
|
|
26
26
|
updatedAt: string;
|
|
27
|
-
|
|
27
|
+
clientId: string;
|
|
28
28
|
}): boolean;
|
|
29
29
|
/**
|
|
30
30
|
* Reads the LWW fields off a doc when it carries them. Storage payloads are
|
|
31
|
-
* generic over `{ id: string }`, so docs without `updatedAt`/`
|
|
31
|
+
* generic over `{ id: string }`, so docs without `updatedAt`/`clientId` are
|
|
32
32
|
* legal; callers fall back to their own rule for those.
|
|
33
33
|
*
|
|
34
34
|
* @param doc {unknown}
|
|
35
|
-
* @returns {{ updatedAt: string,
|
|
35
|
+
* @returns {{ updatedAt: string, clientId: string } | null}
|
|
36
36
|
*/
|
|
37
37
|
export declare function lwwFields(doc: unknown): {
|
|
38
38
|
updatedAt: string;
|
|
39
|
-
|
|
39
|
+
clientId: string;
|
|
40
40
|
} | null;
|
|
41
41
|
//# sourceMappingURL=lww.d.ts.map
|
package/dist/sync/lww.js
CHANGED
|
@@ -7,36 +7,36 @@
|
|
|
7
7
|
* two payloads and converge on the same winner with no coordination.
|
|
8
8
|
*
|
|
9
9
|
* `updatedAt` (an ISO-8601 string, so lexical compare == chronological compare)
|
|
10
|
-
* decides; `
|
|
10
|
+
* decides; `clientId` breaks an exact tie deterministically. Both fields live in
|
|
11
11
|
* the payload (never the envelope-level checkpoint `updatedAt`).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Whether the remote payload wins over the local one under last-write-wins.
|
|
15
15
|
* Later `updatedAt` wins; on an exact `updatedAt` tie the lexically greater
|
|
16
|
-
* `
|
|
16
|
+
* `clientId` wins (an arbitrary but deterministic, replica-independent choice).
|
|
17
17
|
*
|
|
18
|
-
* @param remote {{ updatedAt: string;
|
|
19
|
-
* @param local {{ updatedAt: string;
|
|
18
|
+
* @param remote {{ updatedAt: string; clientId: string }}
|
|
19
|
+
* @param local {{ updatedAt: string; clientId: string }}
|
|
20
20
|
* @returns {boolean} true if the remote payload should replace the local one
|
|
21
21
|
*/
|
|
22
22
|
export function remotePayloadWins(remote, local) {
|
|
23
23
|
if (remote.updatedAt !== local.updatedAt) {
|
|
24
24
|
return remote.updatedAt > local.updatedAt;
|
|
25
25
|
}
|
|
26
|
-
return remote.
|
|
26
|
+
return remote.clientId > local.clientId;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Reads the LWW fields off a doc when it carries them. Storage payloads are
|
|
30
|
-
* generic over `{ id: string }`, so docs without `updatedAt`/`
|
|
30
|
+
* generic over `{ id: string }`, so docs without `updatedAt`/`clientId` are
|
|
31
31
|
* legal; callers fall back to their own rule for those.
|
|
32
32
|
*
|
|
33
33
|
* @param doc {unknown}
|
|
34
|
-
* @returns {{ updatedAt: string,
|
|
34
|
+
* @returns {{ updatedAt: string, clientId: string } | null}
|
|
35
35
|
*/
|
|
36
36
|
export function lwwFields(doc) {
|
|
37
|
-
const { updatedAt,
|
|
38
|
-
return typeof updatedAt === 'string' && typeof
|
|
39
|
-
? { updatedAt,
|
|
37
|
+
const { updatedAt, clientId } = doc;
|
|
38
|
+
return typeof updatedAt === 'string' && typeof clientId === 'string'
|
|
39
|
+
? { updatedAt, clientId }
|
|
40
40
|
: null;
|
|
41
41
|
}
|
|
42
42
|
//# sourceMappingURL=lww.js.map
|
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
* correct for content-addressed (immutable-per-id) collections but wrong here:
|
|
8
8
|
* every entity is a mutable head that two devices can edit concurrently, so a
|
|
9
9
|
* genuine content conflict must be settled by last-write-wins on the payload's
|
|
10
|
-
* own `updatedAt` (
|
|
10
|
+
* own `updatedAt` (clientId tiebreak) -- exactly the rule two offline replicas
|
|
11
11
|
* apply independently to converge.
|
|
12
12
|
*
|
|
13
13
|
* The wrinkle is that the conflicting bodies are EDV envelopes (ciphertext), so
|
|
14
14
|
* `resolve` must decrypt both sides through this collection's cipher before it
|
|
15
|
-
* can compare the plaintext `updatedAt` / `
|
|
15
|
+
* can compare the plaintext `updatedAt` / `clientId`. `isEqual` stays cheap and
|
|
16
16
|
* synchronous (a structural compare of the opaque bodies), as RxDB requires.
|
|
17
17
|
*
|
|
18
18
|
* Convergence: the server holds ONE winner of the push race as `realMasterState`;
|
|
19
19
|
* every replica compares that same master against its own local edit, and the
|
|
20
|
-
* `payloadWins` comparator is a total order over `(updatedAt,
|
|
20
|
+
* `payloadWins` comparator is a total order over `(updatedAt, clientId)`, so the
|
|
21
21
|
* globally-latest payload wins on every replica with no coordination.
|
|
22
22
|
*
|
|
23
23
|
* The resolution rules, in order:
|
|
@@ -57,7 +57,7 @@ import type { Json, SyncedDoc } from './types.js';
|
|
|
57
57
|
*/
|
|
58
58
|
interface LwwPayload {
|
|
59
59
|
updatedAt: string;
|
|
60
|
-
|
|
60
|
+
clientId: string;
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Builds an RxDB conflict handler that settles content conflicts by payload LWW,
|
|
@@ -67,7 +67,7 @@ interface LwwPayload {
|
|
|
67
67
|
* @param [payloadWins] {(remote: LwwPayload, local: LwwPayload) => boolean}
|
|
68
68
|
* the total-order comparator deciding whether the remote payload replaces the
|
|
69
69
|
* local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins,
|
|
70
|
-
* `
|
|
70
|
+
* `clientId` breaks a tie)
|
|
71
71
|
* @returns {import('rxdb/plugins/core').RxConflictHandler<SyncedDoc>}
|
|
72
72
|
*/
|
|
73
73
|
export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Promise<Json>, payloadWins?: (remote: LwwPayload, local: LwwPayload) => boolean): {
|
|
@@ -6,7 +6,7 @@ function bodiesEqual(a, b) {
|
|
|
6
6
|
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Decrypts one side's envelope into its `{ updatedAt,
|
|
9
|
+
* Decrypts one side's envelope into its `{ updatedAt, clientId }`, or `null`
|
|
10
10
|
* when the side has no decryptable payload (a tombstone, or an absent/corrupt
|
|
11
11
|
* body). `null` means "no comparable timestamp", handled by the caller.
|
|
12
12
|
*/
|
|
@@ -17,8 +17,8 @@ async function lwwFieldsOf(doc, decrypt) {
|
|
|
17
17
|
try {
|
|
18
18
|
const payload = (await decrypt(doc.data));
|
|
19
19
|
if (typeof payload.updatedAt === 'string' &&
|
|
20
|
-
typeof payload.
|
|
21
|
-
return { updatedAt: payload.updatedAt,
|
|
20
|
+
typeof payload.clientId === 'string') {
|
|
21
|
+
return { updatedAt: payload.updatedAt, clientId: payload.clientId };
|
|
22
22
|
}
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
@@ -34,7 +34,7 @@ async function lwwFieldsOf(doc, decrypt) {
|
|
|
34
34
|
* @param [payloadWins] {(remote: LwwPayload, local: LwwPayload) => boolean}
|
|
35
35
|
* the total-order comparator deciding whether the remote payload replaces the
|
|
36
36
|
* local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins,
|
|
37
|
-
* `
|
|
37
|
+
* `clientId` breaks a tie)
|
|
38
38
|
* @returns {import('rxdb/plugins/core').RxConflictHandler<SyncedDoc>}
|
|
39
39
|
*/
|
|
40
40
|
export function makeLwwConflictHandler(decrypt, payloadWins = remotePayloadWins) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interop/was-react",
|
|
3
3
|
"description": "React library for building 'Bring Your Own Everything' (BYOE) apps on Wallet Attached Storage: DID Auth login via CHAPI wallet, local-first encrypted storage, and background sync to a WAS server.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "pnpm run clear && tsc",
|
|
@@ -18,9 +18,6 @@
|
|
|
18
18
|
"test:node": "vitest run",
|
|
19
19
|
"test:coverage": "vitest run --coverage"
|
|
20
20
|
},
|
|
21
|
-
"bin": {
|
|
22
|
-
"was-provision-dev-grants": "./dist/dev/cli.js"
|
|
23
|
-
},
|
|
24
21
|
"files": [
|
|
25
22
|
"dist",
|
|
26
23
|
"README.md",
|
package/dist/dev/cli.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Decodes a seed string as hex (all hex chars, even length) or base64url.
|
|
4
|
-
*
|
|
5
|
-
* @param input {string}
|
|
6
|
-
* @returns {Uint8Array}
|
|
7
|
-
*/
|
|
8
|
-
export declare function parseSeed(input: string): Uint8Array;
|
|
9
|
-
/**
|
|
10
|
-
* Splits a comma-separated collection-ids string into a trimmed, non-empty list.
|
|
11
|
-
*
|
|
12
|
-
* @param input {string}
|
|
13
|
-
* @returns {string[]}
|
|
14
|
-
*/
|
|
15
|
-
export declare function parseCollections(input: string): string[];
|
|
16
|
-
//# sourceMappingURL=cli.d.ts.map
|
package/dist/dev/cli.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/dev/cli.ts"],"names":[],"mappings":";AAqCA;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAOnD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAKxD"}
|
package/dist/dev/cli.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/*!
|
|
3
|
-
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* CLI wrapper around `provisionDevGrants`. Creates a dev Space + collections on a
|
|
7
|
-
* running was-teaching-server and delegates per-collection RW zcaps to the app
|
|
8
|
-
* DID derived from `--seed`, optionally writing the grants JSON to `--out`.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* was-provision-dev-grants \
|
|
12
|
-
* --server-url http://localhost:3002 \
|
|
13
|
-
* --seed <hex-or-base64url 32-byte seed> \
|
|
14
|
-
* --collections action-items,projects,goals \
|
|
15
|
-
* [--space-name "Dev Space"] [--out ./public/dev-grants.local.json] [--probe]
|
|
16
|
-
*/
|
|
17
|
-
import { parseArgs } from 'node:util';
|
|
18
|
-
import { argv } from 'node:process';
|
|
19
|
-
import { pathToFileURL } from 'node:url';
|
|
20
|
-
import { base64urlnopad, hex } from '@scure/base';
|
|
21
|
-
import { provisionDevGrants } from './provisionDevGrants.js';
|
|
22
|
-
const USAGE = `Usage: was-provision-dev-grants [options]
|
|
23
|
-
|
|
24
|
-
Required:
|
|
25
|
-
--server-url <url> base URL of a running was-teaching-server
|
|
26
|
-
--seed <string> the app master seed, hex or base64url encoded
|
|
27
|
-
--collections <ids> comma-separated WAS collection ids
|
|
28
|
-
|
|
29
|
-
Optional:
|
|
30
|
-
--space-name <name> Space name (default "Dev Space")
|
|
31
|
-
--out <path> write the { grants } JSON to this file
|
|
32
|
-
--probe probe whether the delegated RW zcap authorizes the
|
|
33
|
-
encryption-marker PUT
|
|
34
|
-
--help show this message
|
|
35
|
-
`;
|
|
36
|
-
/**
|
|
37
|
-
* Decodes a seed string as hex (all hex chars, even length) or base64url.
|
|
38
|
-
*
|
|
39
|
-
* @param input {string}
|
|
40
|
-
* @returns {Uint8Array}
|
|
41
|
-
*/
|
|
42
|
-
export function parseSeed(input) {
|
|
43
|
-
const value = input.trim();
|
|
44
|
-
if (/^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0) {
|
|
45
|
-
// hex.decode requires lowercase, even-length input.
|
|
46
|
-
return hex.decode(value.toLowerCase());
|
|
47
|
-
}
|
|
48
|
-
return base64urlnopad.decode(value);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Splits a comma-separated collection-ids string into a trimmed, non-empty list.
|
|
52
|
-
*
|
|
53
|
-
* @param input {string}
|
|
54
|
-
* @returns {string[]}
|
|
55
|
-
*/
|
|
56
|
-
export function parseCollections(input) {
|
|
57
|
-
return input
|
|
58
|
-
.split(',')
|
|
59
|
-
.map(id => id.trim())
|
|
60
|
-
.filter(id => id.length > 0);
|
|
61
|
-
}
|
|
62
|
-
async function main() {
|
|
63
|
-
const { values } = parseArgs({
|
|
64
|
-
options: {
|
|
65
|
-
'server-url': { type: 'string' },
|
|
66
|
-
seed: { type: 'string' },
|
|
67
|
-
collections: { type: 'string' },
|
|
68
|
-
'space-name': { type: 'string' },
|
|
69
|
-
out: { type: 'string' },
|
|
70
|
-
probe: { type: 'boolean', default: false },
|
|
71
|
-
help: { type: 'boolean', default: false }
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
if (values.help) {
|
|
75
|
-
console.log(USAGE);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const serverUrl = values['server-url'];
|
|
79
|
-
const seedInput = values.seed;
|
|
80
|
-
const collectionsInput = values.collections;
|
|
81
|
-
const missing = [];
|
|
82
|
-
if (!serverUrl) {
|
|
83
|
-
missing.push('--server-url');
|
|
84
|
-
}
|
|
85
|
-
if (!seedInput) {
|
|
86
|
-
missing.push('--seed');
|
|
87
|
-
}
|
|
88
|
-
if (!collectionsInput) {
|
|
89
|
-
missing.push('--collections');
|
|
90
|
-
}
|
|
91
|
-
if (missing.length > 0) {
|
|
92
|
-
console.error(`Missing required argument(s): ${missing.join(', ')}\n`);
|
|
93
|
-
console.error(USAGE);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
const collections = parseCollections(collectionsInput);
|
|
97
|
-
if (collections.length === 0) {
|
|
98
|
-
console.error('--collections must list at least one collection id.\n');
|
|
99
|
-
console.error(USAGE);
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
await provisionDevGrants({
|
|
103
|
-
serverUrl: serverUrl,
|
|
104
|
-
seed: parseSeed(seedInput),
|
|
105
|
-
collections,
|
|
106
|
-
spaceName: values['space-name'],
|
|
107
|
-
outFile: values.out,
|
|
108
|
-
probe: values.probe,
|
|
109
|
-
log: console.log
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
// Run only when invoked as a script, not when imported (e.g. by tests).
|
|
113
|
-
if (argv[1] !== undefined && import.meta.url === pathToFileURL(argv[1]).href) {
|
|
114
|
-
main().catch(err => {
|
|
115
|
-
console.error('Provisioning failed:', err);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
//# sourceMappingURL=cli.js.map
|
package/dist/dev/cli.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/dev/cli.ts"],"names":[],"mappings":"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"}
|