@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.
@@ -0,0 +1,238 @@
1
+ import { Hex } from '@coordination-network/identity';
2
+ import { DeviceSigner } from '@coordination-network/credential';
3
+ import { Store, ProofGenerator, CredentialInfo } from '@coordination-network/wallet';
4
+ import { Caller, SecurityLevel, Hex32, Hex66, Hex65, LinkOp, LinkHandlers, DecodedLinkRequest, LinkErrorCode, LinkError } from '@coordination-network/user-app-link';
5
+
6
+ /**
7
+ * The holder P-256 device key. Extends `DeviceSigner` (rather than replacing it) so a `DeviceKey`
8
+ * is accepted as-is anywhere a `DeviceSigner` is expected — `Wallet`, `buildZkPresentationInputs`,
9
+ * `deriveDeviceKeyCommitment`.
10
+ */
11
+ interface DeviceKey extends DeviceSigner {
12
+ /** X coordinate of hwPriv · peerPublicKey, 32 raw bytes. Feeds `deriveEddsaKeypair`. */
13
+ ecdh(peerPublicKey: Uint8Array): Promise<Uint8Array>;
14
+ getSecurityLevel(): Promise<SecurityLevel>;
15
+ }
16
+ /** A minimal HTTP JSON port: no DOM or RN fetch types needed, and a test fake is three lines. */
17
+ interface HttpJsonResponse {
18
+ status: number;
19
+ body: unknown;
20
+ }
21
+ interface HttpClient {
22
+ getJson(url: string): Promise<HttpJsonResponse>;
23
+ postJson(url: string, body: unknown): Promise<HttpJsonResponse>;
24
+ }
25
+ /** One human-readable line of a consent screen, e.g. `{ label: "Programme", value: "demo-programme" }`. */
26
+ interface ConsentDetail {
27
+ label: string;
28
+ value: string;
29
+ /** Set false for a value whose full text is the point and must never hide behind a tap - the callback. */
30
+ truncate?: boolean;
31
+ }
32
+ /** The programme and country a consent sentence names, on the two ops that scope one. */
33
+ interface ConsentScope {
34
+ programme: string;
35
+ country: string;
36
+ }
37
+ interface ConsentSummaryBase {
38
+ caller: Caller;
39
+ /** The technical rows the consent sheet folds away, in the order it lists them. */
40
+ details: ConsentDetail[];
41
+ }
42
+ /**
43
+ * What a request amounts to, in values rather than sentences: the wallet writes every line a
44
+ * person reads, so that it can name the caller mid-sentence in the caller's own colour. Keyed by
45
+ * `op` so a screen that switches on it gets `scope` typed rather than optional.
46
+ */
47
+ type ConsentSummary = (ConsentSummaryBase & {
48
+ op: "registration.sign";
49
+ }) | (ConsentSummaryBase & {
50
+ op: "credential.issue";
51
+ scope: ConsentScope;
52
+ }) | (ConsentSummaryBase & {
53
+ op: "presentation.create";
54
+ scope: ConsentScope;
55
+ });
56
+ /** The single consent gate: shown once per deep link, before dispatch. */
57
+ type UserConsentPrompt = (summary: ConsentSummary) => Promise<boolean>;
58
+ interface PresentationReviewBase {
59
+ verifier: string;
60
+ programme: string;
61
+ country: string;
62
+ issuerNetworkId: Hex32;
63
+ }
64
+ /**
65
+ * What a verifier would learn. `proof` is what the built proof reveals, which is no longer the
66
+ * issuer: a presentation proves membership of the whole approved set and carries one network-wide
67
+ * revocation root, so nothing in it singles an issuer out. `issuerNetworkId` is still here because
68
+ * the person is shown which of their own credentials answered. `revoked` is the refusal itself:
69
+ * telling a verifier that the person holds a revoked credential says something about them that no
70
+ * proof would have, so it is theirs to allow.
71
+ */
72
+ type PresentationReview = (PresentationReviewBase & {
73
+ kind: "proof";
74
+ }) | (PresentationReviewBase & {
75
+ kind: "revoked";
76
+ });
77
+ /**
78
+ * The second gate, on `presentation.create` only: nothing has left the phone yet, so the person
79
+ * can be shown what would. `false` refuses with `CONSENT_DENIED`.
80
+ */
81
+ type PresentationReviewPrompt = (summary: PresentationReview) => Promise<boolean>;
82
+ /**
83
+ * Whether a held credential has been revoked, from the one registry the whole network writes to.
84
+ * `null` is "nobody to ask": no stack is configured, or it could not be reached. The caller then
85
+ * says it has not checked, rather than calling a credential valid on the strength of its dates.
86
+ */
87
+ interface RevocationAuthority {
88
+ isRevoked(commitment: bigint): Promise<boolean | null>;
89
+ }
90
+ interface UserAppPorts {
91
+ store: Store;
92
+ deviceKey: DeviceKey;
93
+ proofGenerator: ProofGenerator;
94
+ http: HttpClient;
95
+ consent: UserConsentPrompt;
96
+ review: PresentationReviewPrompt;
97
+ /** Absent leaves every held credential's revocation unchecked; see {@link RevocationAuthority}. */
98
+ revocations?: RevocationAuthority;
99
+ /** Named steps inside a long handler, for a screen that shows what the wallet is doing. */
100
+ progress: (op: LinkOp, step: string) => void;
101
+ now?: () => number;
102
+ }
103
+ /** What the app's own Identity screen shows; assembled from the device secret, never from a link. */
104
+ interface PublicIdentity {
105
+ networkId: Hex32;
106
+ metaStealthAddress: Hex66;
107
+ accountPublicKey: Hex65;
108
+ securityLevel: SecurityLevel;
109
+ }
110
+ /**
111
+ * What a held credential's state can honestly be called here. The validity window is signed into
112
+ * the credential body, so it is answerable offline. Revocation takes an authority, and there is now
113
+ * exactly one for the whole network — asking it names no issuer, which is what makes this listing
114
+ * able to say "revoked" at all. Issuer approval stays out: an approved-issuer set is still each
115
+ * member's own, so this screen has no one to ask and reports nothing.
116
+ *
117
+ * `unchecked` is a credential inside its window whose revocation could not be established. It is
118
+ * deliberately not folded into `valid`: the difference between "the network says this is live" and
119
+ * "nobody could be asked" is the whole point of showing a status.
120
+ */
121
+ type HeldCredentialStatus = "valid" | "revoked" | "not-yet-valid" | "expired" | "unchecked";
122
+ interface HeldCredential {
123
+ id: string;
124
+ /** See {@link HeldCredentialStatus}: the window and revocation, never issuer approval. */
125
+ status: HeldCredentialStatus;
126
+ /** The three fields the credential body cannot give back, kept in the issuer index at issuance. */
127
+ issuerNetworkId: Hex32;
128
+ programme: string;
129
+ issuedAt: number;
130
+ /** A one-way hash of {@link programme}, as signed into the credential. */
131
+ programmeHash: string;
132
+ /** Unlike `programmeHash`, exactly invertible: `countryCode` just packs three ASCII bytes. */
133
+ country: string;
134
+ notBefore: number;
135
+ expiresAt: number;
136
+ }
137
+ /**
138
+ * The two keys an app needs to scan ERC-5564 announcements for itself, where there is no indexer.
139
+ * `viewingPrivateKey` is a private key: it detects incoming payments but cannot spend, and it is
140
+ * deliberately the narrowest thing that does the job — far less than the device secret every other
141
+ * key derives from.
142
+ */
143
+ interface StealthScanKeys {
144
+ spendingPublicKey: Hex;
145
+ viewingPrivateKey: Hex;
146
+ }
147
+ interface UserAppService {
148
+ readonly handlers: LinkHandlers;
149
+ consent(request: DecodedLinkRequest): Promise<boolean>;
150
+ isOnboarded(): Promise<boolean>;
151
+ createIdentity(): Promise<PublicIdentity>;
152
+ readIdentity(): Promise<PublicIdentity>;
153
+ listCredentials(): Promise<HeldCredential[]>;
154
+ /** See {@link StealthScanKeys}. Throws if no identity has been created. */
155
+ readStealthScanKeys(): Promise<StealthScanKeys>;
156
+ erase(): Promise<void>;
157
+ }
158
+
159
+ declare function createUserAppService(ports: UserAppPorts): UserAppService;
160
+
161
+ /**
162
+ * Preserves the original message on the thrown `LinkError`. `chainStateCode` names the side whose
163
+ * lists were being read: during issuance that is the issuer, not a verifier.
164
+ *
165
+ * Takes `unknown` rather than `WalletError` because the wallet's sync methods are not total over
166
+ * `WalletError`: they parse each stored credential and write each record back, so a corrupt record
167
+ * or a failing device store throws something else entirely. Indexing the table with that error's
168
+ * absent `code` would have produced a `LinkError` with no code at all.
169
+ */
170
+ declare function mapWalletError(error: unknown, chainStateCode?: LinkErrorCode): LinkError;
171
+
172
+ interface CredentialSelectionRequest {
173
+ programme: string;
174
+ country: string;
175
+ }
176
+ /**
177
+ * `presentation.create` carries no credential id, so this narrows by what the credential body
178
+ * says: the requested programme and country. Status is deliberately not a filter — each candidate
179
+ * is then synced against its own issuer's snapshot, and a caller that finds none valid still
180
+ * needs to know whether it held a revoked one.
181
+ */
182
+ declare function matchingCredentials<T extends CredentialInfo>(credentials: T[], request: CredentialSelectionRequest): T[];
183
+ /** Of the credentials still usable, the one that stays usable longest, tie-broken by id for determinism. */
184
+ declare function latestExpiring<T extends CredentialInfo>(credentials: T[]): T | null;
185
+
186
+ /**
187
+ * `createRequestHandler` calls this after confirming `request.op` is a known op and validating
188
+ * `params` against its schema, so the casts below are re-attaching types the caller already
189
+ * proved, not asserting past them — an exhaustive switch covers every case with no defensive
190
+ * default branch needed. Every value here ends up behind the sheet's "Technical details" toggle;
191
+ * the sentence above it is the app's, not this package's.
192
+ */
193
+ declare function buildConsentSummary(request: DecodedLinkRequest): ConsentSummary;
194
+
195
+ /**
196
+ * This package holds exactly one identity, so unlike `Wallet`'s `key:`-prefixed store (built for
197
+ * several imported keys) the device secret lives under one fixed key.
198
+ */
199
+ declare const DEVICE_SECRET_KEY = "user-app-core:device-secret";
200
+
201
+ /**
202
+ * The steps `credential.issue` reports, in the order it reports them. Named here rather than
203
+ * spelled out twice, so the screen that groups the six into the three a person approved cannot
204
+ * drift from the handler reporting them.
205
+ */
206
+ declare const CREDENTIAL_ISSUE_STEPS: {
207
+ readonly held: "Checking what you hold";
208
+ readonly lists: "Checking the lists";
209
+ readonly challenge: "Getting a challenge";
210
+ readonly binding: "Binding to this phone";
211
+ readonly signing: "Asking the issuer to sign";
212
+ readonly storing: "Saving to your wallet";
213
+ };
214
+
215
+ /**
216
+ * One entry per issuer the wallet holds a credential from. It carries what the credential body
217
+ * cannot give back — the plaintext programme and the issuer — so a listing can name both.
218
+ */
219
+ interface IssuerIndexEntry {
220
+ credentialId: string;
221
+ /** The Baby Jubjub key that signed the credential, as decimal field elements. */
222
+ issuerPublicKey: [string, string];
223
+ /** Where the issuer published its own approved-issuer set at issuance. Nothing reads it yet:
224
+ * a presentation reads the verifier's, which is why a hostile verifier can still probe for the
225
+ * issuer by serving a subset. Recorded so that check can be anchored here instead. */
226
+ issuersUrl: string;
227
+ programme: string;
228
+ country: string;
229
+ issuedAt: number;
230
+ }
231
+ declare function readIssuerEntry(store: Store, issuerNetworkId: Hex32, scope: ConsentScope): Promise<IssuerIndexEntry | null>;
232
+ /** Every entry the wallet holds, for the joins that start from a credential rather than an issuer. */
233
+ declare function listIssuerEntries(store: Store): Promise<(IssuerIndexEntry & {
234
+ issuerNetworkId: Hex32;
235
+ })[]>;
236
+
237
+ export { CREDENTIAL_ISSUE_STEPS, DEVICE_SECRET_KEY, buildConsentSummary, createUserAppService, latestExpiring, listIssuerEntries, mapWalletError, matchingCredentials, readIssuerEntry };
238
+ export type { ConsentDetail, ConsentScope, ConsentSummary, CredentialSelectionRequest, DeviceKey, HeldCredential, HeldCredentialStatus, HttpClient, HttpJsonResponse, IssuerIndexEntry, PresentationReview, PresentationReviewPrompt, PublicIdentity, RevocationAuthority, StealthScanKeys, UserAppPorts, UserAppService, UserConsentPrompt };