@learncard/network-plugin 2.13.17 → 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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any -- plugin integration mocks intentionally expose dynamic method bags */
1
2
  import { generateLearnCard } from '@learncard/core';
2
3
  import { getDidKitPlugin } from '@learncard/didkit-plugin';
3
4
  import { getDidKeyPlugin } from '@learncard/didkey-plugin';
@@ -16,8 +17,30 @@ vi.mock('@learncard/core', () => ({ generateLearnCard: vi.fn() }));
16
17
  vi.mock('@learncard/didkit-plugin', () => ({ getDidKitPlugin: vi.fn() }));
17
18
  vi.mock('@learncard/didkey-plugin', () => ({ getDidKeyPlugin: vi.fn() }));
18
19
  vi.mock('@learncard/vc-plugin', () => ({ getVCPlugin: vi.fn() }));
19
- vi.mock('@learncard/helpers', () => ({}));
20
- vi.mock('@learncard/types', () => ({}));
20
+ vi.mock('@learncard/helpers', () => ({
21
+ isVC2Format: (credential: any) => {
22
+ const contexts = credential?.['@context'];
23
+ const list = Array.isArray(contexts) ? contexts : [contexts];
24
+
25
+ return list.includes('https://www.w3.org/ns/credentials/v2');
26
+ },
27
+ getCredentialStatusArray: () => [],
28
+ resolveStorageReadResult: (value: any) => value,
29
+ }));
30
+ vi.mock('@learncard/types', () => ({
31
+ VCValidator: {
32
+ parse: (value: any) => {
33
+ if (!value?.type) throw new Error('Invalid credential');
34
+ return value;
35
+ },
36
+ },
37
+ UnsignedVCValidator: {
38
+ spa: async (value: any) => ({
39
+ success: true,
40
+ data: typeof value === 'string' ? JSON.parse(value) : value,
41
+ }),
42
+ },
43
+ }));
21
44
 
22
45
  const PROFILE = {
23
46
  profileId: 'usera',
@@ -34,12 +57,33 @@ const PROFILE = {
34
57
 
35
58
  const PROMPT_ID = '00000000-0000-4000-8000-000000000001';
36
59
 
60
+ const REFRESH_ID = 'cnVsbGFiZWxseXVuZ3Vlc3NhYmxlcmVmcmVzaGlkMTIzNDU2Nzg';
61
+ const TARGET_DID = 'did:web:network.example:users:userb';
62
+
63
+ const ALLOCATION = {
64
+ refreshId: REFRESH_ID,
65
+ refreshService: {
66
+ id: `https://network.example/refresh/${REFRESH_ID}`,
67
+ type: 'LearnCardCredentialRefresh2026',
68
+ authorization: { type: 'LearnCardDIDAuth' },
69
+ },
70
+ };
71
+
72
+ const getUnsignedBoostTemplate = (overrides: Record<string, unknown> = {}) => ({
73
+ '@context': ['https://www.w3.org/ns/credentials/v2'],
74
+ type: ['VerifiableCredential', 'BoostCredential'],
75
+ issuer: 'did:example:template-issuer',
76
+ validFrom: '2020-01-01T00:00:00.000Z',
77
+ credentialSubject: { id: 'did:example:template-subject' },
78
+ ...overrides,
79
+ });
80
+
37
81
  const getMockLearnCard = () =>
38
82
  ({
39
83
  id: { did: () => PROFILE.did },
40
84
  invoke: { getDidAuthVp: vi.fn() },
41
85
  debug: vi.fn(),
42
- } as any);
86
+ }) as any;
43
87
 
44
88
  const getMockClient = (profile: typeof PROFILE | null = PROFILE) => ({
45
89
  profile: {
@@ -67,9 +111,59 @@ const getMockClient = (profile: typeof PROFILE | null = PROFILE) => ({
67
111
  mutate: vi.fn().mockResolvedValue({ promptId: PROMPT_ID, status: 'CONNECTED' }),
68
112
  },
69
113
  },
114
+ boost: {
115
+ getBoost: { query: vi.fn().mockResolvedValue({ boost: getUnsignedBoostTemplate() }) },
116
+ sendBoost: { mutate: vi.fn().mockResolvedValue('boost-credential-uri') },
117
+ allocateCredentialStatus: { mutate: vi.fn().mockResolvedValue([]) },
118
+ },
119
+ utilities: {
120
+ getDid: { query: vi.fn().mockResolvedValue('did:web:network.example') },
121
+ },
122
+ credentialRefresh: {
123
+ allocateCredentialRefresh: { mutate: vi.fn().mockResolvedValue(ALLOCATION) },
124
+ sendRefreshableCredential: { mutate: vi.fn().mockResolvedValue('managed-credential-uri') },
125
+ publishCredentialRefresh: {
126
+ mutate: vi.fn().mockResolvedValue({
127
+ refreshId: REFRESH_ID,
128
+ version: 2,
129
+ publishedAt: '2026-09-02T00:00:00.000Z',
130
+ notification: 'queued',
131
+ }),
132
+ },
133
+ getCredentialRefreshHistory: {
134
+ query: vi.fn().mockResolvedValue({
135
+ records: [
136
+ { version: 2, publishedAt: '2026-09-02T00:00:00.000Z' },
137
+ { version: 1, publishedAt: '2026-09-01T00:00:00.000Z' },
138
+ ],
139
+ hasMore: false,
140
+ }),
141
+ },
142
+ },
70
143
  });
71
144
 
72
- let learnCards: Record<string, { learnCard: any }> = {};
145
+ const getMockIssuingLearnCard = () => {
146
+ const issuedCredentials: any[] = [];
147
+
148
+ const learnCard = {
149
+ id: { did: () => PROFILE.did },
150
+ invoke: {
151
+ getDidAuthVp: vi.fn(),
152
+ getProfile: vi.fn().mockResolvedValue({ profileId: 'userb', did: TARGET_DID }),
153
+ issueCredential: vi.fn(async (credential: any) => {
154
+ issuedCredentials.push(credential);
155
+
156
+ return { ...credential, proof: { type: 'DataIntegrityProof' } };
157
+ }),
158
+ createDagJwe: vi.fn().mockResolvedValue({ ciphertext: 'encrypted' }),
159
+ },
160
+ debug: vi.fn(),
161
+ } as any;
162
+
163
+ return { learnCard, issuedCredentials };
164
+ };
165
+
166
+ const learnCards: Record<string, { learnCard: any }> = {};
73
167
 
74
168
  const getLearnCard = async (seed = 'a'.repeat(64)) => {
75
169
  if (!learnCards[seed]) {
@@ -152,6 +246,313 @@ describe('connection prompt methods', () => {
152
246
  });
153
247
  });
154
248
 
249
+ describe('credential refresh methods', () => {
250
+ beforeEach(() => {
251
+ vi.clearAllMocks();
252
+ });
253
+
254
+ it('forwards allocateCredentialRefresh input to the credentialRefresh procedure', async () => {
255
+ const client = getMockClient();
256
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
257
+ const learnCard = getMockLearnCard();
258
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
259
+
260
+ const input = {
261
+ holder: { profileId: 'userb', did: TARGET_DID },
262
+ credentialId: 'urn:uuid:credential-1',
263
+ };
264
+
265
+ await expect(plugin.methods?.allocateCredentialRefresh(learnCard, input)).resolves.toEqual(
266
+ ALLOCATION
267
+ );
268
+ expect(client.credentialRefresh.allocateCredentialRefresh.mutate).toHaveBeenCalledWith(
269
+ input
270
+ );
271
+ });
272
+
273
+ it('forwards sendRefreshableCredential to the managed send procedure', async () => {
274
+ const client = getMockClient();
275
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
276
+ const learnCard = getMockLearnCard();
277
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
278
+
279
+ const signedCredential = {
280
+ ...getUnsignedBoostTemplate({ id: 'urn:uuid:credential-1' }),
281
+ proof: { type: 'DataIntegrityProof' },
282
+ };
283
+
284
+ await expect(
285
+ plugin.methods?.sendRefreshableCredential(
286
+ learnCard,
287
+ REFRESH_ID,
288
+ signedCredential as any
289
+ )
290
+ ).resolves.toEqual('managed-credential-uri');
291
+ expect(client.credentialRefresh.sendRefreshableCredential.mutate).toHaveBeenCalledWith({
292
+ refreshId: REFRESH_ID,
293
+ credential: signedCredential,
294
+ boostUri: undefined,
295
+ });
296
+ });
297
+
298
+ it('forwards optional boostUri and notification suppression to the managed send procedure', async () => {
299
+ const client = getMockClient();
300
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
301
+ const learnCard = getMockLearnCard();
302
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
303
+
304
+ const signedCredential = {
305
+ ...getUnsignedBoostTemplate({ id: 'urn:uuid:credential-1' }),
306
+ proof: { type: 'DataIntegrityProof' },
307
+ };
308
+
309
+ await plugin.methods?.sendRefreshableCredential(
310
+ learnCard,
311
+ REFRESH_ID,
312
+ signedCredential as any,
313
+ 'did:example:boost:9',
314
+ true
315
+ );
316
+
317
+ expect(client.credentialRefresh.sendRefreshableCredential.mutate).toHaveBeenCalledWith({
318
+ refreshId: REFRESH_ID,
319
+ credential: signedCredential,
320
+ boostUri: 'did:example:boost:9',
321
+ skipNotification: true,
322
+ });
323
+ });
324
+
325
+ it.each([
326
+ [
327
+ 'issuer-signed',
328
+ {
329
+ mode: 'issuer-signed',
330
+ refreshId: REFRESH_ID,
331
+ signedCredential: {
332
+ ...getUnsignedBoostTemplate({ id: 'urn:uuid:credential-1' }),
333
+ proof: { type: 'DataIntegrityProof' },
334
+ },
335
+ },
336
+ ],
337
+ [
338
+ 'signing-authority',
339
+ {
340
+ mode: 'signing-authority',
341
+ refreshId: REFRESH_ID,
342
+ credential: getUnsignedBoostTemplate({ id: 'urn:uuid:credential-1' }),
343
+ signingAuthority: { type: 'http', endpoint: 'https://sa.example', name: 'sa' },
344
+ updateSummary: 'Updated employer name',
345
+ idempotencyKey: 'publish-attempt-1',
346
+ },
347
+ ],
348
+ ] as const)(
349
+ 'forwards publishCredentialRefresh %s input and returns the typed result',
350
+ async (_mode, input) => {
351
+ const client = getMockClient();
352
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
353
+ const learnCard = getMockLearnCard();
354
+ const plugin = await getLearnCardNetworkPlugin(
355
+ learnCard,
356
+ 'https://network.example/trpc'
357
+ );
358
+
359
+ await expect(
360
+ plugin.methods?.publishCredentialRefresh(learnCard, input as any)
361
+ ).resolves.toEqual({
362
+ refreshId: REFRESH_ID,
363
+ version: 2,
364
+ publishedAt: '2026-09-02T00:00:00.000Z',
365
+ notification: 'queued',
366
+ });
367
+ expect(client.credentialRefresh.publishCredentialRefresh.mutate).toHaveBeenCalledWith(
368
+ input
369
+ );
370
+ }
371
+ );
372
+
373
+ it('forwards getCredentialRefreshHistory input to the history query', async () => {
374
+ const client = getMockClient();
375
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
376
+ const learnCard = getMockLearnCard();
377
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
378
+
379
+ const input = { refreshId: REFRESH_ID, cursor: 'cursor-1', limit: 10 };
380
+
381
+ await expect(
382
+ plugin.methods?.getCredentialRefreshHistory(learnCard, input)
383
+ ).resolves.toEqual({
384
+ records: [
385
+ { version: 2, publishedAt: '2026-09-02T00:00:00.000Z' },
386
+ { version: 1, publishedAt: '2026-09-01T00:00:00.000Z' },
387
+ ],
388
+ hasMore: false,
389
+ });
390
+ expect(client.credentialRefresh.getCredentialRefreshHistory.query).toHaveBeenCalledWith(
391
+ input
392
+ );
393
+ });
394
+
395
+ it.each([
396
+ [
397
+ 'allocateCredentialRefresh',
398
+ [{ holder: { did: TARGET_DID }, credentialId: 'urn:uuid:credential-1' }],
399
+ ],
400
+ ['sendRefreshableCredential', [REFRESH_ID, getUnsignedBoostTemplate()]],
401
+ [
402
+ 'publishCredentialRefresh',
403
+ [
404
+ {
405
+ mode: 'issuer-signed',
406
+ refreshId: REFRESH_ID,
407
+ signedCredential: getUnsignedBoostTemplate(),
408
+ },
409
+ ],
410
+ ],
411
+ ['getCredentialRefreshHistory', [{ refreshId: REFRESH_ID }]],
412
+ ] as const)('runs ensureUser before %s', async (method, args) => {
413
+ const client = getMockClient(null);
414
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
415
+ const learnCard = getMockLearnCard();
416
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
417
+
418
+ await expect((plugin.methods?.[method] as any)(learnCard, ...args)).rejects.toThrow(
419
+ 'Please make an account first!'
420
+ );
421
+
422
+ const operation = {
423
+ allocateCredentialRefresh: client.credentialRefresh.allocateCredentialRefresh.mutate,
424
+ sendRefreshableCredential: client.credentialRefresh.sendRefreshableCredential.mutate,
425
+ publishCredentialRefresh: client.credentialRefresh.publishCredentialRefresh.mutate,
426
+ getCredentialRefreshHistory: client.credentialRefresh.getCredentialRefreshHistory.query,
427
+ }[method];
428
+ expect(operation).not.toHaveBeenCalled();
429
+ });
430
+
431
+ it('sendBoost with enableRefresh allocates before signing, injects the service, and uses managed storage', async () => {
432
+ const client = getMockClient();
433
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
434
+ const { learnCard, issuedCredentials } = getMockIssuingLearnCard();
435
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
436
+
437
+ const uri = await plugin.methods?.sendBoost(learnCard, 'userb', 'did:example:boost:1', {
438
+ enableRefresh: true,
439
+ skipNotification: true,
440
+ });
441
+
442
+ expect(uri).toEqual('managed-credential-uri');
443
+
444
+ // A stable UUID credential ID is generated when the template has none
445
+ expect(client.credentialRefresh.allocateCredentialRefresh.mutate).toHaveBeenCalledTimes(1);
446
+ const allocateInput = client.credentialRefresh.allocateCredentialRefresh.mutate.mock
447
+ .calls[0]?.[0] as any;
448
+ expect(allocateInput.holder).toEqual({ profileId: 'userb', did: TARGET_DID });
449
+ expect(allocateInput.credentialId).toMatch(
450
+ /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
451
+ );
452
+
453
+ // Allocation happens before signing so the service lands in the signed payload
454
+ const allocateOrder =
455
+ client.credentialRefresh.allocateCredentialRefresh.mutate.mock.invocationCallOrder[0]!;
456
+ const signOrder = learnCard.invoke.issueCredential.mock.invocationCallOrder[0]!;
457
+ expect(allocateOrder).toBeLessThan(signOrder);
458
+
459
+ // The allocated service and its inline context are injected into the signed credential
460
+ const signedInput = issuedCredentials[0];
461
+ expect(signedInput.id).toEqual(allocateInput.credentialId);
462
+ expect(signedInput.refreshService).toEqual(ALLOCATION.refreshService);
463
+ expect(signedInput['@context']).toContainEqual(
464
+ expect.objectContaining({ 'LearnCardCredentialRefresh2026': expect.any(String) })
465
+ );
466
+
467
+ // Dedicated managed-send procedure is used instead of legacy credential storage,
468
+ // with holder-only server-side encryption (no client-side JWE, no LCN recipient).
469
+ // The boost URI is forwarded so the issued credential stays linked INSTANCE_OF
470
+ // the boost for canonical recipient management (including revocation).
471
+ expect(client.credentialRefresh.sendRefreshableCredential.mutate).toHaveBeenCalledWith({
472
+ refreshId: REFRESH_ID,
473
+ credential: expect.objectContaining({
474
+ id: allocateInput.credentialId,
475
+ refreshService: ALLOCATION.refreshService,
476
+ proof: { type: 'DataIntegrityProof' },
477
+ }),
478
+ boostUri: 'did:example:boost:1',
479
+ skipNotification: true,
480
+ });
481
+ expect(client.boost.sendBoost.mutate).not.toHaveBeenCalled();
482
+ expect(learnCard.invoke.createDagJwe).not.toHaveBeenCalled();
483
+ expect(client.utilities.getDid.query).not.toHaveBeenCalled();
484
+ });
485
+
486
+ it('sendBoost with enableRefresh reuses an existing credential ID', async () => {
487
+ const client = getMockClient();
488
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
489
+ client.boost.getBoost.query.mockResolvedValue({
490
+ boost: getUnsignedBoostTemplate({ id: 'urn:uuid:existing-credential-id' }),
491
+ });
492
+ const { learnCard } = getMockIssuingLearnCard();
493
+ const plugin = await getLearnCardNetworkPlugin(learnCard, 'https://network.example/trpc');
494
+
495
+ await plugin.methods?.sendBoost(learnCard, 'userb', 'did:example:boost:1', {
496
+ enableRefresh: true,
497
+ });
498
+
499
+ expect(client.credentialRefresh.allocateCredentialRefresh.mutate).toHaveBeenCalledWith({
500
+ holder: { profileId: 'userb', did: TARGET_DID },
501
+ credentialId: 'urn:uuid:existing-credential-id',
502
+ });
503
+ });
504
+
505
+ it.each([[{ encrypt: true }], [{ encrypt: false }], [true], [false], [undefined]] as const)(
506
+ 'sendBoost without enableRefresh keeps the legacy storage path (options=%j)',
507
+ async options => {
508
+ const client = getMockClient();
509
+ vi.mocked(getBrainClient).mockResolvedValue(client as never);
510
+ const { learnCard } = getMockIssuingLearnCard();
511
+ const plugin = await getLearnCardNetworkPlugin(
512
+ learnCard,
513
+ 'https://network.example/trpc'
514
+ );
515
+
516
+ const uri = await plugin.methods?.sendBoost(
517
+ learnCard,
518
+ 'userb',
519
+ 'did:example:boost:1',
520
+ options as any
521
+ );
522
+
523
+ expect(uri).toEqual('boost-credential-uri');
524
+ expect(client.boost.sendBoost.mutate).toHaveBeenCalledTimes(1);
525
+ expect(
526
+ client.credentialRefresh.allocateCredentialRefresh.mutate
527
+ ).not.toHaveBeenCalled();
528
+ expect(
529
+ client.credentialRefresh.sendRefreshableCredential.mutate
530
+ ).not.toHaveBeenCalled();
531
+
532
+ const sentCredential = (client.boost.sendBoost.mutate.mock.calls[0]?.[0] as any)
533
+ ?.credential;
534
+ const encrypts =
535
+ options === undefined ||
536
+ options === true ||
537
+ (typeof options === 'object' && options.encrypt);
538
+
539
+ if (encrypts) {
540
+ expect(learnCard.invoke.createDagJwe).toHaveBeenCalledWith(expect.anything(), [
541
+ PROFILE.did,
542
+ TARGET_DID,
543
+ 'did:web:network.example',
544
+ ]);
545
+ expect(sentCredential).toEqual({ ciphertext: 'encrypted' });
546
+ } else {
547
+ expect(learnCard.invoke.createDagJwe).not.toHaveBeenCalled();
548
+ expect(sentCredential).toEqual(
549
+ expect.objectContaining({ proof: { type: 'DataIntegrityProof' } })
550
+ );
551
+ }
552
+ }
553
+ );
554
+ });
555
+
155
556
  // Skipping until LCN has the updated function and vc-templates can be updated with resolveable VCs.
156
557
  describe.skip('LearnCard Network Plugin', () => {
157
558
  describe('NetworkLearnCard', () => {
@@ -205,3 +606,36 @@ describe.skip('LearnCard Network Plugin', () => {
205
606
  });
206
607
  });
207
608
  });
609
+
610
+ it('recovers valid deliveries despite decryption and validation failures on the page', async () => {
611
+ const client = {
612
+ ...getMockClient(),
613
+ inbox: {
614
+ getMyInboxDeliveries: {
615
+ query: vi.fn().mockResolvedValue({
616
+ records: [
617
+ { id: 'bad-decrypt', credential: 'broken' },
618
+ { id: 'bad-vc', credential: {} },
619
+ { id: 'good', credential: { type: ['VerifiableCredential'] } },
620
+ ],
621
+ hasMore: true,
622
+ cursor: 'good',
623
+ }),
624
+ },
625
+ },
626
+ };
627
+ vi.mocked(getBrainClient).mockResolvedValue(client as any);
628
+ const wallet = getMockLearnCard();
629
+ wallet.id.keypair = () => ({ publicKey: 'key' });
630
+ wallet.invoke.decryptDagJwe = vi.fn(async (credential: any) => {
631
+ if (credential === 'broken') throw new Error('Cannot decrypt');
632
+ return credential;
633
+ });
634
+ const plugin = await getLearnCardNetworkPlugin(wallet, 'https://network.example');
635
+ expect(await plugin.methods?.recoverInboxCredentials(wallet, {})).toEqual({
636
+ records: [{ id: 'good', credential: { type: ['VerifiableCredential'] } }],
637
+ failed: 2,
638
+ hasMore: true,
639
+ cursor: 'good',
640
+ });
641
+ });
package/src/types.ts CHANGED
@@ -111,6 +111,12 @@ import {
111
111
  CredentialActivityStats,
112
112
  BitstringCredentialStatusPurpose,
113
113
  BitstringCredentialStatusEntry,
114
+ AllocateCredentialRefreshInput,
115
+ AllocateCredentialRefreshResult,
116
+ PublishCredentialRefreshInput,
117
+ PublishCredentialRefreshResult,
118
+ GetCredentialRefreshHistoryInput,
119
+ GetCredentialRefreshHistoryResult,
114
120
  } from '@learncard/types';
115
121
  import { Plugin } from '@learncard/core';
116
122
  import { ProofOptions } from '@learncard/didkit-plugin';
@@ -411,6 +417,47 @@ export type LearnCardNetworkPluginMethods = {
411
417
  statusPurposes?: BitstringCredentialStatusPurpose[];
412
418
  listSize?: number;
413
419
  }) => Promise<BitstringCredentialStatusEntry[]>;
420
+
421
+ // Managed Credential Refresh (LC-2117 / LC-2135 / LC-2136)
422
+
423
+ /**
424
+ * Allocates an unguessable managed refresh service for a credential BEFORE it is
425
+ * signed. The returned `refreshService` descriptor must be injected into the
426
+ * unsigned credential so it becomes part of the signed payload; `credentialId`
427
+ * binds the allocation to the credential's stable ID.
428
+ */
429
+ allocateCredentialRefresh: (
430
+ input: AllocateCredentialRefreshInput
431
+ ) => Promise<AllocateCredentialRefreshResult>;
432
+ /**
433
+ * Binds a signed credential to its allocated refresh aggregate. The plaintext VC is
434
+ * verified server-side and persisted only as a holder-encrypted JWE — the issuer
435
+ * and the service never retain a readable copy. When the credential was issued from
436
+ * a boost, pass `boostUri` so the stored instance stays linked INSTANCE_OF the
437
+ * boost for canonical recipient management (including revocation).
438
+ */
439
+ sendRefreshableCredential: (
440
+ refreshId: string,
441
+ credential: VC,
442
+ boostUri?: string,
443
+ skipNotification?: boolean
444
+ ) => Promise<string>;
445
+ /**
446
+ * Publishes a new immutable version of a refreshable credential and atomically
447
+ * advances the refresh head. Issuer-signed mode takes a fully signed VC;
448
+ * signing-authority mode takes updated unsigned claims plus an authorized signing
449
+ * authority.
450
+ */
451
+ publishCredentialRefresh: (
452
+ input: PublishCredentialRefreshInput
453
+ ) => Promise<PublishCredentialRefreshResult>;
454
+ /**
455
+ * Cursor-paginated, metadata-only issuer audit history for a managed refresh.
456
+ * Never includes credential bodies or encrypted payloads.
457
+ */
458
+ getCredentialRefreshHistory: (
459
+ input: GetCredentialRefreshHistoryInput
460
+ ) => Promise<GetCredentialRefreshHistoryResult>;
414
461
  revokeBoostRecipient: (
415
462
  boostUri: string,
416
463
  recipientProfileId: string,
@@ -437,6 +484,16 @@ export type LearnCardNetworkPluginMethods = {
437
484
  skipNotification?: boolean;
438
485
  templateData?: Record<string, unknown>;
439
486
  statusPurposes?: BitstringCredentialStatusPurpose[];
487
+ /**
488
+ * Opt into a managed refresh service. When true, a stable UUID
489
+ * credential ID is generated if the boost template has none, a
490
+ * refresh service is allocated BEFORE signing and injected into the
491
+ * signed credential, and the credential is sent via the dedicated
492
+ * managed-send procedure (holder-encrypted storage only) instead of
493
+ * legacy credential storage. The `encrypt` option is ignored in this
494
+ * mode — managed storage is always holder-only.
495
+ */
496
+ enableRefresh?: boolean;
440
497
  }
441
498
  ) => Promise<string>;
442
499
 
@@ -630,12 +687,21 @@ export type LearnCardNetworkPluginMethods = {
630
687
  ) => Promise<PaginatedInboxCredentialsType>;
631
688
 
632
689
  getInboxCredential: (id: string) => Promise<InboxCredentialType | null>;
690
+ /** Recover and locally decrypt claims for seven days; deduplicate by the stable inbox id. */
691
+ recoverInboxCredentials: (options?: { limit?: number; cursor?: string }) => Promise<{
692
+ records: { id: string; credential: VC; expiresAt: string }[];
693
+ /** Deliveries that could not be decrypted or validated on this page. */
694
+ failed: number;
695
+ hasMore: boolean;
696
+ cursor?: string;
697
+ }>;
633
698
  finalizeInboxCredentials: () => Promise<{
634
699
  processed: number;
635
700
  claimed: number;
636
701
  errors: number;
637
702
  guardianPending: number;
638
703
  verifiableCredentials: VC[];
704
+ deliveries: { id: string; credential: VC }[];
639
705
  }>;
640
706
 
641
707
  // Guardian Approval