@learncard/types 5.18.3 → 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.3",
3
+ "version": "5.19.0",
4
4
  "description": "Shared types for learn card",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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
@@ -512,6 +512,15 @@ export const SendOptionsValidator = z.object({
512
512
  .email()
513
513
  .optional()
514
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
+ ),
515
524
  });
516
525
  export type SendOptions = z.infer<typeof SendOptionsValidator>;
517
526
 
@@ -972,6 +981,7 @@ export const LCNNotificationTypeEnumValidator = z.enum([
972
981
  'CREDENTIAL_REVOKED',
973
982
  'CREDENTIAL_SUSPENDED',
974
983
  'CREDENTIAL_UNSUSPENDED',
984
+ 'CREDENTIAL_REFRESHED',
975
985
  ]);
976
986
 
977
987
  export type LCNNotificationTypeEnum = z.infer<typeof LCNNotificationTypeEnumValidator>;
@@ -1186,12 +1196,16 @@ export type CreateContactMethodSessionResponseType = z.infer<
1186
1196
  // Inbox Credentials
1187
1197
  export const InboxCredentialValidator = z.object({
1188
1198
  id: z.string(),
1189
- credential: z.string(),
1199
+ credential: z.string().optional(),
1190
1200
  isSigned: z.boolean(),
1191
1201
  currentStatus: LCNInboxStatusEnumValidator,
1192
1202
  isAccepted: z.boolean().optional(),
1193
1203
  expiresAt: z.string(),
1194
1204
  createdAt: z.string(),
1205
+ finalizedAt: z.string().optional(),
1206
+ expiredAt: z.string().optional(),
1207
+ credentialName: z.string().optional(),
1208
+ achievementType: z.string().optional(),
1195
1209
  issuerDid: z.string(),
1196
1210
  webhookUrl: z.string().optional(),
1197
1211
  boostUri: z.string().optional(),
@@ -1278,10 +1292,13 @@ export const IssueInboxCredentialValidator = z
1278
1292
  .describe('The webhook URL to receive credential issuance events.'),
1279
1293
  expiresInDays: z
1280
1294
  .number()
1295
+ .int()
1281
1296
  .min(1)
1282
- .max(365)
1297
+ .max(720)
1283
1298
  .optional()
1284
- .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
+ ),
1285
1302
  templateData: z
1286
1303
  .record(z.string(), z.unknown())
1287
1304
  .optional()
@@ -1401,6 +1418,15 @@ export const ClaimInboxCredentialValidator = z.object({
1401
1418
  configuration: z
1402
1419
  .object({
1403
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
+ ),
1404
1430
  signingAuthorityName: z.string().optional(),
1405
1431
  listingId: z.string().optional(),
1406
1432
  listingSlug: z.string().optional(),