@learncard/network-plugin 2.13.16 → 2.14.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/dist/lcn-plugin.cjs.development.cjs +355 -17
- package/dist/lcn-plugin.cjs.development.cjs.map +3 -3
- package/dist/lcn-plugin.cjs.production.min.cjs +34 -34
- package/dist/lcn-plugin.cjs.production.min.cjs.map +3 -3
- package/dist/lcn-plugin.esm.js +355 -17
- package/dist/lcn-plugin.esm.js.map +3 -3
- package/dist/plugin.d.ts +26 -6
- package/dist/plugin.d.ts.map +1 -1
- package/dist/types.d.ts +57 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/plugin.ts +304 -32
- package/src/test/index.test.ts +438 -4
- package/src/types.ts +66 -0
package/src/plugin.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
VC,
|
|
15
15
|
BitstringCredentialStatusEntry,
|
|
16
16
|
BitstringCredentialStatusPurpose,
|
|
17
|
+
ManagedCredentialRefreshService,
|
|
17
18
|
StoredCredentialEnvelope,
|
|
18
19
|
StoredCredentialEnvelopeValidator,
|
|
19
20
|
isStoredCredentialEnvelope,
|
|
@@ -34,6 +35,135 @@ import {
|
|
|
34
35
|
TrustedBoostRegistryEntry,
|
|
35
36
|
} from './types';
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Trusted LearnCard domain suffixes for federation.
|
|
40
|
+
* Only dotted suffixes - the isLearnCardDomain check handles apex domains via
|
|
41
|
+
* `host === suffix.replace(/^\./, '')`. Bare entries would allow bypass
|
|
42
|
+
* (e.g., 'evillearncard.com'.endsWith('learncard.com') === true).
|
|
43
|
+
*/
|
|
44
|
+
const LEARNCARD_DOMAIN_SUFFIXES = ['.learncard.com', '.learncard.app', '.learncard.ai'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for federation URL validation.
|
|
48
|
+
*/
|
|
49
|
+
export interface FederationConfig {
|
|
50
|
+
/**
|
|
51
|
+
* Explicit list of trusted hosts. If not provided, federation is open (any HTTPS host allowed).
|
|
52
|
+
* This is safe for client-side code where SSRF doesn't apply.
|
|
53
|
+
* Server-side code should provide an explicit allowlist.
|
|
54
|
+
*/
|
|
55
|
+
trustedFederationHosts?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Allow localhost/127.0.0.1 for development.
|
|
58
|
+
* Auto-detected: defaults to true when serviceUrl is localhost, false otherwise.
|
|
59
|
+
*/
|
|
60
|
+
allowLocalhostFederation?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The origin of this plugin's service URL. When set, endpoints matching this origin
|
|
63
|
+
* skip validation (they're self-generated, not from external DID documents).
|
|
64
|
+
*/
|
|
65
|
+
serviceOrigin?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a hostname matches any LearnCard domain suffix.
|
|
70
|
+
*/
|
|
71
|
+
const isLearnCardDomain = (host: string): boolean => {
|
|
72
|
+
return LEARNCARD_DOMAIN_SUFFIXES.some(
|
|
73
|
+
suffix => host === suffix.replace(/^\./, '') || host.endsWith(suffix)
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks if a hostname is localhost or loopback.
|
|
79
|
+
*/
|
|
80
|
+
const isLocalhostHost = (host: string): boolean => {
|
|
81
|
+
const lower = host.toLowerCase();
|
|
82
|
+
return lower === 'localhost' || lower === '127.0.0.1';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validates and normalizes a federation URL.
|
|
87
|
+
*
|
|
88
|
+
* This runs in the CLIENT plugin where SSRF risk is lower (the browser's same-origin
|
|
89
|
+
* policy provides protection). By default, federation is open to any HTTPS host.
|
|
90
|
+
* Server-side code should provide an explicit trustedFederationHosts list.
|
|
91
|
+
*
|
|
92
|
+
* Note: For local-service recipients, getInboxEndpointForDid returns a fixed
|
|
93
|
+
* `/api/inbox/receive` path. Only external DID doc serviceEndpoints preserve
|
|
94
|
+
* their original path.
|
|
95
|
+
*
|
|
96
|
+
* @param userProvidedUrl - The inbox endpoint URL (from DID doc or local service)
|
|
97
|
+
* @param config - Federation configuration with trusted hosts
|
|
98
|
+
* @returns The validated URL
|
|
99
|
+
* @throws Error if the URL is invalid or host is not trusted
|
|
100
|
+
*/
|
|
101
|
+
const validateFederationUrl = (userProvidedUrl: string, config: FederationConfig): string => {
|
|
102
|
+
let parsed: URL;
|
|
103
|
+
try {
|
|
104
|
+
parsed = new URL(userProvidedUrl);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(`Invalid federation endpoint URL: ${userProvidedUrl}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If this endpoint matches our own service origin, skip validation entirely.
|
|
110
|
+
// These URLs are self-generated (e.g., from getInboxEndpointForDid when the recipient
|
|
111
|
+
// resolves to our local service), not from external DID documents.
|
|
112
|
+
if (config.serviceOrigin) {
|
|
113
|
+
const endpointOrigin = parsed.origin;
|
|
114
|
+
if (endpointOrigin === config.serviceOrigin) {
|
|
115
|
+
// Self-referential URL - return parsed.href so CodeQL sees URL sanitization
|
|
116
|
+
return parsed.href;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const host = parsed.hostname.toLowerCase();
|
|
121
|
+
const isLocalhost = isLocalhostHost(host);
|
|
122
|
+
|
|
123
|
+
// Handle localhost: check allowLocalhostFederation and convert https->http
|
|
124
|
+
if (isLocalhost) {
|
|
125
|
+
if (!config.allowLocalhostFederation) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Localhost federation is disabled. Set allowLocalhostFederation: true for development.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
// Local dev typically doesn't have TLS - convert https to http
|
|
131
|
+
if (parsed.protocol === 'https:') {
|
|
132
|
+
parsed.protocol = 'http:';
|
|
133
|
+
}
|
|
134
|
+
// Return the validated/normalized URL
|
|
135
|
+
return parsed.href;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Validate protocol - require HTTPS for non-localhost
|
|
139
|
+
if (parsed.protocol !== 'https:') {
|
|
140
|
+
throw new Error(`Federation requires HTTPS for non-localhost hosts: ${userProvidedUrl}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If trustedFederationHosts is provided, use explicit allowlist mode
|
|
144
|
+
if (config.trustedFederationHosts !== undefined) {
|
|
145
|
+
const trustedHosts = new Set(config.trustedFederationHosts);
|
|
146
|
+
const allowAnyHost = trustedHosts.has('*');
|
|
147
|
+
|
|
148
|
+
// Check explicit list, LearnCard domains, or wildcard.
|
|
149
|
+
// Note: LearnCard domains are always trusted even with an explicit allowlist -
|
|
150
|
+
// this is intentional to ensure core federation always works.
|
|
151
|
+
if (!allowAnyHost && !isLearnCardDomain(host) && !trustedHosts.has(host)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Federation host '${host}' is not trusted. ` +
|
|
154
|
+
`Add it to trustedFederationHosts or use '*' to allow any host.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// If trustedFederationHosts is not provided, allow any HTTPS host (client-side default)
|
|
159
|
+
// This enables third-party did:web federation without requiring explicit opt-in
|
|
160
|
+
|
|
161
|
+
// Return the validated URL - CodeQL: at this point the URL has been validated
|
|
162
|
+
// as either matching our service origin, being localhost with permission, or
|
|
163
|
+
// being HTTPS with a trusted/allowed host
|
|
164
|
+
return parsed.href;
|
|
165
|
+
};
|
|
166
|
+
|
|
37
167
|
const uint8ArrayToBase64Url = (bytes: Uint8Array): string => {
|
|
38
168
|
let binary = '';
|
|
39
169
|
for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]!);
|
|
@@ -312,6 +442,52 @@ const issueCredentialWithNetworkStatus = async (
|
|
|
312
442
|
);
|
|
313
443
|
};
|
|
314
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Inline JSON-LD context fragment required to sign credentials carrying a
|
|
447
|
+
* LearnCard-managed `LearnCardCredentialRefresh2026` refresh service.
|
|
448
|
+
*
|
|
449
|
+
* Neither VCDM 1.1 (which defines only `ManualRefreshService2018`), VCDM 2.0, nor the
|
|
450
|
+
* live OBv3 contexts define the term `LearnCardCredentialRefresh2026` (nor the LearnCard
|
|
451
|
+
* `authorization` / `LearnCardDIDAuth` extension terms), so DIDKit's data-loss
|
|
452
|
+
* detection refuses to sign unless issuers define these terms inline.
|
|
453
|
+
*/
|
|
454
|
+
const MANAGED_REFRESH_SERVICE_CONTEXT = {
|
|
455
|
+
'LearnCardCredentialRefresh2026':
|
|
456
|
+
'https://learncard.com/refresh#LearnCardCredentialRefresh2026',
|
|
457
|
+
authorization: {
|
|
458
|
+
'@id': 'https://purl.imsglobal.org/spec/ob/v3p0#authorization',
|
|
459
|
+
'@context': {
|
|
460
|
+
LearnCardDIDAuth: 'https://docs.learncard.com/definitions#LearnCardDIDAuth',
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Injects an allocated managed refresh service into an unsigned credential so the
|
|
467
|
+
* service becomes part of the signed payload. Appends the inline context fragment
|
|
468
|
+
* unless an equivalent mapping is already present.
|
|
469
|
+
*/
|
|
470
|
+
const injectManagedRefreshService = (
|
|
471
|
+
credential: UnsignedVC,
|
|
472
|
+
refreshService: ManagedCredentialRefreshService
|
|
473
|
+
): UnsignedVC => {
|
|
474
|
+
const existingContext = (credential as Record<string, unknown>)['@context'];
|
|
475
|
+
const contextList = Array.isArray(existingContext) ? existingContext : [existingContext];
|
|
476
|
+
|
|
477
|
+
const hasMapping = contextList.some(
|
|
478
|
+
entry =>
|
|
479
|
+
!!entry &&
|
|
480
|
+
typeof entry === 'object' &&
|
|
481
|
+
'LearnCardCredentialRefresh2026' in (entry as Record<string, unknown>)
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
...credential,
|
|
486
|
+
'@context': hasMapping ? contextList : [...contextList, MANAGED_REFRESH_SERVICE_CONTEXT],
|
|
487
|
+
refreshService,
|
|
488
|
+
} as UnsignedVC;
|
|
489
|
+
};
|
|
490
|
+
|
|
315
491
|
export * from './types';
|
|
316
492
|
|
|
317
493
|
export type GuardianApprovalGetter = () => string | undefined | Promise<string | undefined>;
|
|
@@ -319,46 +495,52 @@ export type GuardianApprovalGetter = () => string | undefined | Promise<string |
|
|
|
319
495
|
/**
|
|
320
496
|
* @group Plugins
|
|
321
497
|
*/
|
|
498
|
+
/** Options for the LearnCard Network Plugin */
|
|
499
|
+
export interface LearnCardNetworkPluginOptions extends FederationConfig {
|
|
500
|
+
guardianApprovalGetter?: GuardianApprovalGetter;
|
|
501
|
+
extraHeaders?: Record<string, string>;
|
|
502
|
+
}
|
|
503
|
+
|
|
322
504
|
export async function getLearnCardNetworkPlugin(
|
|
323
505
|
learnCard: LearnCard<any, 'id', LearnCardNetworkPluginDependentMethods>,
|
|
324
506
|
url: string,
|
|
325
|
-
apiTokenOrOptions?:
|
|
326
|
-
guardianApprovalGetter?: GuardianApprovalGetter;
|
|
327
|
-
extraHeaders?: Record<string, string>;
|
|
328
|
-
}
|
|
507
|
+
apiTokenOrOptions?: LearnCardNetworkPluginOptions
|
|
329
508
|
): Promise<LearnCardNetworkPlugin>;
|
|
330
509
|
export async function getLearnCardNetworkPlugin(
|
|
331
510
|
learnCard: LearnCard<any, any, LearnCardNetworkPluginDependentMethods>,
|
|
332
511
|
url: string,
|
|
333
512
|
apiToken: string,
|
|
334
|
-
options?:
|
|
335
|
-
guardianApprovalGetter?: GuardianApprovalGetter;
|
|
336
|
-
extraHeaders?: Record<string, string>;
|
|
337
|
-
}
|
|
513
|
+
options?: LearnCardNetworkPluginOptions
|
|
338
514
|
): Promise<LearnCardNetworkPlugin>;
|
|
339
515
|
export async function getLearnCardNetworkPlugin(
|
|
340
516
|
learnCard: LearnCard<any, any, LearnCardNetworkPluginDependentMethods>,
|
|
341
517
|
url: string,
|
|
342
|
-
apiTokenOrOptions?:
|
|
343
|
-
|
|
344
|
-
| {
|
|
345
|
-
guardianApprovalGetter?: GuardianApprovalGetter;
|
|
346
|
-
extraHeaders?: Record<string, string>;
|
|
347
|
-
},
|
|
348
|
-
options?: {
|
|
349
|
-
guardianApprovalGetter?: GuardianApprovalGetter;
|
|
350
|
-
extraHeaders?: Record<string, string>;
|
|
351
|
-
}
|
|
518
|
+
apiTokenOrOptions?: string | LearnCardNetworkPluginOptions,
|
|
519
|
+
options?: LearnCardNetworkPluginOptions
|
|
352
520
|
): Promise<LearnCardNetworkPlugin> {
|
|
353
521
|
const apiToken = typeof apiTokenOrOptions === 'string' ? apiTokenOrOptions : undefined;
|
|
354
|
-
const
|
|
355
|
-
(typeof apiTokenOrOptions === 'object'
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
522
|
+
const resolvedOptions: LearnCardNetworkPluginOptions =
|
|
523
|
+
(typeof apiTokenOrOptions === 'object' ? apiTokenOrOptions : options) ?? {};
|
|
524
|
+
|
|
525
|
+
const {
|
|
526
|
+
guardianApprovalGetter,
|
|
527
|
+
extraHeaders,
|
|
528
|
+
trustedFederationHosts,
|
|
529
|
+
allowLocalhostFederation,
|
|
530
|
+
} = resolvedOptions;
|
|
531
|
+
|
|
532
|
+
// Parse service URL to determine origin and auto-detect localhost
|
|
533
|
+
const serviceUrl = new URL(url);
|
|
534
|
+
const serviceIsLocalhost = isLocalhostHost(serviceUrl.hostname);
|
|
535
|
+
|
|
536
|
+
// Federation config for URL validation
|
|
537
|
+
// - serviceOrigin: skip validation for self-referential URLs
|
|
538
|
+
// - allowLocalhostFederation: auto-detect from service URL if not explicitly set
|
|
539
|
+
const federationConfig: FederationConfig = {
|
|
540
|
+
trustedFederationHosts,
|
|
541
|
+
allowLocalhostFederation: allowLocalhostFederation ?? serviceIsLocalhost,
|
|
542
|
+
serviceOrigin: serviceUrl.origin,
|
|
543
|
+
};
|
|
362
544
|
// Initialize DID safely: in API-key mode there may be no local ID plane provider
|
|
363
545
|
let did = '';
|
|
364
546
|
try {
|
|
@@ -713,7 +895,11 @@ export async function getLearnCardNetworkPlugin(
|
|
|
713
895
|
return client.profileManager.updateProfileManager.mutate(manager);
|
|
714
896
|
},
|
|
715
897
|
deleteProfile: async () => {
|
|
716
|
-
|
|
898
|
+
// Settle initialization before deletion so it cannot repopulate userData later.
|
|
899
|
+
await initialQuery;
|
|
900
|
+
// API tokens may grant profiles:delete without profiles:read. The server
|
|
901
|
+
// validates the profile and scope; a local read must not gate deletion.
|
|
902
|
+
if (!apiToken) await ensureUser();
|
|
717
903
|
|
|
718
904
|
const result = await client.profile.deleteProfile.mutate();
|
|
719
905
|
|
|
@@ -910,13 +1096,14 @@ export async function getLearnCardNetworkPlugin(
|
|
|
910
1096
|
challenge: `inbox-federation-${crypto.randomUUID()}`,
|
|
911
1097
|
});
|
|
912
1098
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
1099
|
+
// Validate the federation URL. For external DIDs, inboxEndpoint comes
|
|
1100
|
+
// from the DID doc's serviceEndpoint; for local-service DIDs, it's
|
|
1101
|
+
// a fixed /api/inbox/receive path from getInboxEndpointForDid.
|
|
1102
|
+
const receiveUrl = validateFederationUrl(inboxEndpoint, federationConfig);
|
|
918
1103
|
|
|
919
1104
|
const response = await fetch(receiveUrl, {
|
|
1105
|
+
// A redirect must not bypass endpoint validation or forward credentials.
|
|
1106
|
+
redirect: 'error',
|
|
920
1107
|
method: 'POST',
|
|
921
1108
|
headers: {
|
|
922
1109
|
'Content-Type': 'application/json',
|
|
@@ -1369,6 +1556,37 @@ export async function getLearnCardNetworkPlugin(
|
|
|
1369
1556
|
|
|
1370
1557
|
return client.boost.allocateCredentialStatus.mutate(options);
|
|
1371
1558
|
},
|
|
1559
|
+
allocateCredentialRefresh: async (_learnCard, input) => {
|
|
1560
|
+
await ensureUser();
|
|
1561
|
+
|
|
1562
|
+
return client.credentialRefresh.allocateCredentialRefresh.mutate(input);
|
|
1563
|
+
},
|
|
1564
|
+
sendRefreshableCredential: async (
|
|
1565
|
+
_learnCard,
|
|
1566
|
+
refreshId,
|
|
1567
|
+
credential,
|
|
1568
|
+
boostUri,
|
|
1569
|
+
skipNotification
|
|
1570
|
+
) => {
|
|
1571
|
+
await ensureUser();
|
|
1572
|
+
|
|
1573
|
+
return client.credentialRefresh.sendRefreshableCredential.mutate({
|
|
1574
|
+
refreshId,
|
|
1575
|
+
credential,
|
|
1576
|
+
boostUri,
|
|
1577
|
+
...(skipNotification ? { skipNotification: true } : {}),
|
|
1578
|
+
});
|
|
1579
|
+
},
|
|
1580
|
+
publishCredentialRefresh: async (_learnCard, input) => {
|
|
1581
|
+
await ensureUser();
|
|
1582
|
+
|
|
1583
|
+
return client.credentialRefresh.publishCredentialRefresh.mutate(input);
|
|
1584
|
+
},
|
|
1585
|
+
getCredentialRefreshHistory: async (_learnCard, input) => {
|
|
1586
|
+
await ensureUser();
|
|
1587
|
+
|
|
1588
|
+
return client.credentialRefresh.getCredentialRefreshHistory.query(input);
|
|
1589
|
+
},
|
|
1372
1590
|
revokeBoostRecipient: async (
|
|
1373
1591
|
_learnCard,
|
|
1374
1592
|
boostUri,
|
|
@@ -1483,6 +1701,27 @@ export async function getLearnCardNetworkPlugin(
|
|
|
1483
1701
|
boost = options.overideFn(boost);
|
|
1484
1702
|
}
|
|
1485
1703
|
|
|
1704
|
+
const enableRefresh = typeof options === 'object' && options.enableRefresh === true;
|
|
1705
|
+
|
|
1706
|
+
let managedRefreshId: string | undefined;
|
|
1707
|
+
|
|
1708
|
+
if (enableRefresh) {
|
|
1709
|
+
// Generate a stable UUID credential ID when the template has none;
|
|
1710
|
+
// the allocation is permanently bound to this ID.
|
|
1711
|
+
if (!boost.id) boost.id = `urn:uuid:${crypto.randomUUID()}`;
|
|
1712
|
+
|
|
1713
|
+
// Allocate BEFORE signing: the refresh service must be part of the
|
|
1714
|
+
// signed payload, so it is injected before proof creation.
|
|
1715
|
+
const allocation =
|
|
1716
|
+
await client.credentialRefresh.allocateCredentialRefresh.mutate({
|
|
1717
|
+
holder: { profileId, did: targetProfile.did },
|
|
1718
|
+
credentialId: boost.id,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
managedRefreshId = allocation.refreshId;
|
|
1722
|
+
boost = injectManagedRefreshService(boost, allocation.refreshService);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1486
1725
|
const statusPurposes =
|
|
1487
1726
|
typeof options === 'object' ? options.statusPurposes : undefined;
|
|
1488
1727
|
const vc = await issueCredentialWithNetworkStatus(
|
|
@@ -1492,6 +1731,22 @@ export async function getLearnCardNetworkPlugin(
|
|
|
1492
1731
|
statusPurposes
|
|
1493
1732
|
);
|
|
1494
1733
|
|
|
1734
|
+
if (managedRefreshId) {
|
|
1735
|
+
// Dedicated managed send: brain-service verifies the proof and
|
|
1736
|
+
// persists ONLY a holder-encrypted JWE. Legacy credential storage
|
|
1737
|
+
// (issuer/LCN-readable JWE or plaintext) is intentionally bypassed.
|
|
1738
|
+
// The boost URI is forwarded so the credential stays linked
|
|
1739
|
+
// INSTANCE_OF the boost for canonical recipient management.
|
|
1740
|
+
return client.credentialRefresh.sendRefreshableCredential.mutate({
|
|
1741
|
+
refreshId: managedRefreshId,
|
|
1742
|
+
credential: vc,
|
|
1743
|
+
boostUri,
|
|
1744
|
+
...(typeof options === 'object' && options.skipNotification
|
|
1745
|
+
? { skipNotification: true }
|
|
1746
|
+
: {}),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1495
1750
|
// options is allowed to be a boolean to maintain backwards compatibility
|
|
1496
1751
|
if ((typeof options === 'object' && !options.encrypt) || !options) {
|
|
1497
1752
|
return client.boost.sendBoost.mutate({
|
|
@@ -2153,6 +2408,23 @@ export async function getLearnCardNetworkPlugin(
|
|
|
2153
2408
|
|
|
2154
2409
|
return client.inbox.finalize.mutate();
|
|
2155
2410
|
},
|
|
2411
|
+
recoverInboxCredentials: async (learnCard, options = {}) => {
|
|
2412
|
+
const result = await client.inbox.getMyInboxDeliveries.query(options);
|
|
2413
|
+
const results = await Promise.allSettled(
|
|
2414
|
+
result.records.map(async record => ({
|
|
2415
|
+
...record,
|
|
2416
|
+
credential: VCValidator.parse(
|
|
2417
|
+
await learnCard.invoke.decryptDagJwe(record.credential, [
|
|
2418
|
+
learnCard.id.keypair(),
|
|
2419
|
+
])
|
|
2420
|
+
),
|
|
2421
|
+
}))
|
|
2422
|
+
);
|
|
2423
|
+
const records = results.flatMap(record =>
|
|
2424
|
+
record.status === 'fulfilled' ? [record.value] : []
|
|
2425
|
+
);
|
|
2426
|
+
return { ...result, records, failed: results.length - records.length };
|
|
2427
|
+
},
|
|
2156
2428
|
sendGuardianApprovalEmail: async (_learnCard, options) => {
|
|
2157
2429
|
await ensureUser();
|
|
2158
2430
|
|