@coordination-network/user-app-core 0.0.1 → 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/README.md +53 -29
- package/dist/index.cjs +286 -91
- package/dist/index.js +285 -94
- package/package.json +9 -9
- package/src/chain-state.ts +28 -32
- package/src/errors.ts +24 -0
- package/src/handlers/credential-issue.ts +104 -29
- package/src/handlers/presentation-create.ts +41 -40
- package/src/index.ts +6 -1
- package/src/issuer-index.ts +22 -6
- package/src/member-api.ts +3 -1
- package/src/security-level.ts +8 -0
- package/src/selection.ts +2 -2
- package/src/service.ts +50 -6
- package/src/types.ts +61 -8
- package/src/wallet-factory.ts +62 -6
- package/types/index.d.ts +72 -15
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ import { createRequestHandler } from "@coordination-network/user-app-link"
|
|
|
17
17
|
|
|
18
18
|
const service = createUserAppService({
|
|
19
19
|
store, // Store from @coordination-network/wallet, e.g. expo-secure-store-backed
|
|
20
|
-
deviceKey, // DeviceKey:
|
|
20
|
+
deviceKey, // DeviceKey: signerFor(prompt), ecdh(peer, prompt), getPublicKeyJwk(), getSecurityLevel()
|
|
21
21
|
proofGenerator, // ProofGenerator from @coordination-network/wallet
|
|
22
22
|
http: { getJson, postJson },
|
|
23
23
|
consent: async (summary) => showConsentScreen(summary),
|
|
@@ -35,8 +35,9 @@ const responseUrl = await handler.handle(incomingUrl)
|
|
|
35
35
|
|
|
36
36
|
`service.consent` is the single gate `createRequestHandler` calls before dispatch; `Wallet`'s own
|
|
37
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`,
|
|
39
|
-
so
|
|
38
|
+
screens. `presentation.create` asks once more through `review`, before it proves anything or asks
|
|
39
|
+
the device key to sign, so a refusal costs no key use and the question is about what the proof
|
|
40
|
+
reveals. Handler
|
|
40
41
|
bodies run one at a time — a second deep link arriving mid-sync waits for the first to finish
|
|
41
42
|
rather than interleaving with it.
|
|
42
43
|
|
|
@@ -47,14 +48,32 @@ and reads the one inside it.
|
|
|
47
48
|
## Ports
|
|
48
49
|
|
|
49
50
|
- `store` / `proofGenerator` — `@coordination-network/wallet`'s own interfaces, taken verbatim.
|
|
50
|
-
- `deviceKey: DeviceKey` —
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
- `deviceKey: DeviceKey` — the holder's two P-256 hardware keys: `getPublicKeyJwk()` for the
|
|
52
|
+
signing key, `signerFor(prompt)` for a `DeviceSigner` whose every signature raises `prompt`,
|
|
53
|
+
`ecdh(peerPublicKey, prompt)` (the X coordinate of `agreementPriv · peerPublicKey`, 32 raw
|
|
54
|
+
bytes — feeds `deriveEddsaKeypair`) and `getSecurityLevel()`, which reports the lower of the two
|
|
55
|
+
keys' levels, and `getAttestationChain()`, the base64 DER key attestation chain of the signing
|
|
56
|
+
key, leaf first, or `[]` when it has none. The `signerFor` signer is accepted as-is by `Wallet` and
|
|
57
|
+
`buildZkPresentationInputs`. A cancelled prompt, an invalidated key or a failed authentication
|
|
58
|
+
throws `DeviceKeyError`; a cancel reaches the caller as `CONSENT_DENIED`, the other two as
|
|
59
|
+
`INTERNAL`. The identity seed is unlocked by one ECDH prompt the first time a process needs it,
|
|
60
|
+
then cached until `erase()`, or until `removeUnboundCredentials()` removes something. Listing and
|
|
61
|
+
`registration.sign` never touch a key. Issuance posts the chain as `deviceAttestation` (omitted
|
|
62
|
+
when empty), and `createIdentity` refuses a level below `minimumDeviceSecurityLevel`.
|
|
63
|
+
- `minimumDeviceSecurityLevel?: SecurityLevel` — the lowest level `createIdentity` accepts. Absent
|
|
64
|
+
means no floor; `UNKNOWN` ranks with `SOFTWARE`. The issuer enforces its own minimum on
|
|
65
|
+
the attested level.
|
|
56
66
|
- `http: HttpClient` — `getJson`/`postJson`, each returning `{ status, body }`. Deliberately
|
|
57
67
|
structural: no DOM or Node fetch types leak into either app, and a test fake is three lines.
|
|
68
|
+
- `directory: MemberDirectory` — `issuerKeyLeavesOf(networkId)`, the leaves of a member's live issuer
|
|
69
|
+
keys from `ApprovedIssuerSet.issuerKeysOf`. `credential.issue` requires the signing key to be among
|
|
70
|
+
them; a rejection means the chain could not be read, an unknown member (removed, or missing from
|
|
71
|
+
the wallet's own member list) answers `[]`. An implementation must not reveal to the chain node
|
|
72
|
+
which member is asked about.
|
|
73
|
+
- `roots: RootAuthority` — `currentRoots()`, the chain's current approved-issuer-set root and
|
|
74
|
+
leaf count, and revocation root. `credential.issue` and `presentation.create` check the lists a
|
|
75
|
+
server serves against these; a rejection means the chain could not be read (`CHAIN_UNREACHABLE`). An
|
|
76
|
+
implementation must read the chain, never a server.
|
|
58
77
|
- `consent: UserConsentPrompt` — `(summary: ConsentSummary) => Promise<boolean>`. The summary is
|
|
59
78
|
values, never prose: the caller, the rows a screen folds behind its technical toggle, and — on
|
|
60
79
|
the two ops that scope one — the programme and country. The app writes the sentence, so it can
|
|
@@ -63,26 +82,25 @@ and reads the one inside it.
|
|
|
63
82
|
been selected yet for `presentation.create`, so its summary names the verifier, programme and
|
|
64
83
|
country rather than a credential — that is the material fact anyway.
|
|
65
84
|
- `review: PresentationReviewPrompt` — `(summary: PresentationReview) => Promise<boolean>`, asked
|
|
66
|
-
after `presentation.create` has
|
|
67
|
-
verifier, programme, country and the credential's issuer,
|
|
68
|
-
refuses with `CONSENT_DENIED`.
|
|
85
|
+
after `presentation.create` has chosen a credential and before it proves or posts. The summary names the
|
|
86
|
+
verifier, programme, country and the credential's issuer, so the person sees which credential
|
|
87
|
+
answered; the proof itself no longer reveals the issuer. `false` refuses with `CONSENT_DENIED`.
|
|
69
88
|
- `progress: (op, step) => void` — named steps inside a long handler, for a screen that shows what
|
|
70
89
|
the wallet is doing. `credential.issue` reports six; `presentation.create` reports three.
|
|
71
90
|
- `now?: () => number` — defaults to `Date.now`; inject it for deterministic tests.
|
|
72
91
|
|
|
73
|
-
##
|
|
92
|
+
## Honesty note
|
|
74
93
|
|
|
75
|
-
**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
property `GET /v1/revocations/snapshot`'s own `root` already had.
|
|
94
|
+
**Served lists are checked against the chain, not against the server.** The leaves from
|
|
95
|
+
`GET /v1/issuers` and the snapshot from `GET /v1/revocations/snapshot` are bulk data: the `root`
|
|
96
|
+
each also serves is never read. Both handlers read the chain's current roots through `roots`. The
|
|
97
|
+
lists must hash to them, and the issuer set must have the chain's leaf count: an internal node can
|
|
98
|
+
stand in for the leaves under it and keep the root, so the count is what fixes every leaf. A server
|
|
99
|
+
therefore cannot choose which root a holder proves against, nor learn which side of a cut an issuer
|
|
100
|
+
is on. A mismatch is `VERIFIER_UNREACHABLE` on `presentation.create` and
|
|
101
|
+
`ISSUER_UNREACHABLE` on `credential.issue`; roots that cannot be read are `CHAIN_UNREACHABLE`. There
|
|
102
|
+
is no retry and no root cache, so a wallet offline from the chain cannot present or issue. When a
|
|
103
|
+
list and the chain disagree, the person runs the request again.
|
|
86
104
|
|
|
87
105
|
## Store keys
|
|
88
106
|
|
|
@@ -95,14 +113,20 @@ and `vc:`-prefixed entries in that store; this package owns two prefixes of its
|
|
|
95
113
|
- `user-app-core:issuer-index:<issuerNetworkId>` — what a credential body cannot give back: the
|
|
96
114
|
issuer's `networkId` (the body carries its Baby Jubjub public key instead), the plaintext
|
|
97
115
|
programme (the body carries a hash), the country, when this wallet stored it, and where that
|
|
98
|
-
issuer publishes its approved-issuer set
|
|
99
|
-
read by `presentation.create` to
|
|
100
|
-
issuer, and by `credential.issue` itself to answer `alreadyHeld`.
|
|
116
|
+
issuer publishes its approved-issuer set (`issuersUrl`). Written by `credential.issue`;
|
|
117
|
+
read by `presentation.create` to match each credential to its issuer, by `listCredentials` to
|
|
118
|
+
name the issuer, and by `credential.issue` itself to answer `alreadyHeld`.
|
|
101
119
|
|
|
102
120
|
The revocation secret is no longer stored between deep links: `credential.issue` generates it,
|
|
103
121
|
opens the credential with it and hands it to `Wallet` inside one call, so it never leaves that
|
|
104
122
|
function.
|
|
105
123
|
|
|
124
|
+
`removeUnboundCredentials()` compares each held credential's `deviceKeyCommitment` with the current
|
|
125
|
+
signing key and deletes the ones bound to another key, with their revocation secrets and issuer
|
|
126
|
+
index entries, then resolves to how many it removed. It reads only the public key, so it raises no
|
|
127
|
+
prompt, and keeps the device secret and every other key, so the person stays registered. Run it at
|
|
128
|
+
every start: a run cut short leaves the rest still unbound, and the next one removes them.
|
|
129
|
+
|
|
106
130
|
`erase()` deletes every key in `store`, not only this package's own three prefixes and `Wallet`'s —
|
|
107
131
|
a host app shares the same store for its own state (the user app keeps its stack descriptor there),
|
|
108
132
|
so a full wipe reaches that too. A caller that wants to survive `erase()` keeps its own state
|
|
@@ -112,7 +136,7 @@ elsewhere.
|
|
|
112
136
|
|
|
113
137
|
`presentation.create`'s params carry no credential id. Held credentials are filtered to those whose
|
|
114
138
|
body matches the requested programme and country — no status filter — and each survivor is then
|
|
115
|
-
synced against
|
|
139
|
+
synced against the verifier's lists, which must hash to the chain's current roots. The one expiring latest among those still `valid` wins,
|
|
116
140
|
ties broken by id for determinism. None valid but one revoked throws `CREDENTIAL_REVOKED`; none at
|
|
117
141
|
all throws `NO_CREDENTIAL`. Both stay distinguishable from a `PROOF_FAILED` thrown once proving has
|
|
118
142
|
actually started.
|
|
@@ -124,7 +148,7 @@ yarn test:library user-app-core --watchman=false
|
|
|
124
148
|
```
|
|
125
149
|
|
|
126
150
|
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
|
|
151
|
+
software P-256 `DeviceKey` (`LocalP256Signer` plus a plain ECDH that records its prompts), real `ApprovedIssuerSet` and
|
|
128
152
|
`RevocationRegistry`, and credentials issued with the real `issueZkCredential`. Only the proof
|
|
129
153
|
generator and the HTTP port are faked.
|
|
130
154
|
|