@coordination-network/user-app-core 0.0.1
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 +21 -0
- package/README.md +133 -0
- package/dist/index.cjs +754 -0
- package/dist/index.js +744 -0
- package/package.json +55 -0
- package/src/chain-state.ts +61 -0
- package/src/consent.ts +69 -0
- package/src/device-secret.ts +16 -0
- package/src/errors.ts +42 -0
- package/src/handlers/credential-issue.ts +176 -0
- package/src/handlers/presentation-create.ts +168 -0
- package/src/handlers/registration-sign.ts +19 -0
- package/src/hex.ts +8 -0
- package/src/identity.ts +15 -0
- package/src/index.ts +30 -0
- package/src/issuer-index.ts +64 -0
- package/src/member-api.ts +68 -0
- package/src/now.ts +6 -0
- package/src/selection.ts +38 -0
- package/src/service.ts +206 -0
- package/src/steps.ts +13 -0
- package/src/types.ts +175 -0
- package/src/wallet-factory.ts +42 -0
- package/types/index.d.ts +238 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ethereum Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# `@coordination-network/user-app-core`
|
|
2
|
+
|
|
3
|
+
The three `@coordination-network/user-app-link` operations a user app serves —
|
|
4
|
+
`registration.sign`, `credential.issue`, `presentation.create` — implemented over injected device,
|
|
5
|
+
storage, HTTP and proving ports. One operation per flow, each behind one consent. Runs
|
|
6
|
+
unchanged on the Expo app (`expo-secure-store`, a native prover) and the browser app (IndexedDB,
|
|
7
|
+
`bb.js` in a Worker): every platform difference lives in the ports, not here.
|
|
8
|
+
|
|
9
|
+
This package holds no business logic. It holds keys, held credentials, and revocation secrets, and
|
|
10
|
+
serves any number of network member apps over the deep-link protocol.
|
|
11
|
+
|
|
12
|
+
## Use
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createUserAppService } from "@coordination-network/user-app-core"
|
|
16
|
+
import { createRequestHandler } from "@coordination-network/user-app-link"
|
|
17
|
+
|
|
18
|
+
const service = createUserAppService({
|
|
19
|
+
store, // Store from @coordination-network/wallet, e.g. expo-secure-store-backed
|
|
20
|
+
deviceKey, // DeviceKey: a DeviceSigner plus ecdh() and getSecurityLevel()
|
|
21
|
+
proofGenerator, // ProofGenerator from @coordination-network/wallet
|
|
22
|
+
http: { getJson, postJson },
|
|
23
|
+
consent: async (summary) => showConsentScreen(summary),
|
|
24
|
+
review: async (summary) => showShareScreen(summary),
|
|
25
|
+
progress: (op, step) => showStep(op, step)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (!(await service.isOnboarded())) {
|
|
29
|
+
await service.createIdentity()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handler = createRequestHandler({ handlers: service.handlers, consent: service.consent })
|
|
33
|
+
const responseUrl = await handler.handle(incomingUrl)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`service.consent` is the single gate `createRequestHandler` calls before dispatch; `Wallet`'s own
|
|
37
|
+
`ConsentPrompt` is wired to a constant `true` internally, so one deep link never shows two consent
|
|
38
|
+
screens. `presentation.create` asks once more through `review`, but only after the proof exists,
|
|
39
|
+
so the question is about what the proof reveals rather than about starting the work. Handler
|
|
40
|
+
bodies run one at a time — a second deep link arriving mid-sync waits for the first to finish
|
|
41
|
+
rather than interleaving with it.
|
|
42
|
+
|
|
43
|
+
`service.readIdentity()` is how the app's own Identity screen reads the identity. No operation
|
|
44
|
+
hands it over a link: a member app that wants a `networkId` asks for a signed registration request
|
|
45
|
+
and reads the one inside it.
|
|
46
|
+
|
|
47
|
+
## Ports
|
|
48
|
+
|
|
49
|
+
- `store` / `proofGenerator` — `@coordination-network/wallet`'s own interfaces, taken verbatim.
|
|
50
|
+
- `deviceKey: DeviceKey` — extends `@coordination-network/credential`'s `DeviceSigner`
|
|
51
|
+
(`getPublicKeyJwk`, `sign`, `signDigest`) with `ecdh(peerPublicKey)` (the X coordinate of
|
|
52
|
+
`hwPriv · peerPublicKey`, 32 raw bytes — feeds `deriveEddsaKeypair`) and `getSecurityLevel()`.
|
|
53
|
+
Extending rather than replacing `DeviceSigner` means a `DeviceKey` is accepted as-is by `Wallet`,
|
|
54
|
+
`buildZkPresentationInputs` and `deriveDeviceKeyCommitment`. `SecurityLevel` is reported, never
|
|
55
|
+
enforced — fail-closed policy belongs in `apps/identity-testbed`, not here.
|
|
56
|
+
- `http: HttpClient` — `getJson`/`postJson`, each returning `{ status, body }`. Deliberately
|
|
57
|
+
structural: no DOM or Node fetch types leak into either app, and a test fake is three lines.
|
|
58
|
+
- `consent: UserConsentPrompt` — `(summary: ConsentSummary) => Promise<boolean>`. The summary is
|
|
59
|
+
values, never prose: the caller, the rows a screen folds behind its technical toggle, and — on
|
|
60
|
+
the two ops that scope one — the programme and country. The app writes the sentence, so it can
|
|
61
|
+
name the caller mid-line in the caller's own colour. `ConsentSummary` is keyed by `op`, so a
|
|
62
|
+
screen switching on it gets `scope` typed rather than optional. At prompt time no credential has
|
|
63
|
+
been selected yet for `presentation.create`, so its summary names the verifier, programme and
|
|
64
|
+
country rather than a credential — that is the material fact anyway.
|
|
65
|
+
- `review: PresentationReviewPrompt` — `(summary: PresentationReview) => Promise<boolean>`, asked
|
|
66
|
+
after `presentation.create` has built its proof and before it is posted. The summary names the
|
|
67
|
+
verifier, programme, country and the credential's issuer, which the proof itself reveals. `false`
|
|
68
|
+
refuses with `CONSENT_DENIED`.
|
|
69
|
+
- `progress: (op, step) => void` — named steps inside a long handler, for a screen that shows what
|
|
70
|
+
the wallet is doing. `credential.issue` reports six; `presentation.create` reports three.
|
|
71
|
+
- `now?: () => number` — defaults to `Date.now`; inject it for deterministic tests.
|
|
72
|
+
|
|
73
|
+
## Two honesty notes
|
|
74
|
+
|
|
75
|
+
**Revocation is answered by the issuer, never by the asker.** A member publishes a revocation
|
|
76
|
+
registry covering only its own credentials, so a verifier's snapshot cannot say whether another
|
|
77
|
+
member's credential still stands. `presentation.create` therefore takes no `revocationsUrl`: each
|
|
78
|
+
candidate credential is checked against the snapshot its own issuer published, read from this
|
|
79
|
+
package's issuer index. A candidate found revoked refuses with `CREDENTIAL_REVOKED`, before any
|
|
80
|
+
proof is built and without posting anything.
|
|
81
|
+
|
|
82
|
+
**The approved-issuer cross-check is real on both sides.** `GET /v1/issuers` returns both the
|
|
83
|
+
`leaves` and a `root` the server read from chain independently of them, and `Wallet` compares that
|
|
84
|
+
`approvedIssuerRoot` against the tree rebuilt from `leaves` before trusting either — the same
|
|
85
|
+
property `GET /v1/revocations/snapshot`'s own `root` already had.
|
|
86
|
+
|
|
87
|
+
## Store keys
|
|
88
|
+
|
|
89
|
+
`credential.issue` and `presentation.create` each construct a fresh `Wallet` over the same
|
|
90
|
+
persistent `store` `ports` supplies; `registration.sign` never needs one. `Wallet` owns the `key:`
|
|
91
|
+
and `vc:`-prefixed entries in that store; this package owns two prefixes of its own:
|
|
92
|
+
|
|
93
|
+
- `user-app-core:device-secret` — the one device secret this package ever holds (unlike `Wallet`'s
|
|
94
|
+
own per-`networkId` key store, this package supports exactly one identity at a time).
|
|
95
|
+
- `user-app-core:issuer-index:<issuerNetworkId>` — what a credential body cannot give back: the
|
|
96
|
+
issuer's `networkId` (the body carries its Baby Jubjub public key instead), the plaintext
|
|
97
|
+
programme (the body carries a hash), the country, when this wallet stored it, and where that
|
|
98
|
+
issuer publishes its approved-issuer set and revocation snapshot. Written by `credential.issue`;
|
|
99
|
+
read by `presentation.create` to reach the right snapshot, by `listCredentials` to name the
|
|
100
|
+
issuer, and by `credential.issue` itself to answer `alreadyHeld`.
|
|
101
|
+
|
|
102
|
+
The revocation secret is no longer stored between deep links: `credential.issue` generates it,
|
|
103
|
+
opens the credential with it and hands it to `Wallet` inside one call, so it never leaves that
|
|
104
|
+
function.
|
|
105
|
+
|
|
106
|
+
`erase()` deletes every key in `store`, not only this package's own three prefixes and `Wallet`'s —
|
|
107
|
+
a host app shares the same store for its own state (the user app keeps its stack descriptor there),
|
|
108
|
+
so a full wipe reaches that too. A caller that wants to survive `erase()` keeps its own state
|
|
109
|
+
elsewhere.
|
|
110
|
+
|
|
111
|
+
## Selecting a credential for `presentation.create`
|
|
112
|
+
|
|
113
|
+
`presentation.create`'s params carry no credential id. Held credentials are filtered to those whose
|
|
114
|
+
body matches the requested programme and country — no status filter — and each survivor is then
|
|
115
|
+
synced against its own issuer's snapshot. The one expiring latest among those still `valid` wins,
|
|
116
|
+
ties broken by id for determinism. None valid but one revoked throws `CREDENTIAL_REVOKED`; none at
|
|
117
|
+
all throws `NO_CREDENTIAL`. Both stay distinguishable from a `PROOF_FAILED` thrown once proving has
|
|
118
|
+
actually started.
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
yarn test:library user-app-core --watchman=false
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Real collaborators are used wherever they are pure: a real `Wallet` over a real `MemoryStore`, a
|
|
127
|
+
software P-256 `DeviceKey` (`LocalP256Signer` plus a plain ECDH), real `ApprovedIssuerSet` and
|
|
128
|
+
`RevocationRegistry`, and credentials issued with the real `issueZkCredential`. Only the proof
|
|
129
|
+
generator and the HTTP port are faked.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT, see [`LICENSE`](./LICENSE).
|