@learncard/types 5.18.2 → 5.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/types",
3
- "version": "5.18.2",
3
+ "version": "5.19.0",
4
4
  "description": "Shared types for learn card",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -24,6 +24,7 @@
24
24
  ],
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
+ "typecheck": "tsc --noEmit -p tsconfig.json",
27
28
  "start": "aqu watch"
28
29
  },
29
30
  "author": "Learning Economy Foundation (www.learningeconomy.io)",
@@ -0,0 +1,336 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ import { UnsignedVCValidator, VCValidator } from './vc';
4
+ import { JWEValidator } from './crypto';
5
+
6
+ /**
7
+ * Shared contracts for managed credential refresh (LC-2117, LC-2135, LC-2136).
8
+ *
9
+ * The generic `RefreshServiceValidator` in vc.ts intentionally remains permissive so
10
+ * unknown third-party refresh services keep parsing. These validators model the
11
+ * LearnCard-managed `LearnCardCredentialRefresh2026` service, its allocation/publication
12
+ * lifecycle, the holder-facing response envelopes, and safe refresh outcomes.
13
+ */
14
+
15
+ /** LearnCard DID-auth extension descriptor for a managed refresh service */
16
+ export const LearnCardRefreshAuthorizationValidator = z
17
+ .object({ type: z.literal('LearnCardDIDAuth') })
18
+ .catchall(z.any());
19
+ export type LearnCardRefreshAuthorization = z.infer<typeof LearnCardRefreshAuthorizationValidator>;
20
+
21
+ /**
22
+ * Managed refresh service descriptor.
23
+ *
24
+ * Requires a resolvable `id` and the versioned LearnCard type, and optionally carries a
25
+ * LearnCard authorization descriptor. This is distinct from the 1EdTech protocol.
26
+ */
27
+ export const ManagedCredentialRefreshServiceValidator = z
28
+ .object({
29
+ id: z.string().min(1),
30
+ type: z.literal('LearnCardCredentialRefresh2026'),
31
+ authorization: LearnCardRefreshAuthorizationValidator.optional(),
32
+ })
33
+ .catchall(z.any());
34
+ export type ManagedCredentialRefreshService = z.infer<
35
+ typeof ManagedCredentialRefreshServiceValidator
36
+ >;
37
+
38
+ /** Standard 1EdTech protocol: HTTPS GET returning a signed VC or VC-JWT. */
39
+ export const StandardCredentialRefreshServiceValidator = z
40
+ .object({
41
+ id: z.string().min(1),
42
+ type: z.literal('1EdTechCredentialRefresh'),
43
+ })
44
+ .catchall(z.any());
45
+ export const SupportedCredentialRefreshServiceValidator = z.union([
46
+ ManagedCredentialRefreshServiceValidator,
47
+ StandardCredentialRefreshServiceValidator,
48
+ ]);
49
+ export type SupportedCredentialRefreshService = z.infer<
50
+ typeof SupportedCredentialRefreshServiceValidator
51
+ >;
52
+
53
+ // --- Allocation (before signing) -------------------------------------------
54
+
55
+ export const AllocateCredentialRefreshInputValidator = z.object({
56
+ holder: z.object({
57
+ profileId: z.string().optional(),
58
+ did: z.string().min(1),
59
+ }),
60
+ credentialId: z.string().min(1),
61
+ });
62
+ export type AllocateCredentialRefreshInput = z.infer<
63
+ typeof AllocateCredentialRefreshInputValidator
64
+ >;
65
+
66
+ export const AllocateCredentialRefreshResultValidator = z.object({
67
+ refreshId: z.string().min(1),
68
+ refreshService: ManagedCredentialRefreshServiceValidator.extend({
69
+ authorization: LearnCardRefreshAuthorizationValidator,
70
+ }),
71
+ });
72
+ export type AllocateCredentialRefreshResult = z.infer<
73
+ typeof AllocateCredentialRefreshResultValidator
74
+ >;
75
+
76
+ // --- Publication ------------------------------------------------------------
77
+
78
+ export const CredentialRefreshSigningModeValidator = z.enum(['issuer-signed', 'signing-authority']);
79
+ export type CredentialRefreshSigningMode = z.infer<typeof CredentialRefreshSigningModeValidator>;
80
+
81
+ const PublishCredentialRefreshBaseFields = {
82
+ refreshId: z.string().min(1),
83
+ notifyHolder: z.boolean().optional(),
84
+ updateSummary: z.string().optional(),
85
+ idempotencyKey: z.string().optional(),
86
+ };
87
+
88
+ /** Issuer-signed mode: the caller supplies a fully signed updated VC */
89
+ export const PublishIssuerSignedRefreshValidator = z.object({
90
+ ...PublishCredentialRefreshBaseFields,
91
+ mode: z.literal('issuer-signed'),
92
+ signedCredential: VCValidator,
93
+ });
94
+ export type PublishIssuerSignedRefresh = z.infer<typeof PublishIssuerSignedRefreshValidator>;
95
+
96
+ /** Signing-authority mode: the caller supplies updated unsigned claims and brain-service signs */
97
+ export const PublishSigningAuthorityRefreshValidator = z.object({
98
+ ...PublishCredentialRefreshBaseFields,
99
+ mode: z.literal('signing-authority'),
100
+ credential: UnsignedVCValidator,
101
+ signingAuthority: z
102
+ .object({
103
+ type: z.string().min(1),
104
+ })
105
+ .catchall(z.any()),
106
+ });
107
+ export type PublishSigningAuthorityRefresh = z.infer<
108
+ typeof PublishSigningAuthorityRefreshValidator
109
+ >;
110
+
111
+ /**
112
+ * Top-level object form required by trpc-to-openapi.
113
+ *
114
+ * Keep the public TypeScript type discriminated even though the runtime object has
115
+ * optional fields for both modes. The refinement preserves the same required-field
116
+ * behavior as the former z.discriminatedUnion without crashing OpenAPI generation.
117
+ */
118
+ export const PublishCredentialRefreshInputValidator = z
119
+ .object({
120
+ ...PublishCredentialRefreshBaseFields,
121
+ mode: CredentialRefreshSigningModeValidator,
122
+ signedCredential: VCValidator.optional(),
123
+ credential: UnsignedVCValidator.optional(),
124
+ signingAuthority: z
125
+ .object({
126
+ type: z.string().min(1),
127
+ })
128
+ .catchall(z.any())
129
+ .optional(),
130
+ })
131
+ .superRefine((input, ctx) => {
132
+ if (input.mode === 'issuer-signed' && !input.signedCredential) {
133
+ ctx.addIssue({
134
+ code: 'custom',
135
+ path: ['signedCredential'],
136
+ message: 'signedCredential is required for issuer-signed publication',
137
+ });
138
+ }
139
+
140
+ if (
141
+ input.mode === 'issuer-signed' &&
142
+ (input.credential !== undefined || input.signingAuthority !== undefined)
143
+ ) {
144
+ ctx.addIssue({
145
+ code: 'custom',
146
+ path: ['mode'],
147
+ message: 'issuer-signed publication cannot include signing-authority fields',
148
+ });
149
+ }
150
+
151
+ if (input.mode === 'signing-authority') {
152
+ if (input.signedCredential !== undefined) {
153
+ ctx.addIssue({
154
+ code: 'custom',
155
+ path: ['signedCredential'],
156
+ message: 'signing-authority publication cannot include signedCredential',
157
+ });
158
+ }
159
+
160
+ if (!input.credential) {
161
+ ctx.addIssue({
162
+ code: 'custom',
163
+ path: ['credential'],
164
+ message: 'credential is required for signing-authority publication',
165
+ });
166
+ }
167
+
168
+ if (!input.signingAuthority) {
169
+ ctx.addIssue({
170
+ code: 'custom',
171
+ path: ['signingAuthority'],
172
+ message: 'signingAuthority is required for signing-authority publication',
173
+ });
174
+ }
175
+ }
176
+ });
177
+ export type PublishCredentialRefreshInput =
178
+ PublishIssuerSignedRefresh | PublishSigningAuthorityRefresh;
179
+
180
+ export const PublishCredentialRefreshNotificationValidator = z.enum([
181
+ 'queued',
182
+ 'suppressed',
183
+ 'not-applicable',
184
+ /** Publication succeeded, but the post-commit notification enqueue must be retried. */
185
+ 'delivery-failed',
186
+ ]);
187
+ export type PublishCredentialRefreshNotification = z.infer<
188
+ typeof PublishCredentialRefreshNotificationValidator
189
+ >;
190
+
191
+ export const PublishCredentialRefreshResultValidator = z.object({
192
+ refreshId: z.string().min(1),
193
+ version: z.number().int().positive(),
194
+ publishedAt: z.string().min(1),
195
+ notification: PublishCredentialRefreshNotificationValidator,
196
+ });
197
+ export type PublishCredentialRefreshResult = z.infer<
198
+ typeof PublishCredentialRefreshResultValidator
199
+ >;
200
+
201
+ // --- Version history (metadata only, never credential bodies) ----------------
202
+
203
+ export const CredentialRefreshVersionMetadataValidator = z.object({
204
+ version: z.number().int().positive(),
205
+ publishedAt: z.string().min(1),
206
+ effectiveAt: z.string().optional(),
207
+ etag: z.string().optional(),
208
+ signingMode: CredentialRefreshSigningModeValidator.optional(),
209
+ updateSummary: z.string().optional(),
210
+ });
211
+ export type CredentialRefreshVersionMetadata = z.infer<
212
+ typeof CredentialRefreshVersionMetadataValidator
213
+ >;
214
+
215
+ export const GetCredentialRefreshHistoryInputValidator = z.object({
216
+ refreshId: z.string().min(1),
217
+ cursor: z.string().optional(),
218
+ limit: z.number().int().positive().optional(),
219
+ });
220
+ export type GetCredentialRefreshHistoryInput = z.infer<
221
+ typeof GetCredentialRefreshHistoryInputValidator
222
+ >;
223
+
224
+ export const GetCredentialRefreshHistoryResultValidator = z.object({
225
+ records: CredentialRefreshVersionMetadataValidator.array(),
226
+ hasMore: z.boolean(),
227
+ cursor: z.string().optional(),
228
+ });
229
+ export type GetCredentialRefreshHistoryResult = z.infer<
230
+ typeof GetCredentialRefreshHistoryResultValidator
231
+ >;
232
+
233
+ // --- Holder authentication challenge ------------------------------------------
234
+
235
+ /**
236
+ * Short-lived, single-use challenge returned by a managed refresh endpoint. Contains
237
+ * no holder, issuer, credential, or lifecycle information.
238
+ */
239
+ export const CredentialRefreshChallengeValidator = z.object({
240
+ challenge: z.string().min(1),
241
+ expiresAt: z.string().min(1),
242
+ domain: z.string().optional(),
243
+ scheme: z.literal('LearnCardDIDAuth').optional(),
244
+ });
245
+ export type CredentialRefreshChallenge = z.infer<typeof CredentialRefreshChallengeValidator>;
246
+
247
+ // --- Response envelopes --------------------------------------------------------
248
+
249
+ /** Plain (interoperable) response: an unencrypted signed VC */
250
+ export const PublicCredentialRefreshEnvelopeValidator = z.object({
251
+ format: z.literal('vc'),
252
+ credential: VCValidator,
253
+ etag: z.string().optional(),
254
+ });
255
+ export type PublicCredentialRefreshEnvelope = z.infer<
256
+ typeof PublicCredentialRefreshEnvelopeValidator
257
+ >;
258
+
259
+ /** Managed response: the credential encrypted to the holder as a JWE */
260
+ export const JweCredentialRefreshEnvelopeValidator = z.object({
261
+ format: z.literal('jwe'),
262
+ jwe: JWEValidator,
263
+ etag: z.string().optional(),
264
+ /** Present on LearnCard-managed endpoints; optional for interoperable JWE services. */
265
+ version: z.number().int().positive().optional(),
266
+ });
267
+ export type JweCredentialRefreshEnvelope = z.infer<typeof JweCredentialRefreshEnvelopeValidator>;
268
+
269
+ export const CredentialRefreshResponseEnvelopeValidator = z.discriminatedUnion('format', [
270
+ PublicCredentialRefreshEnvelopeValidator,
271
+ JweCredentialRefreshEnvelopeValidator,
272
+ ]);
273
+ export type CredentialRefreshResponseEnvelope = z.infer<
274
+ typeof CredentialRefreshResponseEnvelopeValidator
275
+ >;
276
+
277
+ // --- Refresh outcomes ----------------------------------------------------------
278
+
279
+ /**
280
+ * Safe, machine-readable failure codes. Raw response bodies are never surfaced.
281
+ */
282
+ export const CredentialRefreshFailureCodeValidator = z.enum([
283
+ 'UNAVAILABLE',
284
+ 'TIMEOUT',
285
+ 'UNSUPPORTED_SERVICE',
286
+ 'UNAUTHORIZED',
287
+ 'MALFORMED_RESPONSE',
288
+ 'INVALID_PROOF',
289
+ 'ISSUER_MISMATCH',
290
+ 'ID_MISMATCH',
291
+ 'ROLLBACK',
292
+ 'REVOKED',
293
+ 'UNSAFE_ENDPOINT',
294
+ ]);
295
+ export type CredentialRefreshFailureCode = z.infer<typeof CredentialRefreshFailureCodeValidator>;
296
+
297
+ export const CredentialRefreshUpdatedResultValidator = z.object({
298
+ status: z.literal('updated'),
299
+ credential: VCValidator,
300
+ etag: z.string().optional(),
301
+ managedVersion: z.number().int().positive().optional(),
302
+ });
303
+ export type CredentialRefreshUpdatedResult = z.infer<
304
+ typeof CredentialRefreshUpdatedResultValidator
305
+ >;
306
+
307
+ export const CredentialRefreshUnchangedResultValidator = z.object({
308
+ status: z.literal('unchanged'),
309
+ checkedAt: z.string().min(1),
310
+ etag: z.string().optional(),
311
+ });
312
+ export type CredentialRefreshUnchangedResult = z.infer<
313
+ typeof CredentialRefreshUnchangedResultValidator
314
+ >;
315
+
316
+ export const CredentialRefreshUnsupportedResultValidator = z.object({
317
+ status: z.literal('unsupported'),
318
+ });
319
+ export type CredentialRefreshUnsupportedResult = z.infer<
320
+ typeof CredentialRefreshUnsupportedResultValidator
321
+ >;
322
+
323
+ export const CredentialRefreshFailedResultValidator = z.object({
324
+ status: z.literal('failed'),
325
+ code: CredentialRefreshFailureCodeValidator,
326
+ retryable: z.boolean(),
327
+ });
328
+ export type CredentialRefreshFailedResult = z.infer<typeof CredentialRefreshFailedResultValidator>;
329
+
330
+ export const CredentialRefreshResultValidator = z.discriminatedUnion('status', [
331
+ CredentialRefreshUpdatedResultValidator,
332
+ CredentialRefreshUnchangedResultValidator,
333
+ CredentialRefreshUnsupportedResultValidator,
334
+ CredentialRefreshFailedResultValidator,
335
+ ]);
336
+ export type CredentialRefreshResult = z.infer<typeof CredentialRefreshResultValidator>;
package/src/index.ts CHANGED
@@ -16,3 +16,4 @@ export * from './queries';
16
16
  export * from './auth';
17
17
  export * from './bitstring-status-list';
18
18
  export * from './inAppMessages';
19
+ export * from './credential-refresh';
package/src/lcn.ts CHANGED
@@ -154,6 +154,10 @@ export type LCNAuthedProfile = z.infer<typeof LCNAuthedProfileValidator>;
154
154
 
155
155
  export const LCNConnectionProfileValidator = LCNAuthedProfileValidator.extend({
156
156
  email: LCNProfileValidator.shape.email,
157
+ connectedAt: z.iso
158
+ .datetime()
159
+ .optional()
160
+ .describe('When the viewer and this profile became connected.'),
157
161
  });
158
162
  export type LCNConnectionProfile = z.infer<typeof LCNConnectionProfileValidator>;
159
163
 
@@ -508,6 +512,15 @@ export const SendOptionsValidator = z.object({
508
512
  .email()
509
513
  .optional()
510
514
  .describe('Guardian email that must approve before student can claim'),
515
+ expiresInDays: z
516
+ .number()
517
+ .int()
518
+ .min(1)
519
+ .max(720)
520
+ .optional()
521
+ .describe(
522
+ 'How many days the credential stays claimable in the Universal Inbox (default 30). Does not change the credential validity period.'
523
+ ),
511
524
  });
512
525
  export type SendOptions = z.infer<typeof SendOptionsValidator>;
513
526
 
@@ -968,6 +981,7 @@ export const LCNNotificationTypeEnumValidator = z.enum([
968
981
  'CREDENTIAL_REVOKED',
969
982
  'CREDENTIAL_SUSPENDED',
970
983
  'CREDENTIAL_UNSUSPENDED',
984
+ 'CREDENTIAL_REFRESHED',
971
985
  ]);
972
986
 
973
987
  export type LCNNotificationTypeEnum = z.infer<typeof LCNNotificationTypeEnumValidator>;
@@ -1182,12 +1196,16 @@ export type CreateContactMethodSessionResponseType = z.infer<
1182
1196
  // Inbox Credentials
1183
1197
  export const InboxCredentialValidator = z.object({
1184
1198
  id: z.string(),
1185
- credential: z.string(),
1199
+ credential: z.string().optional(),
1186
1200
  isSigned: z.boolean(),
1187
1201
  currentStatus: LCNInboxStatusEnumValidator,
1188
1202
  isAccepted: z.boolean().optional(),
1189
1203
  expiresAt: z.string(),
1190
1204
  createdAt: z.string(),
1205
+ finalizedAt: z.string().optional(),
1206
+ expiredAt: z.string().optional(),
1207
+ credentialName: z.string().optional(),
1208
+ achievementType: z.string().optional(),
1191
1209
  issuerDid: z.string(),
1192
1210
  webhookUrl: z.string().optional(),
1193
1211
  boostUri: z.string().optional(),
@@ -1274,10 +1292,13 @@ export const IssueInboxCredentialValidator = z
1274
1292
  .describe('The webhook URL to receive credential issuance events.'),
1275
1293
  expiresInDays: z
1276
1294
  .number()
1295
+ .int()
1277
1296
  .min(1)
1278
- .max(365)
1297
+ .max(720)
1279
1298
  .optional()
1280
- .describe('The number of days the credential will be valid for.'),
1299
+ .describe(
1300
+ 'How many days the encrypted inbox payload remains claimable. This does not change the credential validity period.'
1301
+ ),
1281
1302
  templateData: z
1282
1303
  .record(z.string(), z.unknown())
1283
1304
  .optional()
@@ -1397,6 +1418,15 @@ export const ClaimInboxCredentialValidator = z.object({
1397
1418
  configuration: z
1398
1419
  .object({
1399
1420
  publishableKey: z.string(),
1421
+ expiresInDays: z
1422
+ .number()
1423
+ .int()
1424
+ .min(1)
1425
+ .max(720)
1426
+ .optional()
1427
+ .describe(
1428
+ 'Inbox claim window in days. Defaults to 720; use a shorter window for sensitive records.'
1429
+ ),
1400
1430
  signingAuthorityName: z.string().optional(),
1401
1431
  listingId: z.string().optional(),
1402
1432
  listingSlug: z.string().optional(),