@ayni.org/ayni-ui-core 0.1.0 → 0.3.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.
@@ -0,0 +1,350 @@
1
+ import { type TrustResult } from './homeDesign.js';
2
+ import type { Accreditation, CredentialCard, TrustDetails } from './trustDetails.js';
3
+ export declare const PROOF_OF_TRUST_DESIGN: {
4
+ /** the card itself; on a phone it takes the screen, with no border and no radius */
5
+ readonly card: {
6
+ readonly maxWidth: 760;
7
+ readonly radius: 18;
8
+ };
9
+ readonly head: {
10
+ readonly gap: 6;
11
+ readonly paddingY: 12;
12
+ readonly paddingRight: 14;
13
+ readonly paddingLeft: 18;
14
+ readonly dotSize: 10;
15
+ readonly didFontSize: 12;
16
+ readonly toolIconSize: 15;
17
+ readonly closeIconSize: 18;
18
+ };
19
+ readonly body: {
20
+ readonly paddingTop: 18;
21
+ readonly paddingX: 22;
22
+ readonly paddingBottom: 22;
23
+ readonly gap: 18;
24
+ };
25
+ readonly verdict: {
26
+ readonly gap: 10;
27
+ readonly paddingY: 12;
28
+ readonly paddingX: 16;
29
+ readonly radius: 14;
30
+ readonly borderWidth: 2;
31
+ readonly iconSize: 22;
32
+ /** the share of the verdict's colour mixed into the background */
33
+ readonly tint: 0.08;
34
+ readonly labelFontSize: 18;
35
+ readonly labelWeight: 700;
36
+ readonly labelSpacing: 0.06;
37
+ readonly subFontSize: 12;
38
+ };
39
+ readonly reasons: {
40
+ readonly fontSize: 14;
41
+ readonly paddingLeft: 18;
42
+ };
43
+ readonly loading: {
44
+ readonly gap: 8;
45
+ readonly fontSize: 13;
46
+ readonly iconSize: 16;
47
+ };
48
+ /** one identity step: its node on the line, then who it is */
49
+ readonly step: {
50
+ readonly gap: 16;
51
+ readonly paddingBottom: 22;
52
+ readonly bodyGap: 8;
53
+ };
54
+ /** the line that joins a step's node to the next one */
55
+ readonly line: {
56
+ readonly left: 15;
57
+ readonly top: 32;
58
+ readonly width: 2;
59
+ readonly okShare: 0.4;
60
+ };
61
+ readonly node: {
62
+ readonly size: 32;
63
+ readonly borderWidth: 2;
64
+ readonly iconSize: 16;
65
+ };
66
+ readonly kicker: {
67
+ readonly fontSize: 11;
68
+ readonly fontWeight: 700;
69
+ readonly letterSpacing: 0.08;
70
+ readonly paddingTop: 7;
71
+ };
72
+ readonly identity: {
73
+ readonly gap: 14;
74
+ readonly avatarSize: 56;
75
+ readonly avatarRadius: 14;
76
+ readonly avatarFontSize: 18;
77
+ readonly textGap: 4;
78
+ };
79
+ readonly name: {
80
+ readonly fontSize: 20;
81
+ readonly fontWeight: 700;
82
+ readonly gap: 8;
83
+ readonly lineHeight: 1.2;
84
+ readonly iconSize: 18;
85
+ };
86
+ readonly facts: {
87
+ readonly gap: 14;
88
+ readonly columnGap: 2;
89
+ readonly fontSize: 13;
90
+ readonly linkGap: 3;
91
+ readonly linkIconSize: 11;
92
+ };
93
+ readonly provenance: {
94
+ readonly fontSize: 13;
95
+ };
96
+ readonly section: {
97
+ readonly paddingTop: 12;
98
+ readonly titleMarginBottom: 8;
99
+ readonly titleFontSize: 12;
100
+ readonly titleWeight: 700;
101
+ readonly titleSpacing: 0.08;
102
+ readonly titleGap: 8;
103
+ readonly iconSize: 16;
104
+ };
105
+ readonly count: {
106
+ readonly minWidth: 20;
107
+ readonly height: 20;
108
+ readonly paddingX: 6;
109
+ readonly radius: 10;
110
+ readonly fontSize: 11;
111
+ readonly tint: 0.14;
112
+ };
113
+ readonly row: {
114
+ readonly gap: 10;
115
+ readonly paddingY: 7;
116
+ readonly fontSize: 14;
117
+ readonly titleWeight: 600;
118
+ readonly titleGap: 6;
119
+ readonly titleIconSize: 14;
120
+ readonly linkIconSize: 12;
121
+ };
122
+ readonly valid: {
123
+ readonly gap: 4;
124
+ readonly fontSize: 13;
125
+ readonly fontWeight: 600;
126
+ readonly iconSize: 14;
127
+ };
128
+ readonly role: {
129
+ readonly fontSize: 11;
130
+ readonly fontWeight: 700;
131
+ readonly letterSpacing: 0.05;
132
+ readonly paddingY: 2;
133
+ readonly paddingX: 8;
134
+ readonly radius: 6;
135
+ readonly tint: 0.12;
136
+ readonly ecosystemTint: 0.14;
137
+ };
138
+ readonly tag: {
139
+ readonly fontSize: 11;
140
+ readonly fontWeight: 600;
141
+ readonly radius: 6;
142
+ readonly paddingY: 1;
143
+ readonly paddingX: 6;
144
+ readonly tint: 0.12;
145
+ };
146
+ readonly claims: {
147
+ readonly marginTop: 2;
148
+ readonly rowGap: 2;
149
+ readonly columnGap: 12;
150
+ readonly fontSize: 13;
151
+ };
152
+ readonly endpoint: {
153
+ readonly fontSize: 12;
154
+ };
155
+ readonly actions: {
156
+ readonly gap: 10;
157
+ readonly paddingTop: 14;
158
+ };
159
+ };
160
+ export type ProofOfTrustDesign = typeof PROOF_OF_TRUST_DESIGN;
161
+ /** Which colour role paints what on the Proof of Trust. */
162
+ export declare const PROOF_OF_TRUST_COLORS: {
163
+ readonly view: {
164
+ readonly background: "surface";
165
+ };
166
+ readonly card: {
167
+ readonly background: "bg";
168
+ readonly border: "border";
169
+ };
170
+ readonly did: {
171
+ readonly text: "muted";
172
+ };
173
+ readonly verdictSub: {
174
+ readonly text: "muted";
175
+ };
176
+ readonly reasons: {
177
+ readonly text: "danger";
178
+ };
179
+ readonly loading: {
180
+ readonly text: "muted";
181
+ };
182
+ readonly line: {
183
+ readonly ok: "ok";
184
+ readonly rest: "border";
185
+ };
186
+ readonly node: {
187
+ readonly ok: "ok";
188
+ readonly bad: "danger";
189
+ readonly background: "bg";
190
+ };
191
+ readonly kicker: {
192
+ readonly text: "muted";
193
+ };
194
+ readonly nameIcon: {
195
+ readonly color: "muted";
196
+ };
197
+ readonly facts: {
198
+ readonly text: "muted";
199
+ };
200
+ readonly provenance: {
201
+ readonly text: "muted";
202
+ };
203
+ readonly section: {
204
+ readonly border: "border";
205
+ readonly title: "muted";
206
+ };
207
+ readonly count: {
208
+ readonly background: "accent";
209
+ readonly text: "accent";
210
+ };
211
+ readonly valid: {
212
+ readonly ok: "ok";
213
+ readonly bad: "danger";
214
+ };
215
+ readonly role: {
216
+ readonly text: "accent";
217
+ readonly ecosystem: "warning";
218
+ };
219
+ readonly tag: {
220
+ readonly text: "accent";
221
+ };
222
+ readonly claims: {
223
+ readonly key: "muted";
224
+ };
225
+ };
226
+ /** The verdict the card shows: the registry's answer once loaded, the agent's last known result before. */
227
+ export declare function verdictOf(details: Pick<TrustDetails, 'trusted'> | null | undefined, fallback: TrustResult): TrustResult;
228
+ /** The icon and the colour of a verdict: those of the trust mark. */
229
+ export declare const VERDICT_MARK: {
230
+ readonly trusted: {
231
+ readonly icon: "shield-check";
232
+ readonly color: "ok";
233
+ };
234
+ readonly untrusted: {
235
+ readonly icon: "shield-alert";
236
+ readonly color: "danger";
237
+ };
238
+ readonly unverified: {
239
+ readonly icon: "shield-question";
240
+ readonly color: "warning";
241
+ };
242
+ };
243
+ /** A DID short enough for one line: its two ends. */
244
+ export declare function shortDid(did: string): string;
245
+ export declare function schemaLabel(title: string): string;
246
+ /** The DID in the Verana search, from the graph's address. */
247
+ export declare function didSearchUrl(graphUrl: string, did: string): string;
248
+ /**
249
+ * The wording of the card, in English: the browser's translation files hold the same keys (a test compares the
250
+ * English one with this), and the app, which speaks English for now, reads it from here.
251
+ */
252
+ export declare const PROOF_OF_TRUST_COPY: {
253
+ readonly title: "Proof of Trust";
254
+ readonly loading: "Checking the registry";
255
+ readonly copyDid: "Copy the DID";
256
+ readonly openInGraph: "Open in the Verana search";
257
+ readonly evaluated: "Evaluated {{time}}";
258
+ readonly block: "block {{block}}";
259
+ readonly service: "Service";
260
+ readonly operatedBy: "Operated by";
261
+ readonly selfIssued: "Self-issued";
262
+ readonly issuedBy: "Issued by {{name}}";
263
+ readonly issuedByUnknown: "Issuer accredited";
264
+ readonly since: "since {{time}}";
265
+ readonly validUntil: "valid until {{time}}";
266
+ readonly minimumAge: "Minimum age {{age}}";
267
+ readonly registryId: "Registry id";
268
+ readonly jurisdiction: "Jurisdiction";
269
+ readonly alsoPresents: "Also presents";
270
+ readonly verified: "verified";
271
+ readonly invalid: "invalid";
272
+ readonly accreditations: "Accreditations";
273
+ readonly endpoints: "Endpoints in the DID Document";
274
+ readonly governance: "Governance";
275
+ readonly ecosystem: "Ecosystem";
276
+ readonly root: "accredited by the ecosystem";
277
+ readonly egf: "Governance framework";
278
+ readonly cgf: "Corporate governance framework";
279
+ readonly corporation: "Corporation {{id}}";
280
+ readonly via: "via {{chain}}";
281
+ readonly 'role.ECOSYSTEM': "Ecosystem";
282
+ readonly 'role.ISSUER_GRANTOR': "Issuer grantor";
283
+ readonly 'role.VERIFIER_GRANTOR': "Verifier grantor";
284
+ readonly 'role.ISSUER': "Issuer";
285
+ readonly 'role.VERIFIER': "Verifier";
286
+ readonly 'role.HOLDER': "Holder";
287
+ };
288
+ export type ProofOfTrustKey = keyof typeof PROOF_OF_TRUST_COPY;
289
+ /** The wording of a verdict and of the reasons behind one that is not "trusted" (the browser's `trust.*` keys). */
290
+ export declare const TRUST_COPY: {
291
+ readonly trusted: "Trusted";
292
+ readonly untrusted: "Untrusted";
293
+ readonly unverified: "Could not verify";
294
+ readonly 'reasons.not-trusted': "The Verana registry does not vouch for this service.";
295
+ readonly 'reasons.did-not-found': "The DID is unknown to the registry.";
296
+ readonly 'reasons.resolution-unavailable': "The trust resolution service could not be reached.";
297
+ readonly 'reasons.no-service-credential': "No service credential was found.";
298
+ };
299
+ /** A text of the card with its values in place: what the renderer's translator returns. */
300
+ export type ProofOfTrustText = (key: ProofOfTrustKey, values?: Record<string, string | number>) => string;
301
+ /** Fills the `{{name}}` places of a text. */
302
+ export declare function fillText(template: string, values?: Record<string, string | number>): string;
303
+ /** The card's wording in English, for a renderer with no translation files. */
304
+ export declare const proofOfTrustEnglish: ProofOfTrustText;
305
+ /** One identity step as the card draws it: who the credential names, and what it says about them. */
306
+ export interface IdentityStepView {
307
+ kind: 'service' | 'operator';
308
+ valid: boolean;
309
+ /** "Service · ECS-Service" */
310
+ kicker: string;
311
+ name: string;
312
+ logo?: string;
313
+ /** the icon before an operator's name */
314
+ nameIcon?: 'building' | 'user';
315
+ flag?: string;
316
+ countryCode?: string;
317
+ /** a service: its type, as a tag */
318
+ tag?: string;
319
+ description?: string;
320
+ /** a service: the facts on one line; an operator: one per line */
321
+ factsLayout: 'row' | 'column';
322
+ facts: {
323
+ label?: string;
324
+ value: string;
325
+ }[];
326
+ links: {
327
+ label: 'terms' | 'privacy';
328
+ url: string;
329
+ }[];
330
+ /** "Issued by … · ecosystem · since … · valid until …" */
331
+ provenance: string;
332
+ }
333
+ /**
334
+ * An identity credential as its step shows it (UI-TRUST-003). `t` gives the wording, `day` writes a date; the
335
+ * terms and privacy links carry a label the renderer words itself (they are the column's).
336
+ */
337
+ export declare function identityStepOf(kind: 'service' | 'operator', card: CredentialCard, t: ProofOfTrustText, day: (iso: string) => string): IdentityStepView;
338
+ /** The line under another credential the DID presents: its issuer, its ecosystem, its end. */
339
+ export declare function otherCredentialLine(card: CredentialCard, t: ProofOfTrustText, when: (iso: string) => string): string;
340
+ /**
341
+ * The line of an accreditation: its ecosystem, then the grantors between the ecosystem's root and this participant
342
+ * (none means the ecosystem accredited it directly), then its state when it is not active.
343
+ */
344
+ export declare function accreditationLine(a: Accreditation, t: ProofOfTrustText): string;
345
+ /** The claims of a non-identity credential, flat key and value, strings and numbers only. */
346
+ export declare function claimRows(claims: Record<string, unknown>): {
347
+ key: string;
348
+ value: string;
349
+ link: boolean;
350
+ }[];
@@ -0,0 +1,264 @@
1
+ import { flagOf } from './flag.js';
2
+ import { TRUST_MARK } from './homeDesign.js';
3
+ export const PROOF_OF_TRUST_DESIGN = {
4
+ /** the card itself; on a phone it takes the screen, with no border and no radius */
5
+ card: { maxWidth: 760, radius: 18 },
6
+ head: {
7
+ gap: 6,
8
+ paddingY: 12,
9
+ paddingRight: 14,
10
+ paddingLeft: 18,
11
+ dotSize: 10,
12
+ didFontSize: 12,
13
+ toolIconSize: 15,
14
+ closeIconSize: 18,
15
+ },
16
+ body: { paddingTop: 18, paddingX: 22, paddingBottom: 22, gap: 18 },
17
+ verdict: {
18
+ gap: 10,
19
+ paddingY: 12,
20
+ paddingX: 16,
21
+ radius: 14,
22
+ borderWidth: 2,
23
+ iconSize: 22,
24
+ /** the share of the verdict's colour mixed into the background */
25
+ tint: 0.08,
26
+ labelFontSize: 18,
27
+ labelWeight: 700,
28
+ labelSpacing: 0.06,
29
+ subFontSize: 12,
30
+ },
31
+ reasons: { fontSize: 14, paddingLeft: 18 },
32
+ loading: { gap: 8, fontSize: 13, iconSize: 16 },
33
+ /** one identity step: its node on the line, then who it is */
34
+ step: { gap: 16, paddingBottom: 22, bodyGap: 8 },
35
+ /** the line that joins a step's node to the next one */
36
+ line: { left: 15, top: 32, width: 2, okShare: 0.4 },
37
+ node: { size: 32, borderWidth: 2, iconSize: 16 },
38
+ kicker: { fontSize: 11, fontWeight: 700, letterSpacing: 0.08, paddingTop: 7 },
39
+ identity: { gap: 14, avatarSize: 56, avatarRadius: 14, avatarFontSize: 18, textGap: 4 },
40
+ name: { fontSize: 20, fontWeight: 700, gap: 8, lineHeight: 1.2, iconSize: 18 },
41
+ facts: { gap: 14, columnGap: 2, fontSize: 13, linkGap: 3, linkIconSize: 11 },
42
+ provenance: { fontSize: 13 },
43
+ section: {
44
+ paddingTop: 12,
45
+ titleMarginBottom: 8,
46
+ titleFontSize: 12,
47
+ titleWeight: 700,
48
+ titleSpacing: 0.08,
49
+ titleGap: 8,
50
+ iconSize: 16,
51
+ },
52
+ count: { minWidth: 20, height: 20, paddingX: 6, radius: 10, fontSize: 11, tint: 0.14 },
53
+ row: { gap: 10, paddingY: 7, fontSize: 14, titleWeight: 600, titleGap: 6, titleIconSize: 14, linkIconSize: 12 },
54
+ valid: { gap: 4, fontSize: 13, fontWeight: 600, iconSize: 14 },
55
+ role: {
56
+ fontSize: 11,
57
+ fontWeight: 700,
58
+ letterSpacing: 0.05,
59
+ paddingY: 2,
60
+ paddingX: 8,
61
+ radius: 6,
62
+ tint: 0.12,
63
+ ecosystemTint: 0.14,
64
+ },
65
+ tag: { fontSize: 11, fontWeight: 600, radius: 6, paddingY: 1, paddingX: 6, tint: 0.12 },
66
+ claims: { marginTop: 2, rowGap: 2, columnGap: 12, fontSize: 13 },
67
+ endpoint: { fontSize: 12 },
68
+ actions: { gap: 10, paddingTop: 14 },
69
+ };
70
+ /** Which colour role paints what on the Proof of Trust. */
71
+ export const PROOF_OF_TRUST_COLORS = {
72
+ view: { background: 'surface' },
73
+ card: { background: 'bg', border: 'border' },
74
+ did: { text: 'muted' },
75
+ verdictSub: { text: 'muted' },
76
+ reasons: { text: 'danger' },
77
+ loading: { text: 'muted' },
78
+ line: { ok: 'ok', rest: 'border' },
79
+ node: { ok: 'ok', bad: 'danger', background: 'bg' },
80
+ kicker: { text: 'muted' },
81
+ nameIcon: { color: 'muted' },
82
+ facts: { text: 'muted' },
83
+ provenance: { text: 'muted' },
84
+ section: { border: 'border', title: 'muted' },
85
+ count: { background: 'accent', text: 'accent' },
86
+ valid: { ok: 'ok', bad: 'danger' },
87
+ role: { text: 'accent', ecosystem: 'warning' },
88
+ tag: { text: 'accent' },
89
+ claims: { key: 'muted' },
90
+ };
91
+ /** The verdict the card shows: the registry's answer once loaded, the agent's last known result before. */
92
+ export function verdictOf(details, fallback) {
93
+ if (!details)
94
+ return fallback;
95
+ return details.trusted === null ? 'unverified' : details.trusted ? 'trusted' : 'untrusted';
96
+ }
97
+ /** The icon and the colour of a verdict: those of the trust mark. */
98
+ export const VERDICT_MARK = TRUST_MARK;
99
+ /** A DID short enough for one line: its two ends. */
100
+ export function shortDid(did) {
101
+ return did.length > 48 ? `${did.slice(0, 22)}…${did.slice(-20)}` : did;
102
+ }
103
+ /** ECS schema titles as people read them on the wallet cards. */
104
+ const ECS_LABEL = {
105
+ ServiceCredential: 'ECS-Service',
106
+ OrganizationCredential: 'ECS-Organization',
107
+ PersonaCredential: 'ECS-Persona',
108
+ UserAgentCredential: 'ECS-UserAgent',
109
+ };
110
+ export function schemaLabel(title) {
111
+ return ECS_LABEL[title] ?? title;
112
+ }
113
+ /** The DID in the Verana search, from the graph's address. */
114
+ export function didSearchUrl(graphUrl, did) {
115
+ return `${graphUrl.replace(/graph\./, 'search.')}/?q=${encodeURIComponent(did)}`;
116
+ }
117
+ /**
118
+ * The wording of the card, in English: the browser's translation files hold the same keys (a test compares the
119
+ * English one with this), and the app, which speaks English for now, reads it from here.
120
+ */
121
+ export const PROOF_OF_TRUST_COPY = {
122
+ title: 'Proof of Trust',
123
+ loading: 'Checking the registry',
124
+ copyDid: 'Copy the DID',
125
+ openInGraph: 'Open in the Verana search',
126
+ evaluated: 'Evaluated {{time}}',
127
+ block: 'block {{block}}',
128
+ service: 'Service',
129
+ operatedBy: 'Operated by',
130
+ selfIssued: 'Self-issued',
131
+ issuedBy: 'Issued by {{name}}',
132
+ issuedByUnknown: 'Issuer accredited',
133
+ since: 'since {{time}}',
134
+ validUntil: 'valid until {{time}}',
135
+ minimumAge: 'Minimum age {{age}}',
136
+ registryId: 'Registry id',
137
+ jurisdiction: 'Jurisdiction',
138
+ alsoPresents: 'Also presents',
139
+ verified: 'verified',
140
+ invalid: 'invalid',
141
+ accreditations: 'Accreditations',
142
+ endpoints: 'Endpoints in the DID Document',
143
+ governance: 'Governance',
144
+ ecosystem: 'Ecosystem',
145
+ root: 'accredited by the ecosystem',
146
+ egf: 'Governance framework',
147
+ cgf: 'Corporate governance framework',
148
+ corporation: 'Corporation {{id}}',
149
+ via: 'via {{chain}}',
150
+ 'role.ECOSYSTEM': 'Ecosystem',
151
+ 'role.ISSUER_GRANTOR': 'Issuer grantor',
152
+ 'role.VERIFIER_GRANTOR': 'Verifier grantor',
153
+ 'role.ISSUER': 'Issuer',
154
+ 'role.VERIFIER': 'Verifier',
155
+ 'role.HOLDER': 'Holder',
156
+ };
157
+ /** The wording of a verdict and of the reasons behind one that is not "trusted" (the browser's `trust.*` keys). */
158
+ export const TRUST_COPY = {
159
+ trusted: 'Trusted',
160
+ untrusted: 'Untrusted',
161
+ unverified: 'Could not verify',
162
+ 'reasons.not-trusted': 'The Verana registry does not vouch for this service.',
163
+ 'reasons.did-not-found': 'The DID is unknown to the registry.',
164
+ 'reasons.resolution-unavailable': 'The trust resolution service could not be reached.',
165
+ 'reasons.no-service-credential': 'No service credential was found.',
166
+ };
167
+ /** Fills the `{{name}}` places of a text. */
168
+ export function fillText(template, values = {}) {
169
+ return template.replace(/\{\{(\w+)\}\}/g, (_, name) => String(values[name] ?? ''));
170
+ }
171
+ /** The card's wording in English, for a renderer with no translation files. */
172
+ export const proofOfTrustEnglish = (key, values) => fillText(PROOF_OF_TRUST_COPY[key], values);
173
+ const text = (v) => (typeof v === 'string' && v ? v : undefined);
174
+ /**
175
+ * An identity credential as its step shows it (UI-TRUST-003). `t` gives the wording, `day` writes a date; the
176
+ * terms and privacy links carry a label the renderer words itself (they are the column's).
177
+ */
178
+ export function identityStepOf(kind, card, t, day) {
179
+ const c = card.claims;
180
+ const facts = [];
181
+ const links = [];
182
+ if (kind === 'service') {
183
+ if (typeof c.minimumAgeRequired === 'number' && c.minimumAgeRequired > 0)
184
+ facts.push({ value: t('minimumAge', { age: c.minimumAgeRequired }) });
185
+ const terms = text(c.termsAndConditionsUri);
186
+ const privacy = text(c.privacyPolicyUri);
187
+ if (terms)
188
+ links.push({ label: 'terms', url: terms });
189
+ if (privacy)
190
+ links.push({ label: 'privacy', url: privacy });
191
+ }
192
+ else {
193
+ const address = text(c.address);
194
+ if (address)
195
+ facts.push({ value: address });
196
+ if (c.registryId)
197
+ facts.push({ label: t('registryId'), value: String(c.registryId) });
198
+ if (c.lei)
199
+ facts.push({ label: 'LEI', value: String(c.lei) });
200
+ if (c.legalJurisdiction)
201
+ facts.push({ label: t('jurisdiction'), value: String(c.legalJurisdiction) });
202
+ }
203
+ const provenance = [
204
+ card.selfIssued
205
+ ? t('selfIssued')
206
+ : card.issuer.name
207
+ ? t('issuedBy', { name: card.issuer.name })
208
+ : t('issuedByUnknown'),
209
+ card.ecosystem.name,
210
+ card.issuedAt ? t('since', { time: day(card.issuedAt) }) : undefined,
211
+ card.validUntil ? t('validUntil', { time: day(card.validUntil) }) : undefined,
212
+ ]
213
+ .filter(Boolean)
214
+ .join(' · ');
215
+ const countryCode = text(c.countryCode);
216
+ return {
217
+ kind,
218
+ valid: card.valid,
219
+ kicker: `${t(kind === 'service' ? 'service' : 'operatedBy')} · ${schemaLabel(card.schemaTitle)}`,
220
+ name: String(c.name ?? ''),
221
+ logo: text(c.logoUri) ?? text(c.avatarUri),
222
+ nameIcon: kind === 'operator' ? (card.ecsSchema === 'PersonaCredential' ? 'user' : 'building') : undefined,
223
+ flag: flagOf(countryCode) || undefined,
224
+ countryCode,
225
+ tag: kind === 'service' ? text(c.type) : undefined,
226
+ description: kind === 'service' ? text(c.description) : undefined,
227
+ factsLayout: kind === 'service' ? 'row' : 'column',
228
+ facts,
229
+ links,
230
+ provenance,
231
+ };
232
+ }
233
+ /** The line under another credential the DID presents: its issuer, its ecosystem, its end. */
234
+ export function otherCredentialLine(card, t, when) {
235
+ return [
236
+ card.issuer.name ? t('issuedBy', { name: card.issuer.name }) : undefined,
237
+ card.ecosystem.name,
238
+ card.validUntil ? t('validUntil', { time: when(card.validUntil) }) : undefined,
239
+ ]
240
+ .filter(Boolean)
241
+ .join(' · ');
242
+ }
243
+ /**
244
+ * The line of an accreditation: its ecosystem, then the grantors between the ecosystem's root and this participant
245
+ * (none means the ecosystem accredited it directly), then its state when it is not active.
246
+ */
247
+ export function accreditationLine(a, t) {
248
+ const grantors = a.chain.filter((c) => c.participantId !== a.participantId && c.role !== 'ECOSYSTEM');
249
+ return [
250
+ a.ecosystem.name ?? `${t('ecosystem')} ${a.ecosystem.id}`,
251
+ grantors.length
252
+ ? t('via', { chain: grantors.map((c) => c.who.name ?? t(`role.${c.role}`)).join(' → ') })
253
+ : t('root'),
254
+ a.state !== 'ACTIVE' ? a.state.toLowerCase() : undefined,
255
+ ]
256
+ .filter(Boolean)
257
+ .join(' · ');
258
+ }
259
+ /** The claims of a non-identity credential, flat key and value, strings and numbers only. */
260
+ export function claimRows(claims) {
261
+ return Object.entries(claims)
262
+ .filter(([k, v]) => k !== 'id' && !/Sri$/.test(k) && (typeof v === 'string' || typeof v === 'number'))
263
+ .map(([key, v]) => ({ key, value: String(v), link: /^https?:\/\//.test(String(v)) }));
264
+ }
package/dist/reducer.d.ts CHANGED
@@ -4,9 +4,11 @@ export interface Reduction {
4
4
  transcript: Transcript;
5
5
  session: SessionRecord;
6
6
  /** signals the shell reacts to (title, badge, sound) */
7
- signals: ('message' | 'bound' | 'closed' | 'token' | 'approval')[];
7
+ signals: ('message' | 'bound' | 'closed' | 'token' | 'approval' | 'identified')[];
8
8
  }
9
9
  export declare function emptyTranscript(sessionId: string): Transcript;
10
+ /** The five closed states of a flow (flow profile §5.2). */
11
+ export declare function isClosedFlow(state: unknown): boolean;
10
12
  export declare function reduce(transcript: Transcript, session: SessionRecord, event: AgUiEvent, fromRun: boolean, isOnScreen: boolean): Reduction;
11
13
  /** History messages (MessagesSnapshot shape) to transcript messages (UI-CONV-012). */
12
14
  export declare function toTranscriptMessage(m: Record<string, unknown>): TranscriptMessage;
package/dist/reducer.js CHANGED
@@ -7,6 +7,10 @@ export function emptyTranscript(sessionId) {
7
7
  return { sessionId, messages: [], oldestLoadedId: null, complete: false, run: null, routed: {}, reactions: {} };
8
8
  }
9
9
  const now = () => new Date().toISOString();
10
+ /** The five closed states of a flow (flow profile §5.2). */
11
+ export function isClosedFlow(state) {
12
+ return (state === 'completed' || state === 'declined' || state === 'cancelled' || state === 'failed' || state === 'expired');
13
+ }
10
14
  function upsertMessage(messages, message) {
11
15
  const index = messages.findIndex((m) => m.id === message.id);
12
16
  if (index < 0)
@@ -116,17 +120,28 @@ export function reduce(transcript, session, event, fromRun, isOnScreen) {
116
120
  const id = String(event.messageId);
117
121
  // a late artifact replaces its generating placeholder at the initiation position (UI-CONV-006)
118
122
  let messages = t.messages;
123
+ let routed = t.routed;
124
+ // a flow card sits at the flow's open time and is replaced in place at its close (UI-CONV-021, PROF-MAP-007);
125
+ // once closed it takes over from the "continue on your phone" card of the same flow (UI-CAP-005)
126
+ const flowAt = activityType === ACTIVITY.flow && typeof content.openedAt === 'string' ? content.openedAt : at;
127
+ if (activityType === ACTIVITY.flow && content.flowId && isClosedFlow(content.state)) {
128
+ routed = Object.fromEntries(Object.entries(routed).filter(([, r]) => r.flowId !== content.flowId));
129
+ // a credential issued in this conversation identifies its controller on the rail (D37)
130
+ if (content.kind === 'issue' && content.state === 'completed')
131
+ signals.push('identified');
132
+ }
119
133
  if (activityType === ACTIVITY.attachment && content.entryId) {
120
134
  messages = messages.filter((m) => !(m.role === 'activity' && m.activityType === ACTIVITY.generating && m.content.entryId === content.entryId));
121
135
  }
122
136
  t = {
123
137
  ...t,
138
+ routed,
124
139
  messages: upsertMessage(messages, {
125
140
  id,
126
141
  role: 'activity',
127
142
  activityType,
128
143
  content,
129
- at,
144
+ at: flowAt,
130
145
  entryId: content.entryId,
131
146
  }),
132
147
  };
@@ -200,6 +215,7 @@ export function reduce(transcript, session, event, fromRun, isOnScreen) {
200
215
  status: String(value.status),
201
216
  message: value.message,
202
217
  at,
218
+ flowId: typeof value.flowId === 'string' ? value.flowId : undefined,
203
219
  },
204
220
  },
205
221
  };