@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.
Files changed (274) hide show
  1. package/.env.example +4 -0
  2. package/AGENTS.md +177 -159
  3. package/CHANGELOG.md +148 -0
  4. package/README.md +26 -8
  5. package/compose.inbox.yml +7 -0
  6. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  7. package/dist/accesslayer/auth-grant/read.d.ts +1 -0
  8. package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
  9. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +3 -2
  10. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  11. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +1 -0
  12. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  13. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +5 -3
  14. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  15. package/dist/accesslayer/credential/create.d.ts +2 -1
  16. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  17. package/dist/accesslayer/credential/create.test.d.ts +2 -0
  18. package/dist/accesslayer/credential/create.test.d.ts.map +1 -0
  19. package/dist/accesslayer/credential/relationships/read.d.ts +1 -0
  20. package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
  21. package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
  22. package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
  23. package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
  24. package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
  25. package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
  26. package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
  27. package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
  28. package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
  29. package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
  30. package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
  31. package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
  32. package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
  33. package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
  34. package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
  35. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  36. package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
  37. package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
  38. package/dist/cache/atomic-counter.test.d.ts +2 -0
  39. package/dist/cache/atomic-counter.test.d.ts.map +1 -0
  40. package/dist/cache/index.d.ts +1 -1
  41. package/dist/cache/index.d.ts.map +1 -1
  42. package/dist/config/environment.d.ts +46 -0
  43. package/dist/config/environment.d.ts.map +1 -1
  44. package/dist/credential-refresh.d.ts.map +1 -1
  45. package/dist/didkit_wasm_bg.wasm +0 -0
  46. package/dist/helpers/activity.helpers.d.ts +4 -0
  47. package/dist/helpers/activity.helpers.d.ts.map +1 -1
  48. package/dist/helpers/boost-storage.helpers.test.d.ts +2 -0
  49. package/dist/helpers/boost-storage.helpers.test.d.ts.map +1 -0
  50. package/dist/helpers/boost.helpers.d.ts +3 -7
  51. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  52. package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
  53. package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
  54. package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
  55. package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
  56. package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
  57. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
  58. package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
  59. package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
  60. package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
  61. package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
  62. package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
  63. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
  64. package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
  65. package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
  66. package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
  67. package/dist/helpers/credential.helpers.d.ts +4 -2
  68. package/dist/helpers/credential.helpers.d.ts.map +1 -1
  69. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  70. package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
  71. package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
  72. package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
  73. package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
  74. package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
  75. package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
  76. package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
  77. package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
  78. package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
  79. package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
  80. package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
  81. package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
  82. package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
  83. package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
  84. package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
  85. package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
  86. package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
  87. package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
  88. package/dist/helpers/inbox.helpers.d.ts +9 -1
  89. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  90. package/dist/helpers/issuedCredentialStatus.helpers.d.ts +5 -0
  91. package/dist/helpers/issuedCredentialStatus.helpers.d.ts.map +1 -0
  92. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  93. package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
  94. package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
  95. package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
  96. package/dist/helpers/shim.d.ts +1 -1
  97. package/dist/helpers/shim.d.ts.map +1 -1
  98. package/dist/helpers/signingAuthority.helpers.d.ts +3 -2
  99. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  100. package/dist/helpers/signingAuthority.helpers.test.d.ts +2 -0
  101. package/dist/helpers/signingAuthority.helpers.test.d.ts.map +1 -0
  102. package/dist/helpers/status-list.helpers.d.ts.map +1 -1
  103. package/dist/helpers/status-list.helpers.test.d.ts +2 -0
  104. package/dist/helpers/status-list.helpers.test.d.ts.map +1 -0
  105. package/dist/inbox-queue-local.d.ts +2 -0
  106. package/dist/inbox-queue-local.d.ts.map +1 -0
  107. package/dist/index.js +2 -2
  108. package/dist/models/AuthGrant.d.ts.map +1 -1
  109. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  110. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  111. package/dist/models/Credential.d.ts +1 -0
  112. package/dist/models/Credential.d.ts.map +1 -1
  113. package/dist/models/CredentialActivity.d.ts +1 -0
  114. package/dist/models/CredentialActivity.d.ts.map +1 -1
  115. package/dist/models/CredentialRefresh.d.ts +5 -2
  116. package/dist/models/CredentialRefresh.d.ts.map +1 -1
  117. package/dist/models/InboxCredential.d.ts +1 -0
  118. package/dist/models/InboxCredential.d.ts.map +1 -1
  119. package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
  120. package/dist/openapi.d.ts.map +1 -1
  121. package/dist/routes/act-as.test.d.ts +2 -0
  122. package/dist/routes/act-as.test.d.ts.map +1 -0
  123. package/dist/routes/activity.d.ts +3 -0
  124. package/dist/routes/activity.d.ts.map +1 -1
  125. package/dist/routes/app-store.d.ts.map +1 -1
  126. package/dist/routes/auth-grants.d.ts +10 -0
  127. package/dist/routes/auth-grants.d.ts.map +1 -1
  128. package/dist/routes/boosts.d.ts +506 -12
  129. package/dist/routes/boosts.d.ts.map +1 -1
  130. package/dist/routes/contracts.d.ts +48 -2
  131. package/dist/routes/contracts.d.ts.map +1 -1
  132. package/dist/routes/credential-refreshes.d.ts +4 -4
  133. package/dist/routes/credential-refreshes.d.ts.map +1 -1
  134. package/dist/routes/credentials.d.ts +8 -2
  135. package/dist/routes/credentials.d.ts.map +1 -1
  136. package/dist/routes/did-metadata.d.ts +8 -8
  137. package/dist/routes/federation.d.ts +2 -2
  138. package/dist/routes/inbox.d.ts +973 -31
  139. package/dist/routes/inbox.d.ts.map +1 -1
  140. package/dist/routes/index.d.ts +47 -127
  141. package/dist/routes/index.d.ts.map +1 -1
  142. package/dist/routes/presentations.d.ts +6 -6
  143. package/dist/routes/storage.d.ts +63 -63
  144. package/dist/routes/test.d.ts +6 -0
  145. package/dist/routes/test.d.ts.map +1 -1
  146. package/dist/routes/workflows.d.ts +14 -14
  147. package/dist/routes/workflows.d.ts.map +1 -1
  148. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  149. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
  150. package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
  151. package/dist/services/registry/registry.factory.d.ts.map +1 -1
  152. package/dist/types/activity.d.ts +4 -0
  153. package/dist/types/activity.d.ts.map +1 -1
  154. package/dist/types/consentflowcontract.d.ts +22 -8
  155. package/dist/types/consentflowcontract.d.ts.map +1 -1
  156. package/dist/types/credential-refresh.d.ts +10 -1
  157. package/dist/types/credential-refresh.d.ts.map +1 -1
  158. package/dist/types/credential.d.ts +7 -0
  159. package/dist/types/credential.d.ts.map +1 -1
  160. package/dist/types/did-metadata.d.ts +2 -2
  161. package/dist/types/inbox-batch.d.ts +38 -0
  162. package/dist/types/inbox-batch.d.ts.map +1 -0
  163. package/inbox-e2e-setup.ts +53 -1
  164. package/inbox-elasticmq.conf +10 -0
  165. package/lambda.ts +61 -16
  166. package/package.json +18 -16
  167. package/scripts/setup-inbox-queues.ts +60 -0
  168. package/serverless-local.yml +2 -0
  169. package/serverless.yml +75 -0
  170. package/src/accesslayer/app-store-listing/relationships/read.ts +4 -3
  171. package/src/accesslayer/auth-grant/read.ts +2 -1
  172. package/src/accesslayer/consentflowcontract/relationships/create.ts +15 -5
  173. package/src/accesslayer/consentflowcontract/relationships/read.ts +161 -63
  174. package/src/accesslayer/consentflowcontract/relationships/update.ts +43 -10
  175. package/src/accesslayer/credential/create.test.ts +141 -0
  176. package/src/accesslayer/credential/create.ts +27 -3
  177. package/src/accesslayer/credential-activity/create.ts +1 -0
  178. package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
  179. package/src/accesslayer/credential-refresh/index.ts +1 -0
  180. package/src/accesslayer/credential-refresh/read.ts +8 -2
  181. package/src/accesslayer/credential-refresh/update.ts +4 -0
  182. package/src/accesslayer/inbox-batch/store.ts +469 -0
  183. package/src/accesslayer/inbox-credential/create.ts +51 -19
  184. package/src/accesslayer/inbox-credential/refresh.ts +221 -0
  185. package/src/accesslayer/inbox-credential/update.ts +1 -1
  186. package/src/cache/atomic-counter.test-helpers.ts +27 -0
  187. package/src/cache/atomic-counter.test.ts +6 -0
  188. package/src/cache/index.ts +25 -13
  189. package/src/config/environment.ts +26 -0
  190. package/src/credential-refresh.ts +5 -1
  191. package/src/docker-entry.ts +8 -0
  192. package/src/helpers/activity.helpers.ts +4 -0
  193. package/src/helpers/app-counter.helpers.ts +1 -1
  194. package/src/helpers/boost-storage.helpers.test.ts +242 -0
  195. package/src/helpers/boost.helpers.ts +58 -437
  196. package/src/helpers/contact-method.helpers.test.ts +43 -0
  197. package/src/helpers/contact-method.helpers.ts +3 -4
  198. package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
  199. package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
  200. package/src/helpers/credential-refresh-email.helpers.ts +201 -0
  201. package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
  202. package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
  203. package/src/helpers/credential-refresh.helpers.ts +385 -107
  204. package/src/helpers/credential.helpers.ts +6 -2
  205. package/src/helpers/finalize-inbox.helpers.ts +39 -22
  206. package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
  207. package/src/helpers/inbox-batch-http.helpers.ts +150 -0
  208. package/src/helpers/inbox-batch-status.helpers.ts +11 -0
  209. package/src/helpers/inbox-batch.helpers.test.ts +668 -0
  210. package/src/helpers/inbox-batch.helpers.ts +374 -0
  211. package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
  212. package/src/helpers/inbox-queue.helpers.test.ts +253 -0
  213. package/src/helpers/inbox-queue.helpers.ts +372 -0
  214. package/src/helpers/inbox-refresh.helpers.ts +432 -0
  215. package/src/helpers/inbox.helpers.ts +301 -50
  216. package/src/helpers/issuedCredentialStatus.helpers.ts +11 -0
  217. package/src/helpers/notifications.helpers.ts +4 -2
  218. package/src/helpers/rateLimit.helpers.ts +2 -5
  219. package/src/helpers/refresh-send-intent.helpers.ts +273 -0
  220. package/src/helpers/shim.ts +32 -31
  221. package/src/helpers/signingAuthority.helpers.test.ts +198 -0
  222. package/src/helpers/signingAuthority.helpers.ts +41 -18
  223. package/src/helpers/status-list.helpers.test.ts +77 -0
  224. package/src/helpers/status-list.helpers.ts +31 -8
  225. package/src/inbox-queue-local.ts +28 -0
  226. package/src/models/AuthGrant.ts +1 -0
  227. package/src/models/ConsentFlowTerms.ts +4 -0
  228. package/src/models/ConsentFlowTransaction.ts +4 -0
  229. package/src/models/Credential.ts +1 -0
  230. package/src/models/CredentialActivity.ts +3 -5
  231. package/src/models/CredentialRefresh.ts +4 -2
  232. package/src/models/InboxCredential.ts +2 -0
  233. package/src/models/credential-refresh-constraints.test.ts +8 -3
  234. package/src/models/credential-refresh-constraints.ts +5 -0
  235. package/src/openapi.ts +8 -0
  236. package/src/routes/act-as.test.ts +239 -0
  237. package/src/routes/app-store.ts +69 -52
  238. package/src/routes/auth-grants.ts +13 -11
  239. package/src/routes/boosts.ts +943 -120
  240. package/src/routes/contracts.ts +55 -7
  241. package/src/routes/credential-refreshes.test.ts +8 -2
  242. package/src/routes/credential-refreshes.ts +4 -1
  243. package/src/routes/credentials.ts +2 -1
  244. package/src/routes/inbox.ts +95 -49
  245. package/src/routes/index.ts +164 -88
  246. package/src/routes/skills.ts +1 -1
  247. package/src/routes/workflows.ts +80 -31
  248. package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
  249. package/src/services/delivery/adapters/postmark.adapter.ts +12 -0
  250. package/src/services/registry/registry.factory.ts +2 -1
  251. package/src/types/activity.ts +2 -0
  252. package/src/types/consentflowcontract.ts +21 -3
  253. package/src/types/credential-refresh.ts +21 -2
  254. package/src/types/credential.ts +13 -1
  255. package/src/types/inbox-batch.ts +46 -0
  256. package/test/app-store-listing.spec.ts +66 -30
  257. package/test/atomic-counter.e2e.ts +12 -0
  258. package/test/auth-grant.spec.ts +21 -21
  259. package/test/bitstring-status-list.spec.ts +12 -9
  260. package/test/consentflow.spec.ts +453 -26
  261. package/test/credential-refresh-email.spec.ts +663 -0
  262. package/test/credential-refresh-publication.spec.ts +13 -5
  263. package/test/guardian-gated-routes.spec.ts +337 -180
  264. package/test/helpers/inbox-e2e-environment.ts +6 -0
  265. package/test/inbox-batch.spec.ts +1338 -0
  266. package/test/inbox-refresh.spec.ts +534 -0
  267. package/test/inbox-security.e2e.ts +48 -3
  268. package/test/openapi.spec.ts +41 -1
  269. package/test/refresh-send.spec.ts +1799 -0
  270. package/test/registry.spec.ts +56 -11
  271. package/test/revoke-suspend-per-instance.spec.ts +9 -1
  272. package/vitest.config.ts +13 -0
  273. package/vitest.inbox-e2e.config.ts +5 -1
  274. package/vitest.integration.config.ts +2 -0
package/.env.example CHANGED
@@ -22,6 +22,10 @@ OIDC_EXPECTED_AUDIENCE=
22
22
 
23
23
  AWS_REGION=
24
24
  NOTIFICATIONS_QUEUE_URL=
25
+ INBOX_QUEUE_URL=
26
+ INBOX_DEAD_LETTER_QUEUE_URL=
27
+ # Set only for a local SQS emulator; production uses the AWS endpoint and IAM role.
28
+ INBOX_QUEUE_ENDPOINT=
25
29
  NOTIFICATIONS_QUEUE_POLL_URL=
26
30
  NOTIFICATIONS_SERVICE_WEBHOOK_URL=
27
31
  NOTIFICATIONS_SERVICE_PORT=5100
package/AGENTS.md CHANGED
@@ -2,54 +2,54 @@
2
2
 
3
3
  ## Build & Development Commands
4
4
 
5
- - Build: `bun run build` or `bun run build:docker`
6
- - Dev: `bun run dev` - Watches files and rebuilds
7
- - Start server: `bun run start` - Runs local server on port 3000
8
- - Start on port 4000: `bun run start-p-4000`
9
- - Deploy: `bun run serverless-deploy`
5
+ - Build: `bun run build` or `bun run build:docker`
6
+ - Dev: `bun run dev` - Watches files and rebuilds
7
+ - Start server: `bun run start` - Runs local server on port 3000
8
+ - Start on port 4000: `bun run start-p-4000`
9
+ - Deploy: `bun run serverless-deploy`
10
10
 
11
11
  ### Skill Framework Seeding
12
12
 
13
- - Auto-bootstrap seeds default public skill frameworks in non-production when the database has no `SkillFramework` nodes.
14
- - Disable auto-bootstrap with `SKIP_SKILL_FRAMEWORK_SEED=true`.
15
- - Re-seed manually with `bun run skill-frameworks seed [local|staging]` from `services/learn-card-network/brain-service`.
16
- - Grant framework admin access to an existing profile with `bun run skill-frameworks add-admin [local|staging]` and enter the profile id when prompted.
17
- - If no stage is provided, the CLI defaults to `local` and prints a note.
18
- - Use `SKILL_FRAMEWORK_SEED_OWNER_PROFILE_ID` to override the default `network-seed` owner profile when needed.
19
- - The seed command is idempotent and will not duplicate framework or skill nodes.
13
+ - Auto-bootstrap seeds default public skill frameworks in non-production when the database has no `SkillFramework` nodes.
14
+ - Disable auto-bootstrap with `SKIP_SKILL_FRAMEWORK_SEED=true`.
15
+ - Re-seed manually with `bun run skill-frameworks seed [local|staging]` from `services/learn-card-network/brain-service`.
16
+ - Grant framework admin access to an existing profile with `bun run skill-frameworks add-admin [local|staging]` and enter the profile id when prompted.
17
+ - If no stage is provided, the CLI defaults to `local` and prints a note.
18
+ - Use `SKILL_FRAMEWORK_SEED_OWNER_PROFILE_ID` to override the default `network-seed` owner profile when needed.
19
+ - The seed command is idempotent and will not duplicate framework or skill nodes.
20
20
 
21
21
  ## Test Commands
22
22
 
23
- - Run all tests: `bun run test`
24
- - Run all tests once (non-watch): `bun run test -- run` (equivalent to `vitest run`)
25
- - Watch mode: `bun run test:watch`
26
- - Run single test: `bun run test test/consentflow.spec.ts`
27
- - Run single test once (non-watch): `bun run test -- run test/consentflow.spec.ts`
28
- - Run specific test: `bun run test -t "should allow setting and retrieving the image field for a contract"`
23
+ - Run all tests: `bun run test`
24
+ - Run all tests once (non-watch): `bun run test -- run` (equivalent to `vitest run`)
25
+ - Watch mode: `bun run test:watch`
26
+ - Run single test: `bun run test test/consentflow.spec.ts`
27
+ - Run single test once (non-watch): `bun run test -- run test/consentflow.spec.ts`
28
+ - Run specific test: `bun run test -t "should allow setting and retrieving the image field for a contract"`
29
29
 
30
30
  ## Code Style Guidelines
31
31
 
32
- - **TypeScript**: Strict typing with interfaces/types defined in `src/types/`
33
- - **Imports**: Use path aliases (`@helpers/*`, `@models`, `@routes`, etc.)
34
- - **Error Handling**: Use trpc error codes (`UNAUTHORIZED`, `BAD_REQUEST`, etc.)
35
- - **Naming**:
36
- - PascalCase for classes, types, interfaces
37
- - camelCase for variables, functions, methods
38
- - **Models Structure**: Database models in `src/models/` with relationships defined
39
- - **Access Layer**: Database operations go in `src/accesslayer/` folders
40
- - **Routes**: API endpoints defined in `src/routes/` using tRPC
41
- - **Variable Exports**: Avoid one-line default exports; use named exports
42
- - **Return Types**: Always specify explicit return types for functions
32
+ - **TypeScript**: Strict typing with interfaces/types defined in `src/types/`
33
+ - **Imports**: Use path aliases (`@helpers/*`, `@models`, `@routes`, etc.)
34
+ - **Error Handling**: Use trpc error codes (`UNAUTHORIZED`, `BAD_REQUEST`, etc.)
35
+ - **Naming**:
36
+ - PascalCase for classes, types, interfaces
37
+ - camelCase for variables, functions, methods
38
+ - **Models Structure**: Database models in `src/models/` with relationships defined
39
+ - **Access Layer**: Database operations go in `src/accesslayer/` folders
40
+ - **Routes**: API endpoints defined in `src/routes/` using tRPC
41
+ - **Variable Exports**: Avoid one-line default exports; use named exports
42
+ - **Return Types**: Always specify explicit return types for functions
43
43
 
44
44
  ## ConsentFlow Architecture
45
45
 
46
46
  ConsentFlow is a consent management system where:
47
47
 
48
- - **Profiles** can create and consent to **Contracts**
49
- - **Contracts** define data access requirements (read/write permissions)
50
- - **Terms** record a profile's consent to a contract
51
- - **Transactions** record actions taken against Terms (consent, withdraw, update, sync)
52
- - **Credentials** can be issued through contract consent or synced by users from their existing credentials
48
+ - **Profiles** can create and consent to **Contracts**
49
+ - **Contracts** define data access requirements (read/write permissions)
50
+ - **Terms** record a profile's consent to a contract
51
+ - **Transactions** record actions taken against Terms (consent, withdraw, update, sync)
52
+ - **Credentials** can be issued through contract consent or synced by users from their existing credentials
53
53
 
54
54
  ### Relationship Flow
55
55
 
@@ -62,62 +62,62 @@ ConsentFlow is a consent management system where:
62
62
 
63
63
  ### Transaction Types
64
64
 
65
- - **Consent Transaction**: Created when a user initially consents to a contract
66
- - **Update Transaction**: Created when a user updates their terms
67
- - **Withdraw Transaction**: Created when a user withdraws consent
68
- - **Sync Transaction**: Created when a user syncs credentials to specific categories
69
- - **Write Transaction**: Created when credentials are issued through a contract (usually via auto-boosts)
65
+ - **Consent Transaction**: Created when a user initially consents to a contract
66
+ - **Update Transaction**: Created when a user updates their terms
67
+ - **Withdraw Transaction**: Created when a user withdraws consent
68
+ - **Sync Transaction**: Created when a user syncs credentials to specific categories
69
+ - **Write Transaction**: Created when credentials are issued through a contract (usually via auto-boosts)
70
70
 
71
71
  ### Neo4j & Neogma Integration
72
72
 
73
- - Neo4j is used as the graph database for storing entities and relationships
74
- - Neogma serves as an ORM providing model definitions with schemas
75
- - Models define node labels, properties, relationships, and primary keys
76
- - Relationships are defined with direction, name, and target model
77
- - QueryBuilder is used for complex queries with relationship traversal
73
+ - Neo4j is used as the graph database for storing entities and relationships
74
+ - Neogma serves as an ORM providing model definitions with schemas
75
+ - Models define node labels, properties, relationships, and primary keys
76
+ - Relationships are defined with direction, name, and target model
77
+ - QueryBuilder is used for complex queries with relationship traversal
78
78
 
79
79
  ### Handling Nested Objects
80
80
 
81
- - Neo4j can't store deeply nested objects directly
82
- - The system uses:
83
- - `flattenObject()`: Converts nested structures to dot-notation keys
84
- - `inflateObject()`: Restores flattened objects to nested structure
85
- - Example conversion:
81
+ - Neo4j can't store deeply nested objects directly
82
+ - The system uses:
83
+ - `flattenObject()`: Converts nested structures to dot-notation keys
84
+ - `inflateObject()`: Restores flattened objects to nested structure
85
+ - Example conversion:
86
86
  ```json
87
87
  // Original
88
88
  { "contract": { "read": { "personal": { "name": true } } } }
89
89
  // Flattened
90
90
  { "contract.read.personal.name": true }
91
91
  ```
92
- - This pattern is essential for storing complex objects in Neo4j
92
+ - This pattern is essential for storing complex objects in Neo4j
93
93
 
94
94
  ### Common Data Flow Patterns
95
95
 
96
- - **Data Queries**: Use `convertDataQueryToNeo4jQuery()` for Neo4j compatibility
97
- - **Category Filtering**: Use `shouldIncludeCategory()` to filter credentials by category
98
- - **Relationship Creation**: Follow the pattern shown in the relationship creation functions
99
- - **Transaction Recording**: Create transactions for all important actions
96
+ - **Data Queries**: Use `convertDataQueryToNeo4jQuery()` for Neo4j compatibility
97
+ - **Category Filtering**: Use `shouldIncludeCategory()` to filter credentials by category
98
+ - **Relationship Creation**: Follow the pattern shown in the relationship creation functions
99
+ - **Transaction Recording**: Create transactions for all important actions
100
100
 
101
101
  ### Signing Authorities
102
102
 
103
- - Signing Authorities are external services that sign credentials on behalf of profiles
104
- - Each profile can register multiple signing authorities via `usesSigningAuthority` relationships
105
- - Signing authorities are stored in the SigningAuthority model with an endpoint URL
106
- - The relationship between Profile and SigningAuthority contains:
107
- - `name`: A name identifier for the signing authority (e.g., "default")
108
- - `did`: The DID that the signing authority will use to sign credentials
109
- - When issuing credentials through a signing authority:
103
+ - Signing Authorities are external services that sign credentials on behalf of profiles
104
+ - Each profile can register multiple signing authorities via `usesSigningAuthority` relationships
105
+ - Signing authorities are stored in the SigningAuthority model with an endpoint URL
106
+ - The relationship between Profile and SigningAuthority contains:
107
+ - `name`: A name identifier for the signing authority (e.g., "default")
108
+ - `did`: The DID that the signing authority will use to sign credentials
109
+ - When issuing credentials through a signing authority:
110
110
  1. The system makes a request to the signing authority's endpoint
111
111
  2. It passes the unsigned credential, owner DID, and signing authority details
112
112
  3. The signing authority signs the credential and returns it
113
113
  4. The `issueCredentialWithSigningAuthority()` helper handles this flow
114
- - A common pattern is to get a signing authority with `getSigningAuthorityForUserByName(profile, endpoint, name)`
114
+ - A common pattern is to get a signing authority with `getSigningAuthorityForUserByName(profile, endpoint, name)`
115
115
 
116
116
  ### AutoBoosts
117
117
 
118
- - AutoBoosts are credentials automatically issued when a user consents to a contract
119
- - An existing Boost is attached to a Contract via the `AUTO_RECEIVE` relationship
120
- - When creating a contract with autoboosts, an array of autoboosts must be provided, each with a boost URI and signing authority:
118
+ - AutoBoosts are credentials automatically issued when a user consents to a contract
119
+ - An existing Boost is attached to a Contract via the `AUTO_RECEIVE` relationship
120
+ - When creating a contract with autoboosts, an array of autoboosts must be provided, each with a boost URI and signing authority:
121
121
  ```typescript
122
122
  {
123
123
  // other contract fields
@@ -139,24 +139,24 @@ ConsentFlow is a consent management system where:
139
139
  ];
140
140
  }
141
141
  ```
142
- - The signing authority information is stored on each `AUTO_RECEIVE` relationship
143
- - When a user consents to a contract, all attached AutoBoosts are:
142
+ - The signing authority information is stored on each `AUTO_RECEIVE` relationship
143
+ - When a user consents to a contract, all attached AutoBoosts are:
144
144
  1. Issued using the specified signing authority of the contract owner
145
145
  2. Sent to the consenting user
146
146
  3. Recorded with a 'write' action transaction on the terms
147
- - AutoBoosts are processed during:
148
- - Initial consent via `consentToContract()`
149
- - Re-consent via `reconsentTerms()`
150
- - Terms update via `updateTerms()`
151
- - The contract owner MUST have the specified signing authority configured for each autoboost to work correctly
152
- - If a specified signing authority doesn't exist, that specific autoboost will be skipped with an error message
153
- - Multiple autoboosts can use the same signing authority or different ones
147
+ - AutoBoosts are processed during:
148
+ - Initial consent via `consentToContract()`
149
+ - Re-consent via `reconsentTerms()`
150
+ - Terms update via `updateTerms()`
151
+ - The contract owner MUST have the specified signing authority configured for each autoboost to work correctly
152
+ - If a specified signing authority doesn't exist, that specific autoboost will be skipped with an error message
153
+ - Multiple autoboosts can use the same signing authority or different ones
154
154
 
155
155
  ### Credential Syncing
156
156
 
157
- - Users can sync their existing credentials to a contract they've consented to
158
- - Syncing uses the `syncCredentialsToContract` API endpoint
159
- - The sync process:
157
+ - Users can sync their existing credentials to a contract they've consented to
158
+ - Syncing uses the `syncCredentialsToContract` API endpoint
159
+ - The sync process:
160
160
  1. User identifies which credentials to share in which categories
161
161
  2. User calls `syncCredentialsToContract` with `termsUri` and a map of categories to credential URIs:
162
162
  ```typescript
@@ -175,38 +175,56 @@ ConsentFlow is a consent management system where:
175
175
  4. System creates a 'sync' transaction with the categorized credentials
176
176
  5. System updates the terms by adding the synced credentials to the shared arrays for each category
177
177
  6. Contract owner receives a notification about the sync action
178
- - The transaction and terms data use the same structure format for categories
179
- - All synced credentials are added to the existing shared arrays (if any) for each category
180
- - Duplicate credential URIs are automatically deduplicated
178
+ - The transaction and terms data use the same structure format for categories
179
+ - All synced credentials are added to the existing shared arrays (if any) for each category
180
+ - Duplicate credential URIs are automatically deduplicated
181
181
 
182
182
  ## Service-Level Boost Sending via Signing Authority
183
183
 
184
+ New Boost issuance always stores the issuer's payload unchanged: `sendBoost()` does
185
+ not decrypt or counter-sign credentials. There is no rollout flag or legacy issuance
186
+ branch. Signing-authority encryption targets the subject and issuing owner, excluding
187
+ brain; the trusted signing authority still has access. Claim links and consent
188
+ autoboosts also request encryption. INSTANCE_OF, sent/received, contract/listing,
189
+ acceptance, and notification behavior is preserved.
190
+
191
+ VerifyBoost supports both existing wrapped credentials and new unwrapped credentials.
192
+ Display and unwrap-if-present helpers retain legacy support. The post-message handler
193
+ only passes `boostCredential` as a modal prop; it does not construct or sign a wrapper.
194
+ Compatibility tests create legacy fixtures independently of production issuance.
195
+
196
+ For encrypted storage, public status-list coordinates are retained from the unsigned SA request in an
197
+ in-memory sidecar and persisted as `Credential.statusEntries`. Revocation and suspension
198
+ use these coordinates without decrypting the credential. Issuers must allocate status
199
+ entries before signing; the SDK's `sendBoost` already does this. Inbox finalization and
200
+ VC-API presentations still explicitly request plaintext for their existing protocols.
201
+
184
202
  ### Context
185
203
 
186
204
  For certain workflows, such as sending a boost (credential) to another profile via HTTP service routes, the server cannot access the user's key material for security reasons. To enable credential issuance in these cases, the system supports **Signing Authorities**.
187
205
 
188
206
  ### Signing Authorities & Credential Issuance
189
207
 
190
- - **Signing Authority**: An external service registered by a user/profile that can sign credentials on their behalf.
191
- - Users register signing authorities by specifying an endpoint and a name.
192
- - When sending a boost via HTTP, the service uses the registered signing authority to sign the credential and deliver it to the recipient.
208
+ - **Signing Authority**: An external service registered by a user/profile that can sign credentials on their behalf.
209
+ - Users register signing authorities by specifying an endpoint and a name.
210
+ - When sending a boost via HTTP, the service uses the registered signing authority to sign the credential and deliver it to the recipient.
193
211
 
194
212
  ### Example: `/boost/send/via-signing-authority` Route
195
213
 
196
- - **Input:** `{ profileId, boostUri, signingAuthority: { name, endpoint } }`
197
- - **Flow:**
214
+ - **Input:** `{ profileId, boostUri, signingAuthority: { name, endpoint } }`
215
+ - **Flow:**
198
216
  1. Fetch the boost by URI.
199
217
  2. Fetch the recipient profile.
200
218
  3. Prepare an unsigned VC for the recipient.
201
219
  4. Use the specified signing authority to sign the VC.
202
220
  5. Deliver the signed VC to the recipient via the network.
203
- - **Testing:** E2E tests should POST directly to the route using fetch, after registering a signing authority for the sender.
221
+ - **Testing:** E2E tests should POST directly to the route using fetch, after registering a signing authority for the sender.
204
222
 
205
223
  ### Best Practices for AI Assistants
206
224
 
207
- - Always check for the availability of key material before issuing credentials server-side. If unavailable, require a signing authority.
208
- - Document the expected flow, security implications, and how to set up signing authorities in both code and tests.
209
- - Prefer direct HTTP calls in E2E tests for service-only routes, and demonstrate signing authority setup in test code.
225
+ - Always check for the availability of key material before issuing credentials server-side. If unavailable, require a signing authority.
226
+ - Document the expected flow, security implications, and how to set up signing authorities in both code and tests.
227
+ - Prefer direct HTTP calls in E2E tests for service-only routes, and demonstrate signing authority setup in test code.
210
228
 
211
229
  ## Profile Connection Invites
212
230
 
@@ -214,25 +232,25 @@ LearnCard supports creating and managing profile connection invitation links wit
214
232
 
215
233
  ### Overview
216
234
 
217
- - Invites are identified by a `challenge` string and are scoped to the profile that created them.
235
+ - Invites are identified by a `challenge` string and are scoped to the profile that created them.
218
236
 
219
- - Each invite tracks `maxUses` and `usesRemaining`.
237
+ - Each invite tracks `maxUses` and `usesRemaining`.
220
238
 
221
- - Unlimited invites have `maxUses = null` and `usesRemaining = null`.
239
+ - Unlimited invites have `maxUses = null` and `usesRemaining = null`.
222
240
 
223
- - Expiration is expressed in seconds. `0` means no expiration.
241
+ - Expiration is expressed in seconds. `0` means no expiration.
224
242
 
225
243
  ### Routes
226
244
 
227
245
  #### Generate Invite
228
246
 
229
- - Method: `POST`
247
+ - Method: `POST`
230
248
 
231
- - Path: `/profile/generate-invite`
249
+ - Path: `/profile/generate-invite`
232
250
 
233
- - Scope: `connections:write`
251
+ - Scope: `connections:write`
234
252
 
235
- - Input:
253
+ - Input:
236
254
 
237
255
  ```json
238
256
  {
@@ -242,13 +260,13 @@ LearnCard supports creating and managing profile connection invitation links wit
242
260
  }
243
261
  ```
244
262
 
245
- - `expiration` (number, optional): Seconds until expiration. Defaults to 30 days. Use `0` for no expiration.
263
+ - `expiration` (number, optional): Seconds until expiration. Defaults to 30 days. Use `0` for no expiration.
246
264
 
247
- - `challenge` (string, optional): Custom challenge; if omitted, a UUID is generated.
265
+ - `challenge` (string, optional): Custom challenge; if omitted, a UUID is generated.
248
266
 
249
- - `maxUses` (integer, optional): Minimum `0`. Defaults to `1` (single-use). Use `0` for unlimited usage.
267
+ - `maxUses` (integer, optional): Minimum `0`. Defaults to `1` (single-use). Use `0` for unlimited usage.
250
268
 
251
- - Response:
269
+ - Response:
252
270
 
253
271
  ```json
254
272
  {
@@ -260,13 +278,13 @@ LearnCard supports creating and managing profile connection invitation links wit
260
278
 
261
279
  #### List Invites
262
280
 
263
- - Method: `GET`
281
+ - Method: `GET`
264
282
 
265
- - Path: `/profile/invites`
283
+ - Path: `/profile/invites`
266
284
 
267
- - Scope: `connections:read`
285
+ - Scope: `connections:read`
268
286
 
269
- - Output: Array of invites
287
+ - Output: Array of invites
270
288
 
271
289
  ```json
272
290
  [
@@ -285,55 +303,55 @@ LearnCard supports creating and managing profile connection invitation links wit
285
303
  ]
286
304
  ```
287
305
 
288
- - Notes:
306
+ - Notes:
289
307
 
290
- - Exhausted invites (where `usesRemaining === 0`) are omitted.
308
+ - Exhausted invites (where `usesRemaining === 0`) are omitted.
291
309
 
292
- - `expiresIn` is `null` if the invite never expires.
310
+ - `expiresIn` is `null` if the invite never expires.
293
311
 
294
312
  #### Invalidate Invite
295
313
 
296
- - Method: `POST`
314
+ - Method: `POST`
297
315
 
298
- - Path: `/profile/invite/{challenge}/invalidate`
316
+ - Path: `/profile/invite/{challenge}/invalidate`
299
317
 
300
- - Scope: `connections:write`
318
+ - Scope: `connections:write`
301
319
 
302
- - Output: `true` on success.
320
+ - Output: `true` on success.
303
321
 
304
322
  #### Connect Using Invite
305
323
 
306
- - Method: `POST`
324
+ - Method: `POST`
307
325
 
308
- - Path: `/profile/{profileId}/connect/{challenge}`
326
+ - Path: `/profile/{profileId}/connect/{challenge}`
309
327
 
310
- - Scope: `profiles:write`
328
+ - Scope: `profiles:write`
311
329
 
312
- - Behavior:
330
+ - Behavior:
313
331
 
314
- - Validates that the invite exists, is not expired, and has remaining uses (or is unlimited).
332
+ - Validates that the invite exists, is not expired, and has remaining uses (or is unlimited).
315
333
 
316
- - On successful connection, consumes one use for finite invites (decrements `usesRemaining`).
334
+ - On successful connection, consumes one use for finite invites (decrements `usesRemaining`).
317
335
 
318
- - Returns `404` if invite is invalid or expired.
336
+ - Returns `404` if invite is invalid or expired.
319
337
 
320
338
  ### Behavior & Storage Details
321
339
 
322
- - Cache key format: `inviteChallenge:{profileId}:{challenge}`.
340
+ - Cache key format: `inviteChallenge:{profileId}:{challenge}`.
323
341
 
324
- - Stored value is JSON with `{ maxUses, usesRemaining }`.
342
+ - Stored value is JSON with `{ maxUses, usesRemaining }`.
325
343
 
326
- - Unlimited usage: `maxUses = null` and `usesRemaining = null`.
344
+ - Unlimited usage: `maxUses = null` and `usesRemaining = null`.
327
345
 
328
- - Expiration TTL:
346
+ - Expiration TTL:
329
347
 
330
- - `expiration = 0` stores without TTL (never expires); `expiresIn` will be `null`.
348
+ - `expiration = 0` stores without TTL (never expires); `expiresIn` will be `null`.
331
349
 
332
- - Positive `expiration` stores the TTL in seconds; listing returns remaining seconds as `expiresIn`.
350
+ - Positive `expiration` stores the TTL in seconds; listing returns remaining seconds as `expiresIn`.
333
351
 
334
- - When `usesRemaining` reaches `0`, the invite is considered exhausted and will not be listed. The key may be deleted during consumption.
352
+ - When `usesRemaining` reaches `0`, the invite is considered exhausted and will not be listed. The key may be deleted during consumption.
335
353
 
336
- - Backward compatibility: Older invites stored as `'valid'` or `'never'` are parsed and treated as single-use or unlimited, respectively.
354
+ - Backward compatibility: Older invites stored as `'valid'` or `'never'` are parsed and treated as single-use or unlimited, respectively.
337
355
 
338
356
  ### Examples
339
357
 
@@ -372,7 +390,7 @@ curl -X POST \
372
390
 
373
391
  ### Testing
374
392
 
375
- - See `test/profiles.spec.ts` for unit/integration coverage of generation, listing, consumption, unlimited behavior, and invalidation.
393
+ - See `test/profiles.spec.ts` for unit/integration coverage of generation, listing, consumption, unlimited behavior, and invalidation.
376
394
 
377
395
  ## Tracing & Performance Monitoring
378
396
 
@@ -380,9 +398,9 @@ The brain service includes a modular tracing system for performance monitoring a
380
398
 
381
399
  ### Architecture
382
400
 
383
- - **TracingManager**: Central coordinator that manages providers and span lifecycle using `AsyncLocalStorage` for request-scoped trace context
384
- - **TracingProvider**: Interface for output backends (console, JSON, Sentry)
385
- - **Spans**: Represent timed operations with `traceId`, `spanId`, `parentSpanId`, timing, and status
401
+ - **TracingManager**: Central coordinator that manages providers and span lifecycle using `AsyncLocalStorage` for request-scoped trace context
402
+ - **TracingProvider**: Interface for output backends (console, JSON, Sentry)
403
+ - **Spans**: Represent timed operations with `traceId`, `spanId`, `parentSpanId`, timing, and status
386
404
 
387
405
  ### Trace Functions
388
406
 
@@ -573,10 +591,10 @@ ON MATCH SET received.status = "revoked", received.revokedAt = $revokedAt
573
591
 
574
592
  ### Connection Source Keys
575
593
 
576
- - Pattern: `boost:${boostId}`
577
- - Connections can have multiple sources (from multiple boosts)
578
- - On revoke, only the source key from the revoked boost is removed
579
- - Connection deleted only when all source keys are removed
594
+ - Pattern: `boost:${boostId}`
595
+ - Connections can have multiple sources (from multiple boosts)
596
+ - On revoke, only the source key from the revoked boost is removed
597
+ - Connection deleted only when all source keys are removed
580
598
 
581
599
  ### Parent Boost Handling
582
600
 
@@ -616,16 +634,16 @@ Apps can send notifications to users via the `APP_NOTIFICATION` type. There are
616
634
 
617
635
  When the user has the app open in LearnCard, the Partner Connect SDK can trigger a self-notification via the `send-notification` app event type. This is handled by `handleSendNotificationEvent` in `src/routes/app-store.ts`.
618
636
 
619
- - **Rate limit**: 10 notifications per user per app per hour (Redis key: `app-notif-rate:{listingId}:{profileId}`)
620
- - **Auth**: User must have the app installed or own the integration
637
+ - **Rate limit**: 10 notifications per user per app per hour (Redis key: `app-notif-rate:{listingId}:{profileId}`)
638
+ - **Auth**: User must have the app installed or own the integration
621
639
 
622
640
  ### 2. Server-to-Server Route (Primary)
623
641
 
624
642
  App backends send notifications to arbitrary users who have the app installed.
625
643
 
626
- - **Route**: `POST /app-store/listing/{listingId}/notify`
627
- - **Scope**: `app-store:write`
628
- - **Auth**: Caller must own the listing's integration (`verifyListingOwnership`)
644
+ - **Route**: `POST /app-store/listing/{listingId}/notify`
645
+ - **Scope**: `app-store:write`
646
+ - **Auth**: Caller must own the listing's integration (`verifyListingOwnership`)
629
647
 
630
648
  **Input:**
631
649
 
@@ -641,16 +659,16 @@ App backends send notifications to arbitrary users who have the app installed.
641
659
  }
642
660
  ```
643
661
 
644
- - `recipient`: Accepts a profileId, `did:web:...`, or `did:key:...` (resolved via `getProfileIdFromString`)
645
- - `title` / `body`: At least one required
646
- - `actionPath`: Deep link path within the app
647
- - `category`: Grouping category (e.g. `'reward'`, `'announcement'`)
648
- - `priority`: `'normal'` (default) or `'high'`
662
+ - `recipient`: Accepts a profileId, `did:web:...`, or `did:key:...` (resolved via `getProfileIdFromString`)
663
+ - `title` / `body`: At least one required
664
+ - `actionPath`: Deep link path within the app
665
+ - `category`: Grouping category (e.g. `'reward'`, `'announcement'`)
666
+ - `priority`: `'normal'` (default) or `'high'`
649
667
 
650
668
  **Guards:**
651
669
 
652
- - Recipient must have the app installed (`hasProfileInstalledApp`)
653
- - Rate limit: 60 notifications per listing per hour (Redis key: `app-notif-server-rate:{listingId}`)
670
+ - Recipient must have the app installed (`hasProfileInstalledApp`)
671
+ - Rate limit: 60 notifications per listing per hour (Redis key: `app-notif-server-rate:{listingId}`)
654
672
 
655
673
  **Notification shape:**
656
674
 
@@ -708,10 +726,10 @@ Multiple parallel `APP_COUNTER` relationships (one per key) can exist between th
708
726
 
709
727
  ### Limits
710
728
 
711
- - Max **50 counter keys** per user per app (`MAX_COUNTER_KEYS_PER_USER_PER_APP`)
712
- - Max **100 writes** per user per app per minute (Redis rate limit key: `app-counter-rate:{listingId}:{profileId}`)
713
- - Counter keys: alphanumeric + `_` / `-`, max 64 chars
714
- - Amount: any finite integer
729
+ - Max **50 counter keys** per user per app (`MAX_COUNTER_KEYS_PER_USER_PER_APP`)
730
+ - Max **100 writes** per user per app per minute (Redis rate limit key: `app-counter-rate:{listingId}:{profileId}`)
731
+ - Counter keys: alphanumeric + `_` / `-`, max 64 chars
732
+ - Amount: any finite integer
715
733
 
716
734
  ### Key Files
717
735
 
@@ -799,10 +817,10 @@ bun run seed:dev-app -- --promotion FEATURED_CAROUSEL
799
817
 
800
818
  ### Prerequisites
801
819
 
802
- - Neo4j running (`bun run dev:services` from `apps/learn-card-app` or local Docker)
803
- - Redis running (same)
804
- - MongoDB running (same)
805
- - Falls back to local docker-compose defaults if no `.env` is present
820
+ - Neo4j running (`bun run dev:services` from `apps/learn-card-app` or local Docker)
821
+ - Redis running (same)
822
+ - MongoDB running (same)
823
+ - Falls back to local docker-compose defaults if no `.env` is present
806
824
 
807
825
  ### Re-running
808
826
 
@@ -857,10 +875,10 @@ Tenant is resolved once per request in `createContext` via `resolveTenantFromReq
857
875
 
858
876
  ### Rules for Route Authors
859
877
 
860
- - **Always pass `branding: ctx.tenant?.emailBranding`** to `deliveryService.send()`. Emails sent without it render with LearnCard defaults regardless of the caller's tenant.
861
- - **Use `getFrom({ mailbox, branding })`** for the from-address so the domain matches the tenant (e.g. `recovery@vetpass.app` for VetPass).
862
- - **Use the local template ID as the `templateAlias`** (e.g. `'inbox-claim'`, `'guardian-approval'`). These are pre-registered as sentinels in `LOCAL_TEMPLATE_MAP` — env-var overrides are optional, never required.
863
- - **Do not write plain-text fallbacks.** If local rendering fails for a sentinel alias, the adapter re-throws so the error surfaces — don't silently send unbranded text.
878
+ - **Always pass `branding: ctx.tenant?.emailBranding`** to `deliveryService.send()`. Emails sent without it render with LearnCard defaults regardless of the caller's tenant.
879
+ - **Use `getFrom({ mailbox, branding })`** for the from-address so the domain matches the tenant (e.g. `recovery@vetpass.app` for VetPass).
880
+ - **Use the local template ID as the `templateAlias`** (e.g. `'inbox-claim'`, `'guardian-approval'`). These are pre-registered as sentinels in `LOCAL_TEMPLATE_MAP` — env-var overrides are optional, never required.
881
+ - **Do not write plain-text fallbacks.** If local rendering fails for a sentinel alias, the adapter re-throws so the error surfaces — don't silently send unbranded text.
864
882
 
865
883
  ### Adding a New Template
866
884