@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.
- package/LICENSE.md +20 -0
- package/README.md +410 -0
- package/dist/auth/chapi.d.ts +43 -0
- package/dist/auth/chapi.d.ts.map +1 -0
- package/dist/auth/chapi.js +107 -0
- package/dist/auth/chapi.js.map +1 -0
- package/dist/auth/loginFlow.d.ts +93 -0
- package/dist/auth/loginFlow.d.ts.map +1 -0
- package/dist/auth/loginFlow.js +149 -0
- package/dist/auth/loginFlow.js.map +1 -0
- package/dist/auth/loginRequest.d.ts +66 -0
- package/dist/auth/loginRequest.d.ts.map +1 -0
- package/dist/auth/loginRequest.js +83 -0
- package/dist/auth/loginRequest.js.map +1 -0
- package/dist/auth/verifyResponse.d.ts +52 -0
- package/dist/auth/verifyResponse.d.ts.map +1 -0
- package/dist/auth/verifyResponse.js +138 -0
- package/dist/auth/verifyResponse.js.map +1 -0
- package/dist/auth/verifyResponse.test.d.ts +2 -0
- package/dist/auth/verifyResponse.test.d.ts.map +1 -0
- package/dist/auth/verifyResponse.test.js +282 -0
- package/dist/auth/verifyResponse.test.js.map +1 -0
- package/dist/auth/walletRequestTypes.d.ts +143 -0
- package/dist/auth/walletRequestTypes.d.ts.map +1 -0
- package/dist/auth/walletRequestTypes.js +2 -0
- package/dist/auth/walletRequestTypes.js.map +1 -0
- package/dist/config.d.ts +150 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/dev/cli.d.ts +16 -0
- package/dist/dev/cli.d.ts.map +1 -0
- package/dist/dev/cli.js +122 -0
- package/dist/dev/cli.js.map +1 -0
- package/dist/dev/cli.test.d.ts +2 -0
- package/dist/dev/cli.test.d.ts.map +1 -0
- package/dist/dev/cli.test.js +39 -0
- package/dist/dev/cli.test.js.map +1 -0
- package/dist/dev/index.d.ts +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +10 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/provisionDevGrants.d.ts +73 -0
- package/dist/dev/provisionDevGrants.d.ts.map +1 -0
- package/dist/dev/provisionDevGrants.js +176 -0
- package/dist/dev/provisionDevGrants.js.map +1 -0
- package/dist/grants.d.ts +59 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +82 -0
- package/dist/grants.js.map +1 -0
- package/dist/grants.test.d.ts +2 -0
- package/dist/grants.test.d.ts.map +1 -0
- package/dist/grants.test.js +79 -0
- package/dist/grants.test.js.map +1 -0
- package/dist/identity/agents.d.ts +99 -0
- package/dist/identity/agents.d.ts.map +1 -0
- package/dist/identity/agents.js +132 -0
- package/dist/identity/agents.js.map +1 -0
- package/dist/identity/appSession.d.ts +78 -0
- package/dist/identity/appSession.d.ts.map +1 -0
- package/dist/identity/appSession.js +93 -0
- package/dist/identity/appSession.js.map +1 -0
- package/dist/identity/documentLoader.d.ts +20 -0
- package/dist/identity/documentLoader.d.ts.map +1 -0
- package/dist/identity/documentLoader.js +99 -0
- package/dist/identity/documentLoader.js.map +1 -0
- package/dist/identity/initAppSession.d.ts +27 -0
- package/dist/identity/initAppSession.d.ts.map +1 -0
- package/dist/identity/initAppSession.js +30 -0
- package/dist/identity/initAppSession.js.map +1 -0
- package/dist/identity/seedCredential.d.ts +74 -0
- package/dist/identity/seedCredential.d.ts.map +1 -0
- package/dist/identity/seedCredential.js +165 -0
- package/dist/identity/seedCredential.js.map +1 -0
- package/dist/identity/seedCredential.test.d.ts +2 -0
- package/dist/identity/seedCredential.test.d.ts.map +1 -0
- package/dist/identity/seedCredential.test.js +205 -0
- package/dist/identity/seedCredential.test.js.map +1 -0
- package/dist/identity/seedStore.d.ts +27 -0
- package/dist/identity/seedStore.d.ts.map +1 -0
- package/dist/identity/seedStore.js +74 -0
- package/dist/identity/seedStore.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mui/ProtectedRoute.d.ts +10 -0
- package/dist/mui/ProtectedRoute.d.ts.map +1 -0
- package/dist/mui/ProtectedRoute.js +54 -0
- package/dist/mui/ProtectedRoute.js.map +1 -0
- package/dist/mui/ReconnectBanner.d.ts +2 -0
- package/dist/mui/ReconnectBanner.d.ts.map +1 -0
- package/dist/mui/ReconnectBanner.js +20 -0
- package/dist/mui/ReconnectBanner.js.map +1 -0
- package/dist/mui/SyncStatusChip.d.ts +2 -0
- package/dist/mui/SyncStatusChip.d.ts.map +1 -0
- package/dist/mui/SyncStatusChip.js +27 -0
- package/dist/mui/SyncStatusChip.js.map +1 -0
- package/dist/mui/index.d.ts +12 -0
- package/dist/mui/index.d.ts.map +1 -0
- package/dist/mui/index.js +12 -0
- package/dist/mui/index.js.map +1 -0
- package/dist/react/WasSessionProvider.d.ts +36 -0
- package/dist/react/WasSessionProvider.d.ts.map +1 -0
- package/dist/react/WasSessionProvider.js +35 -0
- package/dist/react/WasSessionProvider.js.map +1 -0
- package/dist/react/hooks.d.ts +66 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +121 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +11 -0
- package/dist/react/index.js.map +1 -0
- package/dist/session/appReadyStore.d.ts +10 -0
- package/dist/session/appReadyStore.d.ts.map +1 -0
- package/dist/session/appReadyStore.js +22 -0
- package/dist/session/appReadyStore.js.map +1 -0
- package/dist/session/authStore.d.ts +71 -0
- package/dist/session/authStore.d.ts.map +1 -0
- package/dist/session/authStore.js +347 -0
- package/dist/session/authStore.js.map +1 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +10 -0
- package/dist/session/index.js.map +1 -0
- package/dist/storage/entityStore.d.ts +51 -0
- package/dist/storage/entityStore.d.ts.map +1 -0
- package/dist/storage/entityStore.js +78 -0
- package/dist/storage/entityStore.js.map +1 -0
- package/dist/storage/localStore.d.ts +144 -0
- package/dist/storage/localStore.d.ts.map +1 -0
- package/dist/storage/localStore.js +289 -0
- package/dist/storage/localStore.js.map +1 -0
- package/dist/storage/rehydrate.d.ts +56 -0
- package/dist/storage/rehydrate.d.ts.map +1 -0
- package/dist/storage/rehydrate.js +87 -0
- package/dist/storage/rehydrate.js.map +1 -0
- package/dist/storage/rehydrate.test.d.ts +2 -0
- package/dist/storage/rehydrate.test.d.ts.map +1 -0
- package/dist/storage/rehydrate.test.js +172 -0
- package/dist/storage/rehydrate.test.js.map +1 -0
- package/dist/storage/storageManager.d.ts +39 -0
- package/dist/storage/storageManager.d.ts.map +1 -0
- package/dist/storage/storageManager.js +68 -0
- package/dist/storage/storageManager.js.map +1 -0
- package/dist/storage/syncController.d.ts +79 -0
- package/dist/storage/syncController.d.ts.map +1 -0
- package/dist/storage/syncController.js +189 -0
- package/dist/storage/syncController.js.map +1 -0
- package/dist/storage/syncStatusStore.d.ts +17 -0
- package/dist/storage/syncStatusStore.d.ts.map +1 -0
- package/dist/storage/syncStatusStore.js +19 -0
- package/dist/storage/syncStatusStore.js.map +1 -0
- package/dist/storage/wasRemoteStore.d.ts +73 -0
- package/dist/storage/wasRemoteStore.d.ts.map +1 -0
- package/dist/storage/wasRemoteStore.js +76 -0
- package/dist/storage/wasRemoteStore.js.map +1 -0
- package/dist/storage/wasSync.d.ts +42 -0
- package/dist/storage/wasSync.d.ts.map +1 -0
- package/dist/storage/wasSync.js +34 -0
- package/dist/storage/wasSync.js.map +1 -0
- package/dist/sync/changesQuery.d.ts +44 -0
- package/dist/sync/changesQuery.d.ts.map +1 -0
- package/dist/sync/changesQuery.js +61 -0
- package/dist/sync/changesQuery.js.map +1 -0
- package/dist/sync/changesQuery.test.d.ts +2 -0
- package/dist/sync/changesQuery.test.d.ts.map +1 -0
- package/dist/sync/changesQuery.test.js +145 -0
- package/dist/sync/changesQuery.test.js.map +1 -0
- package/dist/sync/docCipher.d.ts +68 -0
- package/dist/sync/docCipher.d.ts.map +1 -0
- package/dist/sync/docCipher.js +89 -0
- package/dist/sync/docCipher.js.map +1 -0
- package/dist/sync/feedMasterPort.d.ts +30 -0
- package/dist/sync/feedMasterPort.d.ts.map +1 -0
- package/dist/sync/feedMasterPort.js +58 -0
- package/dist/sync/feedMasterPort.js.map +1 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +21 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/lww.d.ts +29 -0
- package/dist/sync/lww.d.ts.map +1 -0
- package/dist/sync/lww.js +28 -0
- package/dist/sync/lww.js.map +1 -0
- package/dist/sync/lww.test.d.ts +2 -0
- package/dist/sync/lww.test.d.ts.map +1 -0
- package/dist/sync/lww.test.js +28 -0
- package/dist/sync/lww.test.js.map +1 -0
- package/dist/sync/lwwConflictHandler.d.ts +49 -0
- package/dist/sync/lwwConflictHandler.d.ts.map +1 -0
- package/dist/sync/lwwConflictHandler.js +65 -0
- package/dist/sync/lwwConflictHandler.js.map +1 -0
- package/dist/sync/lwwConflictHandler.test.d.ts +2 -0
- package/dist/sync/lwwConflictHandler.test.d.ts.map +1 -0
- package/dist/sync/lwwConflictHandler.test.js +78 -0
- package/dist/sync/lwwConflictHandler.test.js.map +1 -0
- package/dist/sync/pushWrites.d.ts +57 -0
- package/dist/sync/pushWrites.d.ts.map +1 -0
- package/dist/sync/pushWrites.js +159 -0
- package/dist/sync/pushWrites.js.map +1 -0
- package/dist/sync/pushWrites.test.d.ts +2 -0
- package/dist/sync/pushWrites.test.d.ts.map +1 -0
- package/dist/sync/pushWrites.test.js +287 -0
- package/dist/sync/pushWrites.test.js.map +1 -0
- package/dist/sync/syncedDocSchema.d.ts +22 -0
- package/dist/sync/syncedDocSchema.d.ts.map +1 -0
- package/dist/sync/syncedDocSchema.js +26 -0
- package/dist/sync/syncedDocSchema.js.map +1 -0
- package/dist/sync/types.d.ts +194 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +36 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/wasReplication.d.ts +47 -0
- package/dist/sync/wasReplication.d.ts.map +1 -0
- package/dist/sync/wasReplication.js +55 -0
- package/dist/sync/wasReplication.js.map +1 -0
- package/dist/sync/wasSyncPort.d.ts +45 -0
- package/dist/sync/wasSyncPort.d.ts.map +1 -0
- package/dist/sync/wasSyncPort.js +170 -0
- package/dist/sync/wasSyncPort.js.map +1 -0
- 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
|
+
[](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
|