@learncard/lca-api-service 1.6.3 → 1.7.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.
Files changed (79) hide show
  1. package/.env.example +2 -0
  2. package/CHANGELOG.md +114 -0
  3. package/config/config.dev.json +1 -0
  4. package/config/config.example.json +1 -0
  5. package/config/config.production.json +1 -0
  6. package/config/config.scouts.dev.json +1 -0
  7. package/config/config.scouts.production.json +1 -0
  8. package/dist/accesslayer/notifications/create.d.ts +2 -1
  9. package/dist/app.d.ts +70 -8
  10. package/dist/config/environment.d.ts +6 -0
  11. package/dist/config/stageConfig.d.ts +4 -0
  12. package/dist/helpers/analyticsEligibility.d.ts +6 -0
  13. package/dist/helpers/brainUrl.d.ts +1 -0
  14. package/dist/helpers/brainUrl.test.d.ts +1 -0
  15. package/dist/helpers/currentProfile.d.ts +6 -0
  16. package/dist/helpers/currentProfile.test.d.ts +1 -0
  17. package/dist/helpers/retryingOnce.d.ts +1 -0
  18. package/dist/helpers/retryingOnce.test.d.ts +1 -0
  19. package/dist/index.js +2 -2
  20. package/dist/models/EscrowHold.d.ts +1 -0
  21. package/dist/models/EscrowHold.indexes.test.d.ts +1 -0
  22. package/dist/models/Notification.d.ts +114 -2
  23. package/dist/models/SigningAuthority.d.ts +1 -1
  24. package/dist/models/UserKey.d.ts +23 -0
  25. package/dist/publish-metafile.json +1 -1
  26. package/dist/routes/credentials.d.ts +1 -1
  27. package/dist/routes/index.d.ts +5 -0
  28. package/dist/routes/keys.d.ts +1 -0
  29. package/dist/routes/notifications.d.ts +65 -4
  30. package/dist/routes/preferences.test.d.ts +1 -0
  31. package/dist/routes/signing-authority.d.ts +3 -3
  32. package/dist/services/escrow-enclave/index.d.ts +2 -0
  33. package/dist/services/escrow-enclave/logFailure.d.ts +1 -0
  34. package/dist/types/credentials.d.ts +1 -1
  35. package/dist/types/notifications.d.ts +118 -2
  36. package/docs/bedrock-generation.md +7 -6
  37. package/lambdaApp.test.ts +114 -0
  38. package/lambdaApp.ts +23 -11
  39. package/package.json +11 -11
  40. package/scripts/escrow-lambda-http.ts +74 -0
  41. package/scripts/verify-escrow-startup.mjs +463 -0
  42. package/serverless-local.yml +1 -1
  43. package/serverless.yml +1 -1
  44. package/src/accesslayer/notifications/create.test.ts +80 -0
  45. package/src/accesslayer/notifications/create.ts +97 -2
  46. package/src/config/environment.test.ts +27 -1
  47. package/src/config/environment.ts +16 -0
  48. package/src/config/escrowDeployment.test.ts +1 -0
  49. package/src/docker-entry.ts +1 -2
  50. package/src/helpers/analyticsEligibility.ts +15 -0
  51. package/src/helpers/brainUrl.test.ts +13 -0
  52. package/src/helpers/brainUrl.ts +3 -0
  53. package/src/helpers/currentProfile.test.ts +55 -0
  54. package/src/helpers/currentProfile.ts +58 -0
  55. package/src/helpers/locale.helpers.test.ts +4 -1
  56. package/src/helpers/locale.helpers.ts +4 -2
  57. package/src/helpers/retryingOnce.test.ts +33 -0
  58. package/src/helpers/retryingOnce.ts +18 -0
  59. package/src/models/EscrowHold.indexes.test.ts +208 -0
  60. package/src/models/EscrowHold.ts +106 -51
  61. package/src/models/UserKey.ts +69 -1
  62. package/src/routes/escrow.ts +18 -11
  63. package/src/routes/index.ts +5 -8
  64. package/src/routes/keys.ts +8 -0
  65. package/src/routes/notifications.ts +34 -1
  66. package/src/routes/preferences.test.ts +216 -0
  67. package/src/routes/preferences.ts +54 -4
  68. package/src/services/escrow-enclave/index.ts +38 -1
  69. package/src/services/escrow-enclave/logFailure.ts +5 -1
  70. package/src/services/escrow-enclave/production.test.ts +64 -0
  71. package/src/services/escrow-enclave/softwareEnclave.test.ts +14 -2
  72. package/test/jobs/escrowHoldReminders.spec.ts +21 -4
  73. package/test/keys-escrow.spec.ts +131 -3
  74. package/test/keys-lifecycle-invariants.spec.ts +235 -0
  75. package/test/notifications.spec.ts +102 -0
  76. package/tsconfig.entrypoints.json +1 -1
  77. package/vitest.config.ts +1 -0
  78. package/scripts/node24-runtime.cjs +0 -9
  79. package/scripts/verify-node24-layer.cjs +0 -49
@@ -154,8 +154,8 @@ export declare const credentialsRouter: import("@trpc/server").TRPCBuiltRouter<{
154
154
  };
155
155
  signingAuthority: {
156
156
  did: string;
157
- ownerDid: string;
158
157
  name: string;
158
+ ownerDid: string;
159
159
  };
160
160
  options?: {
161
161
  created?: string | undefined;
@@ -25,6 +25,7 @@ export type Context = {
25
25
  debug?: boolean;
26
26
  clientIp?: string;
27
27
  providerToken?: string;
28
+ authorizationHeader?: string;
28
29
  };
29
30
  export declare const t: import("@trpc/server").TRPCRootObject<Context, OpenApiMeta, import("@trpc/server").TRPCRuntimeConfigOptions<Context, OpenApiMeta>, {
30
31
  ctx: Context;
@@ -45,6 +46,7 @@ export declare const openRoute: import("@trpc/server").TRPCProcedureBuilder<Cont
45
46
  domain: string;
46
47
  clientIp: string | undefined;
47
48
  providerToken: string | undefined;
49
+ authorizationHeader: string | undefined;
48
50
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
49
51
  export declare const didRoute: import("@trpc/server").TRPCProcedureBuilder<Context, OpenApiMeta, {
50
52
  tenant: ResolvedTenant;
@@ -57,6 +59,7 @@ export declare const didRoute: import("@trpc/server").TRPCProcedureBuilder<Conte
57
59
  domain: string;
58
60
  clientIp: string | undefined;
59
61
  providerToken: string | undefined;
62
+ authorizationHeader: string | undefined;
60
63
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
61
64
  export declare const authorizedDidRoute: import("@trpc/server").TRPCProcedureBuilder<Context, OpenApiMeta, {
62
65
  tenant: ResolvedTenant;
@@ -69,6 +72,7 @@ export declare const authorizedDidRoute: import("@trpc/server").TRPCProcedureBui
69
72
  domain: string;
70
73
  clientIp: string | undefined;
71
74
  providerToken: string | undefined;
75
+ authorizationHeader: string | undefined;
72
76
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
73
77
  export declare const didAndChallengeRoute: import("@trpc/server").TRPCProcedureBuilder<Context, OpenApiMeta, {
74
78
  tenant: ResolvedTenant;
@@ -81,4 +85,5 @@ export declare const didAndChallengeRoute: import("@trpc/server").TRPCProcedureB
81
85
  domain: string;
82
86
  clientIp: string | undefined;
83
87
  providerToken: string | undefined;
88
+ authorizationHeader: string | undefined;
84
89
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
@@ -199,6 +199,7 @@ export declare const keysRouter: import("@trpc/server").TRPCBuiltRouter<{
199
199
  };
200
200
  priorEscrowEnrollment: boolean;
201
201
  sssActivationState: "active" | "provisional";
202
+ isWeb3AuthLogin: boolean;
202
203
  escrowStale?: "mode-mismatch" | "key-rotated" | undefined;
203
204
  escrowBlobIdentity?: string | undefined;
204
205
  escrowUpdating?: boolean | undefined;
@@ -16,7 +16,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
16
16
  archived?: boolean | undefined;
17
17
  read?: boolean | undefined;
18
18
  actionStatus?: "PENDING" | "COMPLETED" | "REJECTED" | undefined;
19
- type?: "GUARDIAN_APPROVED" | "GUARDIAN_REJECTED" | "CONNECTION_REQUEST" | "CONNECTION_REQUEST_EXPIRED_INVITE" | "CONNECTION_ACCEPTED" | "CREDENTIAL_RECEIVED" | "CREDENTIAL_ACCEPTED" | "BOOST_RECEIVED" | "BOOST_ACCEPTED" | "PRESENTATION_REQUEST" | "PRESENTATION_RECEIVED" | "CONSENT_FLOW_TRANSACTION" | "ISSUANCE_CLAIMED" | "ISSUANCE_DELIVERED" | "ISSUANCE_ERROR" | "PROFILE_PARENT_APPROVED" | "APP_LISTING_SUBMITTED" | "APP_LISTING_APPROVED" | "APP_LISTING_REJECTED" | "APP_LISTING_WITHDRAWN" | "DEVICE_LINK_REQUEST" | "GUARDIAN_APPROVAL_PENDING" | "APP_NOTIFICATION" | "CREDENTIAL_REVOKED" | "CREDENTIAL_SUSPENDED" | "CREDENTIAL_UNSUSPENDED" | "CREDENTIAL_REFRESHED" | undefined;
19
+ type?: "GUARDIAN_APPROVED" | "GUARDIAN_REJECTED" | "CONNECTION_REQUEST" | "CONNECTION_REQUEST_EXPIRED_INVITE" | "CONNECTION_ACCEPTED" | "CREDENTIAL_RECEIVED" | "CREDENTIAL_ACCEPTED" | "BOOST_RECEIVED" | "BOOST_ACCEPTED" | "PRESENTATION_REQUEST" | "PRESENTATION_RECEIVED" | "CONSENT_FLOW_TRANSACTION" | "ISSUANCE_CLAIMED" | "ISSUANCE_DELIVERED" | "ISSUANCE_ERROR" | "PROFILE_PARENT_APPROVED" | "APP_LISTING_SUBMITTED" | "APP_LISTING_APPROVED" | "APP_LISTING_REJECTED" | "APP_LISTING_WITHDRAWN" | "DEVICE_LINK_REQUEST" | "GUARDIAN_APPROVAL_PENDING" | "APP_NOTIFICATION" | "ANNOUNCEMENT" | "CREDENTIAL_REVOKED" | "CREDENTIAL_SUSPENDED" | "CREDENTIAL_UNSUSPENDED" | "CREDENTIAL_REFRESHED" | "CONSENT_FLOW_CONTRACT_UPDATED" | undefined;
20
20
  } | undefined;
21
21
  };
22
22
  output: {
@@ -29,7 +29,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
29
29
  queryNotifications: import("@trpc/server").TRPCQueryProcedure<{
30
30
  input: {
31
31
  query: {
32
- type?: "GUARDIAN_APPROVED" | "GUARDIAN_REJECTED" | "CONNECTION_REQUEST" | "CONNECTION_REQUEST_EXPIRED_INVITE" | "CONNECTION_ACCEPTED" | "CREDENTIAL_RECEIVED" | "CREDENTIAL_ACCEPTED" | "BOOST_RECEIVED" | "BOOST_ACCEPTED" | "PRESENTATION_REQUEST" | "PRESENTATION_RECEIVED" | "CONSENT_FLOW_TRANSACTION" | "ISSUANCE_CLAIMED" | "ISSUANCE_DELIVERED" | "ISSUANCE_ERROR" | "PROFILE_PARENT_APPROVED" | "APP_LISTING_SUBMITTED" | "APP_LISTING_APPROVED" | "APP_LISTING_REJECTED" | "APP_LISTING_WITHDRAWN" | "DEVICE_LINK_REQUEST" | "GUARDIAN_APPROVAL_PENDING" | "APP_NOTIFICATION" | "CREDENTIAL_REVOKED" | "CREDENTIAL_SUSPENDED" | "CREDENTIAL_UNSUSPENDED" | "CREDENTIAL_REFRESHED" | undefined;
32
+ type?: "GUARDIAN_APPROVED" | "GUARDIAN_REJECTED" | "CONNECTION_REQUEST" | "CONNECTION_REQUEST_EXPIRED_INVITE" | "CONNECTION_ACCEPTED" | "CREDENTIAL_RECEIVED" | "CREDENTIAL_ACCEPTED" | "BOOST_RECEIVED" | "BOOST_ACCEPTED" | "PRESENTATION_REQUEST" | "PRESENTATION_RECEIVED" | "CONSENT_FLOW_TRANSACTION" | "ISSUANCE_CLAIMED" | "ISSUANCE_DELIVERED" | "ISSUANCE_ERROR" | "PROFILE_PARENT_APPROVED" | "APP_LISTING_SUBMITTED" | "APP_LISTING_APPROVED" | "APP_LISTING_REJECTED" | "APP_LISTING_WITHDRAWN" | "DEVICE_LINK_REQUEST" | "GUARDIAN_APPROVAL_PENDING" | "APP_NOTIFICATION" | "ANNOUNCEMENT" | "CREDENTIAL_REVOKED" | "CREDENTIAL_SUSPENDED" | "CREDENTIAL_UNSUSPENDED" | "CREDENTIAL_REFRESHED" | "CONSENT_FLOW_CONTRACT_UPDATED" | undefined;
33
33
  'from.did'?: string | undefined;
34
34
  'from.profileId'?: string | undefined;
35
35
  'data.vcUris'?: string | string[] | undefined;
@@ -85,7 +85,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
85
85
  }>;
86
86
  sendNotification: import("@trpc/server").TRPCMutationProcedure<{
87
87
  input: {
88
- type: "GUARDIAN_APPROVED" | "GUARDIAN_REJECTED" | "CONNECTION_REQUEST" | "CONNECTION_REQUEST_EXPIRED_INVITE" | "CONNECTION_ACCEPTED" | "CREDENTIAL_RECEIVED" | "CREDENTIAL_ACCEPTED" | "BOOST_RECEIVED" | "BOOST_ACCEPTED" | "PRESENTATION_REQUEST" | "PRESENTATION_RECEIVED" | "CONSENT_FLOW_TRANSACTION" | "ISSUANCE_CLAIMED" | "ISSUANCE_DELIVERED" | "ISSUANCE_ERROR" | "PROFILE_PARENT_APPROVED" | "APP_LISTING_SUBMITTED" | "APP_LISTING_APPROVED" | "APP_LISTING_REJECTED" | "APP_LISTING_WITHDRAWN" | "DEVICE_LINK_REQUEST" | "GUARDIAN_APPROVAL_PENDING" | "APP_NOTIFICATION" | "CREDENTIAL_REVOKED" | "CREDENTIAL_SUSPENDED" | "CREDENTIAL_UNSUSPENDED" | "CREDENTIAL_REFRESHED";
88
+ type: "GUARDIAN_APPROVED" | "GUARDIAN_REJECTED" | "CONNECTION_REQUEST" | "CONNECTION_REQUEST_EXPIRED_INVITE" | "CONNECTION_ACCEPTED" | "CREDENTIAL_RECEIVED" | "CREDENTIAL_ACCEPTED" | "BOOST_RECEIVED" | "BOOST_ACCEPTED" | "PRESENTATION_REQUEST" | "PRESENTATION_RECEIVED" | "CONSENT_FLOW_TRANSACTION" | "ISSUANCE_CLAIMED" | "ISSUANCE_DELIVERED" | "ISSUANCE_ERROR" | "PROFILE_PARENT_APPROVED" | "APP_LISTING_SUBMITTED" | "APP_LISTING_APPROVED" | "APP_LISTING_REJECTED" | "APP_LISTING_WITHDRAWN" | "DEVICE_LINK_REQUEST" | "GUARDIAN_APPROVAL_PENDING" | "APP_NOTIFICATION" | "ANNOUNCEMENT" | "CREDENTIAL_REVOKED" | "CREDENTIAL_SUSPENDED" | "CREDENTIAL_UNSUSPENDED" | "CREDENTIAL_REFRESHED" | "CONSENT_FLOW_CONTRACT_UPDATED";
89
89
  to: {
90
90
  profileId?: string | undefined;
91
91
  displayName?: string | undefined;
@@ -176,7 +176,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
176
176
  vpUris?: string[] | undefined;
177
177
  transaction?: {
178
178
  id: string;
179
- action: "write" | "update" | "consent" | "sync" | "withdraw";
179
+ action: "write" | "consent" | "update" | "sync" | "withdraw" | "reconsent";
180
180
  date: string;
181
181
  expiresAt?: string | undefined;
182
182
  oneTime?: boolean | undefined;
@@ -214,7 +214,64 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
214
214
  requestedBy: string;
215
215
  externalReferenceId?: string | undefined;
216
216
  } | undefined;
217
+ kind?: "relaxed" | "expanded_optional" | "requires_consent" | undefined;
217
218
  uris?: string[] | undefined;
219
+ consentedVersion?: number | undefined;
220
+ reconsentDeadline?: string | undefined;
221
+ newOptionsUntil?: string | undefined;
222
+ } | undefined;
223
+ contractUri?: string | undefined;
224
+ contractName?: string | undefined;
225
+ fromVersion?: number | undefined;
226
+ toVersion?: number | undefined;
227
+ kind?: "relaxed" | "expanded_optional" | "requires_consent" | undefined;
228
+ reason?: string | undefined;
229
+ diff?: {
230
+ added: {
231
+ area: "read" | "write";
232
+ type: "personal" | "credential";
233
+ key: string;
234
+ required: boolean;
235
+ reason?: string | undefined;
236
+ }[];
237
+ removed: {
238
+ area: "read" | "write";
239
+ type: "personal" | "credential";
240
+ key: string;
241
+ required: boolean;
242
+ reason?: string | undefined;
243
+ }[];
244
+ changed: {
245
+ area: "read" | "write";
246
+ type: "personal" | "credential";
247
+ key: string;
248
+ required: boolean;
249
+ reason?: string | undefined;
250
+ }[];
251
+ details?: {
252
+ field: "expiresAt" | "name" | "description" | "image" | "subtitle" | "reasonForAccessing" | "needsGuardianConsent" | "redirectUrl" | "frontDoorBoostUri" | "writers" | "autoboosts";
253
+ before?: string | boolean | string[] | undefined;
254
+ after?: string | boolean | string[] | undefined;
255
+ }[] | undefined;
256
+ } | undefined;
257
+ reconsentDeadline?: string | undefined;
258
+ reviewUrl?: string | undefined;
259
+ preview?: {
260
+ headline: string;
261
+ subhead: string;
262
+ rows: {
263
+ scope: {
264
+ area: "read" | "write";
265
+ type: "personal" | "credential";
266
+ key: string;
267
+ required: boolean;
268
+ reason?: string | undefined;
269
+ };
270
+ change: "added" | "removed" | "changed";
271
+ label: string;
272
+ subtitle: string;
273
+ }[];
274
+ primaryLabel: string;
218
275
  } | undefined;
219
276
  inbox?: {
220
277
  issuanceId: string;
@@ -234,6 +291,10 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
234
291
  promptId: string;
235
292
  counterpartProfileId: string;
236
293
  } | undefined;
294
+ campaignId?: string | undefined;
295
+ campaignCreatedAt?: string | undefined;
296
+ campaignHasInApp?: boolean | undefined;
297
+ campaignTestId?: string | undefined;
237
298
  } | undefined;
238
299
  } | undefined;
239
300
  sent?: string | undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -10,8 +10,8 @@ export declare const signingAuthorityRouter: import("@trpc/server").TRPCBuiltRou
10
10
  ownerDid?: string | undefined;
11
11
  };
12
12
  output: {
13
- ownerDid: string;
14
13
  name: string;
14
+ ownerDid: string;
15
15
  did: string;
16
16
  endpoint: string;
17
17
  _id?: string | undefined;
@@ -24,8 +24,8 @@ export declare const signingAuthorityRouter: import("@trpc/server").TRPCBuiltRou
24
24
  ownerDid: string;
25
25
  };
26
26
  output: {
27
- ownerDid: string;
28
27
  name: string;
28
+ ownerDid: string;
29
29
  did: string;
30
30
  endpoint: string;
31
31
  _id?: string | undefined;
@@ -35,8 +35,8 @@ export declare const signingAuthorityRouter: import("@trpc/server").TRPCBuiltRou
35
35
  signingAuthorities: import("@trpc/server").TRPCQueryProcedure<{
36
36
  input: void;
37
37
  output: {
38
- ownerDid: string;
39
38
  name: string;
39
+ ownerDid: string;
40
40
  did: string;
41
41
  endpoint: string;
42
42
  _id?: string | undefined;
@@ -5,8 +5,10 @@ export * from './softwareEnclave';
5
5
  export * from './remoteEnclave';
6
6
  export * from './notifications';
7
7
  export declare const isEscrowEnabled: () => boolean;
8
+ export declare const checkEscrowRemoteConfig: () => void;
8
9
  export declare const isEscrowRemoteMode: () => boolean;
9
10
  export declare const activeEscrowClientMode: () => "software" | "nitro";
11
+ export declare const getEscrowAuthorityTenant: () => string;
10
12
  export declare const getEscrowHoldDurationMs: () => number;
11
13
  export declare const getEscrowHoldRestartMinAgeMs: () => number;
12
14
  export declare const getEscrowEnclave: () => EscrowEnclave;
@@ -1 +1,2 @@
1
1
  export declare const logEscrowFailure: (error: unknown) => void;
2
+ export declare const emitEnclaveFailureMetric: (reason: string) => void;
@@ -159,8 +159,8 @@ export declare const IssueEndpointValidator: z.ZodObject<{
159
159
  }, z.core.$strip>>;
160
160
  signingAuthority: z.ZodObject<{
161
161
  did: z.ZodString;
162
- ownerDid: z.ZodString;
163
162
  name: z.ZodString;
163
+ ownerDid: z.ZodString;
164
164
  }, z.core.$strip>;
165
165
  encryption: z.ZodOptional<z.ZodObject<{
166
166
  recipients: z.ZodArray<z.ZodString>;
@@ -52,10 +52,12 @@ export declare const NotificationQueryFiltersValidator: z.ZodObject<{
52
52
  DEVICE_LINK_REQUEST: "DEVICE_LINK_REQUEST";
53
53
  GUARDIAN_APPROVAL_PENDING: "GUARDIAN_APPROVAL_PENDING";
54
54
  APP_NOTIFICATION: "APP_NOTIFICATION";
55
+ ANNOUNCEMENT: "ANNOUNCEMENT";
55
56
  CREDENTIAL_REVOKED: "CREDENTIAL_REVOKED";
56
57
  CREDENTIAL_SUSPENDED: "CREDENTIAL_SUSPENDED";
57
58
  CREDENTIAL_UNSUSPENDED: "CREDENTIAL_UNSUSPENDED";
58
59
  CREDENTIAL_REFRESHED: "CREDENTIAL_REFRESHED";
60
+ CONSENT_FLOW_CONTRACT_UPDATED: "CONSENT_FLOW_CONTRACT_UPDATED";
59
61
  }>>;
60
62
  }, z.core.$strip>;
61
63
  export type NotificationQueryFiltersType = z.infer<typeof NotificationQueryFiltersValidator>;
@@ -101,10 +103,12 @@ export declare const PaginatedNotificationsValidator: z.ZodObject<{
101
103
  DEVICE_LINK_REQUEST: "DEVICE_LINK_REQUEST";
102
104
  GUARDIAN_APPROVAL_PENDING: "GUARDIAN_APPROVAL_PENDING";
103
105
  APP_NOTIFICATION: "APP_NOTIFICATION";
106
+ ANNOUNCEMENT: "ANNOUNCEMENT";
104
107
  CREDENTIAL_REVOKED: "CREDENTIAL_REVOKED";
105
108
  CREDENTIAL_SUSPENDED: "CREDENTIAL_SUSPENDED";
106
109
  CREDENTIAL_UNSUSPENDED: "CREDENTIAL_UNSUSPENDED";
107
110
  CREDENTIAL_REFRESHED: "CREDENTIAL_REFRESHED";
111
+ CONSENT_FLOW_CONTRACT_UPDATED: "CONSENT_FLOW_CONTRACT_UPDATED";
108
112
  }>;
109
113
  to: z.ZodIntersection<z.ZodObject<{
110
114
  profileId: z.ZodOptional<z.ZodString>;
@@ -246,14 +250,120 @@ export declare const PaginatedNotificationsValidator: z.ZodObject<{
246
250
  }, z.core.$strip>>;
247
251
  id: z.ZodString;
248
252
  action: z.ZodEnum<{
249
- update: "update";
250
- write: "write";
251
253
  consent: "consent";
254
+ write: "write";
255
+ update: "update";
252
256
  sync: "sync";
253
257
  withdraw: "withdraw";
258
+ reconsent: "reconsent";
254
259
  }>;
260
+ kind: z.ZodOptional<z.ZodEnum<{
261
+ relaxed: "relaxed";
262
+ expanded_optional: "expanded_optional";
263
+ requires_consent: "requires_consent";
264
+ }>>;
255
265
  date: z.ZodString;
256
266
  uris: z.ZodOptional<z.ZodArray<z.ZodString>>;
267
+ consentedVersion: z.ZodOptional<z.ZodNumber>;
268
+ reconsentDeadline: z.ZodOptional<z.ZodString>;
269
+ newOptionsUntil: z.ZodOptional<z.ZodString>;
270
+ }, z.core.$strip>>;
271
+ contractUri: z.ZodOptional<z.ZodString>;
272
+ contractName: z.ZodOptional<z.ZodString>;
273
+ fromVersion: z.ZodOptional<z.ZodNumber>;
274
+ toVersion: z.ZodOptional<z.ZodNumber>;
275
+ kind: z.ZodOptional<z.ZodEnum<{
276
+ relaxed: "relaxed";
277
+ expanded_optional: "expanded_optional";
278
+ requires_consent: "requires_consent";
279
+ }>>;
280
+ reason: z.ZodOptional<z.ZodString>;
281
+ diff: z.ZodOptional<z.ZodObject<{
282
+ added: z.ZodArray<z.ZodObject<{
283
+ area: z.ZodEnum<{
284
+ read: "read";
285
+ write: "write";
286
+ }>;
287
+ type: z.ZodEnum<{
288
+ credential: "credential";
289
+ personal: "personal";
290
+ }>;
291
+ key: z.ZodString;
292
+ required: z.ZodBoolean;
293
+ reason: z.ZodOptional<z.ZodString>;
294
+ }, z.core.$strip>>;
295
+ removed: z.ZodArray<z.ZodObject<{
296
+ area: z.ZodEnum<{
297
+ read: "read";
298
+ write: "write";
299
+ }>;
300
+ type: z.ZodEnum<{
301
+ credential: "credential";
302
+ personal: "personal";
303
+ }>;
304
+ key: z.ZodString;
305
+ required: z.ZodBoolean;
306
+ reason: z.ZodOptional<z.ZodString>;
307
+ }, z.core.$strip>>;
308
+ changed: z.ZodArray<z.ZodObject<{
309
+ area: z.ZodEnum<{
310
+ read: "read";
311
+ write: "write";
312
+ }>;
313
+ type: z.ZodEnum<{
314
+ credential: "credential";
315
+ personal: "personal";
316
+ }>;
317
+ key: z.ZodString;
318
+ required: z.ZodBoolean;
319
+ reason: z.ZodOptional<z.ZodString>;
320
+ }, z.core.$strip>>;
321
+ details: z.ZodOptional<z.ZodArray<z.ZodObject<{
322
+ field: z.ZodEnum<{
323
+ name: "name";
324
+ description: "description";
325
+ image: "image";
326
+ expiresAt: "expiresAt";
327
+ subtitle: "subtitle";
328
+ reasonForAccessing: "reasonForAccessing";
329
+ needsGuardianConsent: "needsGuardianConsent";
330
+ redirectUrl: "redirectUrl";
331
+ frontDoorBoostUri: "frontDoorBoostUri";
332
+ writers: "writers";
333
+ autoboosts: "autoboosts";
334
+ }>;
335
+ before: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodArray<z.ZodString>]>>;
336
+ after: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodArray<z.ZodString>]>>;
337
+ }, z.core.$strip>>>;
338
+ }, z.core.$strip>>;
339
+ reconsentDeadline: z.ZodOptional<z.ZodString>;
340
+ reviewUrl: z.ZodOptional<z.ZodString>;
341
+ preview: z.ZodOptional<z.ZodObject<{
342
+ headline: z.ZodString;
343
+ subhead: z.ZodString;
344
+ rows: z.ZodArray<z.ZodObject<{
345
+ scope: z.ZodObject<{
346
+ area: z.ZodEnum<{
347
+ read: "read";
348
+ write: "write";
349
+ }>;
350
+ type: z.ZodEnum<{
351
+ credential: "credential";
352
+ personal: "personal";
353
+ }>;
354
+ key: z.ZodString;
355
+ required: z.ZodBoolean;
356
+ reason: z.ZodOptional<z.ZodString>;
357
+ }, z.core.$strip>;
358
+ change: z.ZodEnum<{
359
+ added: "added";
360
+ removed: "removed";
361
+ changed: "changed";
362
+ }>;
363
+ label: z.ZodString;
364
+ subtitle: z.ZodString;
365
+ }, z.core.$strip>>;
366
+ primaryLabel: z.ZodString;
257
367
  }, z.core.$strip>>;
258
368
  inbox: z.ZodOptional<z.ZodObject<{
259
369
  issuanceId: z.ZodString;
@@ -278,6 +388,10 @@ export declare const PaginatedNotificationsValidator: z.ZodObject<{
278
388
  promptId: z.ZodString;
279
389
  counterpartProfileId: z.ZodString;
280
390
  }, z.core.$strip>>;
391
+ campaignId: z.ZodOptional<z.ZodString>;
392
+ campaignCreatedAt: z.ZodOptional<z.ZodISODateTime>;
393
+ campaignHasInApp: z.ZodOptional<z.ZodBoolean>;
394
+ campaignTestId: z.ZodOptional<z.ZodUUID>;
281
395
  }, z.core.$catchall<z.ZodUnknown>>>;
282
396
  }, z.core.$loose>>;
283
397
  sent: z.ZodOptional<z.ZodISODateTime>;
@@ -318,10 +432,12 @@ export declare const NotificationQueryInputValidator: z.ZodObject<{
318
432
  DEVICE_LINK_REQUEST: "DEVICE_LINK_REQUEST";
319
433
  GUARDIAN_APPROVAL_PENDING: "GUARDIAN_APPROVAL_PENDING";
320
434
  APP_NOTIFICATION: "APP_NOTIFICATION";
435
+ ANNOUNCEMENT: "ANNOUNCEMENT";
321
436
  CREDENTIAL_REVOKED: "CREDENTIAL_REVOKED";
322
437
  CREDENTIAL_SUSPENDED: "CREDENTIAL_SUSPENDED";
323
438
  CREDENTIAL_UNSUSPENDED: "CREDENTIAL_UNSUSPENDED";
324
439
  CREDENTIAL_REFRESHED: "CREDENTIAL_REFRESHED";
440
+ CONSENT_FLOW_CONTRACT_UPDATED: "CONSENT_FLOW_CONTRACT_UPDATED";
325
441
  }>>;
326
442
  'from.did': z.ZodOptional<z.ZodString>;
327
443
  'from.profileId': z.ZodOptional<z.ZodString>;
@@ -88,16 +88,17 @@ explicit deployment approval. There is no runtime silent fallback to the old mod
88
88
 
89
89
  All LCA Lambda functions and the native DIDKit layer declare `nodejs24.x`; esbuild
90
90
  targets Node 24. The native addon uses stable N-API v4, supported by Node 24.
91
- Both Node 20 and Node 24 Lambda use AL2023. Before deployment, the LCA-only
92
- `verify-node24-layer.cjs` preflight requires the Linux x64 GNU artifact, both
91
+ Both Node 20 and Node 24 Lambda use AL2023. Before deployment, the shared
92
+ `scripts/verify-node24-didkit-layer.cjs` preflight (also used by brain-service and
93
+ learn-cloud-service) requires the Linux x64 GNU artifact, both
93
94
  packaged runtime dependencies, libc symbols no newer than glibc 2.34, and an actual
94
95
  Node 24 native key-to-DID smoke check. Missing/incompatible artifacts stop the action
95
96
  before Serverless deployment; WASM fallback is not evidence of native compatibility.
96
97
 
97
- LCA pins serverless-esbuild 1.57.2 for Node 24 target support. The service-local
98
- `node24-runtime.cjs` plugin adds Node 24 to Serverless 3.40's older runtime schema
99
- without disabling validation or changing other services. Deployment tools run on
100
- Node 24.12.0 in the LCA action. Local offline configuration uses Node 24 too.
98
+ LCA pins serverless-esbuild 1.57.2 for Node 24 target support. The shared
99
+ `scripts/serverless-node24-runtime.cjs` plugin adds Node 24 to Serverless 3.40's older
100
+ runtime schema without disabling validation. Deployment tools run on
101
+ the repo `.nvmrc` Node 24 in the LCA action. Local offline configuration uses Node 24 too.
101
102
 
102
103
  The `LcaDevBedrockGenerationPolicy` CloudFormation resource attaches the scoped
103
104
  policy to `SigningAuthorityExecutionRole` only for stage `dev` in `us-east-1`.
@@ -0,0 +1,114 @@
1
+ import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ const handlers = vi.hoisted(() => ({
5
+ api: vi.fn(async () => ({ statusCode: 200 })),
6
+ trpc: vi.fn(async () => ({ statusCode: 200 })),
7
+ ensureUserKeysIndexes: vi.fn(async () => undefined),
8
+ createEscrowHoldsIndexes: vi.fn(async () => {
9
+ throw new Error('Escrow hold index initialization failed', {
10
+ cause: { code: 11000, codeName: 'DuplicateKey' },
11
+ });
12
+ }),
13
+ }));
14
+ vi.mock('serverless-http', () => ({ default: () => vi.fn() }));
15
+ vi.mock('@trpc/server/adapters/aws-lambda', () => ({
16
+ awsLambdaRequestHandler: () => handlers.trpc,
17
+ }));
18
+ vi.mock('@sentry/serverless', () => ({
19
+ AWSLambda: { init: vi.fn(), wrapHandler: (handler: unknown) => handler },
20
+ Integrations: { Console: class {}, Http: class {}, ContextLines: class {}, Mongo: class {} },
21
+ }));
22
+ vi.mock('./src/openapi', () => ({ default: {} }));
23
+ vi.mock('./src/dids', () => ({ default: {} }));
24
+ vi.mock('./src/app', () => ({ appRouter: {}, createContext: vi.fn() }));
25
+ vi.mock('./src/helpers/learnCard.helpers', () => ({ getEmptyLearnCard: async () => ({}) }));
26
+ vi.mock('./src/helpers/shim', () => ({ createOpenApiAwsLambdaHandler: () => handlers.api }));
27
+ vi.mock('./src/helpers/sentry.helpers', () => ({
28
+ handleTrpcError: vi.fn(),
29
+ sentryBeforeSend: vi.fn(),
30
+ getTracesSampleRate: () => 0,
31
+ }));
32
+ vi.mock('./src/config/environment', () => ({ environment: {} }));
33
+ vi.mock('./src/helpers/serverlessApplication', () => ({ toServerlessApplication: () => ({}) }));
34
+ vi.mock('./src/models', () => handlers);
35
+ vi.mock('./src/jobs/escrowHoldReminders', () => ({ runEscrowHoldReminders: vi.fn() }));
36
+ vi.mock('./src/jobs/escrowBlobRewrap', () => ({ runEscrowBlobRewrap: vi.fn() }));
37
+ vi.mock('./src/services/escrow-enclave', () => ({ checkEscrowRemoteConfig: vi.fn() }));
38
+
39
+ const event = (method: string): APIGatewayProxyEventV2 => ({
40
+ version: '2.0',
41
+ routeKey: '$default',
42
+ rawPath: '/unrelated',
43
+ rawQueryString: '',
44
+ headers: {},
45
+ isBase64Encoded: false,
46
+ requestContext: {
47
+ accountId: 'test',
48
+ apiId: 'test',
49
+ domainName: 'localhost',
50
+ domainPrefix: 'localhost',
51
+ http: {
52
+ method,
53
+ path: '/unrelated',
54
+ protocol: 'HTTP/1.1',
55
+ sourceIp: '127.0.0.1',
56
+ userAgent: 'test',
57
+ },
58
+ requestId: 'test',
59
+ routeKey: '$default',
60
+ stage: '$default',
61
+ time: '',
62
+ timeEpoch: 0,
63
+ },
64
+ });
65
+ const context: Context = {
66
+ callbackWaitsForEmptyEventLoop: false,
67
+ functionName: 'test',
68
+ functionVersion: 'test',
69
+ invokedFunctionArn: 'test',
70
+ memoryLimitInMB: '128',
71
+ awsRequestId: 'test',
72
+ logGroupName: 'test',
73
+ logStreamName: 'test',
74
+ getRemainingTimeInMillis: () => 30_000,
75
+ done: () => undefined,
76
+ fail: () => undefined,
77
+ succeed: () => undefined,
78
+ };
79
+
80
+ beforeEach(() => {
81
+ vi.resetModules();
82
+ vi.clearAllMocks();
83
+ });
84
+ describe('Lambda startup isolation', () => {
85
+ it('retries transient non-escrow startup failures on the next request', async () => {
86
+ const diagnostic = vi.spyOn(console, 'error').mockImplementation(() => undefined);
87
+ handlers.ensureUserKeysIndexes.mockRejectedValueOnce(
88
+ new Error('transient startup failure')
89
+ );
90
+ const app = await import('./lambdaApp');
91
+ await vi.waitFor(() => expect(diagnostic).toHaveBeenCalled());
92
+ await expect(
93
+ app.openApiHandler(event('GET'), context, () => undefined)
94
+ ).resolves.toMatchObject({ statusCode: 200 });
95
+ expect(handlers.ensureUserKeysIndexes).toHaveBeenCalledTimes(2);
96
+ expect(handlers.createEscrowHoldsIndexes).not.toHaveBeenCalled();
97
+ diagnostic.mockRestore();
98
+ });
99
+
100
+ it.each(['openApiHandler', 'trpcHandler'] as const)(
101
+ '%s serves unrelated requests and OPTIONS without migrating escrow indexes',
102
+ async name => {
103
+ const app = await import('./lambdaApp');
104
+ await expect(app[name](event('GET'), context, () => undefined)).resolves.toMatchObject({
105
+ statusCode: 200,
106
+ });
107
+ await expect(
108
+ app[name](event('OPTIONS'), context, () => undefined)
109
+ ).resolves.toMatchObject({ statusCode: 200 });
110
+ expect(handlers.ensureUserKeysIndexes).toHaveBeenCalledOnce();
111
+ expect(handlers.createEscrowHoldsIndexes).not.toHaveBeenCalled();
112
+ }
113
+ );
114
+ });
package/lambdaApp.ts CHANGED
@@ -15,15 +15,27 @@ import {
15
15
  } from './src/helpers/sentry.helpers';
16
16
  import { environment } from './src/config/environment';
17
17
  import { toServerlessApplication } from './src/helpers/serverlessApplication';
18
- import { ensureUserKeysIndexes, createEscrowHoldsIndexes } from './src/models';
18
+ import { ensureUserKeysIndexes } from './src/models';
19
19
  import { runEscrowHoldReminders } from './src/jobs/escrowHoldReminders';
20
20
  import { runEscrowBlobRewrap } from './src/jobs/escrowBlobRewrap';
21
+ import { checkEscrowRemoteConfig } from './src/services/escrow-enclave';
22
+ import { createRetryingOnce } from './src/helpers/retryingOnce';
23
+
24
+ checkEscrowRemoteConfig();
25
+
26
+ // Retry global startup (WASM + user-keys) without caching failures, and without
27
+ // letting an escrow migration reject unrelated calls.
28
+ const ensureStartup = createRetryingOnce(() =>
29
+ Promise.all([
30
+ getEmptyLearnCard(), // Load WASM in for better cold starts
31
+ ensureUserKeysIndexes(),
32
+ ])
33
+ );
21
34
 
22
- const startupPromise = Promise.all([
23
- getEmptyLearnCard(), // Load WASM in for better cold starts
24
- ensureUserKeysIndexes(),
25
- createEscrowHoldsIndexes(),
26
- ]);
35
+ // Start during init for faster first requests; handlers await (and retry) it.
36
+ ensureStartup().catch(error => {
37
+ console.error('lca-api startup failed; the next request retries', error);
38
+ });
27
39
 
28
40
  const isWarmupEvent = (event: APIGatewayProxyEventV2): boolean =>
29
41
  'source' in event && event.source === 'serverless-plugin-warmup';
@@ -79,7 +91,7 @@ export const _trpcHandler = awsLambdaRequestHandler({
79
91
 
80
92
  export const openApiHandler = Sentry.AWSLambda.wrapHandler(
81
93
  async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
82
- await startupPromise;
94
+ await ensureStartup();
83
95
 
84
96
  if (isWarmupEvent(event)) {
85
97
  console.log('[Warmup] Initializing LearnCard...');
@@ -105,7 +117,7 @@ export const openApiHandler = Sentry.AWSLambda.wrapHandler(
105
117
 
106
118
  export const trpcHandler = Sentry.AWSLambda.wrapHandler(
107
119
  async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
108
- await startupPromise;
120
+ await ensureStartup();
109
121
 
110
122
  if (isWarmupEvent(event)) {
111
123
  console.log('[Warmup] Initializing LearnCard...');
@@ -130,9 +142,9 @@ export const trpcHandler = Sentry.AWSLambda.wrapHandler(
130
142
  );
131
143
 
132
144
  // Scheduled (EventBridge) functions, not API-Gateway-triggered: they share
133
- // startupPromise with the handlers above; warmup/OPTIONS handling doesn't apply.
145
+ // ensureStartup with the handlers above; warmup/OPTIONS handling doesn't apply.
134
146
  export const escrowHoldRemindersHandler = Sentry.AWSLambda.wrapHandler(async (): Promise<void> => {
135
- await startupPromise;
147
+ await ensureStartup();
136
148
  const counts = await runEscrowHoldReminders();
137
149
  console.log('[escrow-hold-reminders] completed', counts);
138
150
  });
@@ -140,7 +152,7 @@ export const escrowHoldRemindersHandler = Sentry.AWSLambda.wrapHandler(async ():
140
152
  // P9.3: migrates escrow blobs off a retiring enclave key for accounts that
141
153
  // never sign in again (P9.2 already covers accounts that do, on next login).
142
154
  export const escrowBlobRewrapHandler = Sentry.AWSLambda.wrapHandler(async (): Promise<void> => {
143
- await startupPromise;
155
+ await ensureStartup();
144
156
  const counts = await runEscrowBlobRewrap();
145
157
  console.log('[escrow-blob-rewrap] completed', counts);
146
158
  });