@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/dist/credential-refresh.d.ts +1634 -0
- package/dist/credential-refresh.d.ts.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lcn.d.ts +17 -3
- package/dist/lcn.d.ts.map +1 -1
- package/dist/types.cjs.development.cjs +229 -4
- package/dist/types.cjs.development.cjs.map +3 -3
- package/dist/types.cjs.production.min.cjs +7 -7
- package/dist/types.cjs.production.min.cjs.map +4 -4
- package/dist/types.esm.js +230 -4
- package/dist/types.esm.js.map +4 -4
- package/package.json +1 -1
- package/src/credential-refresh.ts +336 -0
- package/src/index.ts +1 -0
- package/src/lcn.ts +29 -3
package/package.json
CHANGED
|
@@ -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
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(
|
|
1297
|
+
.max(720)
|
|
1283
1298
|
.optional()
|
|
1284
|
-
.describe(
|
|
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(),
|