@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
@@ -225,6 +225,19 @@ describe('Guardian-approved consent mutations', () => {
225
225
  expect(await ConsentFlowTransaction.findMany({ where: {} })).toEqual([]);
226
226
  });
227
227
 
228
+ it('does not guardian-gate a managed service profile', async () => {
229
+ await Profile.update({ isServiceProfile: true }, { where: { profileId: 'child-user' } });
230
+ await manageChild();
231
+
232
+ const { termsUri } = await childCaller().contracts.consentToContract({
233
+ contractUri,
234
+ terms: minimalTerms,
235
+ });
236
+ const record = await getContractTermsByUri(termsUri);
237
+ expect(record?.terms.status).toBe('live');
238
+ expect(record?.terms.guardianApproval).toBeUndefined();
239
+ });
240
+
228
241
  it('records the verified guardian and contract version on consent and permission updates', async () => {
229
242
  await manageChild();
230
243
  const beforeApproval = Date.now();
@@ -0,0 +1,94 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { v4 as uuid } from 'uuid';
3
+ import { neogma } from '@instance';
4
+ import { computeShareLinkRequestHash } from '@helpers/share-link-lifecycle';
5
+ import { reserveCreate } from '../../src/accesslayer/share-link';
6
+
7
+ export const nextShareId = (): string => randomBytes(16).toString('base64url');
8
+
9
+ export const contentBinding = (seed: string) => ({
10
+ contentHash: createHash('sha256').update(`content:${seed}`).digest('hex'),
11
+ contentBytes: 128,
12
+ recoveryHash: createHash('sha256').update(`recovery:${seed}`).digest('hex'),
13
+ recoveryBytes: 256,
14
+ });
15
+
16
+ export const clearLifecycleGraph = async (): Promise<void> => {
17
+ await neogma.queryRunner.run(
18
+ `MATCH (n)
19
+ WHERE n:ShareLink OR n:ShareLinkReservation OR n:ShareLinkOperation OR n:ShareContentCleanupJob
20
+ DETACH DELETE n`
21
+ );
22
+ };
23
+
24
+ export type ReservedCreate = {
25
+ namespace: string;
26
+ ownerProfileId: string;
27
+ shareId: string;
28
+ clientRequestId: string;
29
+ requestHash: string;
30
+ operationId: string;
31
+ objectRef: string;
32
+ generation: number;
33
+ leaseOwner: string;
34
+ };
35
+
36
+ export const reserveShareFixture = async (options: {
37
+ ownerProfileId: string;
38
+ shareId?: string;
39
+ expiresAt?: string | null;
40
+ selectedCount?: number;
41
+ title?: string;
42
+ leaseMs?: number;
43
+ now: Date;
44
+ clientRequestId?: string;
45
+ requestHash?: string;
46
+ namespace: string;
47
+ }): Promise<ReservedCreate> => {
48
+ const namespace = options.namespace;
49
+ const ownerProfileId = options.ownerProfileId;
50
+ const shareId = options.shareId ?? nextShareId();
51
+ const clientRequestId = options.clientRequestId ?? uuid();
52
+ const selectedCount = options.selectedCount ?? 1;
53
+ const content = contentBinding(shareId);
54
+ const requestHash =
55
+ options.requestHash ??
56
+ computeShareLinkRequestHash('create', {
57
+ id: shareId,
58
+ title: options.title ?? 'Shared credentials',
59
+ selectedCount,
60
+ ...content,
61
+ });
62
+
63
+ const result = await reserveCreate({
64
+ namespace,
65
+ ownerProfileId,
66
+ clientRequestId,
67
+ shareId,
68
+ title: options.title ?? 'Shared credentials',
69
+ note: null,
70
+ expiresAt: options.expiresAt ?? null,
71
+ selectedCount,
72
+ content,
73
+ requestHash,
74
+ leaseOwner: 'worker-1',
75
+ leaseMs: options.leaseMs,
76
+ now: options.now,
77
+ });
78
+
79
+ if (result.outcome !== 'reserved' || !result.reservation.objectRef) {
80
+ throw new Error(`expected reserved create, got ${result.outcome}`);
81
+ }
82
+
83
+ return {
84
+ namespace,
85
+ ownerProfileId,
86
+ shareId,
87
+ clientRequestId,
88
+ requestHash,
89
+ operationId: result.reservation.operationId,
90
+ objectRef: result.reservation.objectRef,
91
+ generation: result.reservation.generation,
92
+ leaseOwner: result.reservation.leaseOwner,
93
+ };
94
+ };
@@ -0,0 +1,130 @@
1
+ import { vi, describe, it, expect, beforeEach, afterAll, beforeAll } from 'vitest';
2
+ import { getClient, getUser } from './helpers/getClient';
3
+ import { testUnsignedBoost } from './helpers/send';
4
+ import {
5
+ Profile,
6
+ InboxCredential,
7
+ ContactMethod,
8
+ SigningAuthority,
9
+ Boost,
10
+ ProfileManager,
11
+ } from '@models';
12
+ import { createContactMethod } from '@accesslayer/contact-method/create';
13
+ import { createProfileContactMethodRelationship } from '@accesslayer/contact-method/relationships/create';
14
+ import { sendSpy } from './helpers/spies';
15
+
16
+ vi.mock('@services/delivery/delivery.factory', () => ({
17
+ getDeliveryService: () => ({ send: sendSpy }),
18
+ }));
19
+
20
+ type SentNotification = {
21
+ contactMethod: { value: string };
22
+ templateId: string;
23
+ templateModel?: { claimUrl?: string };
24
+ };
25
+
26
+ let userA: Awaited<ReturnType<typeof getUser>>;
27
+
28
+ const clearDb = async (): Promise<void> => {
29
+ await ProfileManager.delete({ detach: true, where: {} });
30
+ await Profile.delete({ detach: true, where: {} });
31
+ await InboxCredential.delete({ detach: true, where: {} });
32
+ await ContactMethod.delete({ detach: true, where: {} });
33
+ await SigningAuthority.delete({ detach: true, where: {} });
34
+ await Boost.delete({ detach: true, where: {} });
35
+ };
36
+
37
+ const createManagedRecipient = async (
38
+ profileId: string,
39
+ email: string,
40
+ isServiceProfile: boolean
41
+ ): Promise<void> => {
42
+ const boostUri = await userA.clients.fullAuth.boost.createBoost({
43
+ credential: testUnsignedBoost,
44
+ });
45
+ const managerDid = await userA.clients.fullAuth.profileManager.createChildProfileManager({
46
+ parentUri: boostUri,
47
+ profile: {},
48
+ });
49
+ const managerClient = getClient({ did: managerDid, isChallengeValid: true });
50
+ await managerClient.profileManager.createManagedProfile({
51
+ profileId,
52
+ displayName: profileId,
53
+ isServiceProfile,
54
+ });
55
+
56
+ const contactMethod = await createContactMethod({
57
+ type: 'email',
58
+ value: email,
59
+ isVerified: true,
60
+ isPrimary: true,
61
+ });
62
+ await createProfileContactMethodRelationship(profileId, contactMethod.id);
63
+ };
64
+
65
+ const sendTo = async (recipient: string) => {
66
+ const signedVc = await userA.learnCard.invoke.issueCredential({
67
+ ...testUnsignedBoost,
68
+ issuer: userA.learnCard.id.did(),
69
+ });
70
+
71
+ return userA.clients.fullAuth.boost.send({
72
+ type: 'boost',
73
+ recipient,
74
+ template: { credential: testUnsignedBoost },
75
+ signedCredential: signedVc,
76
+ });
77
+ };
78
+
79
+ describe('Inbox send to managed recipients', () => {
80
+ beforeAll(async () => {
81
+ userA = await getUser('a'.repeat(64));
82
+ });
83
+
84
+ beforeEach(async () => {
85
+ sendSpy.mockClear();
86
+ await clearDb();
87
+ await userA.clients.fullAuth.profile.createProfile({
88
+ profileId: 'usera',
89
+ displayName: 'User A',
90
+ });
91
+ });
92
+
93
+ afterAll(async () => {
94
+ sendSpy.mockClear();
95
+ await clearDb();
96
+ });
97
+
98
+ it('delivers directly to a managed service profile without a guardian gate', async () => {
99
+ await createManagedRecipient('managed-org', 'org@example.com', true);
100
+
101
+ const result = await sendTo('org@example.com');
102
+
103
+ expect(result.inbox?.status).toBe('ISSUED');
104
+ expect(result.inbox?.guardianStatus).toBeUndefined();
105
+
106
+ const credentials = await InboxCredential.findMany({ where: {} });
107
+ expect(credentials).toHaveLength(1);
108
+ expect(credentials[0]!.guardianStatus).toBeFalsy();
109
+ });
110
+
111
+ it('gates a managed child and sends the claim email with a valid template', async () => {
112
+ await createManagedRecipient('managed-child', 'child@example.com', false);
113
+
114
+ const result = await sendTo('child@example.com');
115
+
116
+ expect(result.inbox?.status).toBe('PENDING');
117
+ expect(result.inbox?.guardianStatus).toBe('AWAITING_GUARDIAN');
118
+
119
+ const credentials = await InboxCredential.findMany({ where: {} });
120
+ expect(credentials).toHaveLength(1);
121
+ expect(credentials[0]!.guardianStatus).toBe('AWAITING_GUARDIAN');
122
+
123
+ const childEmail = sendSpy.mock.calls
124
+ .map(call => call[0] as SentNotification)
125
+ .find(notification => notification.contactMethod.value === 'child@example.com');
126
+
127
+ expect(childEmail?.templateId).toBe('universal-inbox-claim');
128
+ expect(childEmail?.templateModel?.claimUrl).toBeTruthy();
129
+ });
130
+ });
@@ -45,6 +45,7 @@ const SUPPORTED_KEYS = [
45
45
  'credentialSuspendedUnnamed',
46
46
  'credentialRestoredNamed',
47
47
  'credentialRestoredUnnamed',
48
+ 'shareViewed',
48
49
  ] as NotificationMessageKey[];
49
50
 
50
51
  const LOCALES = ['en', 'es', 'fr', 'ar'];
@@ -72,6 +73,18 @@ describe('notificationMessages catalog', () => {
72
73
  });
73
74
 
74
75
  describe('getNotificationMessage — interpolation', () => {
76
+ it('localizes private-share view alerts in every supported locale', () => {
77
+ const titles = ['Share viewed', 'Enlace visto', 'Partage consulté', 'تمت مشاهدة المشاركة'];
78
+ for (const [index, locale] of LOCALES.entries()) {
79
+ const message = getNotificationMessage('shareViewed', locale, {
80
+ title: 'Career highlights',
81
+ count: '3',
82
+ });
83
+ expect(message.title).toBe(titles[index]);
84
+ expect(message.body).toContain('Career highlights');
85
+ expect(message.body).toContain('3');
86
+ }
87
+ });
75
88
  it.each([
76
89
  ['en', 'Ada claimed your credential — connect?'],
77
90
  ['es', 'Ada reclamó tu credencial. ¿Conectar?'],
@@ -32,6 +32,26 @@ describe('OpenAPI generation', () => {
32
32
  expect(Object.keys(openApiDocument.paths ?? {}).length).toBeGreaterThan(0);
33
33
  });
34
34
 
35
+ it('mounts the anonymous public share routes on paths distinct from the owner route', () => {
36
+ const paths = Object.keys(openApiDocument.paths ?? {});
37
+
38
+ // The import above would have thrown Duplicate procedure for the old
39
+ // colliding GET /share-links/{id} registration.
40
+ expect(paths.filter(path => path === '/share-links/{id}')).toHaveLength(1);
41
+ expect(paths).toContain('/public/share-links/{id}');
42
+ expect(paths).toContain('/public/share-links/{id}/content');
43
+ expect(paths).toContain('/public/share-links/acknowledge-view');
44
+ });
45
+
46
+ it('mounts the bounded owner list on GET /share-links, distinct from /share-links/{id}', () => {
47
+ const paths = Object.keys(openApiDocument.paths ?? {});
48
+
49
+ // A collection path with no trailing segment cannot be captured by the
50
+ // `{id}` param route, so /list -> /{id} shadowing is impossible.
51
+ expect(paths.filter(path => path === '/share-links')).toHaveLength(1);
52
+ expect(openApiDocument.paths?.['/share-links']?.get).toBeDefined();
53
+ });
54
+
35
55
  it('includes the skill-search route whose $regex query previously broke generation', () => {
36
56
  const paths = Object.keys(openApiDocument.paths ?? {});
37
57
 
@@ -2,6 +2,9 @@ import { vi } from 'vitest';
2
2
  import { getClient, getUser } from './helpers/getClient';
3
3
  import { testVp, sendPresentation } from './helpers/send';
4
4
  import { Profile, Credential, Presentation } from '@models';
5
+ import * as Notifications from '@helpers/notifications.helpers';
6
+ import { addNotificationToQueueSpy } from './helpers/spies';
7
+ import { LCNNotificationTypeEnumValidator } from '@learncard/types';
5
8
 
6
9
  const noAuthClient = getClient();
7
10
  let userA: Awaited<ReturnType<typeof getUser>>;
@@ -13,6 +16,10 @@ describe('Presentations', () => {
13
16
  userA = await getUser();
14
17
  userB = await getUser('b'.repeat(64));
15
18
  userC = await getUser('c'.repeat(64));
19
+
20
+ vi.spyOn(Notifications, 'addNotificationToQueue').mockImplementation(
21
+ addNotificationToQueueSpy
22
+ );
16
23
  });
17
24
 
18
25
  describe('sendPresentation', () => {
@@ -22,6 +29,8 @@ describe('Presentations', () => {
22
29
  await Presentation.delete({ detach: true, where: {} });
23
30
  await userA.clients.fullAuth.profile.createProfile({ profileId: 'usera' });
24
31
  await userB.clients.fullAuth.profile.createProfile({ profileId: 'userb' });
32
+
33
+ addNotificationToQueueSpy.mockReset();
25
34
  });
26
35
 
27
36
  afterAll(async () => {
@@ -52,6 +61,25 @@ describe('Presentations', () => {
52
61
  presentation: testVp,
53
62
  })
54
63
  ).resolves.not.toThrow();
64
+
65
+ expect(addNotificationToQueueSpy).toHaveBeenCalledWith(
66
+ expect.objectContaining({
67
+ type: LCNNotificationTypeEnumValidator.enum.PRESENTATION_RECEIVED,
68
+ to: expect.objectContaining({ profileId: 'userb' }),
69
+ from: expect.objectContaining({ profileId: 'usera' }),
70
+ })
71
+ );
72
+ });
73
+
74
+ it('should not notify a profile when it sends a presentation to itself', async () => {
75
+ await expect(
76
+ userA.clients.fullAuth.presentation.sendPresentation({
77
+ profileId: 'usera',
78
+ presentation: testVp,
79
+ })
80
+ ).resolves.not.toThrow();
81
+
82
+ expect(addNotificationToQueueSpy).not.toHaveBeenCalled();
55
83
  });
56
84
 
57
85
  it('should allow sending a presentation to did:web', async () => {
@@ -137,7 +165,7 @@ describe('Presentations', () => {
137
165
  ).resolves.not.toThrow();
138
166
  });
139
167
 
140
- it('should not allow accepting the same presentation twice', async () => {
168
+ it('should accept the same presentation idempotently', async () => {
141
169
  const uri = await userA.clients.fullAuth.presentation.sendPresentation({
142
170
  profileId: 'userb',
143
171
  presentation: testVp,
@@ -148,13 +176,14 @@ describe('Presentations', () => {
148
176
  userB.clients.fullAuth.presentation.acceptPresentation({ uri })
149
177
  ).resolves.not.toThrow();
150
178
 
151
- // Second acceptance should fail
179
+ // A retry after a lost response should also succeed without creating another edge.
152
180
  await expect(
153
181
  userB.clients.fullAuth.presentation.acceptPresentation({ uri })
154
- ).rejects.toMatchObject({
155
- code: 'BAD_REQUEST',
156
- message: expect.stringContaining('already been received'),
157
- });
182
+ ).resolves.not.toThrow();
183
+
184
+ await expect(
185
+ userB.clients.fullAuth.presentation.receivedPresentations()
186
+ ).resolves.toHaveLength(1);
158
187
  });
159
188
  });
160
189
 
@@ -228,6 +257,25 @@ describe('Presentations', () => {
228
257
  vi.useRealTimers();
229
258
  });
230
259
 
260
+ it('should preserve presentation metadata after acceptance', async () => {
261
+ const metadata = {
262
+ type: 'learncard.share-link.v1',
263
+ shareId: 'AAAAAAAAAAAAAAAAAAAAAA',
264
+ title: 'Career highlights',
265
+ sharer: { profileId: 'usera', displayName: 'User A' },
266
+ };
267
+ const uri = await userA.clients.fullAuth.presentation.sendPresentation({
268
+ profileId: 'userb',
269
+ presentation: testVp,
270
+ metadata,
271
+ });
272
+
273
+ await userB.clients.fullAuth.presentation.acceptPresentation({ uri });
274
+
275
+ const presentations = await userB.clients.fullAuth.presentation.receivedPresentations();
276
+ expect(presentations[0]?.metadata).toEqual(metadata);
277
+ });
278
+
231
279
  it('should allow filtering received presentations by who sent them', async () => {
232
280
  await userC.clients.fullAuth.profile.createProfile({ profileId: 'userc' });
233
281
 
@@ -5,7 +5,15 @@ import {
5
5
  ProfileVisibilityEnum,
6
6
  } from '@learncard/types';
7
7
  import { getClient, getUser } from './helpers/getClient';
8
- import { Profile, SigningAuthority, Credential, Boost, ClaimHook, ContactMethod } from '@models';
8
+ import {
9
+ Profile,
10
+ ProfileManager,
11
+ SigningAuthority,
12
+ Credential,
13
+ Boost,
14
+ ClaimHook,
15
+ ContactMethod,
16
+ } from '@models';
9
17
  import cache from '@cache';
10
18
  import { testVc, sendBoost, testVp, testUnsignedBoost } from './helpers/send';
11
19
 
@@ -28,6 +36,30 @@ describe('Profiles', () => {
28
36
  userC = await getUser('c'.repeat(64));
29
37
  });
30
38
 
39
+ describe('createManagedProfile', () => {
40
+ beforeEach(async () => {
41
+ await ProfileManager.delete({ detach: true, where: {} });
42
+ await Profile.delete({ detach: true, where: {} });
43
+ await userA.clients.fullAuth.profile.createProfile({ profileId: 'usera' });
44
+ });
45
+
46
+ afterAll(async () => {
47
+ await ProfileManager.delete({ detach: true, where: {} });
48
+ await Profile.delete({ detach: true, where: {} });
49
+ });
50
+
51
+ it('should reserve sample persona profileIds', async () => {
52
+ const managerDid = await userA.clients.fullAuth.profileManager.createProfileManager({});
53
+ const managerClient = getClient({ did: managerDid, isChallengeValid: true });
54
+
55
+ await expect(
56
+ managerClient.profileManager.createManagedProfile({
57
+ profileId: 'Sample-college-board',
58
+ })
59
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
60
+ });
61
+ });
62
+
31
63
  describe('createProfile', () => {
32
64
  beforeEach(async () => {
33
65
  await Profile.delete({ detach: true, where: {} });
@@ -63,6 +95,14 @@ describe('Profiles', () => {
63
95
  ).rejects.toThrow();
64
96
  });
65
97
 
98
+ it('should reserve sample persona profileIds', async () => {
99
+ await expect(
100
+ userA.clients.fullAuth.profile.createProfile({
101
+ profileId: 'Sample-college-board',
102
+ })
103
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
104
+ });
105
+
66
106
  it('should not allow creating a profile with an email that has already been taken', async () => {
67
107
  await expect(
68
108
  userA.clients.fullAuth.profile.createProfile({
@@ -387,6 +427,14 @@ describe('Profiles', () => {
387
427
  ).rejects.toThrow();
388
428
  });
389
429
 
430
+ it('should reserve sample persona profileIds', async () => {
431
+ await expect(
432
+ userA.clients.fullAuth.profile.createServiceProfile({
433
+ profileId: 'sample-college-board',
434
+ })
435
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
436
+ });
437
+
390
438
  it('should not allow creating a profile with an email that has already been taken', async () => {
391
439
  await expect(
392
440
  userA.clients.fullAuth.profile.createServiceProfile({
@@ -490,6 +538,16 @@ describe('Profiles', () => {
490
538
  })
491
539
  ).resolves.not.toThrow();
492
540
  });
541
+
542
+ it('should reserve sample persona profileIds', async () => {
543
+ await userA.clients.fullAuth.profile.createProfile({ profileId: 'usera' });
544
+
545
+ await expect(
546
+ userA.clients.fullAuth.profile.createManagedServiceProfile({
547
+ profileId: 'sample-college-board',
548
+ })
549
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
550
+ });
493
551
  });
494
552
 
495
553
  describe('getManagedServiceProfile', () => {
@@ -1255,6 +1313,14 @@ describe('Profiles', () => {
1255
1313
  ).rejects.toMatchObject({ code: 'CONFLICT' });
1256
1314
  });
1257
1315
 
1316
+ it('should not allow changing a profileId to the sample persona namespace', async () => {
1317
+ await expect(
1318
+ userA.clients.fullAuth.profile.updateProfile({
1319
+ profileId: 'sample-college-board',
1320
+ })
1321
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
1322
+ });
1323
+
1258
1324
  it('should allow you to update your email', async () => {
1259
1325
  await expect(
1260
1326
  userA.clients.fullAuth.profile.updateProfile({ email: 'nice@test.com' })