@interop/was-react 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 (223) hide show
  1. package/LICENSE.md +20 -0
  2. package/README.md +410 -0
  3. package/dist/auth/chapi.d.ts +43 -0
  4. package/dist/auth/chapi.d.ts.map +1 -0
  5. package/dist/auth/chapi.js +107 -0
  6. package/dist/auth/chapi.js.map +1 -0
  7. package/dist/auth/loginFlow.d.ts +93 -0
  8. package/dist/auth/loginFlow.d.ts.map +1 -0
  9. package/dist/auth/loginFlow.js +149 -0
  10. package/dist/auth/loginFlow.js.map +1 -0
  11. package/dist/auth/loginRequest.d.ts +66 -0
  12. package/dist/auth/loginRequest.d.ts.map +1 -0
  13. package/dist/auth/loginRequest.js +83 -0
  14. package/dist/auth/loginRequest.js.map +1 -0
  15. package/dist/auth/verifyResponse.d.ts +52 -0
  16. package/dist/auth/verifyResponse.d.ts.map +1 -0
  17. package/dist/auth/verifyResponse.js +138 -0
  18. package/dist/auth/verifyResponse.js.map +1 -0
  19. package/dist/auth/verifyResponse.test.d.ts +2 -0
  20. package/dist/auth/verifyResponse.test.d.ts.map +1 -0
  21. package/dist/auth/verifyResponse.test.js +282 -0
  22. package/dist/auth/verifyResponse.test.js.map +1 -0
  23. package/dist/auth/walletRequestTypes.d.ts +143 -0
  24. package/dist/auth/walletRequestTypes.d.ts.map +1 -0
  25. package/dist/auth/walletRequestTypes.js +2 -0
  26. package/dist/auth/walletRequestTypes.js.map +1 -0
  27. package/dist/config.d.ts +150 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +27 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/dev/cli.d.ts +16 -0
  32. package/dist/dev/cli.d.ts.map +1 -0
  33. package/dist/dev/cli.js +122 -0
  34. package/dist/dev/cli.js.map +1 -0
  35. package/dist/dev/cli.test.d.ts +2 -0
  36. package/dist/dev/cli.test.d.ts.map +1 -0
  37. package/dist/dev/cli.test.js +39 -0
  38. package/dist/dev/cli.test.js.map +1 -0
  39. package/dist/dev/index.d.ts +10 -0
  40. package/dist/dev/index.d.ts.map +1 -0
  41. package/dist/dev/index.js +10 -0
  42. package/dist/dev/index.js.map +1 -0
  43. package/dist/dev/provisionDevGrants.d.ts +73 -0
  44. package/dist/dev/provisionDevGrants.d.ts.map +1 -0
  45. package/dist/dev/provisionDevGrants.js +176 -0
  46. package/dist/dev/provisionDevGrants.js.map +1 -0
  47. package/dist/grants.d.ts +59 -0
  48. package/dist/grants.d.ts.map +1 -0
  49. package/dist/grants.js +82 -0
  50. package/dist/grants.js.map +1 -0
  51. package/dist/grants.test.d.ts +2 -0
  52. package/dist/grants.test.d.ts.map +1 -0
  53. package/dist/grants.test.js +79 -0
  54. package/dist/grants.test.js.map +1 -0
  55. package/dist/identity/agents.d.ts +99 -0
  56. package/dist/identity/agents.d.ts.map +1 -0
  57. package/dist/identity/agents.js +132 -0
  58. package/dist/identity/agents.js.map +1 -0
  59. package/dist/identity/appSession.d.ts +78 -0
  60. package/dist/identity/appSession.d.ts.map +1 -0
  61. package/dist/identity/appSession.js +93 -0
  62. package/dist/identity/appSession.js.map +1 -0
  63. package/dist/identity/documentLoader.d.ts +20 -0
  64. package/dist/identity/documentLoader.d.ts.map +1 -0
  65. package/dist/identity/documentLoader.js +99 -0
  66. package/dist/identity/documentLoader.js.map +1 -0
  67. package/dist/identity/initAppSession.d.ts +27 -0
  68. package/dist/identity/initAppSession.d.ts.map +1 -0
  69. package/dist/identity/initAppSession.js +30 -0
  70. package/dist/identity/initAppSession.js.map +1 -0
  71. package/dist/identity/seedCredential.d.ts +74 -0
  72. package/dist/identity/seedCredential.d.ts.map +1 -0
  73. package/dist/identity/seedCredential.js +165 -0
  74. package/dist/identity/seedCredential.js.map +1 -0
  75. package/dist/identity/seedCredential.test.d.ts +2 -0
  76. package/dist/identity/seedCredential.test.d.ts.map +1 -0
  77. package/dist/identity/seedCredential.test.js +205 -0
  78. package/dist/identity/seedCredential.test.js.map +1 -0
  79. package/dist/identity/seedStore.d.ts +27 -0
  80. package/dist/identity/seedStore.d.ts.map +1 -0
  81. package/dist/identity/seedStore.js +74 -0
  82. package/dist/identity/seedStore.js.map +1 -0
  83. package/dist/index.d.ts +37 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +46 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/mui/ProtectedRoute.d.ts +10 -0
  88. package/dist/mui/ProtectedRoute.d.ts.map +1 -0
  89. package/dist/mui/ProtectedRoute.js +54 -0
  90. package/dist/mui/ProtectedRoute.js.map +1 -0
  91. package/dist/mui/ReconnectBanner.d.ts +2 -0
  92. package/dist/mui/ReconnectBanner.d.ts.map +1 -0
  93. package/dist/mui/ReconnectBanner.js +20 -0
  94. package/dist/mui/ReconnectBanner.js.map +1 -0
  95. package/dist/mui/SyncStatusChip.d.ts +2 -0
  96. package/dist/mui/SyncStatusChip.d.ts.map +1 -0
  97. package/dist/mui/SyncStatusChip.js +27 -0
  98. package/dist/mui/SyncStatusChip.js.map +1 -0
  99. package/dist/mui/index.d.ts +12 -0
  100. package/dist/mui/index.d.ts.map +1 -0
  101. package/dist/mui/index.js +12 -0
  102. package/dist/mui/index.js.map +1 -0
  103. package/dist/react/WasSessionProvider.d.ts +36 -0
  104. package/dist/react/WasSessionProvider.d.ts.map +1 -0
  105. package/dist/react/WasSessionProvider.js +35 -0
  106. package/dist/react/WasSessionProvider.js.map +1 -0
  107. package/dist/react/hooks.d.ts +66 -0
  108. package/dist/react/hooks.d.ts.map +1 -0
  109. package/dist/react/hooks.js +121 -0
  110. package/dist/react/hooks.js.map +1 -0
  111. package/dist/react/index.d.ts +11 -0
  112. package/dist/react/index.d.ts.map +1 -0
  113. package/dist/react/index.js +11 -0
  114. package/dist/react/index.js.map +1 -0
  115. package/dist/session/appReadyStore.d.ts +10 -0
  116. package/dist/session/appReadyStore.d.ts.map +1 -0
  117. package/dist/session/appReadyStore.js +22 -0
  118. package/dist/session/appReadyStore.js.map +1 -0
  119. package/dist/session/authStore.d.ts +71 -0
  120. package/dist/session/authStore.d.ts.map +1 -0
  121. package/dist/session/authStore.js +347 -0
  122. package/dist/session/authStore.js.map +1 -0
  123. package/dist/session/index.d.ts +10 -0
  124. package/dist/session/index.d.ts.map +1 -0
  125. package/dist/session/index.js +10 -0
  126. package/dist/session/index.js.map +1 -0
  127. package/dist/storage/entityStore.d.ts +51 -0
  128. package/dist/storage/entityStore.d.ts.map +1 -0
  129. package/dist/storage/entityStore.js +78 -0
  130. package/dist/storage/entityStore.js.map +1 -0
  131. package/dist/storage/localStore.d.ts +144 -0
  132. package/dist/storage/localStore.d.ts.map +1 -0
  133. package/dist/storage/localStore.js +289 -0
  134. package/dist/storage/localStore.js.map +1 -0
  135. package/dist/storage/rehydrate.d.ts +56 -0
  136. package/dist/storage/rehydrate.d.ts.map +1 -0
  137. package/dist/storage/rehydrate.js +87 -0
  138. package/dist/storage/rehydrate.js.map +1 -0
  139. package/dist/storage/rehydrate.test.d.ts +2 -0
  140. package/dist/storage/rehydrate.test.d.ts.map +1 -0
  141. package/dist/storage/rehydrate.test.js +172 -0
  142. package/dist/storage/rehydrate.test.js.map +1 -0
  143. package/dist/storage/storageManager.d.ts +39 -0
  144. package/dist/storage/storageManager.d.ts.map +1 -0
  145. package/dist/storage/storageManager.js +68 -0
  146. package/dist/storage/storageManager.js.map +1 -0
  147. package/dist/storage/syncController.d.ts +79 -0
  148. package/dist/storage/syncController.d.ts.map +1 -0
  149. package/dist/storage/syncController.js +189 -0
  150. package/dist/storage/syncController.js.map +1 -0
  151. package/dist/storage/syncStatusStore.d.ts +17 -0
  152. package/dist/storage/syncStatusStore.d.ts.map +1 -0
  153. package/dist/storage/syncStatusStore.js +19 -0
  154. package/dist/storage/syncStatusStore.js.map +1 -0
  155. package/dist/storage/wasRemoteStore.d.ts +73 -0
  156. package/dist/storage/wasRemoteStore.d.ts.map +1 -0
  157. package/dist/storage/wasRemoteStore.js +76 -0
  158. package/dist/storage/wasRemoteStore.js.map +1 -0
  159. package/dist/storage/wasSync.d.ts +42 -0
  160. package/dist/storage/wasSync.d.ts.map +1 -0
  161. package/dist/storage/wasSync.js +34 -0
  162. package/dist/storage/wasSync.js.map +1 -0
  163. package/dist/sync/changesQuery.d.ts +44 -0
  164. package/dist/sync/changesQuery.d.ts.map +1 -0
  165. package/dist/sync/changesQuery.js +61 -0
  166. package/dist/sync/changesQuery.js.map +1 -0
  167. package/dist/sync/changesQuery.test.d.ts +2 -0
  168. package/dist/sync/changesQuery.test.d.ts.map +1 -0
  169. package/dist/sync/changesQuery.test.js +145 -0
  170. package/dist/sync/changesQuery.test.js.map +1 -0
  171. package/dist/sync/docCipher.d.ts +68 -0
  172. package/dist/sync/docCipher.d.ts.map +1 -0
  173. package/dist/sync/docCipher.js +89 -0
  174. package/dist/sync/docCipher.js.map +1 -0
  175. package/dist/sync/feedMasterPort.d.ts +30 -0
  176. package/dist/sync/feedMasterPort.d.ts.map +1 -0
  177. package/dist/sync/feedMasterPort.js +58 -0
  178. package/dist/sync/feedMasterPort.js.map +1 -0
  179. package/dist/sync/index.d.ts +21 -0
  180. package/dist/sync/index.d.ts.map +1 -0
  181. package/dist/sync/index.js +21 -0
  182. package/dist/sync/index.js.map +1 -0
  183. package/dist/sync/lww.d.ts +29 -0
  184. package/dist/sync/lww.d.ts.map +1 -0
  185. package/dist/sync/lww.js +28 -0
  186. package/dist/sync/lww.js.map +1 -0
  187. package/dist/sync/lww.test.d.ts +2 -0
  188. package/dist/sync/lww.test.d.ts.map +1 -0
  189. package/dist/sync/lww.test.js +28 -0
  190. package/dist/sync/lww.test.js.map +1 -0
  191. package/dist/sync/lwwConflictHandler.d.ts +49 -0
  192. package/dist/sync/lwwConflictHandler.d.ts.map +1 -0
  193. package/dist/sync/lwwConflictHandler.js +65 -0
  194. package/dist/sync/lwwConflictHandler.js.map +1 -0
  195. package/dist/sync/lwwConflictHandler.test.d.ts +2 -0
  196. package/dist/sync/lwwConflictHandler.test.d.ts.map +1 -0
  197. package/dist/sync/lwwConflictHandler.test.js +78 -0
  198. package/dist/sync/lwwConflictHandler.test.js.map +1 -0
  199. package/dist/sync/pushWrites.d.ts +57 -0
  200. package/dist/sync/pushWrites.d.ts.map +1 -0
  201. package/dist/sync/pushWrites.js +159 -0
  202. package/dist/sync/pushWrites.js.map +1 -0
  203. package/dist/sync/pushWrites.test.d.ts +2 -0
  204. package/dist/sync/pushWrites.test.d.ts.map +1 -0
  205. package/dist/sync/pushWrites.test.js +287 -0
  206. package/dist/sync/pushWrites.test.js.map +1 -0
  207. package/dist/sync/syncedDocSchema.d.ts +22 -0
  208. package/dist/sync/syncedDocSchema.d.ts.map +1 -0
  209. package/dist/sync/syncedDocSchema.js +26 -0
  210. package/dist/sync/syncedDocSchema.js.map +1 -0
  211. package/dist/sync/types.d.ts +194 -0
  212. package/dist/sync/types.d.ts.map +1 -0
  213. package/dist/sync/types.js +36 -0
  214. package/dist/sync/types.js.map +1 -0
  215. package/dist/sync/wasReplication.d.ts +47 -0
  216. package/dist/sync/wasReplication.d.ts.map +1 -0
  217. package/dist/sync/wasReplication.js +55 -0
  218. package/dist/sync/wasReplication.js.map +1 -0
  219. package/dist/sync/wasSyncPort.d.ts +45 -0
  220. package/dist/sync/wasSyncPort.d.ts.map +1 -0
  221. package/dist/sync/wasSyncPort.js +170 -0
  222. package/dist/sync/wasSyncPort.js.map +1 -0
  223. package/package.json +142 -0
package/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Interop Alliance
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,410 @@
1
+ # @interop/was-react _(@interop/was-react)_
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@interop/was-react.svg)](https://npm.im/@interop/was-react)
4
+
5
+ > React library for building "Bring Your Own Everything" (BYOE) apps on Wallet
6
+ > Attached Storage: DID-Auth login via a CHAPI wallet, local-first encrypted
7
+ > storage, and background sync to a WAS server.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Background](#background)
12
+ - [Install](#install)
13
+ - [Quick start](#quick-start)
14
+ - [Login flow](#login-flow)
15
+ - [Session lifecycle](#session-lifecycle)
16
+ - [Sync architecture](#sync-architecture)
17
+ - [Entry points](#entry-points)
18
+ - [Dev tooling](#dev-tooling)
19
+ - [Testing](#testing)
20
+ - [Contribute](#contribute)
21
+ - [License](#license)
22
+
23
+ ## Background
24
+
25
+ "Bring Your Own Everything" (BYOE) is a way to build web apps with no backend
26
+ that the app owns. The user brings their own identity (a wallet) and their own
27
+ storage (Wallet Attached Storage, WAS), and the app stores everything encrypted
28
+ in that user-owned space. The app is a Relying Party (RP): it authenticates via
29
+ "Login With Wallet" (CHAPI) and reads and writes the user's WAS space using
30
+ wallet-delegated authorization capabilities (zcaps). It never owns the space,
31
+ never holds the wallet's root key, and invokes only the zcaps the wallet grants
32
+ it.
33
+
34
+ "Bring Your Own Storage" (BYOS) is the storage half of that model. Every
35
+ application collection is encrypted client-side as an Encrypted Data Vault
36
+ (EDV): the WAS server only ever sees opaque JWE envelopes and can neither read
37
+ nor search the plaintext. Data is local-first -- a local RxDB (IndexedDB)
38
+ database holds the encrypted envelopes and replicates them to WAS in the
39
+ background. The app works fully offline; sync resumes on reconnect.
40
+
41
+ `@interop/was-react` is the reusable plumbing for that model, extracted from a
42
+ production BYOE app. It wraps
43
+ [`@interop/was-client`](https://npm.im/@interop/was-client) and owns identity
44
+ derivation, the CHAPI login flow, the session lifecycle store, the encrypted
45
+ local replica, WAS replication, and a small set of React hooks and optional MUI
46
+ components. An app supplies its configuration, its collection registry, and its
47
+ own domain and UI; the library owns everything in between.
48
+
49
+ ## Install
50
+
51
+ Node.js 24+ is recommended. The package is ESM-only.
52
+
53
+ ```
54
+ pnpm add @interop/was-react
55
+ ```
56
+
57
+ Peer dependencies (install the ones you use):
58
+
59
+ ```
60
+ pnpm add react zustand rxdb
61
+ ```
62
+
63
+ `react >= 19`, `zustand ^5`, and `rxdb ^17` are required peers. The optional
64
+ `@interop/was-react/mui` entry additionally needs `@mui/material`,
65
+ `@mui/icons-material`, and `react-router`; the core entry never imports them.
66
+
67
+ ```
68
+ pnpm add @mui/material @mui/icons-material @emotion/react @emotion/styled react-router
69
+ ```
70
+
71
+ ## Quick start
72
+
73
+ An app builds one `WasAppConfig`, a `StoreRegistry` (per-collection hydrate and
74
+ patch handlers), wraps its tree in `<WasSessionProvider>`, and drives the
75
+ session through the hooks. The example below wires a single `notes` collection.
76
+
77
+ ### 1. Configuration
78
+
79
+ ```ts
80
+ // app.config.ts
81
+ import type { WasAppConfig } from '@interop/was-react'
82
+
83
+ export const config: WasAppConfig = {
84
+ appName: 'Notes',
85
+ appOrigin: 'https://notes.example',
86
+ wasServerUrl: 'https://was.example',
87
+ collections: [{ key: 'notes', id: 'notes' }],
88
+ credential: {
89
+ credentialType: 'NotesAppKey',
90
+ vocabBase: 'urn:notes-app:vocab#'
91
+ }
92
+ }
93
+ ```
94
+
95
+ `collections` maps each app-side `key` (the local RxDB collection handle) to a
96
+ WAS collection `id` (a deliberately unprefixed, generic name shared across
97
+ interoperable apps). `credential` names the self-issued seed credential the
98
+ first-run flow mints. All other fields (`mediatorBase`, `dbName`,
99
+ `storageKeyPrefix`, `sync`, `expiry`) are optional with documented defaults.
100
+
101
+ ### 2. Entity stores and the registry
102
+
103
+ ```ts
104
+ // stores.ts
105
+ import { createEntityStore, type StoreRegistry } from '@interop/was-react'
106
+
107
+ export interface Note {
108
+ id: string
109
+ title: string
110
+ body: string
111
+ createdAt: string
112
+ updatedAt: string
113
+ }
114
+
115
+ export const useNotes = createEntityStore<Note>('notes')
116
+
117
+ export const registry: StoreRegistry = {
118
+ notes: {
119
+ hydrate: () => useNotes.getState().hydrate(),
120
+ upsert: doc => useNotes.getState().patch(doc as Note),
121
+ drop: uuid => useNotes.getState().drop(uuid),
122
+ clear: () => useNotes.getState().replaceAll([])
123
+ }
124
+ }
125
+ ```
126
+
127
+ `createEntityStore` returns a zustand hook holding the decrypted payloads as a
128
+ `Map<uuid, Note>`; its `insert` / `update` / `remove` verbs persist through the
129
+ encrypted local store, while `hydrate` / `patch` / `drop` / `replaceAll` are the
130
+ handlers the rehydrate mechanism drives on login, remote sync, and logout.
131
+
132
+ ### 3. Provider
133
+
134
+ ```tsx
135
+ // main.tsx
136
+ import { WasSessionProvider } from '@interop/was-react'
137
+ import { config } from './app.config.js'
138
+ import { registry } from './stores.js'
139
+
140
+ export function Root() {
141
+ return (
142
+ <WasSessionProvider config={config} registry={registry}>
143
+ <App />
144
+ </WasSessionProvider>
145
+ )
146
+ }
147
+ ```
148
+
149
+ ### 4. Login page
150
+
151
+ ```tsx
152
+ // LoginPage.tsx
153
+ import { useLogin } from '@interop/was-react'
154
+
155
+ export function LoginPage() {
156
+ const { login, status, phase, error } = useLogin()
157
+ const busy = status === 'authenticating'
158
+
159
+ return (
160
+ <div>
161
+ <button onClick={() => void login()} disabled={busy}>
162
+ {busy ? 'Connecting your wallet...' : 'Login with wallet'}
163
+ </button>
164
+ {busy && phase && <p>{phase}</p>}
165
+ {error && <p role="alert">{error}</p>}
166
+ </div>
167
+ )
168
+ }
169
+ ```
170
+
171
+ ### 5. Reading and writing
172
+
173
+ ```tsx
174
+ import { uuidv7 } from 'uuidv7'
175
+ import { useNotes } from './stores.js'
176
+
177
+ export function Notes() {
178
+ const notes = useNotes(state => [...state.byId.values()])
179
+ const insert = useNotes(state => state.insert)
180
+
181
+ async function addNote() {
182
+ const now = new Date().toISOString()
183
+ await insert({
184
+ id: uuidv7(),
185
+ title: 'Untitled',
186
+ body: '',
187
+ createdAt: now,
188
+ updatedAt: now
189
+ })
190
+ }
191
+
192
+ return (
193
+ <div>
194
+ <button onClick={() => void addNote()}>Add note</button>
195
+ <ul>
196
+ {notes.map(note => (
197
+ <li key={note.id}>{note.title}</li>
198
+ ))}
199
+ </ul>
200
+ </div>
201
+ )
202
+ }
203
+ ```
204
+
205
+ The MUI entry supplies a router gate and status UI on top of this; see
206
+ [Entry points](#entry-points).
207
+
208
+ ## Login flow
209
+
210
+ Login is driven by CHAPI Verifiable Presentation Requests (VPRs). The
211
+ `useLogin().login()` action (backed by `loginWithWallet`) runs the flow, with a
212
+ `phase` string surfaced for a progress line (`probing` to `storing-key` to
213
+ `requesting-grants` to `verifying`):
214
+
215
+ 1. **CHAPI polyfill loads lazily.** The `credential-handler-polyfill` is loaded
216
+ on demand the first time a wallet request is made, not at import time.
217
+ 2. **Probe (popup #1).** A VPR asking for DIDAuthentication plus the app's seed
218
+ credential is sent to the wallet. No credential returned means this is a
219
+ first run.
220
+ 3. **First run: mint and store the seed.** A fresh 32-byte master seed is
221
+ generated (`crypto.getRandomValues`), a seed credential is self-issued
222
+ (`issueSeedCredential`, using the configured `credentialType` and `vocabBase`
223
+ plus the app `origin` anti-phishing bind), and stored in the wallet via
224
+ `chapiStore`. The flow blocks until the wallet confirms the store -- a
225
+ dismissed store would silently break cross-device recovery.
226
+ 4. **Returning login: recover and verify.** When the probe returns the seed
227
+ credential, `parseSeedCredential` recovers the seed and cryptographically
228
+ verifies the credential is self-issued, origin-bound, and seed-to-DID bound.
229
+ 5. **Derive identity.** The stable `did:key` controller and its signer are
230
+ derived deterministically from the seed via `CapabilityAgent.fromSeed`
231
+ (`initAppSession` / `deriveIdentity`). The same seed yields the same identity
232
+ on every device.
233
+ 6. **Request grants (popup #2).** `buildGrantsVpr` requests a per-collection
234
+ read/write zcap (plus a read-only space grant) for the controller DID. The
235
+ wallet provisions the collections and returns a signed presentation.
236
+ 7. **Verify the response.** `verifyLoginPresentation` checks the VP and embedded
237
+ proofs (purpose `authentication`, matching domain and challenge), and
238
+ `checkGrants` asserts every zcap is controlled by the app DID, targets the
239
+ configured `wasServerUrl`, shares one space, and is unexpired.
240
+ 8. **Activate.** The session (seed, grants, earliest expiry) is persisted to
241
+ IndexedDB, the encrypted local store is opened, the entity stores hydrate,
242
+ and background WAS sync starts.
243
+
244
+ ## Session lifecycle
245
+
246
+ The session is owned by a zustand auth store built once per app by the provider
247
+ (`createAuthStore`). Its `status` is `idle` to `restoring` to `unauthenticated`
248
+ / `authenticating` / `authenticated`, and it is the router gate: a protected
249
+ route waits for the restore attempt to settle before choosing between the app
250
+ and the login page.
251
+
252
+ - **Restore (zero popups).** On mount, `restore()` reads the persisted session
253
+ from IndexedDB and, if present and consistent, re-derives the identity, opens
254
+ the local store, hydrates, and starts sync with no wallet interaction. A
255
+ missing, corrupt, or wrong-server record falls through to `unauthenticated`.
256
+ - **Login.** `useLogin().login()` runs the full [login flow](#login-flow).
257
+ - **Reconnect.** Grants are expiry-only. The store watches the earliest grant
258
+ expiry and, once within the warning window (`expiry.warningMs`, default 1h) or
259
+ after a live 401/403, sets `accessExpired`. `useReconnect().reconnect()`
260
+ re-requests grants with the existing seed (one wallet popup, same identity,
261
+ same data) and restarts sync.
262
+ - **Logout.** `useLogout()` stops sync, closes and forgets the local store,
263
+ clears the entity stores, and clears the persisted session.
264
+ - **Expiry.** Because the seed survives grant expiry and the derived DID and
265
+ vault keys are stable, an expired session re-grants in place; previously
266
+ stored data stays readable.
267
+
268
+ Hooks:
269
+
270
+ | Hook | Returns |
271
+ | ----------------- | --------------------------------------------------------------------------------------- |
272
+ | `useSession()` | `status`, `phase`, `error`, `controllerDid`, `expires`, `accessExpired`, `reconnecting` |
273
+ | `useLogin()` | `{ login, status, phase, error }` |
274
+ | `useLogout()` | `() => Promise<void>` |
275
+ | `useReconnect()` | `{ accessExpired, reconnecting, reconnect }` |
276
+ | `useSyncStatus()` | `{ state, label, title }` (see below) |
277
+ | `useAppReady()` | `{ ready, error }` -- the hydration gate |
278
+ | `useAuthStore()` | the bound vanilla store (for `getState().restore()`, etc.) |
279
+
280
+ ## Sync architecture
281
+
282
+ - **Local-first replica.** An always-on local encrypted RxDB (Dexie/IndexedDB)
283
+ database (`LocalStore`) holds one collection per app collection. Every at-rest
284
+ row is `{ id, updatedAt, version, data }`, where `data` is an EDV envelope
285
+ `{ id, sequence, jwe }`. The app reads exclusively from the in-memory entity
286
+ stores hydrated from this replica, so it works fully offline.
287
+ - **Envelope encryption.** Each collection is encrypted with its own X25519
288
+ key-agreement key, HKDF-derived from the master seed with the label
289
+ `kak:v1:<collectionId>` (`deriveCollectionKeys`). HKDF one-wayness means a
290
+ shared per-collection key exposes nothing about the master seed or the sibling
291
+ collections. The WAS server never sees plaintext.
292
+ - **Replication.** A per-session `SyncController` runs RxDB replication per
293
+ collection over a `WasSyncPort` (signed requests authorized by the granted
294
+ zcaps). Pull is driven by the WAS `changes` feed; a low-frequency periodic
295
+ re-sync (`sync.pollMs`, default 15s) keeps open sessions converging.
296
+ - **Conflict resolution.** Last-writer-wins on the payload's own
297
+ `(updatedAt, deviceId)` (ISO lexical compare, with a per-install random
298
+ `deviceId` tiebreaker). Updates re-encrypt in place under the same envelope id
299
+ with `sequence`+1 (a mutable-head model); deletes are soft-delete tombstones.
300
+ - **Status.** `useSyncStatus()` rolls the per-collection replication states up
301
+ to an aggregate: `offline` (no replication running / local-only), or
302
+ `error > syncing > synced`. The `SyncStatusChip` MUI component renders it.
303
+
304
+ ## Entry points
305
+
306
+ The package exposes three entry points. `./mui` and `./dev` are never
307
+ re-exported from the root, so an app that does not use them pays no dependency
308
+ cost.
309
+
310
+ | Import | Contents | Extra peers |
311
+ | ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
312
+ | `@interop/was-react` | Core: config, identity, auth, sync, storage, session store, React provider and hooks | `react`, `zustand`, `rxdb` |
313
+ | `@interop/was-react/mui` | Optional `ProtectedRoute`, `ReconnectBanner`, `SyncStatusChip` | `@mui/material`, `@mui/icons-material`, `react-router` |
314
+ | `@interop/was-react/dev` | Node-only `provisionDevGrants` + the `was-provision-dev-grants` CLI | (none; Node only) |
315
+
316
+ MUI usage:
317
+
318
+ ```tsx
319
+ import { Routes, Route } from 'react-router'
320
+ import {
321
+ ProtectedRoute,
322
+ ReconnectBanner,
323
+ SyncStatusChip
324
+ } from '@interop/was-react/mui'
325
+ import { LoginPage } from './LoginPage.js'
326
+
327
+ export function App() {
328
+ return (
329
+ <Routes>
330
+ <Route path="/login" element={<LoginPage />} />
331
+ <Route element={<ProtectedRoute loginPath="/login" />}>
332
+ <Route
333
+ path="/"
334
+ element={
335
+ <>
336
+ <SyncStatusChip />
337
+ <ReconnectBanner />
338
+ <Notes />
339
+ </>
340
+ }
341
+ />
342
+ </Route>
343
+ </Routes>
344
+ )
345
+ }
346
+ ```
347
+
348
+ `ProtectedRoute` calls `restore()` on mount, shows a spinner while the session
349
+ restores and the stores hydrate, redirects an unauthenticated visitor to
350
+ `loginPath`, and renders the routed `<Outlet />` once ready.
351
+
352
+ ## Dev tooling
353
+
354
+ The `./dev` entry provisions a Space, collections, and delegated read/write
355
+ grants against a running was-teaching-server, so an app can dev-sync without a
356
+ CHAPI wallet in the loop. It is Node-only (uses `node:fs`).
357
+
358
+ CLI:
359
+
360
+ ```
361
+ was-provision-dev-grants \
362
+ --server-url http://localhost:3002 \
363
+ --seed <hex-or-base64url 32-byte seed> \
364
+ --collections notes \
365
+ --space-name "Dev Space" \
366
+ --out ./public/dev-grants.local.json
367
+ ```
368
+
369
+ Programmatic:
370
+
371
+ ```ts
372
+ import { provisionDevGrants } from '@interop/was-react/dev'
373
+
374
+ const result = await provisionDevGrants({
375
+ serverUrl: 'http://localhost:3002',
376
+ seed: mySeedBytes,
377
+ collections: ['notes'],
378
+ outFile: './public/dev-grants.local.json'
379
+ })
380
+ // result.grants, result.spaceId, result.spaceUrl, result.appDid
381
+ ```
382
+
383
+ A throwaway "provisioner" identity owns the created Space (a genuine
384
+ cross-identity delegation, as in the real wallet-to-relying-party flow), and a
385
+ per-collection RW zcap is delegated to the app DID derived from `seed`. Pass
386
+ `--probe` (or `probe: true`) to check whether the delegated zcap authorizes
387
+ PUTting the EDV encryption marker.
388
+
389
+ ## Testing
390
+
391
+ The repo runs three test tiers:
392
+
393
+ ```
394
+ pnpm run test:node # Vitest unit tests (test/node/), Node
395
+ pnpm run test:browser # Playwright tests (test/browser/), real Chromium
396
+ pnpm test # fix + lint + typecheck + node + browser
397
+ ```
398
+
399
+ `pnpm run test:coverage` runs the Vitest suite with V8 coverage. The Playwright
400
+ tier runs against a Vite dev server that serves and transforms the TypeScript
401
+ source on the fly; there is no standalone browser app.
402
+
403
+ ## Contribute
404
+
405
+ PRs accepted. If editing the Readme, please conform to the
406
+ [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
407
+
408
+ ## License
409
+
410
+ [MIT License](LICENSE.md) © 2026 Interop Alliance. </content> </invoke>
@@ -0,0 +1,43 @@
1
+ import type { IVerifiablePresentation } from '@interop/data-integrity-core';
2
+ import type { IVPRDetails } from './walletRequestTypes.js';
3
+ /** The default CHAPI mediator base; the requesting origin is appended at load. */
4
+ export declare const DEFAULT_MEDIATOR_BASE = "https://authn.io/mediator?origin=";
5
+ /**
6
+ * Loads the CHAPI polyfill once (no-op under the e2e bridge).
7
+ *
8
+ * @param [options] {object}
9
+ * @param [options.mediatorBase] {string} the mediator base URL (defaults to
10
+ * `DEFAULT_MEDIATOR_BASE`)
11
+ * @returns {Promise<void>}
12
+ */
13
+ export declare function loadChapi({ mediatorBase }?: {
14
+ mediatorBase?: string;
15
+ }): Promise<void>;
16
+ /**
17
+ * Sends a VPR to the user's wallet via CHAPI `credentials.get()`. Returns the
18
+ * wallet's response VP, or `null` when the user cancelled/dismissed.
19
+ *
20
+ * @param options {object}
21
+ * @param options.vpr {IVPRDetails}
22
+ * @param [options.mediatorBase] {string}
23
+ * @returns {Promise<IVerifiablePresentation | null>}
24
+ */
25
+ export declare function chapiGet({ vpr, mediatorBase }: {
26
+ vpr: IVPRDetails;
27
+ mediatorBase?: string;
28
+ }): Promise<IVerifiablePresentation | null>;
29
+ /**
30
+ * Offers a VP (wrapping a credential) to the wallet via CHAPI
31
+ * `credentials.store()`. Returns true when the wallet confirmed the store,
32
+ * false when the user cancelled/dismissed.
33
+ *
34
+ * @param options {object}
35
+ * @param options.presentation {IVerifiablePresentation}
36
+ * @param [options.mediatorBase] {string}
37
+ * @returns {Promise<boolean>}
38
+ */
39
+ export declare function chapiStore({ presentation, mediatorBase }: {
40
+ presentation: IVerifiablePresentation;
41
+ mediatorBase?: string;
42
+ }): Promise<boolean>;
43
+ //# sourceMappingURL=chapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chapi.d.ts","sourceRoot":"","sources":["../../src/auth/chapi.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAE1D,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,sCAAsC,CAAA;AAmDxE;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,EAC9B,YAAoC,EACrC,GAAE;IACD,YAAY,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,EAC7B,GAAG,EACH,YAAY,EACb,EAAE;IACD,GAAG,EAAE,WAAW,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAgB1C;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,EAC/B,YAAY,EACZ,YAAY,EACb,EAAE;IACD,YAAY,EAAE,uBAAuB,CAAA;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GAAG,OAAO,CAAC,OAAO,CAAC,CAcnB"}
@@ -0,0 +1,107 @@
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /// <reference types="vite/client" />
5
+ /**
6
+ * The CHAPI transport: credential-handler-polyfill loading (authn.io mediator)
7
+ * plus thin `get()` / `store()` wrappers around `navigator.credentials`.
8
+ *
9
+ * E2E seam (non-production builds only): the real CHAPI channel needs the
10
+ * mediator's cross-origin handshake, which a test harness cannot perform.
11
+ * When a Playwright spec sets `window.__WAS_REACT_E2E_CHAPI__ = true` (via
12
+ * addInitScript), requests are queued on `window.__WAS_REACT_E2E_CHAPI_REQUESTS__`
13
+ * instead; the spec observes them, drives the wallet page directly (using
14
+ * freewallet's own `__E2E_CHAPI_GET_EVENT__` injection), and posts the wallet
15
+ * response into `window.__WAS_REACT_E2E_CHAPI_RESPONSES__[id]`. Responses use
16
+ * the CHAPI WebCredential wire shape `{ dataType, data } | null`.
17
+ */
18
+ import { loadOnce, WebCredential } from 'credential-handler-polyfill';
19
+ /** The default CHAPI mediator base; the requesting origin is appended at load. */
20
+ export const DEFAULT_MEDIATOR_BASE = 'https://authn.io/mediator?origin=';
21
+ function e2eBridgeActive() {
22
+ return (import.meta.env.MODE !== 'production' &&
23
+ window.__WAS_REACT_E2E_CHAPI__ === true);
24
+ }
25
+ let e2eRequestId = 0;
26
+ /** Queues a request for the e2e harness and polls for its response. */
27
+ async function e2eRoundTrip(type, body) {
28
+ const win = window;
29
+ win.__WAS_REACT_E2E_CHAPI_REQUESTS__ ??= [];
30
+ win.__WAS_REACT_E2E_CHAPI_RESPONSES__ ??= {};
31
+ const id = ++e2eRequestId;
32
+ win.__WAS_REACT_E2E_CHAPI_REQUESTS__.push({ id, type, body });
33
+ const deadline = Date.now() + 120_000;
34
+ while (Date.now() < deadline) {
35
+ if (id in win.__WAS_REACT_E2E_CHAPI_RESPONSES__) {
36
+ const value = win.__WAS_REACT_E2E_CHAPI_RESPONSES__[id];
37
+ delete win.__WAS_REACT_E2E_CHAPI_RESPONSES__[id];
38
+ return value ?? null;
39
+ }
40
+ await new Promise(resolve => setTimeout(resolve, 150));
41
+ }
42
+ throw new Error('E2E CHAPI bridge timed out waiting for a response.');
43
+ }
44
+ let polyfillLoaded = false;
45
+ /**
46
+ * Loads the CHAPI polyfill once (no-op under the e2e bridge).
47
+ *
48
+ * @param [options] {object}
49
+ * @param [options.mediatorBase] {string} the mediator base URL (defaults to
50
+ * `DEFAULT_MEDIATOR_BASE`)
51
+ * @returns {Promise<void>}
52
+ */
53
+ export async function loadChapi({ mediatorBase = DEFAULT_MEDIATOR_BASE } = {}) {
54
+ if (polyfillLoaded || e2eBridgeActive()) {
55
+ return;
56
+ }
57
+ await loadOnce(mediatorBase + encodeURIComponent(window.location.origin));
58
+ polyfillLoaded = true;
59
+ }
60
+ /**
61
+ * Sends a VPR to the user's wallet via CHAPI `credentials.get()`. Returns the
62
+ * wallet's response VP, or `null` when the user cancelled/dismissed.
63
+ *
64
+ * @param options {object}
65
+ * @param options.vpr {IVPRDetails}
66
+ * @param [options.mediatorBase] {string}
67
+ * @returns {Promise<IVerifiablePresentation | null>}
68
+ */
69
+ export async function chapiGet({ vpr, mediatorBase }) {
70
+ let wire;
71
+ if (e2eBridgeActive()) {
72
+ wire = await e2eRoundTrip('get', vpr);
73
+ }
74
+ else {
75
+ await loadChapi(mediatorBase !== undefined ? { mediatorBase } : {});
76
+ const result = (await navigator.credentials.get({
77
+ // The polyfill extends CredentialRequestOptions with the `web` member.
78
+ web: { VerifiablePresentation: vpr }
79
+ }));
80
+ wire = result;
81
+ }
82
+ if (!wire || wire.data === undefined || wire.data === null) {
83
+ return null;
84
+ }
85
+ return wire.data;
86
+ }
87
+ /**
88
+ * Offers a VP (wrapping a credential) to the wallet via CHAPI
89
+ * `credentials.store()`. Returns true when the wallet confirmed the store,
90
+ * false when the user cancelled/dismissed.
91
+ *
92
+ * @param options {object}
93
+ * @param options.presentation {IVerifiablePresentation}
94
+ * @param [options.mediatorBase] {string}
95
+ * @returns {Promise<boolean>}
96
+ */
97
+ export async function chapiStore({ presentation, mediatorBase }) {
98
+ if (e2eBridgeActive()) {
99
+ const wire = await e2eRoundTrip('store', presentation);
100
+ return wire !== null && wire.data !== undefined && wire.data !== null;
101
+ }
102
+ await loadChapi(mediatorBase !== undefined ? { mediatorBase } : {});
103
+ const credential = new WebCredential('VerifiablePresentation', presentation);
104
+ const result = (await navigator.credentials.store(credential));
105
+ return result !== null && result !== undefined;
106
+ }
107
+ //# sourceMappingURL=chapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chapi.js","sourceRoot":"","sources":["../../src/auth/chapi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /**
5
+ * The Login-With-Wallet orchestration:
6
+ *
7
+ * - First run (the wallet holds no app key): generate a fresh 32-byte master
8
+ * seed, self-issue the credential, and BLOCK until the wallet confirms
9
+ * storing it (a dismissed store would silently break cross-device recovery),
10
+ * then request the storage grants.
11
+ * - Returning (the wallet returns the credential): recover the seed, verify
12
+ * the credential's self-issue/origin/DID binding, then request grants for
13
+ * the same stable controller DID.
14
+ *
15
+ * Hot restore (seed + grants already persisted locally) never reaches this
16
+ * module -- the caller's session restore short-circuits it.
17
+ */
18
+ import type { IZcap } from '@interop/data-integrity-core';
19
+ import type { DocumentLoader } from '../identity/documentLoader.js';
20
+ import { type SeedCredentialConfig } from '../identity/seedCredential.js';
21
+ import type { IdentityAgents } from '../identity/agents.js';
22
+ import { type CheckedGrants } from './verifyResponse.js';
23
+ import type { ParsedGrants } from '../grants.js';
24
+ /**
25
+ * The cohesive configuration for a Login-With-Wallet flow. App-specific values
26
+ * are injected here rather than baked in; this becomes part of the library's
27
+ * central app config later.
28
+ */
29
+ export interface LoginConfig {
30
+ /** This app's own web origin (the anti-phishing bind on the app key). */
31
+ appOrigin: string;
32
+ /** Human-readable app name, used in the wallet consent reason lines. */
33
+ appName: string;
34
+ /** The WAS collection ids to request read/write grants for. */
35
+ collections: string[];
36
+ /** The seed-credential type name + vocabulary namespace. */
37
+ credential: SeedCredentialConfig;
38
+ /** The JSON-LD document loader (see `createDocumentLoader`). */
39
+ documentLoader: DocumentLoader;
40
+ /**
41
+ * The expected WAS server URL. When set, every granted zcap must target it;
42
+ * grants pointing anywhere else are rejected at login.
43
+ */
44
+ wasServerUrl?: string;
45
+ /** The CHAPI mediator base URL (defaults to `DEFAULT_MEDIATOR_BASE`). */
46
+ mediatorBase?: string;
47
+ }
48
+ /** A user-facing progress phase, for the login page's status line. */
49
+ export type LoginPhase = 'probing' | 'storing-key' | 'requesting-grants' | 'verifying';
50
+ export interface LoginOutcome {
51
+ seed: Uint8Array;
52
+ identity: IdentityAgents;
53
+ grants: IZcap[];
54
+ parsed: ParsedGrants;
55
+ /** ISO timestamp: the earliest expiry across the grants. */
56
+ expires: string;
57
+ /** Whether this login created a brand-new app key (first run). */
58
+ firstRun: boolean;
59
+ }
60
+ /** Thrown when the user cancels/dismisses a wallet popup. */
61
+ export declare class LoginCancelledError extends Error {
62
+ constructor(step: string);
63
+ }
64
+ /**
65
+ * Requests storage grants for `identity` and validates them. Shared by the
66
+ * login flow and the expired-access reconnect path.
67
+ *
68
+ * @param options {object}
69
+ * @param options.identity {IdentityAgents}
70
+ * @param options.config {LoginConfig}
71
+ * @param [options.onPhase] {Function}
72
+ * @returns {Promise<CheckedGrants>}
73
+ */
74
+ export declare function requestGrants({ identity, config, onPhase }: {
75
+ identity: IdentityAgents;
76
+ config: LoginConfig;
77
+ onPhase?: (phase: LoginPhase) => void;
78
+ }): Promise<CheckedGrants>;
79
+ /**
80
+ * Runs the full Login-With-Wallet flow (first-run or returning, decided by
81
+ * the seed probe). Throws `LoginCancelledError` on dismissal and `Error` on
82
+ * verification failures; nothing is persisted here (the caller persists).
83
+ *
84
+ * @param options {object}
85
+ * @param options.config {LoginConfig}
86
+ * @param [options.onPhase] {Function}
87
+ * @returns {Promise<LoginOutcome>}
88
+ */
89
+ export declare function loginWithWallet({ config, onPhase }: {
90
+ config: LoginConfig;
91
+ onPhase?: (phase: LoginPhase) => void;
92
+ }): Promise<LoginOutcome>;
93
+ //# sourceMappingURL=loginFlow.d.ts.map