@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 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).