@learncard/types 5.22.0 → 5.23.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.
package/dist/vc.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vc.d.ts","sourceRoot":"","sources":["../src/vc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vc.d.ts","sourceRoot":"","sources":["../src/vc.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/types",
3
- "version": "5.22.0",
3
+ "version": "5.23.1",
4
4
  "description": "Shared types for learn card",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -25,6 +25,7 @@
25
25
  "scripts": {
26
26
  "build": "node ./scripts/build.mjs && shx cp ./scripts/mixedEntypoint.js ./dist/index.cjs && tsc --p tsconfig.json && shx cp ./dist/index.d.ts ./dist/index.d.cts",
27
27
  "typecheck": "tsc --noEmit -p tsconfig.json",
28
+ "test": "vitest run",
28
29
  "start": "aqu watch"
29
30
  },
30
31
  "author": "Learning Economy Foundation (www.learningeconomy.io)",
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { UnsupportedSignInOperationError } from '../auth';
3
+
4
+ describe('UnsupportedSignInOperationError', () => {
5
+ it('preserves the operation and provider for callers', () => {
6
+ const error = new UnsupportedSignInOperationError('sendPhoneOtp', 'keycloak');
7
+ expect(error).toBeInstanceOf(Error);
8
+ expect(error.name).toBe('UnsupportedSignInOperationError');
9
+ expect(error.operation).toBe('sendPhoneOtp');
10
+ expect(error.providerType).toBe('keycloak');
11
+ expect(error.message).toContain('sendPhoneOtp');
12
+ });
13
+ });
package/src/auth.ts CHANGED
@@ -32,6 +32,17 @@ export class AuthSessionError extends Error {
32
32
  // Auth Provider
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
+ /** An action that the selected sign-in provider does not support. */
36
+ export class UnsupportedSignInOperationError extends Error {
37
+ constructor(
38
+ public readonly operation: string,
39
+ public readonly providerType: string
40
+ ) {
41
+ super(`${operation} is not supported by ${providerType}`);
42
+ this.name = 'UnsupportedSignInOperationError';
43
+ }
44
+ }
45
+
35
46
  /**
36
47
  * Auth provider identifier. Known values: 'firebase', 'supertokens', 'keycloak', 'oidc'.
37
48
  * Use any string to support custom auth providers without modifying this type.
@@ -260,6 +271,8 @@ export interface RecoveryResult {
260
271
  export interface IdentityRecoverySession {
261
272
  recoverySessionToken: string;
262
273
  recoveryMethods: RecoveryMethodInfo[];
274
+ /** The verified account's DID (same source as `ServerKeyStatus.primaryDid`). */
275
+ primaryDid: string | null;
263
276
  }
264
277
 
265
278
  // ---------------------------------------------------------------------------
@@ -269,7 +282,15 @@ export interface IdentityRecoverySession {
269
282
  export type SssActivationState = 'provisional' | 'active';
270
283
 
271
284
  /** Optional PIN enrollment requires rotating the existing escrow share. */
272
- export type EscrowEnrollmentOptions = { pin?: string };
285
+ export type EscrowEnrollmentOptions = {
286
+ pin?: string;
287
+ /** P9.1 (attack C): required by the enclave for nitro/authority-mode
288
+ * enroll/replace; bound into the sealed blob + signed intent, never read
289
+ * or changed by lca-api. The authenticated account's email + the app's
290
+ * current locale (one of the compiled template languages). */
291
+ recoveryEmail?: string;
292
+ recoveryLanguage?: string;
293
+ };
273
294
 
274
295
  /** Stable error-message contract shared by PIN recovery clients and servers. */
275
296
  export const ESCROW_PIN_LOCKED_MESSAGE =
@@ -288,10 +309,21 @@ export const EscrowPinStatusValidator = z.object({
288
309
  });
289
310
  export type EscrowPinStatus = z.infer<typeof EscrowPinStatusValidator>;
290
311
 
312
+ /**
313
+ * Reason a stored escrow blob no longer matches the server's active enclave:
314
+ * `mode-mismatch` — blob was sealed for `software`/`nitro` but the server now
315
+ * runs the other backend (e.g. the P6 software→Nitro migration, or a rollback);
316
+ * `key-rotated` — blob's `enclaveKeyId` no longer matches the active
317
+ * attestation's key (e.g. a PCR/measurement rotation).
318
+ */
319
+ export type EscrowBlobStaleReason = 'mode-mismatch' | 'key-rotated';
320
+
291
321
  /** Enrollment details for PIN-aware strategies. Legacy strategies may still return a string. */
292
322
  export interface EscrowEnrollmentState {
293
- state: 'enrolled' | 'not-enrolled' | 'opted-out' | 'disabled';
323
+ state: 'enrolled' | 'not-enrolled' | 'opted-out' | 'disabled' | 'stale';
294
324
  escrowPin?: EscrowPinStatus;
325
+ /** Present only when `state === 'stale'`. */
326
+ staleReason?: EscrowBlobStaleReason;
295
327
  }
296
328
 
297
329
  /**
@@ -309,6 +341,7 @@ export interface ServerKeyStatus {
309
341
  maskedRecoveryEmail?: string | null;
310
342
  escrowOptedOut?: boolean;
311
343
  escrowPin?: EscrowPinStatus;
344
+ escrowStale?: EscrowBlobStaleReason;
312
345
  sssActivationState?: SssActivationState | null;
313
346
  }
314
347
 
@@ -362,6 +395,20 @@ export interface KeyDerivationCapabilities {
362
395
  // Key Derivation Strategy (generic)
363
396
  // ---------------------------------------------------------------------------
364
397
 
398
+ /** Initiating-client bindings. Persist with the recovery secrets and never replace
399
+ * these with fields from a status response. Missing bindings require a new start. */
400
+ export interface EscrowRecoveryExpectations {
401
+ tenant: string;
402
+ did: string;
403
+ holdId: string;
404
+ clientEphemeralPublicKey: string;
405
+ blobHash: string;
406
+ authorityGeneration: string;
407
+ holdDigest: string;
408
+ ledgerPublicKey: string;
409
+ keyId: string;
410
+ }
411
+
365
412
  /**
366
413
  * Key Derivation Strategy
367
414
  *
@@ -527,18 +574,27 @@ export interface KeyDerivationStrategy<
527
574
  privateKey: string;
528
575
  signDidAuthVp: DidAuthVpSigner;
529
576
  pin: string;
577
+ /** P9.1: only `recoveryEmail`/`recoveryLanguage` apply here; `options.pin`
578
+ * is ignored in favor of the required top-level `pin`. */
579
+ options?: EscrowEnrollmentOptions;
530
580
  }): Promise<void>;
531
581
 
532
- /** Remove a PIN by rotating escrow material with an owner proof. */
582
+ /** Remove a PIN by rotating escrow material with an owner proof. In
583
+ * nitro/authority mode this is a `replace` enrollment action, which
584
+ * requires `options.recoveryEmail`/`recoveryLanguage`, same as
585
+ * `setEscrowPin`. */
533
586
  clearEscrowPin?(params: {
534
587
  token: string;
535
588
  providerType: AuthProviderType;
536
589
  privateKey: string;
537
590
  signDidAuthVp: DidAuthVpSigner;
591
+ options?: EscrowEnrollmentOptions;
538
592
  }): Promise<void>;
539
593
 
540
594
  /** Start an escrow hold. Securely persist the returned secrets; null means an existing hold. */
541
595
  startEscrowRecovery?(params: {
596
+ /** Account selected by the initiating client, required for Nitro recovery. */
597
+ expectedDid?: string;
542
598
  token?: string;
543
599
  providerType?: AuthProviderType;
544
600
  recoverySessionToken?: string;
@@ -553,15 +609,19 @@ export interface KeyDerivationStrategy<
553
609
  completedAt?: string;
554
610
  resumeToken: string | null;
555
611
  clientEphemeralPrivateKey: string;
612
+ /** Persist alongside the private key; pass unchanged on status, confirm and restore. */
613
+ expectations?: EscrowRecoveryExpectations;
556
614
  pinSalt?: string;
557
615
  /** Absent on legacy hold-only strategies. */
558
616
  releasePolicy?: 'hold' | 'pin';
617
+ /** Present only for an authority-mode hold-policy hold; see confirmEscrowRecovery. */
618
+ requiresConfirmation?: boolean;
559
619
  }>;
560
620
 
561
621
  /** Read a hold using its resume proof or the active device's provider session. */
562
622
  getEscrowRecoveryStatus?(
563
623
  params:
564
- | { holdId: string; resumeToken: string }
624
+ | { holdId: string; resumeToken: string; expectations?: EscrowRecoveryExpectations }
565
625
  | { token: string; providerType: AuthProviderType }
566
626
  ): Promise<{
567
627
  holdId: string;
@@ -572,6 +632,9 @@ export interface KeyDerivationStrategy<
572
632
  releasePolicy?: 'hold' | 'pin';
573
633
  cancelledAt?: string;
574
634
  completedAt?: string;
635
+ /** Only present for an authority-mode hold-policy hold; `confirmed` only appears alongside it. */
636
+ requiresConfirmation?: boolean;
637
+ confirmed?: boolean;
575
638
  } | null>;
576
639
 
577
640
  /** Cancel a pending hold with a fresh, owner-signed DID challenge. */
@@ -582,6 +645,28 @@ export interface KeyDerivationStrategy<
582
645
  signDidAuthVp: DidAuthVpSigner;
583
646
  }): Promise<{ cancelled: boolean }>;
584
647
 
648
+ /**
649
+ * Submit the confirmation code the enclave emailed to the account's
650
+ * recovery address. Only meaningful when startEscrowRecovery returned
651
+ * `requiresConfirmation: true`; the resume proof (no DID session exists
652
+ * yet at this point in the flow) is the only credential.
653
+ */
654
+ confirmEscrowRecovery?(params: {
655
+ /** Retained initiating-client bindings, required in Nitro mode, including after reload. */
656
+ expectations?: EscrowRecoveryExpectations;
657
+ holdId: string;
658
+ resumeToken: string;
659
+ code: string;
660
+ tenantId?: string;
661
+ }): Promise<{ success: true }>;
662
+
663
+ /** Request a fresh confirmation code for a hold awaiting confirmation. */
664
+ resendEscrowRecoveryCode?(params: {
665
+ holdId: string;
666
+ resumeToken: string;
667
+ tenantId?: string;
668
+ }): Promise<{ success: true }>;
669
+
585
670
  /** Execute a recovery flow and return the recovered private key + DID */
586
671
  executeRecovery(params: {
587
672
  token: string;
@@ -0,0 +1,163 @@
1
+ import { describe, expect, expectTypeOf, it } from 'vitest';
2
+
3
+ import {
4
+ AssociationValidator,
5
+ ClrCredentialValidator,
6
+ ClrSubjectValidator,
7
+ UnsignedClrCredentialValidator,
8
+ type Association,
9
+ } from './clr';
10
+
11
+ const association = {
12
+ type: 'Association' as const,
13
+ associationType: 'isPartOf',
14
+ sourceId: 'https://example.org/achievements/course',
15
+ targetId: 'https://example.org/achievements/program',
16
+ };
17
+
18
+ const supportedTypeNames = [
19
+ 'Association',
20
+ 'https://purl.imsglobal.org/spec/vc/clr/vocab.html#Association',
21
+ 'https://example.org/ExtendedAssociation',
22
+ 'CustomAssociation',
23
+ ];
24
+
25
+ const subject = {
26
+ id: 'did:example:learner',
27
+ type: ['ClrSubject'],
28
+ association: [association],
29
+ };
30
+
31
+ const unsignedClr = {
32
+ '@context': [
33
+ 'https://www.w3.org/ns/credentials/v2',
34
+ 'https://purl.imsglobal.org/spec/clr/v2p0/context.json',
35
+ 'https://purl.imsglobal.org/spec/ob/v3p0/context.json',
36
+ ],
37
+ id: 'https://example.org/credentials/clr',
38
+ type: ['VerifiableCredential', 'ClrCredential'],
39
+ issuer: { id: 'did:example:issuer', type: ['Profile'], name: 'Example University' },
40
+ validFrom: '2026-01-01T00:00:00Z',
41
+ credentialSubject: subject,
42
+ };
43
+
44
+ const proof = {
45
+ type: 'DataIntegrityProof',
46
+ cryptosuite: 'eddsa-rdfc-2022',
47
+ created: '2026-01-01T00:00:00Z',
48
+ proofPurpose: 'assertionMethod',
49
+ verificationMethod: 'did:example:issuer#key',
50
+ proofValue: 'zExampleSignature',
51
+ };
52
+
53
+ describe('CLR Association.type', () => {
54
+ it('accepts the canonical scalar without changing it or extension fields', () => {
55
+ // CLR 2.0 $defs.Association.properties.type requires the scalar "Association".
56
+ // https://purl.imsglobal.org/spec/clr/v2p0/schema/json/clr_v2p0_clrcredential_schema.json
57
+ const input = { ...association, 'ext:description': 'Course belongs to program' };
58
+
59
+ expect(AssociationValidator.parse(input)).toEqual(input);
60
+ });
61
+
62
+ it.each(supportedTypeNames)(
63
+ 'preserves scalar and singleton-array type values through full CLR parsing: %s',
64
+ typeName => {
65
+ for (const type of [typeName, [typeName]]) {
66
+ const input = { ...association, type };
67
+ const credential = {
68
+ ...unsignedClr,
69
+ credentialSubject: { ...subject, association: [input] },
70
+ };
71
+
72
+ // This SDK validator checks structure; the official CLR schema is stricter.
73
+ expect(AssociationValidator.parse(input)).toEqual(input);
74
+ expect(UnsignedClrCredentialValidator.parse(credential)).toEqual(credential);
75
+ expect(ClrCredentialValidator.parse({ ...credential, proof })).toEqual({
76
+ ...credential,
77
+ proof,
78
+ });
79
+ }
80
+ }
81
+ );
82
+
83
+ it.each(
84
+ [
85
+ ['Association'],
86
+ ['Association', 'https://example.org/ExtendedAssociation'],
87
+ ['https://example.org/LegacyAssociation'],
88
+ [''], // Existing arrays accept every string; preserve that contract.
89
+ ].map(type => ({ type }))
90
+ )('preserves supported legacy arrays: $type', ({ type }) => {
91
+ const input = { ...association, type };
92
+
93
+ expect(AssociationValidator.parse(input)).toEqual(input);
94
+ });
95
+
96
+ it.each(
97
+ [undefined, null, false, 42, '', {}, [], ['Association', 42], [['Association']]].map(
98
+ type => ({ type })
99
+ )
100
+ )('rejects invalid values: $type', ({ type }) => {
101
+ expect(AssociationValidator.safeParse({ ...association, type }).success).toBe(false);
102
+ });
103
+
104
+ it('exposes scalar and legacy array types to callers', () => {
105
+ expectTypeOf<Association['type']>().toEqualTypeOf<string | string[]>();
106
+ });
107
+ });
108
+
109
+ describe('CLR credentials with associations', () => {
110
+ it('parses a subject containing both canonical and legacy associations', () => {
111
+ const input = {
112
+ ...subject,
113
+ association: [association, { ...association, type: ['Association'] }],
114
+ };
115
+
116
+ expect(ClrSubjectValidator.parse(input)).toEqual(input);
117
+ });
118
+
119
+ it.each([false, true])('parses unsigned and signed CLR subject arrays: %s', subjectArray => {
120
+ const input = {
121
+ ...unsignedClr,
122
+ credentialSubject: subjectArray ? [subject] : subject,
123
+ };
124
+
125
+ expect(UnsignedClrCredentialValidator.parse(input)).toEqual(input);
126
+ expect(ClrCredentialValidator.parse({ ...input, proof })).toEqual({ ...input, proof });
127
+ });
128
+
129
+ it('preserves nested CLR content and validates its associations when parsed directly', () => {
130
+ const nested = {
131
+ ...unsignedClr,
132
+ id: 'https://example.org/credentials/nested-clr',
133
+ credentialSubject: {
134
+ ...subject,
135
+ association: [{ ...association, type: supportedTypeNames[1] }],
136
+ },
137
+ proof,
138
+ };
139
+ const input = {
140
+ ...unsignedClr,
141
+ credentialSubject: { ...subject, verifiableCredential: [nested] },
142
+ proof,
143
+ };
144
+
145
+ expect(ClrCredentialValidator.parse(input)).toEqual(input);
146
+ // Embedded verifiableCredential values are intentionally opaque in ClrSubjectValidator.
147
+ expect(ClrCredentialValidator.parse(nested)).toEqual(nested);
148
+ });
149
+
150
+ it.each([[], '', ['Association', null]].map(type => ({ type })))(
151
+ 'rejects invalid associations through full CLR parsing: $type',
152
+ ({ type }) => {
153
+ const invalidSubject = { ...subject, association: [{ ...association, type }] };
154
+
155
+ for (const credentialSubject of [invalidSubject, [invalidSubject]]) {
156
+ const input = { ...unsignedClr, credentialSubject };
157
+
158
+ expect(UnsignedClrCredentialValidator.safeParse(input).success).toBe(false);
159
+ expect(ClrCredentialValidator.safeParse({ ...input, proof }).success).toBe(false);
160
+ }
161
+ }
162
+ );
163
+ });
package/src/clr.ts CHANGED
@@ -27,7 +27,8 @@ export type AssociationType = z.infer<typeof AssociationTypeValidator>;
27
27
 
28
28
  export const AssociationValidator = z
29
29
  .object({
30
- type: z.string().array().nonempty(),
30
+ // Accept compacted scalars while retaining the existing permissive array contract.
31
+ type: z.string().min(1).or(z.string().array().nonempty()),
31
32
  associationType: AssociationTypeValidator,
32
33
  sourceId: z.string().optional(),
33
34
  targetId: z.string(),