@learncard/network-brain-service 3.17.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/AGENTS.md +177 -159
- package/CHANGELOG.md +148 -0
- package/README.md +26 -8
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +5 -3
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.test.d.ts +2 -0
- package/dist/accesslayer/credential/create.test.d.ts.map +1 -0
- package/dist/accesslayer/credential/relationships/read.d.ts +1 -0
- package/dist/accesslayer/credential/relationships/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/didkit_wasm_bg.wasm +0 -0
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/boost-storage.helpers.test.d.ts +2 -0
- package/dist/helpers/boost-storage.helpers.test.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts +3 -7
- package/dist/helpers/boost.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 +4 -2
- 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/issuedCredentialStatus.helpers.d.ts +5 -0
- package/dist/helpers/issuedCredentialStatus.helpers.d.ts.map +1 -0
- 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/helpers/signingAuthority.helpers.d.ts +3 -2
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.test.d.ts +2 -0
- package/dist/helpers/signingAuthority.helpers.test.d.ts.map +1 -0
- package/dist/helpers/status-list.helpers.d.ts.map +1 -1
- package/dist/helpers/status-list.helpers.test.d.ts +2 -0
- package/dist/helpers/status-list.helpers.test.d.ts.map +1 -0
- 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/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Credential.d.ts +1 -0
- package/dist/models/Credential.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/app-store.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 +506 -12
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +48 -2
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts +4 -4
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +8 -2
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/did-metadata.d.ts +8 -8
- package/dist/routes/federation.d.ts +2 -2
- package/dist/routes/inbox.d.ts +973 -31
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +47 -127
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +6 -6
- package/dist/routes/storage.d.ts +63 -63
- package/dist/routes/test.d.ts +6 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts +14 -14
- 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/consentflowcontract.d.ts +22 -8
- package/dist/types/consentflowcontract.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/credential.d.ts +7 -0
- package/dist/types/credential.d.ts.map +1 -1
- package/dist/types/did-metadata.d.ts +2 -2
- 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/app-store-listing/relationships/read.ts +4 -3
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/consentflowcontract/relationships/create.ts +15 -5
- package/src/accesslayer/consentflowcontract/relationships/read.ts +161 -63
- package/src/accesslayer/consentflowcontract/relationships/update.ts +43 -10
- package/src/accesslayer/credential/create.test.ts +141 -0
- package/src/accesslayer/credential/create.ts +27 -3
- 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/boost-storage.helpers.test.ts +242 -0
- package/src/helpers/boost.helpers.ts +58 -437
- 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 +385 -107
- package/src/helpers/credential.helpers.ts +6 -2
- package/src/helpers/finalize-inbox.helpers.ts +39 -22
- 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 +301 -50
- package/src/helpers/issuedCredentialStatus.helpers.ts +11 -0
- 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/helpers/signingAuthority.helpers.test.ts +198 -0
- package/src/helpers/signingAuthority.helpers.ts +41 -18
- package/src/helpers/status-list.helpers.test.ts +77 -0
- package/src/helpers/status-list.helpers.ts +31 -8
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/ConsentFlowTerms.ts +4 -0
- package/src/models/ConsentFlowTransaction.ts +4 -0
- package/src/models/Credential.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 +69 -52
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +943 -120
- package/src/routes/contracts.ts +55 -7
- 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 +164 -88
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +80 -31
- 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/consentflowcontract.ts +21 -3
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/credential.ts +13 -1
- package/src/types/inbox-batch.ts +46 -0
- package/test/app-store-listing.spec.ts +66 -30
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/bitstring-status-list.spec.ts +12 -9
- package/test/consentflow.spec.ts +453 -26
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +13 -5
- package/test/guardian-gated-routes.spec.ts +337 -180
- 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/inbox-security.e2e.ts +48 -3
- package/test/openapi.spec.ts +41 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- package/test/revoke-suspend-per-instance.spec.ts +9 -1
- package/vitest.config.ts +13 -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,
|
|
@@ -12,6 +27,8 @@ import {
|
|
|
12
27
|
} from '@learncard/types';
|
|
13
28
|
|
|
14
29
|
import { ProfileType, SigningAuthorityForUserType } from 'types/profile';
|
|
30
|
+
import type { IssuedCredential } from 'types/credential';
|
|
31
|
+
import { getBitstringStatusListEntries } from '@learncard/helpers';
|
|
15
32
|
import { createInboxCredential } from '@accesslayer/inbox-credential/create';
|
|
16
33
|
import { Context } from '@routes';
|
|
17
34
|
import { getAppDidWeb } from '@helpers/did.helpers';
|
|
@@ -30,7 +47,7 @@ import { doesProfileManageProfile } from '@accesslayer/profile-manager/relations
|
|
|
30
47
|
import { getProfilesThatManageAProfile } from '@accesslayer/profile/relationships/read';
|
|
31
48
|
import { getProfileForInboxCredential } from '@accesslayer/inbox-credential/read';
|
|
32
49
|
import { sendCredential } from '@helpers/credential.helpers';
|
|
33
|
-
import { sendBoost } from '@helpers/boost.helpers';
|
|
50
|
+
import { prepareCredentialFromBoost, getBoostUri, sendBoost } from '@helpers/boost.helpers';
|
|
34
51
|
import { getBoostByUri } from '@accesslayer/boost/read';
|
|
35
52
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
36
53
|
import { getSigningAuthorityForUserByName } from '@accesslayer/signing-authority/relationships/read';
|
|
@@ -105,14 +122,16 @@ export const claimIntoInbox = async (
|
|
|
105
122
|
? getAppDidWeb(ctx.domain, listingSlug)
|
|
106
123
|
: undefined;
|
|
107
124
|
|
|
108
|
-
finalCredential = (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
finalCredential = (
|
|
126
|
+
await issueCredentialWithSigningAuthority(
|
|
127
|
+
{ type: 'profile', profile: issuerProfile },
|
|
128
|
+
credential as UnsignedVC,
|
|
129
|
+
signingAuthorityForUser,
|
|
130
|
+
ctx.domain,
|
|
131
|
+
false, // don't encrypt
|
|
132
|
+
ownerDidOverride
|
|
133
|
+
)
|
|
134
|
+
).credential as VC;
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
// Use the explicit-recipient API; the seeded encryption plugin also adds the service DID.
|
|
@@ -124,7 +143,11 @@ export const claimIntoInbox = async (
|
|
|
124
143
|
await sendCredential(
|
|
125
144
|
issuerProfile,
|
|
126
145
|
existingProfile,
|
|
127
|
-
|
|
146
|
+
{
|
|
147
|
+
kind: 'issued-credential',
|
|
148
|
+
credential: encryptedDelivery,
|
|
149
|
+
statusEntries: getBitstringStatusListEntries(finalCredential),
|
|
150
|
+
},
|
|
128
151
|
ctx.domain,
|
|
129
152
|
undefined,
|
|
130
153
|
activityId,
|
|
@@ -184,9 +207,15 @@ export const issueToInbox = async (
|
|
|
184
207
|
activityId?: string;
|
|
185
208
|
integrationId?: string;
|
|
186
209
|
guardianEmail?: string;
|
|
210
|
+
refresh?: boolean;
|
|
211
|
+
idempotencyKey?: string;
|
|
212
|
+
refreshRequestDigest?: string;
|
|
187
213
|
} = {},
|
|
188
|
-
ctx: Context
|
|
214
|
+
ctx: Context,
|
|
215
|
+
/** Queue ownership checkpoint immediately before delivery or inbox persistence. */
|
|
216
|
+
beforeDelivery?: () => Promise<void>
|
|
189
217
|
): Promise<{
|
|
218
|
+
refresh?: import('@learncard/types').InboxCredentialRefreshReceipt;
|
|
190
219
|
status: 'PENDING' | 'ISSUED' | 'EXPIRED' | 'DELIVERED' | 'CLAIMED'; // DELIVERED & CLAIMED are deprecated, use ISSUED
|
|
191
220
|
inboxCredential: InboxCredentialType;
|
|
192
221
|
claimUrl?: string;
|
|
@@ -203,13 +232,35 @@ export const issueToInbox = async (
|
|
|
203
232
|
integrationId,
|
|
204
233
|
guardianEmail,
|
|
205
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
|
+
}
|
|
206
249
|
const isSigned = !!credential?.proof;
|
|
207
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
|
+
};
|
|
208
259
|
|
|
209
260
|
if (recipient.type === 'phone') {
|
|
210
261
|
const isTrusted = await getRegistryService().isTrusted(issuerProfile.did);
|
|
211
262
|
if (!isTrusted) {
|
|
212
|
-
throw new
|
|
263
|
+
throw new InboxIssuancePreflightError({
|
|
213
264
|
code: 'FORBIDDEN',
|
|
214
265
|
message:
|
|
215
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',
|
|
@@ -237,20 +288,35 @@ export const issueToInbox = async (
|
|
|
237
288
|
* By providing this parameter, the developer explicitly takes responsibility for the signing process. We trust them and proceed.
|
|
238
289
|
**/
|
|
239
290
|
if (!(await verifyCredentialCanBeSigned(credential as UnsignedVC))) {
|
|
240
|
-
throw new
|
|
291
|
+
throw new InboxIssuancePreflightError({
|
|
241
292
|
code: 'BAD_REQUEST',
|
|
242
293
|
message:
|
|
243
294
|
'Credential failed to pass a pre-flight issuance test. Please verify that the credential is well-formed and can be issued.',
|
|
244
295
|
});
|
|
245
296
|
}
|
|
246
297
|
} else {
|
|
247
|
-
throw new
|
|
298
|
+
throw new InboxIssuancePreflightError({
|
|
248
299
|
code: 'BAD_REQUEST',
|
|
249
300
|
message: 'Unsigned credentials require a signing authority',
|
|
250
301
|
});
|
|
251
302
|
}
|
|
252
303
|
}
|
|
253
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
|
+
|
|
254
320
|
// Check if recipient already exists with verified email
|
|
255
321
|
const existingProfile = await getProfileByVerifiedContactMethod(
|
|
256
322
|
recipient.type,
|
|
@@ -264,7 +330,7 @@ export const issueToInbox = async (
|
|
|
264
330
|
: [];
|
|
265
331
|
const recipientIsManaged = recipientManagers.length > 0;
|
|
266
332
|
|
|
267
|
-
if (existingProfile && !guardianEmail && !recipientIsManaged) {
|
|
333
|
+
if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
|
|
268
334
|
// Auto-deliver to existing user
|
|
269
335
|
let finalCredential: VC;
|
|
270
336
|
|
|
@@ -279,19 +345,21 @@ export const issueToInbox = async (
|
|
|
279
345
|
);
|
|
280
346
|
|
|
281
347
|
if (!signingAuthorityForUser) {
|
|
282
|
-
throw new
|
|
348
|
+
throw new InboxIssuancePreflightError({
|
|
283
349
|
code: 'NOT_FOUND',
|
|
284
350
|
message: 'Signing authority not found for issuer',
|
|
285
351
|
});
|
|
286
352
|
}
|
|
287
353
|
|
|
288
|
-
finalCredential = (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
354
|
+
finalCredential = (
|
|
355
|
+
await issueCredentialWithSigningAuthority(
|
|
356
|
+
{ type: 'profile', profile: issuerProfile },
|
|
357
|
+
credential as UnsignedVC,
|
|
358
|
+
signingAuthorityForUser,
|
|
359
|
+
ctx.domain,
|
|
360
|
+
false // don't encrypt
|
|
361
|
+
)
|
|
362
|
+
).credential as VC;
|
|
295
363
|
}
|
|
296
364
|
|
|
297
365
|
// Send credential using appropriate helper (sendBoost handles boost tracking)
|
|
@@ -301,36 +369,30 @@ export const issueToInbox = async (
|
|
|
301
369
|
existingProfile.did,
|
|
302
370
|
issuerProfile.did,
|
|
303
371
|
]);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
encryptedDelivery,
|
|
323
|
-
ctx.domain,
|
|
324
|
-
undefined,
|
|
325
|
-
activityId,
|
|
326
|
-
integrationId
|
|
327
|
-
);
|
|
328
|
-
}
|
|
372
|
+
// Carry only public status coordinates alongside the newly encrypted payload.
|
|
373
|
+
const delivery: IssuedCredential = {
|
|
374
|
+
kind: 'issued-credential',
|
|
375
|
+
credential: encryptedDelivery,
|
|
376
|
+
statusEntries: getBitstringStatusListEntries(finalCredential),
|
|
377
|
+
};
|
|
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
|
+
});
|
|
329
390
|
} else {
|
|
391
|
+
// Fall back to ordinary delivery when no boost exists for the supplied URI.
|
|
330
392
|
await sendCredential(
|
|
331
393
|
issuerProfile,
|
|
332
394
|
existingProfile,
|
|
333
|
-
|
|
395
|
+
delivery,
|
|
334
396
|
ctx.domain,
|
|
335
397
|
undefined,
|
|
336
398
|
activityId,
|
|
@@ -357,6 +419,7 @@ export const issueToInbox = async (
|
|
|
357
419
|
await logCredentialDelivered({
|
|
358
420
|
activityId,
|
|
359
421
|
actorProfileId: issuerProfile.profileId,
|
|
422
|
+
onBehalfOf: ctx.user?.onBehalfOf,
|
|
360
423
|
recipientType: recipient.type as 'email' | 'phone',
|
|
361
424
|
recipientIdentifier: recipient.value,
|
|
362
425
|
recipientProfileId: existingProfile.profileId,
|
|
@@ -405,6 +468,11 @@ export const issueToInbox = async (
|
|
|
405
468
|
});
|
|
406
469
|
}
|
|
407
470
|
|
|
471
|
+
assertDeliveryCheckpoint({
|
|
472
|
+
issuanceId: finalizedInboxCredential.id,
|
|
473
|
+
status: LCNInboxStatusEnumValidator.enum.ISSUED,
|
|
474
|
+
recipientDid: existingProfile.did,
|
|
475
|
+
});
|
|
408
476
|
return {
|
|
409
477
|
status: LCNInboxStatusEnumValidator.enum.ISSUED,
|
|
410
478
|
inboxCredential: finalizedInboxCredential,
|
|
@@ -414,7 +482,24 @@ export const issueToInbox = async (
|
|
|
414
482
|
// Store in inbox for claiming
|
|
415
483
|
// Guardian gate if issuer specified guardianEmail OR recipient is a managed child
|
|
416
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();
|
|
417
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
|
+
: {}),
|
|
418
503
|
credential: JSON.stringify(credential),
|
|
419
504
|
isSigned,
|
|
420
505
|
recipient,
|
|
@@ -429,6 +514,101 @@ export const issueToInbox = async (
|
|
|
429
514
|
...(needsGuardianGate ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
|
|
430
515
|
});
|
|
431
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
|
+
|
|
432
612
|
// Generate claim token
|
|
433
613
|
let recipientContactMethod = await getContactMethodByValue(recipient.type, recipient.value);
|
|
434
614
|
if (!recipientContactMethod) {
|
|
@@ -802,13 +982,84 @@ export const issueToInbox = async (
|
|
|
802
982
|
expiresInDays ? 24 * expiresInDays : 24,
|
|
803
983
|
false
|
|
804
984
|
);
|
|
805
|
-
const claimUrl =
|
|
985
|
+
const claimUrl = refresh
|
|
986
|
+
? await recordInboxRefreshClaimUrl(inboxCredential.id, generateClaimUrl(claimToken))
|
|
987
|
+
: generateClaimUrl(claimToken);
|
|
806
988
|
|
|
989
|
+
assertDeliveryCheckpoint({
|
|
990
|
+
issuanceId: inboxCredential.id,
|
|
991
|
+
status: LCNInboxStatusEnumValidator.enum.PENDING,
|
|
992
|
+
claimUrl,
|
|
993
|
+
...(guardianEmail ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
|
|
994
|
+
});
|
|
807
995
|
return {
|
|
808
996
|
status: LCNInboxStatusEnumValidator.enum.PENDING,
|
|
809
997
|
inboxCredential,
|
|
810
998
|
claimUrl,
|
|
999
|
+
...(refresh ? { refresh } : {}),
|
|
811
1000
|
...(guardianEmail ? { guardianStatus: 'AWAITING_GUARDIAN' as const } : {}),
|
|
812
1001
|
};
|
|
813
1002
|
}
|
|
814
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
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JWE, UnsignedVC, VC } from '@learncard/types';
|
|
2
|
+
import type { IssuedCredential } from '../types/credential';
|
|
3
|
+
|
|
4
|
+
/** Internal envelopes never have the top-level fields of a wire-format VC or JWE. */
|
|
5
|
+
export const isIssuedCredential = (
|
|
6
|
+
value: UnsignedVC | VC | JWE | IssuedCredential
|
|
7
|
+
): value is IssuedCredential =>
|
|
8
|
+
!('@context' in value) && !('ciphertext' in value) && value.kind === 'issued-credential';
|
|
9
|
+
|
|
10
|
+
export const getIssuedCredentialPayload = (value: VC | JWE | IssuedCredential): VC | JWE =>
|
|
11
|
+
isIssuedCredential(value) ? value.credential : value;
|
|
@@ -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
|
}
|