@learncard/network-brain-service 3.18.0 → 3.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/.env.example +4 -0
- package/CHANGELOG.md +80 -0
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
- package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
- package/dist/cache/atomic-counter.test.d.ts +2 -0
- package/dist/cache/atomic-counter.test.d.ts.map +1 -0
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/config/environment.d.ts +46 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
- package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential.helpers.d.ts +2 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
- package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
- package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
- package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox.helpers.d.ts +9 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
- package/dist/helpers/shim.d.ts +1 -1
- package/dist/helpers/shim.d.ts.map +1 -1
- package/dist/inbox-queue-local.d.ts +2 -0
- package/dist/inbox-queue-local.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AuthGrant.d.ts.map +1 -1
- package/dist/models/CredentialActivity.d.ts +1 -0
- package/dist/models/CredentialActivity.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -2
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/InboxCredential.d.ts +1 -0
- package/dist/models/InboxCredential.d.ts.map +1 -1
- package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/routes/act-as.test.d.ts +2 -0
- package/dist/routes/act-as.test.d.ts.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +10 -0
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +494 -0
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +953 -11
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +42 -126
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
- package/dist/services/registry/registry.factory.d.ts.map +1 -1
- package/dist/types/activity.d.ts +4 -0
- package/dist/types/activity.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +10 -1
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/inbox-batch.d.ts +38 -0
- package/dist/types/inbox-batch.d.ts.map +1 -0
- package/inbox-e2e-setup.ts +53 -1
- package/inbox-elasticmq.conf +10 -0
- package/lambda.ts +61 -16
- package/package.json +18 -16
- package/scripts/setup-inbox-queues.ts +60 -0
- package/serverless-local.yml +2 -0
- package/serverless.yml +75 -0
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/credential-activity/create.ts +1 -0
- package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
- package/src/accesslayer/credential-refresh/index.ts +1 -0
- package/src/accesslayer/credential-refresh/read.ts +8 -2
- package/src/accesslayer/credential-refresh/update.ts +4 -0
- package/src/accesslayer/inbox-batch/store.ts +469 -0
- package/src/accesslayer/inbox-credential/create.ts +51 -19
- package/src/accesslayer/inbox-credential/refresh.ts +221 -0
- package/src/accesslayer/inbox-credential/update.ts +1 -1
- package/src/cache/atomic-counter.test-helpers.ts +27 -0
- package/src/cache/atomic-counter.test.ts +6 -0
- package/src/cache/index.ts +25 -13
- package/src/config/environment.ts +26 -0
- package/src/credential-refresh.ts +5 -1
- package/src/docker-entry.ts +8 -0
- package/src/helpers/activity.helpers.ts +4 -0
- package/src/helpers/app-counter.helpers.ts +1 -1
- package/src/helpers/contact-method.helpers.test.ts +43 -0
- package/src/helpers/contact-method.helpers.ts +3 -4
- package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
- package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
- package/src/helpers/credential-refresh-email.helpers.ts +201 -0
- package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
- package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
- package/src/helpers/credential-refresh.helpers.ts +375 -99
- package/src/helpers/credential.helpers.ts +4 -1
- package/src/helpers/finalize-inbox.helpers.ts +29 -14
- package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
- package/src/helpers/inbox-batch-http.helpers.ts +150 -0
- package/src/helpers/inbox-batch-status.helpers.ts +11 -0
- package/src/helpers/inbox-batch.helpers.test.ts +668 -0
- package/src/helpers/inbox-batch.helpers.ts +374 -0
- package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
- package/src/helpers/inbox-queue.helpers.test.ts +253 -0
- package/src/helpers/inbox-queue.helpers.ts +372 -0
- package/src/helpers/inbox-refresh.helpers.ts +432 -0
- package/src/helpers/inbox.helpers.ts +268 -32
- package/src/helpers/notifications.helpers.ts +4 -2
- package/src/helpers/rateLimit.helpers.ts +2 -5
- package/src/helpers/refresh-send-intent.helpers.ts +273 -0
- package/src/helpers/shim.ts +32 -31
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/CredentialActivity.ts +3 -5
- package/src/models/CredentialRefresh.ts +4 -2
- package/src/models/InboxCredential.ts +2 -0
- package/src/models/credential-refresh-constraints.test.ts +8 -3
- package/src/models/credential-refresh-constraints.ts +5 -0
- package/src/openapi.ts +8 -0
- package/src/routes/act-as.test.ts +239 -0
- package/src/routes/app-store.ts +2 -2
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +914 -92
- package/src/routes/credential-refreshes.test.ts +8 -2
- package/src/routes/credential-refreshes.ts +4 -1
- package/src/routes/credentials.ts +2 -1
- package/src/routes/inbox.ts +95 -49
- package/src/routes/index.ts +82 -7
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +43 -24
- package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
- package/src/services/delivery/adapters/postmark.adapter.ts +12 -0
- package/src/services/registry/registry.factory.ts +2 -1
- package/src/types/activity.ts +2 -0
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/inbox-batch.ts +46 -0
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +2 -0
- package/test/helpers/inbox-e2e-environment.ts +6 -0
- package/test/inbox-batch.spec.ts +1338 -0
- package/test/inbox-refresh.spec.ts +534 -0
- package/test/openapi.spec.ts +41 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- package/vitest.config.ts +9 -0
- package/vitest.inbox-e2e.config.ts +5 -1
- package/vitest.integration.config.ts +2 -0
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import { environment } from '@environment';
|
|
2
|
+
import { recordInboxRefreshClaimUrl } from '@accesslayer/inbox-credential/refresh';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
assertInboxRefreshEnabled,
|
|
6
|
+
prepareInboxRefresh,
|
|
7
|
+
getInboxRefreshReceipt,
|
|
8
|
+
finalizeInboxRefresh,
|
|
9
|
+
getInboxRefreshIssueKey,
|
|
10
|
+
resumeInboxRefreshDelivery,
|
|
11
|
+
} from './inbox-refresh.helpers';
|
|
1
12
|
import { TRPCError } from '@trpc/server';
|
|
13
|
+
import {
|
|
14
|
+
InboxDeliveryCheckpointError,
|
|
15
|
+
InboxIssuancePreflightError,
|
|
16
|
+
} from './inbox-issuance-error.helpers';
|
|
2
17
|
import {
|
|
3
18
|
VC,
|
|
4
19
|
UnsignedVC,
|
|
@@ -32,7 +47,7 @@ import { doesProfileManageProfile } from '@accesslayer/profile-manager/relations
|
|
|
32
47
|
import { getProfilesThatManageAProfile } from '@accesslayer/profile/relationships/read';
|
|
33
48
|
import { getProfileForInboxCredential } from '@accesslayer/inbox-credential/read';
|
|
34
49
|
import { sendCredential } from '@helpers/credential.helpers';
|
|
35
|
-
import { sendBoost } from '@helpers/boost.helpers';
|
|
50
|
+
import { prepareCredentialFromBoost, getBoostUri, sendBoost } from '@helpers/boost.helpers';
|
|
36
51
|
import { getBoostByUri } from '@accesslayer/boost/read';
|
|
37
52
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
38
53
|
import { getSigningAuthorityForUserByName } from '@accesslayer/signing-authority/relationships/read';
|
|
@@ -192,9 +207,15 @@ export const issueToInbox = async (
|
|
|
192
207
|
activityId?: string;
|
|
193
208
|
integrationId?: string;
|
|
194
209
|
guardianEmail?: string;
|
|
210
|
+
refresh?: boolean;
|
|
211
|
+
idempotencyKey?: string;
|
|
212
|
+
refreshRequestDigest?: string;
|
|
195
213
|
} = {},
|
|
196
|
-
ctx: Context
|
|
214
|
+
ctx: Context,
|
|
215
|
+
/** Queue ownership checkpoint immediately before delivery or inbox persistence. */
|
|
216
|
+
beforeDelivery?: () => Promise<void>
|
|
197
217
|
): Promise<{
|
|
218
|
+
refresh?: import('@learncard/types').InboxCredentialRefreshReceipt;
|
|
198
219
|
status: 'PENDING' | 'ISSUED' | 'EXPIRED' | 'DELIVERED' | 'CLAIMED'; // DELIVERED & CLAIMED are deprecated, use ISSUED
|
|
199
220
|
inboxCredential: InboxCredentialType;
|
|
200
221
|
claimUrl?: string;
|
|
@@ -211,13 +232,35 @@ export const issueToInbox = async (
|
|
|
211
232
|
integrationId,
|
|
212
233
|
guardianEmail,
|
|
213
234
|
} = configuration;
|
|
235
|
+
if (configuration.refresh) {
|
|
236
|
+
await assertInboxRefreshEnabled(ctx.user?.scope);
|
|
237
|
+
if (
|
|
238
|
+
credential.proof ||
|
|
239
|
+
credential.type.includes('VerifiablePresentation') ||
|
|
240
|
+
Array.isArray(credential.credentialSubject)
|
|
241
|
+
) {
|
|
242
|
+
throw new TRPCError({
|
|
243
|
+
code: 'BAD_REQUEST',
|
|
244
|
+
message:
|
|
245
|
+
'Inbox refresh requires an unsigned credential with a single subject and a signing authority.',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
214
249
|
const isSigned = !!credential?.proof;
|
|
215
250
|
let signingAuthority: IssueInboxSigningAuthority | undefined = _signingAuthority;
|
|
251
|
+
let deliveryCheckpointReached = beforeDelivery === undefined;
|
|
252
|
+
const checkpointDelivery = async (): Promise<void> => {
|
|
253
|
+
await beforeDelivery?.();
|
|
254
|
+
deliveryCheckpointReached = true;
|
|
255
|
+
};
|
|
256
|
+
const assertDeliveryCheckpoint = (result: InboxDeliveryCheckpointError['result']): void => {
|
|
257
|
+
if (!deliveryCheckpointReached) throw new InboxDeliveryCheckpointError(result);
|
|
258
|
+
};
|
|
216
259
|
|
|
217
260
|
if (recipient.type === 'phone') {
|
|
218
261
|
const isTrusted = await getRegistryService().isTrusted(issuerProfile.did);
|
|
219
262
|
if (!isTrusted) {
|
|
220
|
-
throw new
|
|
263
|
+
throw new InboxIssuancePreflightError({
|
|
221
264
|
code: 'FORBIDDEN',
|
|
222
265
|
message:
|
|
223
266
|
'Sending credentials via phone is a feature reserved for members of the LearnCard Trusted Registry. Email delivery is available for all issuers. To verify your issuer, visit: https://docs.learncard.com/how-to-guides/verify-my-issuer',
|
|
@@ -245,20 +288,35 @@ export const issueToInbox = async (
|
|
|
245
288
|
* By providing this parameter, the developer explicitly takes responsibility for the signing process. We trust them and proceed.
|
|
246
289
|
**/
|
|
247
290
|
if (!(await verifyCredentialCanBeSigned(credential as UnsignedVC))) {
|
|
248
|
-
throw new
|
|
291
|
+
throw new InboxIssuancePreflightError({
|
|
249
292
|
code: 'BAD_REQUEST',
|
|
250
293
|
message:
|
|
251
294
|
'Credential failed to pass a pre-flight issuance test. Please verify that the credential is well-formed and can be issued.',
|
|
252
295
|
});
|
|
253
296
|
}
|
|
254
297
|
} else {
|
|
255
|
-
throw new
|
|
298
|
+
throw new InboxIssuancePreflightError({
|
|
256
299
|
code: 'BAD_REQUEST',
|
|
257
300
|
message: 'Unsigned credentials require a signing authority',
|
|
258
301
|
});
|
|
259
302
|
}
|
|
260
303
|
}
|
|
261
304
|
|
|
305
|
+
if (
|
|
306
|
+
configuration.refresh &&
|
|
307
|
+
(!signingAuthority ||
|
|
308
|
+
!(await getSigningAuthorityForUserByName(
|
|
309
|
+
issuerProfile,
|
|
310
|
+
signingAuthority.endpoint,
|
|
311
|
+
signingAuthority.name
|
|
312
|
+
)))
|
|
313
|
+
) {
|
|
314
|
+
throw new TRPCError({
|
|
315
|
+
code: 'NOT_FOUND',
|
|
316
|
+
message: 'Signing authority not found for issuer',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
262
320
|
// Check if recipient already exists with verified email
|
|
263
321
|
const existingProfile = await getProfileByVerifiedContactMethod(
|
|
264
322
|
recipient.type,
|
|
@@ -272,7 +330,7 @@ export const issueToInbox = async (
|
|
|
272
330
|
: [];
|
|
273
331
|
const recipientIsManaged = recipientManagers.length > 0;
|
|
274
332
|
|
|
275
|
-
if (existingProfile && !guardianEmail && !recipientIsManaged) {
|
|
333
|
+
if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
|
|
276
334
|
// Auto-deliver to existing user
|
|
277
335
|
let finalCredential: VC;
|
|
278
336
|
|
|
@@ -287,7 +345,7 @@ export const issueToInbox = async (
|
|
|
287
345
|
);
|
|
288
346
|
|
|
289
347
|
if (!signingAuthorityForUser) {
|
|
290
|
-
throw new
|
|
348
|
+
throw new InboxIssuancePreflightError({
|
|
291
349
|
code: 'NOT_FOUND',
|
|
292
350
|
message: 'Signing authority not found for issuer',
|
|
293
351
|
});
|
|
@@ -317,31 +375,20 @@ export const issueToInbox = async (
|
|
|
317
375
|
credential: encryptedDelivery,
|
|
318
376
|
statusEntries: getBitstringStatusListEntries(finalCredential),
|
|
319
377
|
};
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
} else {
|
|
333
|
-
// Fallback to sendCredential if boost not found
|
|
334
|
-
await sendCredential(
|
|
335
|
-
issuerProfile,
|
|
336
|
-
existingProfile,
|
|
337
|
-
delivery,
|
|
338
|
-
ctx.domain,
|
|
339
|
-
undefined,
|
|
340
|
-
activityId,
|
|
341
|
-
integrationId
|
|
342
|
-
);
|
|
343
|
-
}
|
|
378
|
+
const boost = boostUri ? await getBoostByUri(boostUri) : undefined;
|
|
379
|
+
await checkpointDelivery();
|
|
380
|
+
if (boostUri && boost) {
|
|
381
|
+
await sendBoost({
|
|
382
|
+
from: { type: 'profile', profile: issuerProfile },
|
|
383
|
+
to: existingProfile,
|
|
384
|
+
boost,
|
|
385
|
+
credential: delivery,
|
|
386
|
+
domain: ctx.domain,
|
|
387
|
+
activityId,
|
|
388
|
+
integrationId,
|
|
389
|
+
});
|
|
344
390
|
} else {
|
|
391
|
+
// Fall back to ordinary delivery when no boost exists for the supplied URI.
|
|
345
392
|
await sendCredential(
|
|
346
393
|
issuerProfile,
|
|
347
394
|
existingProfile,
|
|
@@ -372,6 +419,7 @@ export const issueToInbox = async (
|
|
|
372
419
|
await logCredentialDelivered({
|
|
373
420
|
activityId,
|
|
374
421
|
actorProfileId: issuerProfile.profileId,
|
|
422
|
+
onBehalfOf: ctx.user?.onBehalfOf,
|
|
375
423
|
recipientType: recipient.type as 'email' | 'phone',
|
|
376
424
|
recipientIdentifier: recipient.value,
|
|
377
425
|
recipientProfileId: existingProfile.profileId,
|
|
@@ -420,6 +468,11 @@ export const issueToInbox = async (
|
|
|
420
468
|
});
|
|
421
469
|
}
|
|
422
470
|
|
|
471
|
+
assertDeliveryCheckpoint({
|
|
472
|
+
issuanceId: finalizedInboxCredential.id,
|
|
473
|
+
status: LCNInboxStatusEnumValidator.enum.ISSUED,
|
|
474
|
+
recipientDid: existingProfile.did,
|
|
475
|
+
});
|
|
423
476
|
return {
|
|
424
477
|
status: LCNInboxStatusEnumValidator.enum.ISSUED,
|
|
425
478
|
inboxCredential: finalizedInboxCredential,
|
|
@@ -429,7 +482,24 @@ export const issueToInbox = async (
|
|
|
429
482
|
// Store in inbox for claiming
|
|
430
483
|
// Guardian gate if issuer specified guardianEmail OR recipient is a managed child
|
|
431
484
|
const needsGuardianGate = !!guardianEmail || recipientIsManaged;
|
|
485
|
+
const preparedRefresh = configuration.refresh
|
|
486
|
+
? await prepareInboxRefresh(credential, issuerProfile, ctx.domain)
|
|
487
|
+
: undefined;
|
|
488
|
+
if (preparedRefresh) credential = preparedRefresh.credential;
|
|
489
|
+
await checkpointDelivery();
|
|
432
490
|
const inboxCredential = await createInboxCredential({
|
|
491
|
+
...(preparedRefresh
|
|
492
|
+
? {
|
|
493
|
+
refresh: {
|
|
494
|
+
aggregate: preparedRefresh.aggregate,
|
|
495
|
+
issueKey: getInboxRefreshIssueKey(
|
|
496
|
+
issuerProfile.profileId,
|
|
497
|
+
configuration.idempotencyKey ?? randomUUID()
|
|
498
|
+
),
|
|
499
|
+
requestDigest: configuration.refreshRequestDigest!,
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
: {}),
|
|
433
503
|
credential: JSON.stringify(credential),
|
|
434
504
|
isSigned,
|
|
435
505
|
recipient,
|
|
@@ -444,6 +514,101 @@ export const issueToInbox = async (
|
|
|
444
514
|
...(needsGuardianGate ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
|
|
445
515
|
});
|
|
446
516
|
|
|
517
|
+
const refresh = inboxCredential.refreshId
|
|
518
|
+
? await getInboxRefreshReceipt(inboxCredential.refreshId, ctx.domain)
|
|
519
|
+
: undefined;
|
|
520
|
+
if (refresh && existingProfile && !needsGuardianGate) {
|
|
521
|
+
await finalizeInboxRefresh({
|
|
522
|
+
inboxId: inboxCredential.id,
|
|
523
|
+
holderDid: existingProfile.did,
|
|
524
|
+
holderProfile: existingProfile,
|
|
525
|
+
domain: ctx.domain,
|
|
526
|
+
accepted: false,
|
|
527
|
+
});
|
|
528
|
+
await resumeInboxRefreshDelivery(refresh.refreshId, ctx.domain);
|
|
529
|
+
await createDeliveredRelationship(
|
|
530
|
+
issuerProfile.profileId,
|
|
531
|
+
inboxCredential.id,
|
|
532
|
+
existingProfile.did,
|
|
533
|
+
'auto-delivery'
|
|
534
|
+
);
|
|
535
|
+
if (activityId)
|
|
536
|
+
await logCredentialDelivered({
|
|
537
|
+
activityId,
|
|
538
|
+
actorProfileId: issuerProfile.profileId,
|
|
539
|
+
onBehalfOf: ctx.user?.onBehalfOf,
|
|
540
|
+
recipientType: recipient.type,
|
|
541
|
+
recipientIdentifier: recipient.value,
|
|
542
|
+
recipientProfileId: existingProfile.profileId,
|
|
543
|
+
boostUri,
|
|
544
|
+
inboxCredentialId: inboxCredential.id,
|
|
545
|
+
integrationId,
|
|
546
|
+
source: 'send',
|
|
547
|
+
});
|
|
548
|
+
if (webhookUrl)
|
|
549
|
+
await addNotificationToQueue({
|
|
550
|
+
webhookUrl,
|
|
551
|
+
type: LCNNotificationTypeEnumValidator.enum.ISSUANCE_DELIVERED,
|
|
552
|
+
from: { did: (await getLearnCard()).id.did() },
|
|
553
|
+
to: issuerProfile,
|
|
554
|
+
message: getNotificationMessage(
|
|
555
|
+
'issuanceDelivered',
|
|
556
|
+
resolveRecipientLocale(issuerProfile),
|
|
557
|
+
{
|
|
558
|
+
issuer: issuerProfile.displayName,
|
|
559
|
+
recipientType: recipient.type,
|
|
560
|
+
recipientValue: recipient.value,
|
|
561
|
+
}
|
|
562
|
+
),
|
|
563
|
+
data: {
|
|
564
|
+
inbox: {
|
|
565
|
+
issuanceId: inboxCredential.id,
|
|
566
|
+
status: LCNInboxStatusEnumValidator.enum.ISSUED,
|
|
567
|
+
recipient: {
|
|
568
|
+
contactMethod: recipient,
|
|
569
|
+
learnCardId: existingProfile.did,
|
|
570
|
+
},
|
|
571
|
+
timestamp: new Date().toISOString(),
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
// Existing accounts receive the signed credential in their app inbox.
|
|
576
|
+
// Send the email there too, without a claim token for an already-bound delivery.
|
|
577
|
+
if (recipient.type === 'email' && !delivery?.suppress) {
|
|
578
|
+
try {
|
|
579
|
+
const appUrl = environment.IS_OFFLINE
|
|
580
|
+
? generateClaimUrl('')
|
|
581
|
+
: ctx.tenant?.emailBranding?.appUrl || generateClaimUrl('');
|
|
582
|
+
await getDeliveryService(recipient).send({
|
|
583
|
+
contactMethod: recipient,
|
|
584
|
+
templateId: 'universal-inbox-claim',
|
|
585
|
+
templateModel: {
|
|
586
|
+
claimUrl: new URL('/notifications', appUrl).href,
|
|
587
|
+
issuer: { name: issuerProfile.displayName },
|
|
588
|
+
credential: { name: credential.name },
|
|
589
|
+
},
|
|
590
|
+
branding: ctx.tenant?.emailBranding,
|
|
591
|
+
locale: resolveRecipientLocale(existingProfile),
|
|
592
|
+
messageStream: 'universal-inbox',
|
|
593
|
+
});
|
|
594
|
+
} catch {
|
|
595
|
+
// The credential and in-app notification are already durable. An
|
|
596
|
+
// email transport failure must not turn that delivery into an error.
|
|
597
|
+
console.warn('Inbox refresh: initial email failed after auto-delivery.');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
status: 'ISSUED',
|
|
602
|
+
inboxCredential: {
|
|
603
|
+
...inboxCredential,
|
|
604
|
+
currentStatus: 'ISSUED',
|
|
605
|
+
credential: undefined,
|
|
606
|
+
},
|
|
607
|
+
recipientDid: existingProfile.did,
|
|
608
|
+
refresh: { ...refresh, holderDid: existingProfile.did },
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
447
612
|
// Generate claim token
|
|
448
613
|
let recipientContactMethod = await getContactMethodByValue(recipient.type, recipient.value);
|
|
449
614
|
if (!recipientContactMethod) {
|
|
@@ -817,13 +982,84 @@ export const issueToInbox = async (
|
|
|
817
982
|
expiresInDays ? 24 * expiresInDays : 24,
|
|
818
983
|
false
|
|
819
984
|
);
|
|
820
|
-
const claimUrl =
|
|
985
|
+
const claimUrl = refresh
|
|
986
|
+
? await recordInboxRefreshClaimUrl(inboxCredential.id, generateClaimUrl(claimToken))
|
|
987
|
+
: generateClaimUrl(claimToken);
|
|
821
988
|
|
|
989
|
+
assertDeliveryCheckpoint({
|
|
990
|
+
issuanceId: inboxCredential.id,
|
|
991
|
+
status: LCNInboxStatusEnumValidator.enum.PENDING,
|
|
992
|
+
claimUrl,
|
|
993
|
+
...(guardianEmail ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
|
|
994
|
+
});
|
|
822
995
|
return {
|
|
823
996
|
status: LCNInboxStatusEnumValidator.enum.PENDING,
|
|
824
997
|
inboxCredential,
|
|
825
998
|
claimUrl,
|
|
999
|
+
...(refresh ? { refresh } : {}),
|
|
826
1000
|
...(guardianEmail ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
|
|
827
1001
|
};
|
|
828
1002
|
}
|
|
829
1003
|
};
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Resolves an input into the credential that issueToInbox consumes. Keeping this outside either
|
|
1007
|
+
* route makes direct credentials and Boost templates behave identically for single and batch
|
|
1008
|
+
* issuance, including template rendering, Boost metadata, and standard client-facing errors.
|
|
1009
|
+
*/
|
|
1010
|
+
export const resolveInboxCredentialInput = async (
|
|
1011
|
+
input: IssueInboxCredentialType,
|
|
1012
|
+
ctx: Context
|
|
1013
|
+
): Promise<{ credential: VC | UnsignedVC | VP; resolvedBoostUri?: string }> => {
|
|
1014
|
+
const { templateUri, configuration } = input;
|
|
1015
|
+
// A direct credential wins when both fields are present, matching the existing single-route
|
|
1016
|
+
// behavior. A template is only loaded when the caller supplied no credential.
|
|
1017
|
+
let credential = input.credential;
|
|
1018
|
+
let resolvedBoostUri: string | undefined;
|
|
1019
|
+
|
|
1020
|
+
if (templateUri && !credential) {
|
|
1021
|
+
const boostInstance = await getBoostByUri(templateUri);
|
|
1022
|
+
|
|
1023
|
+
if (!boostInstance) {
|
|
1024
|
+
throw new TRPCError({
|
|
1025
|
+
code: 'NOT_FOUND',
|
|
1026
|
+
message: `Boost not found: ${templateUri}`,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (!boostInstance.dataValues.boost) {
|
|
1031
|
+
throw new TRPCError({
|
|
1032
|
+
code: 'BAD_REQUEST',
|
|
1033
|
+
message: `Boost does not contain a credential template: ${templateUri}`,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
// Use shared helper to prepare credential with templateData rendering,
|
|
1039
|
+
// issuance date, boostId injection, and OBv3 alignments
|
|
1040
|
+
resolvedBoostUri = getBoostUri(boostInstance.id, ctx.domain);
|
|
1041
|
+
|
|
1042
|
+
credential = await prepareCredentialFromBoost(
|
|
1043
|
+
boostInstance,
|
|
1044
|
+
resolvedBoostUri,
|
|
1045
|
+
ctx.domain,
|
|
1046
|
+
{ templateData: configuration?.templateData as Record<string, unknown> }
|
|
1047
|
+
);
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
console.error('Failed to prepare boost credential', e);
|
|
1050
|
+
throw new TRPCError({
|
|
1051
|
+
code: 'BAD_REQUEST',
|
|
1052
|
+
message: `Failed to prepare boost credential template: ${templateUri}`,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (!credential) {
|
|
1058
|
+
throw new TRPCError({
|
|
1059
|
+
code: 'BAD_REQUEST',
|
|
1060
|
+
message: 'Either credential or templateUri must be provided',
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
return { credential, resolvedBoostUri };
|
|
1065
|
+
};
|
|
@@ -223,8 +223,10 @@ export const buildCredentialRefreshedNotification = (
|
|
|
223
223
|
return {
|
|
224
224
|
notification: {
|
|
225
225
|
type: LCNNotificationTypeEnumValidator.enum.CREDENTIAL_REFRESHED,
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// Local webhook delivery normalizes these DIDs in place. Keep the
|
|
227
|
+
// database profiles intact for the subsequent verified-email lookup.
|
|
228
|
+
to: { ...holderProfile },
|
|
229
|
+
from: { ...issuerProfile },
|
|
228
230
|
message: getNotificationMessage(
|
|
229
231
|
'credentialRefreshed',
|
|
230
232
|
resolveRecipientLocale(holderProfile),
|
|
@@ -24,7 +24,7 @@ export type RateLimitWindow = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Consume
|
|
27
|
+
* Consume the requested units against each window, in order, and throw once any is
|
|
28
28
|
* exhausted.
|
|
29
29
|
*
|
|
30
30
|
* Fails CLOSED: if the cache is unavailable, `cache.incr` returns `undefined`
|
|
@@ -48,11 +48,8 @@ export const enforceRateLimits = async (windows: RateLimitWindow[]): Promise<voi
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (count > limit) {
|
|
51
|
-
// tRPC has no native 429; the existing convention in this service is
|
|
52
|
-
// to cast to BAD_REQUEST while keeping the semantic code, so clients
|
|
53
|
-
// can still discriminate on it.
|
|
54
51
|
throw new TRPCError({
|
|
55
|
-
code: 'TOO_MANY_REQUESTS'
|
|
52
|
+
code: 'TOO_MANY_REQUESTS',
|
|
56
53
|
message: `Rate limit exceeded: ${description}`,
|
|
57
54
|
});
|
|
58
55
|
}
|