@learncard/network-brain-service 3.19.0 → 3.19.1

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 (301) hide show
  1. package/.env.example +34 -2
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +42 -0
  4. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  5. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  6. package/dist/accesslayer/presentation/read.d.ts.map +1 -1
  7. package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
  8. package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
  9. package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
  10. package/dist/accesslayer/share-link/abandon.d.ts +3 -0
  11. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
  12. package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
  13. package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
  14. package/dist/accesslayer/share-link/errors.d.ts +8 -0
  15. package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
  16. package/dist/accesslayer/share-link/finalize.d.ts +3 -0
  17. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
  18. package/dist/accesslayer/share-link/helpers.d.ts +56 -0
  19. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
  20. package/dist/accesslayer/share-link/index.d.ts +12 -0
  21. package/dist/accesslayer/share-link/index.d.ts.map +1 -0
  22. package/dist/accesslayer/share-link/list.d.ts +5 -0
  23. package/dist/accesslayer/share-link/list.d.ts.map +1 -0
  24. package/dist/accesslayer/share-link/read.d.ts +5 -0
  25. package/dist/accesslayer/share-link/read.d.ts.map +1 -0
  26. package/dist/accesslayer/share-link/receipt.d.ts +11 -0
  27. package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
  28. package/dist/accesslayer/share-link/recovery.d.ts +6 -0
  29. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
  30. package/dist/accesslayer/share-link/reserve.d.ts +4 -0
  31. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
  32. package/dist/accesslayer/share-link/revoke.d.ts +3 -0
  33. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
  34. package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
  35. package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
  36. package/dist/accesslayer/share-link/transaction.d.ts +15 -0
  37. package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
  38. package/dist/accesslayer/share-link/types.d.ts +370 -0
  39. package/dist/accesslayer/share-link/types.d.ts.map +1 -0
  40. package/dist/app.d.ts +4 -0
  41. package/dist/app.d.ts.map +1 -1
  42. package/dist/config/environment.d.ts +9 -0
  43. package/dist/config/environment.d.ts.map +1 -1
  44. package/dist/constants/auth-grant.d.ts +2 -0
  45. package/dist/constants/auth-grant.d.ts.map +1 -1
  46. package/dist/helpers/boost.helpers.d.ts.map +1 -1
  47. package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
  48. package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
  49. package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
  50. package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
  51. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  52. package/dist/helpers/notificationMessages.d.ts +1 -1
  53. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  54. package/dist/helpers/presentation-metadata.d.ts +4 -0
  55. package/dist/helpers/presentation-metadata.d.ts.map +1 -0
  56. package/dist/helpers/presentation.helpers.d.ts +1 -1
  57. package/dist/helpers/presentation.helpers.d.ts.map +1 -1
  58. package/dist/helpers/profile.helpers.d.ts +1 -0
  59. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  60. package/dist/helpers/share-content-client/adapters.d.ts +10 -0
  61. package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
  62. package/dist/helpers/share-content-client/canonical.d.ts +7 -0
  63. package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
  64. package/dist/helpers/share-content-client/client.d.ts +5 -0
  65. package/dist/helpers/share-content-client/client.d.ts.map +1 -0
  66. package/dist/helpers/share-content-client/config.d.ts +11 -0
  67. package/dist/helpers/share-content-client/config.d.ts.map +1 -0
  68. package/dist/helpers/share-content-client/index.d.ts +6 -0
  69. package/dist/helpers/share-content-client/index.d.ts.map +1 -0
  70. package/dist/helpers/share-content-client/responses.d.ts +16 -0
  71. package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
  72. package/dist/helpers/share-content-client/types.d.ts +145 -0
  73. package/dist/helpers/share-content-client/types.d.ts.map +1 -0
  74. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
  75. package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
  76. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
  77. package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
  78. package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
  79. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
  80. package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
  81. package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
  82. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
  83. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
  84. package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
  85. package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
  86. package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
  87. package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
  88. package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
  89. package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
  90. package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
  91. package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
  92. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
  93. package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
  94. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
  95. package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
  96. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
  97. package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
  98. package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
  99. package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
  100. package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
  101. package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
  102. package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
  103. package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
  104. package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
  105. package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
  106. package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
  107. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
  108. package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
  109. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
  110. package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
  111. package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
  112. package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
  113. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
  114. package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
  115. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
  116. package/dist/helpers/share-link-owner/config.d.ts +16 -0
  117. package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
  118. package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
  119. package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
  120. package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
  121. package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
  122. package/dist/helpers/share-link-owner-projection.d.ts +4 -0
  123. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
  124. package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
  125. package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
  126. package/dist/helpers/share-link-passcode.d.ts +6 -0
  127. package/dist/helpers/share-link-passcode.d.ts.map +1 -0
  128. package/dist/helpers/share-link-policy/index.d.ts +4 -0
  129. package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
  130. package/dist/helpers/share-link-policy/production.d.ts +13 -0
  131. package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
  132. package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
  133. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
  134. package/dist/helpers/share-link-policy/types.d.ts +16 -0
  135. package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
  136. package/dist/helpers/share-link-public/config.d.ts +13 -0
  137. package/dist/helpers/share-link-public/config.d.ts.map +1 -0
  138. package/dist/helpers/share-link-view-notification.d.ts +2 -0
  139. package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
  140. package/dist/index.js +2 -2
  141. package/dist/models/Presentation.d.ts +2 -0
  142. package/dist/models/Presentation.d.ts.map +1 -1
  143. package/dist/models/Profile.d.ts +2 -0
  144. package/dist/models/Profile.d.ts.map +1 -1
  145. package/dist/models/ShareLink.d.ts +44 -0
  146. package/dist/models/ShareLink.d.ts.map +1 -0
  147. package/dist/models/ShareViewReceipt.d.ts +22 -0
  148. package/dist/models/ShareViewReceipt.d.ts.map +1 -0
  149. package/dist/models/index.d.ts +1 -0
  150. package/dist/models/index.d.ts.map +1 -1
  151. package/dist/models/share-link-constraints.d.ts +3 -0
  152. package/dist/models/share-link-constraints.d.ts.map +1 -0
  153. package/dist/public-share-link-http.d.ts +4 -0
  154. package/dist/public-share-link-http.d.ts.map +1 -0
  155. package/dist/routes/app-store.d.ts.map +1 -1
  156. package/dist/routes/boosts.d.ts.map +1 -1
  157. package/dist/routes/contracts.d.ts +2 -2
  158. package/dist/routes/contracts.d.ts.map +1 -1
  159. package/dist/routes/credentials.d.ts +1 -1
  160. package/dist/routes/index.d.ts +153 -0
  161. package/dist/routes/index.d.ts.map +1 -1
  162. package/dist/routes/presentations.d.ts +1 -0
  163. package/dist/routes/presentations.d.ts.map +1 -1
  164. package/dist/routes/profile-manager.d.ts.map +1 -1
  165. package/dist/routes/profiles.d.ts.map +1 -1
  166. package/dist/routes/public-share-links.d.ts +230 -0
  167. package/dist/routes/public-share-links.d.ts.map +1 -0
  168. package/dist/routes/share-links.d.ts +844 -0
  169. package/dist/routes/share-links.d.ts.map +1 -0
  170. package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
  171. package/dist/types/consentflowcontract.d.ts +1 -1
  172. package/lambda.ts +4 -1
  173. package/package.json +22 -16
  174. package/scripts/seed-demo-persona.ts +425 -0
  175. package/serverless.yml +36 -0
  176. package/shareLinkMaintenanceLambda.ts +49 -0
  177. package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
  178. package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
  179. package/src/accesslayer/presentation/read.ts +57 -25
  180. package/src/accesslayer/presentation/relationships/create.ts +54 -11
  181. package/src/accesslayer/presentation/relationships/read.ts +21 -6
  182. package/src/accesslayer/share-link/abandon.ts +100 -0
  183. package/src/accesslayer/share-link/cleanup.ts +271 -0
  184. package/src/accesslayer/share-link/errors.ts +32 -0
  185. package/src/accesslayer/share-link/finalize.ts +265 -0
  186. package/src/accesslayer/share-link/helpers.ts +419 -0
  187. package/src/accesslayer/share-link/index.ts +11 -0
  188. package/src/accesslayer/share-link/list.ts +162 -0
  189. package/src/accesslayer/share-link/read.ts +107 -0
  190. package/src/accesslayer/share-link/receipt.ts +548 -0
  191. package/src/accesslayer/share-link/recovery.ts +545 -0
  192. package/src/accesslayer/share-link/reserve.ts +684 -0
  193. package/src/accesslayer/share-link/revoke.ts +197 -0
  194. package/src/accesslayer/share-link/transaction-execution.ts +72 -0
  195. package/src/accesslayer/share-link/transaction.ts +126 -0
  196. package/src/accesslayer/share-link/types.ts +601 -0
  197. package/src/app.ts +6 -0
  198. package/src/config/environment.ts +14 -0
  199. package/src/constants/auth-grant.ts +5 -0
  200. package/src/docker-entry.ts +57 -0
  201. package/src/helpers/boost.helpers.ts +4 -31
  202. package/src/helpers/credentialSubject.helpers.test.ts +114 -0
  203. package/src/helpers/credentialSubject.helpers.ts +40 -0
  204. package/src/helpers/inbox.helpers.ts +7 -4
  205. package/src/helpers/notificationMessages.ts +18 -1
  206. package/src/helpers/presentation-metadata.ts +41 -0
  207. package/src/helpers/presentation.helpers.ts +27 -20
  208. package/src/helpers/profile.helpers.ts +7 -0
  209. package/src/helpers/share-content-client/adapters.ts +55 -0
  210. package/src/helpers/share-content-client/canonical.ts +25 -0
  211. package/src/helpers/share-content-client/client.ts +440 -0
  212. package/src/helpers/share-content-client/config.ts +189 -0
  213. package/src/helpers/share-content-client/index.ts +27 -0
  214. package/src/helpers/share-content-client/responses.ts +273 -0
  215. package/src/helpers/share-content-client/types.ts +253 -0
  216. package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
  217. package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
  218. package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
  219. package/src/helpers/share-link-coordinator/index.ts +8 -0
  220. package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
  221. package/src/helpers/share-link-coordinator/runtime.ts +88 -0
  222. package/src/helpers/share-link-coordinator/stat.ts +30 -0
  223. package/src/helpers/share-link-coordinator/types.ts +353 -0
  224. package/src/helpers/share-link-lifecycle/index.ts +4 -0
  225. package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
  226. package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
  227. package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
  228. package/src/helpers/share-link-lifecycle/types.ts +58 -0
  229. package/src/helpers/share-link-maintenance/budget.ts +63 -0
  230. package/src/helpers/share-link-maintenance/config.ts +312 -0
  231. package/src/helpers/share-link-maintenance/index.ts +24 -0
  232. package/src/helpers/share-link-maintenance/runner.ts +223 -0
  233. package/src/helpers/share-link-maintenance/runtime.ts +334 -0
  234. package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
  235. package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
  236. package/src/helpers/share-link-maintenance/types.ts +89 -0
  237. package/src/helpers/share-link-owner/config.ts +48 -0
  238. package/src/helpers/share-link-owner/diagnostics.ts +89 -0
  239. package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
  240. package/src/helpers/share-link-owner-projection.ts +52 -0
  241. package/src/helpers/share-link-passcode-abuse.ts +36 -0
  242. package/src/helpers/share-link-passcode.ts +47 -0
  243. package/src/helpers/share-link-policy/index.ts +7 -0
  244. package/src/helpers/share-link-policy/production.ts +88 -0
  245. package/src/helpers/share-link-policy/resolver.ts +108 -0
  246. package/src/helpers/share-link-policy/types.ts +48 -0
  247. package/src/helpers/share-link-public/config.ts +41 -0
  248. package/src/helpers/share-link-view-notification.ts +18 -0
  249. package/src/models/Presentation.ts +2 -2
  250. package/src/models/Profile.ts +3 -2
  251. package/src/models/ShareLink.ts +130 -0
  252. package/src/models/ShareViewReceipt.ts +69 -0
  253. package/src/models/index.ts +2 -0
  254. package/src/models/share-link-constraints.ts +93 -0
  255. package/src/public-share-link-http.ts +49 -0
  256. package/src/routes/app-store.ts +2 -11
  257. package/src/routes/boosts.ts +2 -11
  258. package/src/routes/contracts.ts +2 -11
  259. package/src/routes/index.ts +139 -44
  260. package/src/routes/presentations.ts +19 -3
  261. package/src/routes/profile-manager.ts +10 -2
  262. package/src/routes/profiles.ts +17 -6
  263. package/src/routes/public-share-links.ts +814 -0
  264. package/src/routes/share-links.ts +640 -0
  265. package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
  266. package/test/dids.spec.ts +10 -1
  267. package/test/guardian-gated-routes.spec.ts +13 -0
  268. package/test/helpers/share-link-fixtures.ts +94 -0
  269. package/test/managed-recipient-inbox.spec.ts +130 -0
  270. package/test/notificationMessages.spec.ts +13 -0
  271. package/test/openapi.spec.ts +20 -0
  272. package/test/presentations.spec.ts +54 -6
  273. package/test/profiles.spec.ts +67 -1
  274. package/test/share-content-client.contract.spec.ts +365 -0
  275. package/test/share-content-client.unit.spec.ts +462 -0
  276. package/test/share-link-abuse.unit.spec.ts +65 -0
  277. package/test/share-link-coordinator.neo4j.spec.ts +684 -0
  278. package/test/share-link-coordinator.unit.spec.ts +990 -0
  279. package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
  280. package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
  281. package/test/share-link-lifecycle.unit.spec.ts +205 -0
  282. package/test/share-link-list-cursor.unit.spec.ts +86 -0
  283. package/test/share-link-list.neo4j.spec.ts +324 -0
  284. package/test/share-link-maintenance.neo4j.spec.ts +355 -0
  285. package/test/share-link-maintenance.unit.spec.ts +1113 -0
  286. package/test/share-link-owner-policy.unit.spec.ts +210 -0
  287. package/test/share-link-owner-route.unit.spec.ts +752 -0
  288. package/test/share-link-owner-runtime.unit.spec.ts +102 -0
  289. package/test/share-link-passcode.unit.spec.ts +29 -0
  290. package/test/share-link-policy.unit.spec.ts +135 -0
  291. package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
  292. package/test/share-link-production-policy.unit.spec.ts +77 -0
  293. package/test/share-link-public-config.unit.spec.ts +64 -0
  294. package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
  295. package/test/share-link-public-route.unit.spec.ts +800 -0
  296. package/test/share-link-receipt.neo4j.spec.ts +943 -0
  297. package/test/share-link-recovery.neo4j.spec.ts +935 -0
  298. package/test/share-link-recovery.unit.spec.ts +726 -0
  299. package/vitest.config.ts +1 -0
  300. package/vitest.share-links-neo4j.config.ts +19 -0
  301. package/vitest.share-links-unit.config.ts +28 -0
@@ -15,6 +15,8 @@ import {
15
15
 
16
16
  import { neogma } from '@instance';
17
17
  import { appRouter, type AppRouter, createContext } from './app';
18
+ import { publicShareLinkCacheControlHeaders } from './routes';
19
+ import { registerPublicShareLinkNoStore } from './public-share-link-http';
18
20
  import { openApiDocument } from './openapi';
19
21
  import { inboxBatchResponseMeta } from './helpers/inbox-batch-http.helpers';
20
22
  import { didFastifyPlugin } from './dids';
@@ -23,10 +25,26 @@ import { statusListsFastifyPlugin } from './status-lists';
23
25
  import { credentialRefreshFastifyPlugin } from './credential-refresh';
24
26
  import { deliverQueuedNotification } from '@helpers/notificationQueue.helpers';
25
27
  import { startSkillEmbeddingBackfill } from '@helpers/skill-embedding.helpers';
28
+ import {
29
+ createConsoleMaintenanceLogger,
30
+ createShareLinkMaintenanceRuntime,
31
+ createShareLinkMaintenanceScheduler,
32
+ type ShareLinkMaintenanceScheduler,
33
+ } from '@helpers/share-link-maintenance';
26
34
  import { maybeAutoSeedSkillFrameworks } from './seed/seedSkillFrameworks';
27
35
 
28
36
  const server = Fastify({ routerOptions: { maxParamLength: 5000 } });
29
37
 
38
+ /**
39
+ * The scheduled maintenance runtime is created only after the server is ready.
40
+ * When its config is disabled/invalid it is inert, so no interval and no
41
+ * graph/remote/signing dependency is initialized at all.
42
+ */
43
+ let shareLinkMaintenanceScheduler: ShareLinkMaintenanceScheduler | null = null;
44
+
45
+ server.addHook('onClose', async () => {
46
+ await shareLinkMaintenanceScheduler?.stop();
47
+ });
30
48
  // Register before either OpenAPI or tRPC registers its wildcard route. The hook raises the
31
49
  // parser ceiling for those shared routes, then narrows it back to 4 MiB only for batch issuance.
32
50
  // Registering this later would leave the already-created adapter routes at Fastify's 1 MiB limit.
@@ -78,6 +96,9 @@ server.register(fastifyTRPCPlugin, {
78
96
  allowMethodOverride: true,
79
97
  router: appRouter,
80
98
  createContext,
99
+ responseMeta: ({ paths }) => ({
100
+ headers: publicShareLinkCacheControlHeaders(paths),
101
+ }),
81
102
  onError({ path, error }) {
82
103
  // report to error monitoring
83
104
  console.error(`Error in tRPC handler on path '${path}':`, error);
@@ -96,6 +117,13 @@ server.register(fastifyTRPCOpenApiPlugin, {
96
117
  },
97
118
  } satisfies CreateOpenApiFastifyPluginOptions<AppRouter>);
98
119
 
120
+ /**
121
+ * LC-2187: the Fastify OpenAPI adapter has no `responseMeta` hook, so the
122
+ * no-store contract for the anonymous public share-link routes is applied at
123
+ * the adapter boundary itself. It never depends on a cache layer above.
124
+ */
125
+ registerPublicShareLinkNoStore(server);
126
+
99
127
  server.get('/docs/openapi.json', () => openApiDocument);
100
128
 
101
129
  const SCALAR_HTML = `<!DOCTYPE html>
@@ -138,12 +166,41 @@ server.register(credentialRefreshFastifyPlugin);
138
166
  } catch (error) {
139
167
  console.error('Skill embedding backfill failed', error);
140
168
  }
169
+
170
+ // Explicit startup after readiness. Disabled/invalid config stays inert.
171
+ const maintenanceRuntime = createShareLinkMaintenanceRuntime();
172
+
173
+ if (maintenanceRuntime.resolution.status === 'enabled') {
174
+ shareLinkMaintenanceScheduler = createShareLinkMaintenanceScheduler({
175
+ runOnce: () => maintenanceRuntime.runOnce(),
176
+ intervalMs: maintenanceRuntime.resolution.config.intervalMs,
177
+ logger: createConsoleMaintenanceLogger(),
178
+ });
179
+ shareLinkMaintenanceScheduler.start();
180
+ }
141
181
  } catch (err) {
142
182
  console.error(err);
143
183
  process.exit(1);
144
184
  }
145
185
  })();
146
186
 
187
+ const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
188
+ console.log(`Received ${signal}; closing server and draining maintenance`);
189
+ try {
190
+ await server.close();
191
+ } catch (error) {
192
+ console.error('Server shutdown failed', error);
193
+ }
194
+ process.exit(0);
195
+ };
196
+
197
+ process.once('SIGTERM', () => {
198
+ void shutdown('SIGTERM');
199
+ });
200
+ process.once('SIGINT', () => {
201
+ void shutdown('SIGINT');
202
+ });
203
+
147
204
  const pollUrl = environment.NOTIFICATIONS_QUEUE_POLL_URL;
148
205
 
149
206
  if (pollUrl) {
@@ -22,6 +22,7 @@ import {
22
22
  parseRenderedTemplate,
23
23
  shouldAutoAppendTemplateEvidence,
24
24
  } from '@helpers/template.helpers';
25
+ import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
25
26
 
26
27
  import { getBoostOwner } from '@accesslayer/boost/relationships/read';
27
28
  import { BoostInstance } from '@models';
@@ -95,15 +96,7 @@ export const convertCredentialToBoostTemplateJSON = (
95
96
  delete template.proof;
96
97
  template.issuer = defaultIssuerDid ?? 'did:example:123';
97
98
 
98
- if (Array.isArray(template.credentialSubject)) {
99
- template.credentialSubject = template.credentialSubject.map(subject => {
100
- subject.id = 'did:example:123';
101
-
102
- return subject;
103
- });
104
- } else {
105
- template.credentialSubject.id = 'did:example:123';
106
- }
99
+ setCredentialSubjectIds(template, 'did:example:123');
107
100
  return JSON.stringify(template);
108
101
  };
109
102
 
@@ -262,14 +255,7 @@ export const issueClaimLinkBoost = async (
262
255
 
263
256
  boostCredential.issuer = signingAuthorityForUser.relationship.did;
264
257
 
265
- if (Array.isArray(boostCredential.credentialSubject)) {
266
- boostCredential.credentialSubject = boostCredential.credentialSubject.map(subject => ({
267
- ...subject,
268
- id: getDidWeb(domain, to.profileId),
269
- }));
270
- } else {
271
- boostCredential.credentialSubject.id = getDidWeb(domain, to.profileId);
272
- }
258
+ setCredentialSubjectIds(boostCredential, getDidWeb(domain, to.profileId));
273
259
 
274
260
  // Embed the boostURI into the boost credential for verification purposes.
275
261
  if (boostCredential.type.includes('BoostCredential')) {
@@ -362,20 +348,7 @@ export const prepareCredentialFromBoost = async (
362
348
  credential.issuer = issuerDid;
363
349
  }
364
350
 
365
- // Set recipient DID in credentialSubject if provided
366
- if (recipientDid) {
367
- if (Array.isArray(credential.credentialSubject)) {
368
- credential.credentialSubject = credential.credentialSubject.map(subject => ({
369
- ...subject,
370
- id: recipientDid,
371
- }));
372
- } else {
373
- credential.credentialSubject = {
374
- ...(credential.credentialSubject || {}),
375
- id: recipientDid,
376
- };
377
- }
378
- }
351
+ if (recipientDid) setCredentialSubjectIds(credential, recipientDid);
379
352
 
380
353
  // Embed boostId for BoostCredential types
381
354
  if (credential?.type?.includes('BoostCredential')) {
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { UnsignedVC } from '@learncard/types';
3
+
4
+ import { setCredentialSubjectIds } from './credentialSubject.helpers';
5
+
6
+ const collectSubjectIds = (credential: Record<string, unknown>): string[] => {
7
+ const subject = credential.credentialSubject;
8
+ const subjects = Array.isArray(subject) ? subject : [subject];
9
+
10
+ return subjects.flatMap(currentSubject => {
11
+ if (!currentSubject || typeof currentSubject !== 'object') return [];
12
+
13
+ const subjectRecord = currentSubject as Record<string, unknown>;
14
+ const embedded = subjectRecord.verifiableCredential;
15
+ const embeddedCredentials = Array.isArray(embedded) ? embedded : embedded ? [embedded] : [];
16
+
17
+ return [
18
+ subjectRecord.id as string,
19
+ ...embeddedCredentials.flatMap(nested =>
20
+ collectSubjectIds(nested as Record<string, unknown>)
21
+ ),
22
+ ];
23
+ });
24
+ };
25
+
26
+ describe('setCredentialSubjectIds', () => {
27
+ it('patches every CLR learner subject without rewriting achievement identities', () => {
28
+ const credential = {
29
+ credentialSubject: {
30
+ id: 'did:example:old-root',
31
+ achievement: {
32
+ id: 'https://example.edu/transcript',
33
+ creator: { id: 'https://example.edu' },
34
+ },
35
+ verifiableCredential: [
36
+ {
37
+ credentialSubject: {
38
+ id: 'did:example:old-course',
39
+ achievement: {
40
+ id: 'https://example.edu/courses/imd-120',
41
+ creator: { id: 'https://example.edu/departments/design' },
42
+ },
43
+ verifiableCredential: {
44
+ credentialSubject: { id: 'did:example:old-assessment' },
45
+ },
46
+ },
47
+ },
48
+ ],
49
+ },
50
+ } as unknown as UnsignedVC;
51
+
52
+ setCredentialSubjectIds(credential, 'did:example:learner');
53
+
54
+ expect(collectSubjectIds(credential as unknown as Record<string, unknown>)).toEqual([
55
+ 'did:example:learner',
56
+ 'did:example:learner',
57
+ 'did:example:learner',
58
+ ]);
59
+ expect(credential.credentialSubject).toMatchObject({
60
+ achievement: {
61
+ id: 'https://example.edu/transcript',
62
+ creator: { id: 'https://example.edu' },
63
+ },
64
+ verifiableCredential: [
65
+ {
66
+ credentialSubject: {
67
+ achievement: {
68
+ id: 'https://example.edu/courses/imd-120',
69
+ creator: { id: 'https://example.edu/departments/design' },
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ });
75
+ });
76
+
77
+ it('preserves signed embedded credentials while patching unsigned subjects', () => {
78
+ const credential = {
79
+ credentialSubject: {
80
+ id: 'did:example:old-root',
81
+ verifiableCredential: [
82
+ {
83
+ credentialSubject: { id: 'did:example:signed-subject' },
84
+ proof: { type: 'DataIntegrityProof', proofValue: 'signed-value' },
85
+ },
86
+ {
87
+ credentialSubject: { id: 'did:example:unsigned-subject' },
88
+ },
89
+ ],
90
+ },
91
+ } as unknown as UnsignedVC;
92
+
93
+ setCredentialSubjectIds(credential, 'did:example:learner');
94
+
95
+ const subject = credential.credentialSubject as Record<string, unknown>;
96
+ const embedded = subject.verifiableCredential as Array<Record<string, unknown>>;
97
+
98
+ expect(subject.id).toBe('did:example:learner');
99
+ expect(embedded[0]?.credentialSubject).toEqual({
100
+ id: 'did:example:signed-subject',
101
+ });
102
+ expect(embedded[1]?.credentialSubject).toEqual({
103
+ id: 'did:example:learner',
104
+ });
105
+ });
106
+
107
+ it('creates an outer credential subject when a legacy template omits it', () => {
108
+ const credential = {} as UnsignedVC;
109
+
110
+ setCredentialSubjectIds(credential, 'did:example:learner');
111
+
112
+ expect(credential.credentialSubject).toEqual({ id: 'did:example:learner' });
113
+ });
114
+ });
@@ -0,0 +1,40 @@
1
+ import type { UnsignedVC, VC } from '@learncard/types';
2
+
3
+ const setSubjectIds = (subject: unknown, subjectDid: string): void => {
4
+ if (Array.isArray(subject)) {
5
+ subject.forEach(nestedSubject => setSubjectIds(nestedSubject, subjectDid));
6
+ return;
7
+ }
8
+
9
+ if (!subject || typeof subject !== 'object') return;
10
+
11
+ const mutableSubject = subject as Record<string, unknown>;
12
+ mutableSubject.id = subjectDid;
13
+
14
+ const embeddedCredentials = mutableSubject.verifiableCredential;
15
+ const credentials = Array.isArray(embeddedCredentials)
16
+ ? embeddedCredentials
17
+ : [embeddedCredentials];
18
+
19
+ credentials.forEach(credential => {
20
+ if (!credential || typeof credential !== 'object' || Array.isArray(credential)) return;
21
+
22
+ const credentialRecord = credential as Record<string, unknown>;
23
+ if (credentialRecord.proof !== undefined) return;
24
+
25
+ setSubjectIds(credentialRecord.credentialSubject, subjectDid);
26
+ });
27
+ };
28
+
29
+ /**
30
+ * Sets the recipient DID on the outer credential and unsigned credentials nested in a CLR.
31
+ * Signed embedded credentials are immutable because changing their subject invalidates the proof.
32
+ */
33
+ export const setCredentialSubjectIds = (credential: UnsignedVC | VC, subjectDid: string): void => {
34
+ if (!credential.credentialSubject) {
35
+ credential.credentialSubject = { id: subjectDid };
36
+ return;
37
+ }
38
+
39
+ setSubjectIds(credential.credentialSubject, subjectDid);
40
+ };
@@ -325,9 +325,11 @@ export const issueToInbox = async (
325
325
 
326
326
  // Check if the recipient is a managed child (has a guardian via MANAGES relationship).
327
327
  // Managed children need guardian approval for all inbox credentials, even without explicit guardianEmail.
328
- const recipientManagers = existingProfile
329
- ? await getProfilesThatManageAProfile(existingProfile.profileId)
330
- : [];
328
+ // Service profiles (orgs) are managed for admin purposes, not guardianship.
329
+ const recipientManagers =
330
+ existingProfile && !existingProfile.isServiceProfile
331
+ ? await getProfilesThatManageAProfile(existingProfile.profileId)
332
+ : [];
331
333
  const recipientIsManaged = recipientManagers.length > 0;
332
334
 
333
335
  if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
@@ -877,8 +879,9 @@ export const issueToInbox = async (
877
879
 
878
880
  await deliveryService.send({
879
881
  contactMethod: recipient,
880
- templateId: delivery?.template?.id ?? 'universal-inbox',
882
+ templateId: delivery?.template?.id || 'universal-inbox-claim',
881
883
  templateModel: {
884
+ claimUrl: emailClaimUrl,
882
885
  emailClaimUrl,
883
886
  claimToken: emailClaimToken,
884
887
  ...injectedTemplateFields,
@@ -57,7 +57,8 @@ export type NotificationMessageKey =
57
57
  | 'credentialRestoredUnnamed'
58
58
  // --- Managed credential refresh (LC-2136) — generic copy only: never name the
59
59
  // credential, the holder's claims, or implementation terms (refresh/sync/etc.) ---
60
- | 'credentialRefreshed';
60
+ | 'credentialRefreshed'
61
+ | 'shareViewed';
61
62
 
62
63
  type MessageTemplate = { title: string; body: string };
63
64
  type LocaleCatalog = Record<NotificationMessageKey, MessageTemplate>;
@@ -216,6 +217,10 @@ const en: LocaleCatalog = {
216
217
  title: 'Credential updated',
217
218
  body: '{from} updated one of your credentials.',
218
219
  },
220
+ shareViewed: {
221
+ title: 'Share viewed',
222
+ body: 'Your share “{title} ({count})” was viewed.',
223
+ },
219
224
  };
220
225
 
221
226
  const es: LocaleCatalog = {
@@ -367,6 +372,10 @@ const es: LocaleCatalog = {
367
372
  title: 'Credencial actualizada',
368
373
  body: '{from} actualizó una de tus credenciales.',
369
374
  },
375
+ shareViewed: {
376
+ title: 'Enlace visto',
377
+ body: 'Se vio tu enlace «{title} ({count})».',
378
+ },
370
379
  };
371
380
 
372
381
  const fr: LocaleCatalog = {
@@ -518,6 +527,10 @@ const fr: LocaleCatalog = {
518
527
  title: 'Titre mis à jour',
519
528
  body: "{from} a mis à jour l'un de vos titres.",
520
529
  },
530
+ shareViewed: {
531
+ title: 'Partage consulté',
532
+ body: 'Votre partage « {title} ({count}) » a été consulté.',
533
+ },
521
534
  };
522
535
 
523
536
  const ar: LocaleCatalog = {
@@ -669,6 +682,10 @@ const ar: LocaleCatalog = {
669
682
  title: 'تم تحديث الشهادة',
670
683
  body: 'قام {from} بتحديث إحدى شهاداتك.',
671
684
  },
685
+ shareViewed: {
686
+ title: 'تمت مشاهدة المشاركة',
687
+ body: 'تمت مشاهدة مشاركتك «{title} ({count})».',
688
+ },
672
689
  };
673
690
 
674
691
  const CATALOGS: Record<NotificationLocale, LocaleCatalog> = { en, es, fr, ar };
@@ -0,0 +1,41 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { ShareLinkIdValidator } from '@learncard/types';
3
+ import { z } from 'zod';
4
+
5
+ // Relationship metadata is not signed VP content. Bound generic metadata and
6
+ // reserve the share-link shape for the recipient's own send/accept workflow.
7
+ const ShareLinkPresentationMetadataValidator = z
8
+ .object({
9
+ type: z.literal('learncard.share-link.v1'),
10
+ shareId: ShareLinkIdValidator,
11
+ title: z.string().min(1).max(120),
12
+ note: z.string().max(500).optional(),
13
+ sharer: z
14
+ .object({
15
+ profileId: z.string().min(1).max(128),
16
+ displayName: z.string().min(1).max(120),
17
+ })
18
+ .strict(),
19
+ })
20
+ .strict();
21
+
22
+ export const PresentationMetadataValidator = z
23
+ .record(z.string().min(1).max(64), z.unknown())
24
+ .refine(value => JSON.stringify(value).length <= 4096, 'Metadata is too large')
25
+ .superRefine((value, ctx) => {
26
+ if (value.type !== 'learncard.share-link.v1') return;
27
+ if (!ShareLinkPresentationMetadataValidator.safeParse(value).success)
28
+ ctx.addIssue({ code: 'custom', message: 'Invalid share-link metadata' });
29
+ });
30
+
31
+ export const assertPresentationMetadataRecipient = (
32
+ metadata: Record<string, unknown> | undefined,
33
+ senderProfileId: string,
34
+ recipientProfileId: string
35
+ ): void => {
36
+ if (metadata?.type === 'learncard.share-link.v1' && senderProfileId !== recipientProfileId)
37
+ throw new TRPCError({
38
+ code: 'BAD_REQUEST',
39
+ message: 'Share-link metadata requires a self-save',
40
+ });
41
+ };
@@ -23,23 +23,29 @@ export const sendPresentation = async (
23
23
  from: ProfileType,
24
24
  to: ProfileType,
25
25
  presentation: VP | JWE,
26
- domain: string
26
+ domain: string,
27
+ metadata?: Record<string, unknown>
27
28
  ): Promise<string> => {
28
29
  const presentationInstance = await storePresentation(presentation);
29
30
 
30
- await createSentPresentationRelationship(from, to, presentationInstance);
31
+ await createSentPresentationRelationship(from, to, presentationInstance, metadata);
31
32
 
32
- let uri = getPresentationUri(presentationInstance.id, domain);
33
+ const uri = getPresentationUri(presentationInstance.id, domain);
33
34
 
34
- await addNotificationToQueue({
35
- type: LCNNotificationTypeEnumValidator.enum.PRESENTATION_RECEIVED,
36
- to,
37
- from,
38
- message: getNotificationMessage('presentationReceived', resolveRecipientLocale(to), {
39
- from: from.displayName,
40
- }),
41
- data: { vpUris: [uri] },
42
- });
35
+ // Saving a shared collection reuses the presentation send/accept pipeline by
36
+ // sending it to the current profile. Keep that storage behavior without
37
+ // creating a meaningless "you sent this to yourself" recipient alert.
38
+ if (from.profileId !== to.profileId) {
39
+ await addNotificationToQueue({
40
+ type: LCNNotificationTypeEnumValidator.enum.PRESENTATION_RECEIVED,
41
+ to,
42
+ from,
43
+ message: getNotificationMessage('presentationReceived', resolveRecipientLocale(to), {
44
+ from: from.displayName,
45
+ }),
46
+ data: { vpUris: [uri] },
47
+ });
48
+ }
43
49
 
44
50
  return uri;
45
51
  };
@@ -63,16 +69,17 @@ export const acceptPresentation = async (profile: ProfileType, uri: string): Pro
63
69
  });
64
70
  }
65
71
 
66
- // Check if presentation has already been received by this profile
72
+ // Acceptance is idempotent so a client can safely recover from a response
73
+ // interruption without producing a second received relationship.
67
74
  const alreadyReceived = await getPresentationReceivedByProfile(id, profile);
68
- if (alreadyReceived) {
69
- throw new TRPCError({
70
- code: 'BAD_REQUEST',
71
- message: 'Presentation has already been received',
72
- });
75
+ if (!alreadyReceived) {
76
+ await createReceivedPresentationRelationship(
77
+ profile,
78
+ pendingVp.source,
79
+ pendingVp.target,
80
+ pendingVp.relationship.metadata
81
+ );
73
82
  }
74
83
 
75
- await createReceivedPresentationRelationship(profile, pendingVp.source, pendingVp.target);
76
-
77
84
  return true;
78
85
  };
@@ -1,3 +1,5 @@
1
+ import { LCNProfileValidator } from '@learncard/types';
2
+
1
3
  import { getProfileByProfileId } from '@accesslayer/profile/read';
2
4
  import { createProfile } from '@accesslayer/profile/create';
3
5
  import { getProfileIdFromDid } from './did.helpers';
@@ -6,6 +8,11 @@ import { ProfileType } from 'types/profile';
6
8
  export const transformProfileId = (rawInput: string): string =>
7
9
  rawInput.toLowerCase().replaceAll(':', '%3A');
8
10
 
11
+ export const PublicProfileIdValidator = LCNProfileValidator.shape.profileId.refine(
12
+ profileId => !transformProfileId(profileId).startsWith('sample-'),
13
+ { message: 'Profile IDs beginning with "sample-" are reserved.' }
14
+ );
15
+
9
16
  /**
10
17
  * Gets or creates a federated profile for cross-instance credential sending.
11
18
  * Federated profiles use email-like format: profileId@service-domain to avoid collisions.
@@ -0,0 +1,55 @@
1
+ import { getDidWebLearnCard, getServerDidWebDID } from '@helpers/learnCard.helpers';
2
+
3
+ import type { ShareContentTokenSigner } from './types';
4
+
5
+ /**
6
+ * Runtime signing adapter for the LC-2187 share-content service token.
7
+ *
8
+ * It reuses the existing did:web LearnCard signing primitives — no new
9
+ * cryptographic algorithm or key material is introduced. The fixed C1 verifier
10
+ * expects a holder-signed VP JWT whose `nonce` is the JSON-serialized claims
11
+ * object, signed with `EdDSA` by an allowlisted did:web verification method. The
12
+ * `kid` did:web fragment is produced by DIDKit from the signing key; the client
13
+ * independently re-checks that it binds the configured signer DID before any
14
+ * request is sent.
15
+ *
16
+ * Exported separately from the client barrel so unit tests never load the
17
+ * LearnCard/environment graph.
18
+ */
19
+ export const createDidWebLearnCardTokenSigner = (dependencies?: {
20
+ getLearnCard?: () => Promise<{
21
+ invoke: { issuePresentation: (...args: unknown[]) => unknown };
22
+ }>;
23
+ getSignerDid?: () => string;
24
+ }): ShareContentTokenSigner => {
25
+ const resolveLearnCard = dependencies?.getLearnCard ?? getDidWebLearnCard;
26
+ const resolveSignerDid = dependencies?.getSignerDid ?? getServerDidWebDID;
27
+
28
+ return async claims => {
29
+ const learnCard = await resolveLearnCard();
30
+ const signerDid = resolveSignerDid();
31
+
32
+ if (claims.iss !== signerDid) {
33
+ throw new Error('share-content signer DID does not match the token issuer binding');
34
+ }
35
+
36
+ const presentation = {
37
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
38
+ type: ['VerifiablePresentation'],
39
+ holder: signerDid,
40
+ verifiableCredential: [],
41
+ };
42
+
43
+ const issued = await learnCard.invoke.issuePresentation(presentation, {
44
+ proofPurpose: 'authentication',
45
+ proofFormat: 'jwt',
46
+ challenge: JSON.stringify(claims),
47
+ });
48
+
49
+ if (typeof issued !== 'string' || issued.length === 0) {
50
+ throw new Error('share-content signer did not return a JWT presentation');
51
+ }
52
+
53
+ return issued;
54
+ };
55
+ };
@@ -0,0 +1,25 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalizeShareContentRequestBody } from '@learncard/helpers';
3
+
4
+ export {
5
+ canonicalizeShareContentRequestBody,
6
+ DEFAULT_MAX_CANONICAL_BYTES,
7
+ DEFAULT_MAX_CANONICAL_DEPTH,
8
+ ShareContentCanonicalizationError,
9
+ } from '@learncard/helpers';
10
+
11
+ /**
12
+ * SHA-256 (lowercase hex) over the canonical request body. This is exactly the
13
+ * value signed into `claims.requestHash` and recomputed by C1.
14
+ */
15
+ export const computeShareContentRequestBodyHash = (
16
+ body: unknown,
17
+ options: { maxBytes?: number; maxDepth?: number } = {}
18
+ ): string =>
19
+ createHash('sha256')
20
+ .update(canonicalizeShareContentRequestBody(body, options), 'utf8')
21
+ .digest('hex');
22
+
23
+ /** Strict lowercase hex SHA-256. */
24
+ export const isSha256Hex = (value: unknown): value is string =>
25
+ typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);