@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
package/lambda.ts
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
import { environment } from './src/config/environment';
|
|
26
26
|
import { toServerlessApplication } from './src/helpers/serverlessApplication';
|
|
27
27
|
import { runInboxMaintenance } from './src/helpers/inbox-maintenance.helpers';
|
|
28
|
+
import {
|
|
29
|
+
inboxBatchResponseMeta,
|
|
30
|
+
withInboxBatchBodyLimit,
|
|
31
|
+
} from './src/helpers/inbox-batch-http.helpers';
|
|
28
32
|
|
|
29
33
|
Sentry.AWSLambda.init({
|
|
30
34
|
dsn: environment.SENTRY_DSN,
|
|
@@ -61,8 +65,9 @@ export const credentialRefreshHandler: typeof credentialRefreshProxy = async (ev
|
|
|
61
65
|
|
|
62
66
|
export const _openApiHandler = createOpenApiAwsLambdaHandler({
|
|
63
67
|
router: appRouter,
|
|
64
|
-
responseMeta:
|
|
68
|
+
responseMeta: meta => {
|
|
65
69
|
return {
|
|
70
|
+
...inboxBatchResponseMeta(meta),
|
|
66
71
|
headers: {
|
|
67
72
|
'Access-Control-Allow-Origin': '*',
|
|
68
73
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
@@ -74,21 +79,24 @@ export const _openApiHandler = createOpenApiAwsLambdaHandler({
|
|
|
74
79
|
onError: handleTrpcError,
|
|
75
80
|
});
|
|
76
81
|
|
|
77
|
-
export const _trpcHandler =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
82
|
+
export const _trpcHandler = withInboxBatchBodyLimit(
|
|
83
|
+
awsLambdaRequestHandler({
|
|
84
|
+
allowMethodOverride: true,
|
|
85
|
+
router: appRouter,
|
|
86
|
+
createContext,
|
|
87
|
+
onError: handleTrpcError,
|
|
88
|
+
responseMeta: () => {
|
|
89
|
+
return {
|
|
90
|
+
headers: {
|
|
91
|
+
'Access-Control-Allow-Origin': '*',
|
|
92
|
+
'Access-Control-Allow-Methods': '*',
|
|
93
|
+
'Access-Control-Allow-Headers': 'authorization',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
'trpc'
|
|
99
|
+
);
|
|
92
100
|
|
|
93
101
|
export const openApiHandler = Sentry.AWSLambda.wrapHandler(
|
|
94
102
|
async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
|
|
@@ -124,6 +132,43 @@ export const trpcHandler = Sentry.AWSLambda.wrapHandler(
|
|
|
124
132
|
}
|
|
125
133
|
);
|
|
126
134
|
|
|
135
|
+
export const inboxQueueWorker: SQSHandler = Sentry.AWSLambda.wrapHandler(async event => {
|
|
136
|
+
const { processInboxQueueMessage } = await import('./src/helpers/inbox-queue.helpers');
|
|
137
|
+
const batchItemFailures = [];
|
|
138
|
+
for (const record of event.Records) {
|
|
139
|
+
try {
|
|
140
|
+
await processInboxQueueMessage(record.body);
|
|
141
|
+
} catch {
|
|
142
|
+
console.error('Inbox worker message failed', { messageId: record.messageId });
|
|
143
|
+
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { batchItemFailures } satisfies SQSBatchResponse;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export const inboxQueueDispatcher = Sentry.AWSLambda.wrapHandler(
|
|
150
|
+
async (_event: unknown, context: Context): Promise<void> => {
|
|
151
|
+
const { dispatchInboxJobs } = await import('./src/helpers/inbox-queue.helpers');
|
|
152
|
+
// Share one clock across publication and recovery, retaining five seconds for shutdown.
|
|
153
|
+
const deadline = Date.now() + Math.max(0, context.getRemainingTimeInMillis() - 5_000);
|
|
154
|
+
await dispatchInboxJobs(deadline);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
export const inboxDeadLetterWorker: SQSHandler = Sentry.AWSLambda.wrapHandler(async event => {
|
|
159
|
+
const { processInboxDeadLetter } = await import('./src/helpers/inbox-queue.helpers');
|
|
160
|
+
const batchItemFailures = [];
|
|
161
|
+
for (const record of event.Records) {
|
|
162
|
+
try {
|
|
163
|
+
await processInboxDeadLetter(record.body);
|
|
164
|
+
} catch {
|
|
165
|
+
console.error('Inbox dead-letter processing failed', { messageId: record.messageId });
|
|
166
|
+
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { batchItemFailures } satisfies SQSBatchResponse;
|
|
170
|
+
});
|
|
171
|
+
|
|
127
172
|
export const notificationsWorker: SQSHandler = Sentry.AWSLambda.wrapHandler(async event => {
|
|
128
173
|
const batchItemFailures = await Promise.all(
|
|
129
174
|
event.Records.map(async record => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/network-brain-service",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.12 <25"
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"types": "dist/index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
|
+
"inbox:setup-queues": "bun scripts/setup-inbox-queues.ts",
|
|
13
|
+
"inbox:worker": "bun --conditions=development src/inbox-queue-local.ts",
|
|
12
14
|
"test": "vitest run --config vitest.config.ts",
|
|
13
15
|
"test:watch": "vitest --config vitest.config.ts",
|
|
14
16
|
"test:coverage": "vitest run --coverage --config vitest.config.ts",
|
|
@@ -59,21 +61,21 @@
|
|
|
59
61
|
"@digitalcredentials/issuer-registry-client": "^3.2.0-beta.5",
|
|
60
62
|
"@fastify/cors": "^11.2.0",
|
|
61
63
|
"@google/genai": "^1.27.0",
|
|
62
|
-
"@learncard/core": "9.4.
|
|
63
|
-
"@learncard/crypto-plugin": "1.1.
|
|
64
|
-
"@learncard/did-web-plugin": "1.1.
|
|
65
|
-
"@learncard/didkey-plugin": "1.1.
|
|
66
|
-
"@learncard/didkit-plugin": "1.10.
|
|
67
|
-
"@learncard/didkit-plugin-node": "0.3.
|
|
68
|
-
"@learncard/dynamic-loader-plugin": "1.1.
|
|
69
|
-
"@learncard/encryption-plugin": "1.1.
|
|
70
|
-
"@learncard/expiration-plugin": "1.2.
|
|
71
|
-
"@learncard/email-templates": "1.0
|
|
72
|
-
"@learncard/helpers": "1.
|
|
73
|
-
"@learncard/learn-card-plugin": "1.2.
|
|
74
|
-
"@learncard/types": "5.
|
|
75
|
-
"@learncard/vc-plugin": "1.6.
|
|
76
|
-
"@learncard/vc-templates-plugin": "1.1.
|
|
64
|
+
"@learncard/core": "9.4.36",
|
|
65
|
+
"@learncard/crypto-plugin": "1.1.36",
|
|
66
|
+
"@learncard/did-web-plugin": "1.1.36",
|
|
67
|
+
"@learncard/didkey-plugin": "1.1.36",
|
|
68
|
+
"@learncard/didkit-plugin": "1.10.1",
|
|
69
|
+
"@learncard/didkit-plugin-node": "0.3.1",
|
|
70
|
+
"@learncard/dynamic-loader-plugin": "1.1.36",
|
|
71
|
+
"@learncard/encryption-plugin": "1.1.36",
|
|
72
|
+
"@learncard/expiration-plugin": "1.2.36",
|
|
73
|
+
"@learncard/email-templates": "1.1.0",
|
|
74
|
+
"@learncard/helpers": "1.6.0",
|
|
75
|
+
"@learncard/learn-card-plugin": "1.2.36",
|
|
76
|
+
"@learncard/types": "5.21.0",
|
|
77
|
+
"@learncard/vc-plugin": "1.6.2",
|
|
78
|
+
"@learncard/vc-templates-plugin": "1.1.36",
|
|
77
79
|
"@sentry/serverless": "7.61.0",
|
|
78
80
|
"@trpc/server": "11.8.0",
|
|
79
81
|
"@types/lodash": "^4.14.191",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SQSClient,
|
|
3
|
+
CreateQueueCommand,
|
|
4
|
+
GetQueueUrlCommand,
|
|
5
|
+
GetQueueAttributesCommand,
|
|
6
|
+
SetQueueAttributesCommand,
|
|
7
|
+
} from '@aws-sdk/client-sqs';
|
|
8
|
+
|
|
9
|
+
const endpoint = process.env.INBOX_QUEUE_ENDPOINT ?? 'http://localhost:9324';
|
|
10
|
+
const client = new SQSClient({
|
|
11
|
+
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
12
|
+
endpoint,
|
|
13
|
+
credentials: { accessKeyId: 'local', secretAccessKey: 'local' },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const ensureQueue = async (name: string): Promise<string> => {
|
|
17
|
+
let url: string | undefined;
|
|
18
|
+
try {
|
|
19
|
+
url = (await client.send(new GetQueueUrlCommand({ QueueName: name }))).QueueUrl;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (
|
|
22
|
+
!(error instanceof Error) ||
|
|
23
|
+
!['QueueDoesNotExist', 'AWS.SimpleQueueService.NonExistentQueue'].includes(error.name)
|
|
24
|
+
)
|
|
25
|
+
throw error;
|
|
26
|
+
url = (await client.send(new CreateQueueCommand({ QueueName: name }))).QueueUrl;
|
|
27
|
+
}
|
|
28
|
+
if (!url) throw new Error(`Missing URL for ${name}`);
|
|
29
|
+
// ElasticMQ may advertise its container hostname; use the caller's reachable endpoint.
|
|
30
|
+
return `${endpoint.replace(/\/$/, '')}${new URL(url).pathname}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const dlqUrl = await ensureQueue('inbox-dlq');
|
|
35
|
+
const attributes = await client.send(
|
|
36
|
+
new GetQueueAttributesCommand({
|
|
37
|
+
QueueUrl: dlqUrl,
|
|
38
|
+
AttributeNames: ['QueueArn'],
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
if (!attributes.Attributes?.QueueArn)
|
|
42
|
+
throw new Error('Inbox dead-letter queue ARN is missing.');
|
|
43
|
+
const queueUrl = await ensureQueue('inbox');
|
|
44
|
+
await client.send(
|
|
45
|
+
new SetQueueAttributesCommand({
|
|
46
|
+
QueueUrl: queueUrl,
|
|
47
|
+
Attributes: {
|
|
48
|
+
VisibilityTimeout: '1800',
|
|
49
|
+
RedrivePolicy: JSON.stringify({
|
|
50
|
+
deadLetterTargetArn: attributes.Attributes.QueueArn,
|
|
51
|
+
maxReceiveCount: '5',
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
console.log(`INBOX_QUEUE_URL=${queueUrl}`);
|
|
57
|
+
console.log(`INBOX_DEAD_LETTER_QUEUE_URL=${dlqUrl}`);
|
|
58
|
+
} finally {
|
|
59
|
+
client.destroy();
|
|
60
|
+
}
|
package/serverless-local.yml
CHANGED
|
@@ -47,6 +47,8 @@ provider:
|
|
|
47
47
|
stage: ${opt:stage, "dev"}
|
|
48
48
|
region: ${opt:region, "us-east-1"}
|
|
49
49
|
environment:
|
|
50
|
+
INBOX_QUEUE_URL: ${env:INBOX_QUEUE_URL, ''}
|
|
51
|
+
INBOX_QUEUE_ENDPOINT: ${env:INBOX_QUEUE_ENDPOINT, ''}
|
|
50
52
|
PORT: ${env:PORT, 3000}
|
|
51
53
|
LAMBDA_STAGE: ${opt:stage, "dev"}
|
|
52
54
|
SEED: ${env:SEED}
|
package/serverless.yml
CHANGED
|
@@ -15,6 +15,20 @@ plugins:
|
|
|
15
15
|
- serverless-prune-plugin
|
|
16
16
|
|
|
17
17
|
constructs:
|
|
18
|
+
inbox-queue:
|
|
19
|
+
type: queue
|
|
20
|
+
batchSize: 1
|
|
21
|
+
maxConcurrency: 10
|
|
22
|
+
maxRetries: 5
|
|
23
|
+
worker:
|
|
24
|
+
handler: lambda.inboxQueueWorker
|
|
25
|
+
timeout: 300
|
|
26
|
+
reservedConcurrency: 10
|
|
27
|
+
vpc:
|
|
28
|
+
securityGroupIds:
|
|
29
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
30
|
+
subnetIds:
|
|
31
|
+
- Ref: PrivateSubnetA
|
|
18
32
|
notifications-queue:
|
|
19
33
|
type: queue
|
|
20
34
|
worker:
|
|
@@ -128,6 +142,7 @@ provider:
|
|
|
128
142
|
PORT: ${env:PORT, 3000}
|
|
129
143
|
INBOX_DELETE_EXPIRED_RECORDS: ${env:INBOX_DELETE_EXPIRED_RECORDS, "false"}
|
|
130
144
|
LAMBDA_STAGE: ${opt:stage, "dev"}
|
|
145
|
+
INBOX_BATCH_ITEMS_PER_HOUR: ${env:INBOX_BATCH_ITEMS_PER_HOUR, '10000'}
|
|
131
146
|
SEED: ${env:SEED}
|
|
132
147
|
DOMAIN_NAME: ${env:DOMAIN_NAME}
|
|
133
148
|
CLIENT_APP_DOMAIN_NAME: ${env:CLIENT_APP_DOMAIN_NAME}
|
|
@@ -153,6 +168,7 @@ provider:
|
|
|
153
168
|
REDIS_PORT:
|
|
154
169
|
'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Port]
|
|
155
170
|
NOTIFICATIONS_QUEUE_URL: ${construct:notifications-queue.queueUrl}
|
|
171
|
+
INBOX_QUEUE_URL: ${construct:inbox-queue.queueUrl}
|
|
156
172
|
SKILL_EMBEDDING_GOOGLE_API_KEY: ${env:SKILL_EMBEDDING_GOOGLE_API_KEY, ""}
|
|
157
173
|
SKILL_EMBEDDING_GOOGLE_MODEL: ${env:SKILL_EMBEDDING_GOOGLE_MODEL, "gemini-embedding-001"}
|
|
158
174
|
SKILL_EMBEDDING_BATCH_SIZE: ${env:SKILL_EMBEDDING_BATCH_SIZE, "25"}
|
|
@@ -190,6 +206,31 @@ provider:
|
|
|
190
206
|
- ETag
|
|
191
207
|
|
|
192
208
|
functions:
|
|
209
|
+
inboxDeadLetterWorker:
|
|
210
|
+
handler: lambda.inboxDeadLetterWorker
|
|
211
|
+
timeout: 30
|
|
212
|
+
reservedConcurrency: 2
|
|
213
|
+
vpc:
|
|
214
|
+
securityGroupIds:
|
|
215
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
216
|
+
subnetIds:
|
|
217
|
+
- Ref: PrivateSubnetA
|
|
218
|
+
events:
|
|
219
|
+
- sqs:
|
|
220
|
+
arn: ${construct:inbox-queue.dlqArn}
|
|
221
|
+
batchSize: 1
|
|
222
|
+
functionResponseType: ReportBatchItemFailures
|
|
223
|
+
inboxQueueDispatcher:
|
|
224
|
+
handler: lambda.inboxQueueDispatcher
|
|
225
|
+
timeout: 60
|
|
226
|
+
reservedConcurrency: 1
|
|
227
|
+
vpc:
|
|
228
|
+
securityGroupIds:
|
|
229
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
230
|
+
subnetIds:
|
|
231
|
+
- Ref: PrivateSubnetA
|
|
232
|
+
events:
|
|
233
|
+
- schedule: rate(1 minute)
|
|
193
234
|
inboxMaintenance:
|
|
194
235
|
handler: lambda.inboxMaintenanceHandler
|
|
195
236
|
timeout: 300
|
|
@@ -288,6 +329,40 @@ functions:
|
|
|
288
329
|
|
|
289
330
|
resources:
|
|
290
331
|
Resources:
|
|
332
|
+
InboxQueueAgeAlarm:
|
|
333
|
+
Type: AWS::CloudWatch::Alarm
|
|
334
|
+
Properties:
|
|
335
|
+
AlarmDescription: Inbox issuance has waited over five minutes
|
|
336
|
+
Namespace: AWS/SQS
|
|
337
|
+
MetricName: ApproximateAgeOfOldestMessage
|
|
338
|
+
Dimensions:
|
|
339
|
+
- Name: QueueName
|
|
340
|
+
Value:
|
|
341
|
+
Fn::Select:
|
|
342
|
+
- 4
|
|
343
|
+
- Fn::Split: ['/', '${construct:inbox-queue.queueUrl}']
|
|
344
|
+
Statistic: Maximum
|
|
345
|
+
Period: 60
|
|
346
|
+
EvaluationPeriods: 2
|
|
347
|
+
Threshold: 300
|
|
348
|
+
ComparisonOperator: GreaterThanThreshold
|
|
349
|
+
TreatMissingData: notBreaching
|
|
350
|
+
InboxDispatcherErrorsAlarm:
|
|
351
|
+
Type: AWS::CloudWatch::Alarm
|
|
352
|
+
Properties:
|
|
353
|
+
AlarmDescription: Durable inbox dispatch or recovery failed in three of five minutes
|
|
354
|
+
Namespace: AWS/Lambda
|
|
355
|
+
MetricName: Errors
|
|
356
|
+
Dimensions:
|
|
357
|
+
- Name: FunctionName
|
|
358
|
+
Value: { Ref: InboxQueueDispatcherLambdaFunction }
|
|
359
|
+
Statistic: Sum
|
|
360
|
+
Period: 60
|
|
361
|
+
EvaluationPeriods: 5
|
|
362
|
+
DatapointsToAlarm: 3
|
|
363
|
+
Threshold: 0
|
|
364
|
+
ComparisonOperator: GreaterThanThreshold
|
|
365
|
+
TreatMissingData: notBreaching
|
|
291
366
|
VPC:
|
|
292
367
|
Type: AWS::EC2::VPC
|
|
293
368
|
Properties:
|
|
@@ -98,7 +98,7 @@ export const isAuthGrantAssociatedWithProfile = async (
|
|
|
98
98
|
export const isAuthGrantChallengeValidForDID = async (
|
|
99
99
|
challenge: string,
|
|
100
100
|
did: string
|
|
101
|
-
): Promise<{ isChallengeValid: boolean; scope: string }> => {
|
|
101
|
+
): Promise<{ isChallengeValid: boolean; scope: string; actAs?: string }> => {
|
|
102
102
|
// TODO: Ideally, this transformation isn't required and we could use DID directly.
|
|
103
103
|
const profileId = getProfileIdFromDid(did);
|
|
104
104
|
if (!profileId) return { isChallengeValid: false, scope: AUTH_GRANT_NO_ACCESS_SCOPE };
|
|
@@ -126,5 +126,6 @@ export const isAuthGrantChallengeValidForDID = async (
|
|
|
126
126
|
return {
|
|
127
127
|
isChallengeValid: result.records.length > 0,
|
|
128
128
|
scope: authGrant?.scope || AUTH_GRANT_NO_ACCESS_SCOPE,
|
|
129
|
+
actAs: authGrant?.actAs,
|
|
129
130
|
};
|
|
130
131
|
};
|
|
@@ -23,6 +23,7 @@ export const createCredentialActivity = async (
|
|
|
23
23
|
eventType: params.eventType,
|
|
24
24
|
timestamp,
|
|
25
25
|
actorProfileId: params.actorProfileId,
|
|
26
|
+
onBehalfOf: params.onBehalfOf,
|
|
26
27
|
recipientType: params.recipientType,
|
|
27
28
|
recipientIdentifier: params.recipientIdentifier,
|
|
28
29
|
boostUri: params.boostUri,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { neogma } from '@instance';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Persisted, unique claim per (refreshId, delivery window) for the managed
|
|
5
|
+
* credential-refresh update email (LC-2198).
|
|
6
|
+
*
|
|
7
|
+
* Why a dedicated node instead of a conditional property write: Neo4j's
|
|
8
|
+
* read-committed isolation does not re-evaluate a `WHERE` after a write lock is
|
|
9
|
+
* acquired, so two concurrent `SET ... WHERE windowKey <> $key` statements can
|
|
10
|
+
* both "win" and send two emails. A unique constraint on `deliveryKey` makes the
|
|
11
|
+
* claim a hard single-writer: exactly one `CREATE` succeeds, every loser observes
|
|
12
|
+
* a constraint violation and skips.
|
|
13
|
+
*
|
|
14
|
+
* This gives at-most-once email delivery per configured window. A send that fails
|
|
15
|
+
* after the claim is recorded as `failed` and is NOT retried inside the same
|
|
16
|
+
* window (see `credential-refresh-email.helpers.ts` for the explicit policy).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type CredentialRefreshEmailDeliveryState = 'claimed' | 'delivered' | 'failed';
|
|
20
|
+
|
|
21
|
+
const deliveryKeyFor = (refreshId: string, windowKey: string): string =>
|
|
22
|
+
`${refreshId}:${windowKey}`;
|
|
23
|
+
|
|
24
|
+
const isUniqueConstraintViolation = (error: unknown): boolean => {
|
|
25
|
+
const code = (error as { code?: string })?.code;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
code === 'Neo.ClientError.Schema.ConstraintValidationFailed' ||
|
|
29
|
+
(error instanceof Error && error.message.includes('already exists'))
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Attempts to claim the email delivery window. Returns `'claimed'` for the single
|
|
35
|
+
* winner and `'duplicate'` for every concurrent or retried caller.
|
|
36
|
+
*/
|
|
37
|
+
export const claimCredentialRefreshEmailDelivery = async (params: {
|
|
38
|
+
refreshId: string;
|
|
39
|
+
windowKey: string;
|
|
40
|
+
}): Promise<'claimed' | 'duplicate'> => {
|
|
41
|
+
const { refreshId, windowKey } = params;
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await neogma.queryRunner.run(
|
|
46
|
+
`CREATE (delivery:CredentialRefreshEmailDelivery {
|
|
47
|
+
deliveryKey: $deliveryKey,
|
|
48
|
+
refreshId: $refreshId,
|
|
49
|
+
windowKey: $windowKey,
|
|
50
|
+
state: 'claimed',
|
|
51
|
+
attempts: 1,
|
|
52
|
+
createdAt: $now,
|
|
53
|
+
updatedAt: $now
|
|
54
|
+
})`,
|
|
55
|
+
{
|
|
56
|
+
deliveryKey: deliveryKeyFor(refreshId, windowKey),
|
|
57
|
+
refreshId,
|
|
58
|
+
windowKey,
|
|
59
|
+
now,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return 'claimed';
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isUniqueConstraintViolation(error)) return 'duplicate';
|
|
66
|
+
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Records the terminal outcome of a claimed delivery attempt. */
|
|
72
|
+
export const finalizeCredentialRefreshEmailDelivery = async (params: {
|
|
73
|
+
refreshId: string;
|
|
74
|
+
windowKey: string;
|
|
75
|
+
state: Extract<CredentialRefreshEmailDeliveryState, 'delivered' | 'failed'>;
|
|
76
|
+
}): Promise<void> => {
|
|
77
|
+
const { refreshId, windowKey, state } = params;
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
|
|
80
|
+
await neogma.queryRunner.run(
|
|
81
|
+
`MATCH (delivery:CredentialRefreshEmailDelivery {deliveryKey: $deliveryKey})
|
|
82
|
+
SET delivery.state = $state, delivery.updatedAt = $now`,
|
|
83
|
+
{ deliveryKey: deliveryKeyFor(refreshId, windowKey), state, now }
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Reads a delivery record for observability and tests. */
|
|
88
|
+
export const getCredentialRefreshEmailDelivery = async (
|
|
89
|
+
refreshId: string,
|
|
90
|
+
windowKey: string
|
|
91
|
+
): Promise<{
|
|
92
|
+
deliveryKey: string;
|
|
93
|
+
refreshId: string;
|
|
94
|
+
windowKey: string;
|
|
95
|
+
state: CredentialRefreshEmailDeliveryState;
|
|
96
|
+
attempts: number;
|
|
97
|
+
} | null> => {
|
|
98
|
+
const result = await neogma.queryRunner.run(
|
|
99
|
+
`MATCH (delivery:CredentialRefreshEmailDelivery {deliveryKey: $deliveryKey})
|
|
100
|
+
RETURN delivery LIMIT 1`,
|
|
101
|
+
{ deliveryKey: deliveryKeyFor(refreshId, windowKey) }
|
|
102
|
+
);
|
|
103
|
+
const node = result.records[0]?.get('delivery');
|
|
104
|
+
|
|
105
|
+
if (!node) return null;
|
|
106
|
+
|
|
107
|
+
const props = node.properties;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
deliveryKey: props.deliveryKey,
|
|
111
|
+
refreshId: props.refreshId,
|
|
112
|
+
windowKey: props.windowKey,
|
|
113
|
+
state: props.state,
|
|
114
|
+
attempts: Number(props.attempts),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -262,8 +262,14 @@ export const getCredentialRefreshVersions = async (
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
const result = await neogma.queryRunner.run(
|
|
265
|
-
`
|
|
266
|
-
|
|
265
|
+
`CALL {
|
|
266
|
+
MATCH (version:Credential {refreshId: $refreshId}) RETURN version
|
|
267
|
+
UNION ALL
|
|
268
|
+
MATCH (version:InboxRefreshPublication {refreshId: $refreshId})
|
|
269
|
+
WHERE NOT EXISTS { MATCH (:Credential {refreshVersionKey: version.refreshVersionKey}) }
|
|
270
|
+
RETURN version
|
|
271
|
+
}
|
|
272
|
+
WITH version WHERE $beforeVersion IS NULL OR version.version < $beforeVersion
|
|
267
273
|
RETURN {
|
|
268
274
|
id: version.id,
|
|
269
275
|
refreshId: version.refreshId,
|
|
@@ -78,6 +78,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
78
78
|
etag,
|
|
79
79
|
materialDigest,
|
|
80
80
|
updateSummary,
|
|
81
|
+
credentialDisplayName,
|
|
81
82
|
effectiveAt,
|
|
82
83
|
notificationOutcome,
|
|
83
84
|
notificationId,
|
|
@@ -103,6 +104,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
103
104
|
etag,
|
|
104
105
|
signingMode,
|
|
105
106
|
updateSummary,
|
|
107
|
+
credentialDisplayName,
|
|
106
108
|
idempotencyKey,
|
|
107
109
|
refreshIdempotencyKey,
|
|
108
110
|
notificationOutcome,
|
|
@@ -160,6 +162,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
160
162
|
refresh.etag = $etag,
|
|
161
163
|
refresh.materialDigest = $materialDigest,
|
|
162
164
|
refresh.updateSummary = $updateSummary,
|
|
165
|
+
refresh.credentialDisplayName = coalesce($credentialDisplayName, refresh.credentialDisplayName),
|
|
163
166
|
refresh.signingMode = $signingMode,
|
|
164
167
|
refresh.lastPublishedAt = $publishedAt,
|
|
165
168
|
refresh.idempotencyKey = $idempotencyKey,
|
|
@@ -176,6 +179,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
176
179
|
etag: etag ?? null,
|
|
177
180
|
materialDigest: materialDigest ?? null,
|
|
178
181
|
updateSummary: updateSummary ?? null,
|
|
182
|
+
credentialDisplayName: credentialDisplayName ?? null,
|
|
179
183
|
signingMode,
|
|
180
184
|
publishedAt,
|
|
181
185
|
now,
|