@learncard/network-brain-service 3.19.1 → 4.0.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 (312) hide show
  1. package/.env.example +44 -111
  2. package/AGENTS.md +52 -0
  3. package/CHANGELOG.md +109 -0
  4. package/README.md +9 -2
  5. package/bootstrapLambda.test.ts +72 -0
  6. package/config/config.dev.json +29 -0
  7. package/config/config.example.json +29 -0
  8. package/config/config.json +1 -0
  9. package/config/config.production.json +29 -0
  10. package/config/config.scouts.dev.json +24 -0
  11. package/config/config.scouts.production.json +24 -0
  12. package/contractEventsLambda.ts +18 -0
  13. package/contractEventsLambdaApp.ts +19 -0
  14. package/didWebLambda.ts +13 -18
  15. package/didWebLambdaApp.ts +22 -0
  16. package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
  17. package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
  18. package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
  19. package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
  20. package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
  21. package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
  22. package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
  23. package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
  24. package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
  25. package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
  26. package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
  27. package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
  28. package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
  29. package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
  30. package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
  31. package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
  32. package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
  33. package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
  34. package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
  35. package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
  36. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
  37. package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
  38. package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
  39. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
  40. package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
  41. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
  42. package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
  43. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
  44. package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
  45. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
  46. package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
  47. package/dist/accesslayer/credential/create.d.ts +2 -1
  48. package/dist/accesslayer/credential/create.d.ts.map +1 -1
  49. package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
  50. package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
  51. package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
  52. package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
  53. package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
  54. package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
  55. package/dist/accesslayer/share-link/abandon.d.ts.map +1 -1
  56. package/dist/accesslayer/share-link/attachment.d.ts +37 -0
  57. package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
  58. package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
  59. package/dist/accesslayer/share-link/helpers.d.ts +3 -0
  60. package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
  61. package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
  62. package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
  63. package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
  64. package/dist/accesslayer/share-link/types.d.ts +8 -3
  65. package/dist/accesslayer/share-link/types.d.ts.map +1 -1
  66. package/dist/cache/app-store.caches.d.ts +49 -0
  67. package/dist/cache/app-store.caches.d.ts.map +1 -1
  68. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  69. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  70. package/dist/config/environment.d.ts +5 -0
  71. package/dist/config/environment.d.ts.map +1 -1
  72. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  73. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  74. package/dist/config/stageConfig.d.ts +112 -0
  75. package/dist/config/stageConfig.d.ts.map +1 -0
  76. package/dist/config/stageConfig.test.d.ts +2 -0
  77. package/dist/config/stageConfig.test.d.ts.map +1 -0
  78. package/dist/didkit_wasm_bg.wasm +0 -0
  79. package/dist/docker-entry.d.ts +1 -1
  80. package/dist/docker-entry.d.ts.map +1 -1
  81. package/dist/helpers/app-manifest.helpers.d.ts +5 -0
  82. package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
  83. package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
  84. package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
  85. package/dist/helpers/consent-referral.helpers.d.ts +2 -0
  86. package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
  87. package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
  88. package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
  89. package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
  90. package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
  91. package/dist/helpers/contract-events.helpers.d.ts +25 -0
  92. package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
  93. package/dist/helpers/contract-requests.helpers.d.ts +19 -0
  94. package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
  95. package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
  96. package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
  97. package/dist/helpers/inbox.helpers.d.ts.map +1 -1
  98. package/dist/helpers/inline-template.helpers.d.ts +45 -0
  99. package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
  100. package/dist/helpers/notificationMessages.d.ts +1 -1
  101. package/dist/helpers/notificationMessages.d.ts.map +1 -1
  102. package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
  103. package/dist/helpers/notifications.helpers.d.ts +6 -0
  104. package/dist/helpers/notifications.helpers.d.ts.map +1 -1
  105. package/dist/helpers/profile.helpers.d.ts +1 -0
  106. package/dist/helpers/profile.helpers.d.ts.map +1 -1
  107. package/dist/helpers/share-link-attachment-access.d.ts +4 -0
  108. package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
  109. package/dist/helpers/share-link-attachment.d.ts +18 -0
  110. package/dist/helpers/share-link-attachment.d.ts.map +1 -0
  111. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
  112. package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
  113. package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
  114. package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
  115. package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
  116. package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
  117. package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
  118. package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
  119. package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
  120. package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
  121. package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
  122. package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
  123. package/dist/helpers/share-link-policy/production.d.ts +3 -5
  124. package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
  125. package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
  126. package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
  127. package/dist/helpers/share-link-policy/types.d.ts +8 -2
  128. package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
  129. package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
  130. package/dist/helpers/smartResume.helpers.d.ts +3 -0
  131. package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
  132. package/dist/index.js +2 -2
  133. package/dist/models/AppManifestVersion.d.ts +20 -0
  134. package/dist/models/AppManifestVersion.d.ts.map +1 -0
  135. package/dist/models/AppStoreListing.d.ts +13 -0
  136. package/dist/models/AppStoreListing.d.ts.map +1 -1
  137. package/dist/models/ConsentFlowContract.d.ts +13 -2
  138. package/dist/models/ConsentFlowContract.d.ts.map +1 -1
  139. package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
  140. package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
  141. package/dist/models/Integration.d.ts +2 -0
  142. package/dist/models/Integration.d.ts.map +1 -1
  143. package/dist/models/ShareLink.d.ts +2 -0
  144. package/dist/models/ShareLink.d.ts.map +1 -1
  145. package/dist/models/index.d.ts +1 -0
  146. package/dist/models/index.d.ts.map +1 -1
  147. package/dist/models/share-link-constraints.d.ts.map +1 -1
  148. package/dist/openapi.d.ts +1 -1
  149. package/dist/openapi.d.ts.map +1 -1
  150. package/dist/routes/app-store.d.ts +619 -0
  151. package/dist/routes/app-store.d.ts.map +1 -1
  152. package/dist/routes/boosts.d.ts +2 -2
  153. package/dist/routes/boosts.d.ts.map +1 -1
  154. package/dist/routes/contracts.d.ts +146 -3
  155. package/dist/routes/contracts.d.ts.map +1 -1
  156. package/dist/routes/credentials.d.ts +34 -0
  157. package/dist/routes/credentials.d.ts.map +1 -1
  158. package/dist/routes/inbox.d.ts +52 -2
  159. package/dist/routes/inbox.d.ts.map +1 -1
  160. package/dist/routes/index.d.ts.map +1 -1
  161. package/dist/routes/profile-manager.d.ts.map +1 -1
  162. package/dist/routes/profiles.d.ts.map +1 -1
  163. package/dist/routes/public-share-links.d.ts +55 -0
  164. package/dist/routes/public-share-links.d.ts.map +1 -1
  165. package/dist/routes/share-links.d.ts +175 -14
  166. package/dist/routes/share-links.d.ts.map +1 -1
  167. package/dist/routes/test.d.ts +5 -0
  168. package/dist/routes/test.d.ts.map +1 -1
  169. package/dist/routes/workflows.d.ts.map +1 -1
  170. package/dist/types/app-manifest.d.ts +339 -0
  171. package/dist/types/app-manifest.d.ts.map +1 -0
  172. package/dist/types/app-store-listing.d.ts +34 -0
  173. package/dist/types/app-store-listing.d.ts.map +1 -1
  174. package/dist/types/consentflowcontract.d.ts +30 -3
  175. package/dist/types/consentflowcontract.d.ts.map +1 -1
  176. package/dist/types/credential-refresh.d.ts +1 -1
  177. package/esbuildPlugins.cjs +1 -0
  178. package/lambda.ts +49 -195
  179. package/lambdaApp.ts +203 -0
  180. package/package.json +20 -18
  181. package/scripts/export-openapi.ts +20 -0
  182. package/scripts/offline-schema-environment.ts +18 -0
  183. package/scripts/verify-python-contracts.ts +72 -0
  184. package/serverless-local.yml +3 -6
  185. package/serverless.function-env.cjs +50 -0
  186. package/serverless.local.yml +0 -1
  187. package/serverless.yml +27 -74
  188. package/shareLinkMaintenanceLambda.ts +13 -44
  189. package/shareLinkMaintenanceLambdaApp.ts +50 -0
  190. package/src/accesslayer/app-manifest-version/create.ts +57 -0
  191. package/src/accesslayer/app-manifest-version/read.ts +129 -0
  192. package/src/accesslayer/app-manifest-version/update.ts +61 -0
  193. package/src/accesslayer/app-store-listing/read.ts +6 -0
  194. package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
  195. package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
  196. package/src/accesslayer/app-store-listing/update.ts +15 -0
  197. package/src/accesslayer/boost/relationships/create.ts +8 -2
  198. package/src/accesslayer/consentflowcontract/create.ts +81 -8
  199. package/src/accesslayer/consentflowcontract/read.ts +84 -10
  200. package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
  201. package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
  202. package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
  203. package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
  204. package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
  205. package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
  206. package/src/accesslayer/credential/create.test.ts +2 -115
  207. package/src/accesslayer/credential/create.ts +11 -6
  208. package/src/accesslayer/credential/relationships/create.ts +17 -10
  209. package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
  210. package/src/accesslayer/inbox-credential/update.ts +4 -2
  211. package/src/accesslayer/share-link/abandon.ts +20 -0
  212. package/src/accesslayer/share-link/attachment.ts +360 -0
  213. package/src/accesslayer/share-link/finalize.ts +55 -7
  214. package/src/accesslayer/share-link/helpers.ts +33 -2
  215. package/src/accesslayer/share-link/recovery.ts +20 -0
  216. package/src/accesslayer/share-link/reserve.ts +85 -6
  217. package/src/accesslayer/share-link/revoke.ts +35 -0
  218. package/src/accesslayer/share-link/types.ts +9 -4
  219. package/src/cache/app-store.caches.ts +35 -0
  220. package/src/config/applyDockerStageConfig.ts +6 -0
  221. package/src/config/environment.test.ts +17 -12
  222. package/src/config/environment.ts +1 -0
  223. package/src/config/serverlessFunctionEnv.test.ts +105 -0
  224. package/src/config/stageConfig.test.ts +25 -0
  225. package/src/config/stageConfig.ts +24 -0
  226. package/src/docker-entry.ts +24 -0
  227. package/src/helpers/app-manifest.helpers.ts +185 -0
  228. package/src/helpers/claim-hooks.helpers.ts +20 -7
  229. package/src/helpers/consent-referral.helpers.ts +6 -0
  230. package/src/helpers/consent-scopes.helpers.ts +58 -0
  231. package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
  232. package/src/helpers/contract-events.helpers.ts +540 -0
  233. package/src/helpers/contract-requests.helpers.ts +211 -0
  234. package/src/helpers/finalize-inbox.helpers.ts +163 -44
  235. package/src/helpers/inbox.helpers.ts +51 -66
  236. package/src/helpers/inline-template.helpers.ts +560 -0
  237. package/src/helpers/notificationMessages.ts +59 -0
  238. package/src/helpers/notificationQueue.helpers.ts +23 -5
  239. package/src/helpers/notifications.helpers.ts +25 -1
  240. package/src/helpers/profile.helpers.ts +6 -0
  241. package/src/helpers/share-link-attachment-access.ts +87 -0
  242. package/src/helpers/share-link-attachment.ts +115 -0
  243. package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
  244. package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
  245. package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
  246. package/src/helpers/share-link-maintenance/runner.ts +28 -0
  247. package/src/helpers/share-link-maintenance/runtime.ts +6 -0
  248. package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
  249. package/src/helpers/share-link-maintenance/types.ts +2 -0
  250. package/src/helpers/share-link-owner-projection.ts +2 -0
  251. package/src/helpers/share-link-policy/production.ts +37 -17
  252. package/src/helpers/share-link-policy/resolver.ts +16 -11
  253. package/src/helpers/share-link-policy/types.ts +15 -7
  254. package/src/helpers/signingAuthority.helpers.ts +23 -14
  255. package/src/helpers/smartResume.helpers.ts +123 -0
  256. package/src/models/AppManifestVersion.ts +43 -0
  257. package/src/models/AppStoreListing.ts +32 -0
  258. package/src/models/ConsentFlowContract.ts +34 -3
  259. package/src/models/ConsentFlowTerms.ts +10 -0
  260. package/src/models/ConsentFlowTransaction.ts +3 -0
  261. package/src/models/Integration.ts +2 -0
  262. package/src/models/ShareLink.ts +4 -0
  263. package/src/models/index.ts +23 -0
  264. package/src/models/share-link-constraints.ts +5 -0
  265. package/src/openapi.ts +44 -8
  266. package/src/routes/app-store.ts +1054 -27
  267. package/src/routes/boosts.ts +12 -14
  268. package/src/routes/contracts.ts +382 -262
  269. package/src/routes/inbox.ts +4 -2
  270. package/src/routes/index.ts +4 -1
  271. package/src/routes/profile-manager.ts +19 -0
  272. package/src/routes/profiles.ts +12 -0
  273. package/src/routes/public-share-links.ts +169 -0
  274. package/src/routes/share-links.ts +69 -0
  275. package/src/routes/workflows.ts +3 -2
  276. package/src/types/app-manifest.ts +28 -0
  277. package/src/types/app-store-listing.ts +13 -0
  278. package/src/types/consentflowcontract.ts +25 -4
  279. package/test/app-manifest.helpers.spec.ts +170 -0
  280. package/test/app-manifest.routes.spec.ts +299 -0
  281. package/test/app-store-listing-updates.spec.ts +236 -0
  282. package/test/app-store-listing.spec.ts +65 -3
  283. package/test/consent-data-boundaries.spec.ts +352 -0
  284. package/test/consent-recipients.spec.ts +513 -0
  285. package/test/consent-scopes.helpers.spec.ts +67 -0
  286. package/test/consentflow.spec.ts +63 -2
  287. package/test/contract-requests.spec.ts +1588 -0
  288. package/test/credential-storage.neo4j.spec.ts +122 -0
  289. package/test/guardian-gated-routes.spec.ts +36 -0
  290. package/test/inbox-security.e2e.ts +26 -10
  291. package/test/inbox.spec.ts +184 -8
  292. package/test/inline-template.helpers.spec.ts +49 -0
  293. package/test/managed-recipient-inbox.spec.ts +24 -2
  294. package/test/openapi.spec.ts +14 -73
  295. package/test/profiles.spec.ts +30 -0
  296. package/test/share-link-attachment.neo4j.spec.ts +262 -0
  297. package/test/share-link-attachment.unit.spec.ts +244 -0
  298. package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
  299. package/test/share-link-maintenance.unit.spec.ts +32 -5
  300. package/test/share-link-owner-route.unit.spec.ts +70 -2
  301. package/test/share-link-policy.unit.spec.ts +18 -4
  302. package/test/share-link-production-policy.unit.spec.ts +58 -9
  303. package/test/share-link-public-route.unit.spec.ts +279 -0
  304. package/test/share-link-service-policy.neo4j.spec.ts +403 -0
  305. package/test/signing-authority-encryption.spec.ts +75 -0
  306. package/test/smart-resume-consent.spec.ts +233 -0
  307. package/test/smart-resume-upload.spec.ts +65 -0
  308. package/tsconfig.build.json +1 -1
  309. package/tsconfig.json +1 -0
  310. package/vitest.config.ts +13 -0
  311. package/vitest.inbox-e2e.config.ts +2 -0
  312. package/workerLambda.test.ts +100 -0
@@ -1,10 +1,16 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
 
3
+ import {
4
+ consumeShareViewReceipt,
5
+ persistShareViewReceipt,
6
+ } from '../src/accesslayer/share-link/receipt';
7
+
3
8
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
4
9
  import { v4 as uuid } from 'uuid';
5
10
 
6
11
  import { computeShareLinkRequestHash } from '@helpers/share-link-lifecycle';
7
12
  import { neogma } from '@instance';
13
+ import { withShareLinkRead } from '../src/accesslayer/share-link/transaction';
8
14
 
9
15
  import {
10
16
  abandonReservation,
@@ -23,7 +29,12 @@ import {
23
29
  import { ensureShareLinkConstraints } from '../src/models/share-link-constraints';
24
30
  import { toOwnerShareLink } from '../src/helpers/share-link-owner-projection';
25
31
  import type { ShareLinkPolicySnapshot } from '../src/helpers/share-link-policy/types';
26
- import { resolveCurrentShareLinkPolicy } from '../src/helpers/share-link-policy/production';
32
+ import {
33
+ createProductionShareLinkPolicySource,
34
+ productionShareViewEligibilitySource,
35
+ resolveCurrentShareLinkPolicy,
36
+ } from '../src/helpers/share-link-policy/production';
37
+ import { createShareLinkPolicyResolver } from '../src/helpers/share-link-policy/resolver';
27
38
  import type { ShareLinkRecord } from '../src/models/ShareLink';
28
39
 
29
40
  import {
@@ -782,7 +793,7 @@ describe('share-link lifecycle repository (Neo4j)', () => {
782
793
 
783
794
  const protectedShare = await finalizeReservation({
784
795
  ...protect.reservation,
785
- resolveCurrentPolicy: async () => policy,
796
+ resolveCurrentPolicy: async () => ({ ...policy, isServiceProfile: false }),
786
797
  now: NOW,
787
798
  });
788
799
  if (protectedShare.outcome !== 'finalized') throw new Error('expected protected share');
@@ -1265,4 +1276,136 @@ describe('share-link lifecycle repository (Neo4j)', () => {
1265
1276
  );
1266
1277
  }
1267
1278
  });
1279
+
1280
+ it('exempts managed service owners but keeps managed child owners restricted in Neo4j', async () => {
1281
+ const serviceProfileId = `service-policy-${uuid()}`;
1282
+ const childProfileId = `child-policy-${uuid()}`;
1283
+ const unknownServiceId = `service-unknown-${uuid()}`;
1284
+ const serviceManagerId = `manager-${uuid()}`;
1285
+ const childManagerId = `manager-${uuid()}`;
1286
+ await neogma.queryRunner.run(
1287
+ `CREATE (service:Profile {profileId: $serviceId, isServiceProfile: true})
1288
+ CREATE (serviceManager:ProfileManager {id: $serviceManagerId, created: $created})
1289
+ CREATE (serviceManager)-[:MANAGES]->(service)
1290
+ CREATE (child:Profile {profileId: $childId, dob: '1990-01-01', isServiceProfile: false})
1291
+ CREATE (child)-[:MANAGED_BY]->(guardian:Profile {profileId: $childManagerId})
1292
+ CREATE (serviceManager)-[:ADMINISTRATED_BY]->(guardian)
1293
+ CREATE (:Profile {profileId: $unknownServiceId, isServiceProfile: true})`,
1294
+ {
1295
+ serviceId: serviceProfileId,
1296
+ serviceManagerId,
1297
+ childId: childProfileId,
1298
+ childManagerId,
1299
+ unknownServiceId,
1300
+ created: NOW.toISOString(),
1301
+ }
1302
+ );
1303
+ try {
1304
+ const shareId = nextShareId();
1305
+ const requestId = uuid();
1306
+ const policy: ShareLinkPolicySnapshot = {
1307
+ isMinor: false,
1308
+ policyResolved: true,
1309
+ defaultExpiryDays: 365,
1310
+ viewCountingEnabled: true,
1311
+ };
1312
+ const reserved = await reserveCreate({
1313
+ namespace: NAMESPACE,
1314
+ ownerProfileId: serviceProfileId,
1315
+ clientRequestId: requestId,
1316
+ shareId,
1317
+ title: 'Service policy integration',
1318
+ note: null,
1319
+ expiresAt: null,
1320
+ selectedCount: 1,
1321
+ content: contentBinding(shareId),
1322
+ requestHash: computeShareLinkRequestHash('create', {
1323
+ id: shareId,
1324
+ title: 'Service policy integration',
1325
+ }),
1326
+ leaseOwner: 'worker-1',
1327
+ policy,
1328
+ now: NOW,
1329
+ });
1330
+ if (reserved.outcome !== 'reserved' || !reserved.reservation.objectRef) {
1331
+ throw new Error('expected service share reservation');
1332
+ }
1333
+ const finalized = await finalizeReservation({
1334
+ ...reserved.reservation,
1335
+ resolveCurrentPolicy: resolveCurrentShareLinkPolicy,
1336
+ now: NOW,
1337
+ });
1338
+ const created = {
1339
+ namespace: NAMESPACE,
1340
+ shareId,
1341
+ committed: finalized.share,
1342
+ objectRef: reserved.reservation.objectRef,
1343
+ operationId: reserved.reservation.operationId,
1344
+ };
1345
+ const preflight = createShareLinkPolicyResolver(
1346
+ createProductionShareLinkPolicySource()
1347
+ );
1348
+ expect((await preflight.resolve(serviceProfileId)).viewCountingEnabled).toBe(true);
1349
+ expect((await preflight.resolve(childProfileId)).viewCountingEnabled).toBe(false);
1350
+ expect((await preflight.resolve(unknownServiceId)).viewCountingEnabled).toBe(true);
1351
+ expect(
1352
+ await withShareLinkRead(tx =>
1353
+ resolveCurrentShareLinkPolicy(tx, serviceProfileId, NOW)
1354
+ )
1355
+ ).toMatchObject({ viewCountingEnabled: true });
1356
+ const receipt = randomBytes(32).toString('base64url');
1357
+ expect(
1358
+ await persistShareViewReceipt({
1359
+ receipt,
1360
+ namespace: created.namespace,
1361
+ ownerProfileId: serviceProfileId,
1362
+ shareId: created.shareId,
1363
+ shareVersion: created.committed.version,
1364
+ contentVersion: created.committed.contentVersion,
1365
+ objectRef: created.objectRef,
1366
+ operationId: created.operationId,
1367
+ ttlSeconds: 60,
1368
+ eligibilitySource: productionShareViewEligibilitySource,
1369
+ now: () => NOW,
1370
+ })
1371
+ ).toBe(true);
1372
+ expect(
1373
+ await consumeShareViewReceipt({
1374
+ receipt,
1375
+ namespace: created.namespace,
1376
+ eligibilitySource: productionShareViewEligibilitySource,
1377
+ now: () => NOW,
1378
+ })
1379
+ ).toBe('consumed');
1380
+ const counted = await neogma.queryRunner.run(
1381
+ 'MATCH (s:ShareLink {id: $shareId}) RETURN s.viewCount AS viewCount',
1382
+ { shareId: created.shareId }
1383
+ );
1384
+ expect(Number(counted.records[0]?.get('viewCount'))).toBe(1);
1385
+ expect(
1386
+ await withShareLinkRead(tx =>
1387
+ resolveCurrentShareLinkPolicy(tx, childProfileId, NOW)
1388
+ )
1389
+ ).toMatchObject({ viewCountingEnabled: false });
1390
+ expect(
1391
+ await withShareLinkRead(tx =>
1392
+ resolveCurrentShareLinkPolicy(tx, unknownServiceId, NOW)
1393
+ )
1394
+ ).toMatchObject({ viewCountingEnabled: true });
1395
+ } finally {
1396
+ await neogma.queryRunner.run(
1397
+ `MATCH (n)
1398
+ WHERE (n:Profile AND n.profileId IN [$serviceId, $childId, $unknownServiceId, $childManagerId])
1399
+ OR (n:ProfileManager AND n.id = $serviceManagerId)
1400
+ DETACH DELETE n`,
1401
+ {
1402
+ serviceId: serviceProfileId,
1403
+ childId: childProfileId,
1404
+ unknownServiceId,
1405
+ serviceManagerId,
1406
+ childManagerId,
1407
+ }
1408
+ );
1409
+ }
1410
+ });
1268
1411
  });
@@ -81,10 +81,11 @@ const makeCleanupRepository = (
81
81
 
82
82
  const makeDeleteClient = (
83
83
  result: unknown = { ok: true, value: {} }
84
- ): CleanupRunnerDependencies['client'] =>
84
+ ): Parameters<typeof runShareLinkMaintenancePass>[0]['client'] =>
85
85
  ({
86
86
  delete: vi.fn().mockResolvedValue(result),
87
- }) as unknown as CleanupRunnerDependencies['client'];
87
+ stat: vi.fn().mockResolvedValue({ ok: false, error: 'NOT_FOUND' }),
88
+ }) as Parameters<typeof runShareLinkMaintenancePass>[0]['client'];
88
89
 
89
90
  const makeRecoveryRepository = (): RecoveryRunnerDependencies['repository'] =>
90
91
  ({
@@ -307,6 +308,31 @@ describe('share-link maintenance pass', () => {
307
308
  expect(summary.categories).toEqual({});
308
309
  });
309
310
 
311
+ it('runs bounded attachment stage collection and reports a safe failure for retry', async () => {
312
+ const config = enabledConfig();
313
+ const now = new Date(NOW_ISO);
314
+ const expireAttachmentStages = vi.fn().mockResolvedValue(3);
315
+ const dependencies = {
316
+ ...runnerDependencies(config, { expireAttachmentStages, now: () => now }),
317
+ client: makeDeleteClient(),
318
+ };
319
+ const summary = await runShareLinkMaintenancePass(dependencies);
320
+ expect(expireAttachmentStages).toHaveBeenCalledWith({
321
+ namespace: NAMESPACE,
322
+ limit: 10,
323
+ now,
324
+ transaction: { timeoutMs: config.graphTimeoutMs, noInlineRetry: true },
325
+ });
326
+ expect(summary.attachmentStages).toEqual({ queued: 3 });
327
+ expireAttachmentStages.mockRejectedValueOnce(new Error('raw private detail'));
328
+ const failed = await runShareLinkMaintenancePass(dependencies);
329
+ expect(failed.status).toBe('failed');
330
+ expect(failed.categories).toMatchObject({
331
+ share_link_maintenance_attachment_stage_failed: 1,
332
+ });
333
+ expect(JSON.stringify(toMaintenanceLogEvent(failed))).not.toContain('raw private detail');
334
+ });
335
+
310
336
  it('does not start receipt pruning when the graph budget is exhausted', async () => {
311
337
  let clock = 0;
312
338
  // budget 1000ms, reserve 100ms. Recovery discovery needs 500ms and cleanup
@@ -727,6 +753,7 @@ describe('C7 independent review regressions', () => {
727
753
  client,
728
754
  logger: silentLogger,
729
755
  remainingTimeMs: () => 6_000,
756
+ pruneReceipts: vi.fn().mockResolvedValue(0),
730
757
  });
731
758
  expect(summary.status).toBe('deadline_exhausted');
732
759
  expect(cleanupRepository.claimCleanupJobs).not.toHaveBeenCalled();
@@ -1034,14 +1061,14 @@ describe('C7a dependency setup caching', () => {
1034
1061
 
1035
1062
  describe('C7a scheduler terminal stop and synchronous failure', () => {
1036
1063
  it('does not run a queued timer callback after stop', async () => {
1037
- let queued: (() => void) | null = null;
1064
+ const timer: { queued: (() => void) | null } = { queued: null };
1038
1065
  const runOnce = vi.fn().mockResolvedValue(undefined);
1039
1066
  const scheduler = createShareLinkMaintenanceScheduler({
1040
1067
  runOnce,
1041
1068
  intervalMs: 1_000,
1042
1069
  logger: silentLogger,
1043
1070
  setTimeoutFn: handler => {
1044
- queued = handler;
1071
+ timer.queued = handler;
1045
1072
  return 1;
1046
1073
  },
1047
1074
  clearTimeoutFn: () => undefined,
@@ -1051,7 +1078,7 @@ describe('C7a scheduler terminal stop and synchronous failure', () => {
1051
1078
  await scheduler.stop();
1052
1079
 
1053
1080
  // Simulate a timer callback that was already queued when stop ran.
1054
- queued?.();
1081
+ timer.queued?.();
1055
1082
  await Promise.resolve();
1056
1083
  await Promise.resolve();
1057
1084
 
@@ -220,6 +220,72 @@ beforeEach(() => {
220
220
  recoveryRunner.recoverShareLinkOperation.mockReset();
221
221
  });
222
222
 
223
+ describe('owner attachment route authorization', () => {
224
+ const input = {
225
+ id: SHARE_ID,
226
+ contentVersion: 1,
227
+ attachmentId: OPERATION_ID,
228
+ chunkIndex: 0,
229
+ chunkCount: 1,
230
+ envelope,
231
+ ownerEncryptedRecovery: recovery,
232
+ };
233
+ const setup = () => ({
234
+ ...makeDependencies(),
235
+ attachments: {
236
+ put: vi.fn(async () => ({ ok: true as const })),
237
+ delete: vi.fn(async () => ({ ok: true })),
238
+ },
239
+ });
240
+ it('requires authenticated profile and write scope before any staging', async () => {
241
+ const dependencies = setup();
242
+ for (const user of [
243
+ undefined,
244
+ { ...authenticatedUser, isChallengeValid: false },
245
+ { ...authenticatedUser, scope: AUTH_GRANT_SHARE_LINKS_READ_SCOPE },
246
+ ]) {
247
+ await expect(
248
+ makeCaller(dependencies, { user }).putAttachmentChunk(input)
249
+ ).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
250
+ }
251
+ expect(dependencies.attachments.put).not.toHaveBeenCalled();
252
+ await expect(makeCaller(dependencies).putAttachmentChunk(input)).resolves.toEqual({
253
+ ok: true,
254
+ });
255
+ expect(dependencies.attachments.put).toHaveBeenCalledWith(input, {
256
+ namespace: NAME,
257
+ ownerProfileId: OWNER_PROFILE.profileId,
258
+ });
259
+ });
260
+ it('rejects arbitrary tuples and overlarge chunks before staging', async () => {
261
+ const dependencies = setup();
262
+ await expect(
263
+ makeCaller(dependencies).putAttachmentChunk({
264
+ ...input,
265
+ namespace: 'arbitrary',
266
+ } as never)
267
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
268
+ await expect(
269
+ makeCaller(dependencies).putAttachmentChunk({
270
+ ...input,
271
+ envelope: { ...envelope, ct: Buffer.alloc(256 * 1024 + 17).toString('base64url') },
272
+ })
273
+ ).rejects.toMatchObject({ code: 'BAD_REQUEST' });
274
+ expect(dependencies.attachments.put).not.toHaveBeenCalled();
275
+ });
276
+ it('derives cleanup owner and passes only the validated exact stage key', async () => {
277
+ const dependencies = setup();
278
+ const key = { id: SHARE_ID, contentVersion: 1, attachmentId: OPERATION_ID, chunkCount: 1 };
279
+ await expect(makeCaller(dependencies).deleteAttachmentChunks(key)).resolves.toEqual({
280
+ ok: true,
281
+ });
282
+ expect(dependencies.attachments.delete).toHaveBeenCalledWith(key, {
283
+ namespace: NAME,
284
+ ownerProfileId: OWNER_PROFILE.profileId,
285
+ });
286
+ });
287
+ });
288
+
223
289
  describe.each([
224
290
  ['standard', profileRoute],
225
291
  ['private input', profileRouteWithoutInputCapture],
@@ -678,7 +744,8 @@ describe('share-link owner route boundary', () => {
678
744
  );
679
745
 
680
746
  const result = await caller.list({ limit: 25 });
681
- const record = result.records[0];
747
+ expect(result.records).toHaveLength(1);
748
+ const record = result.records[0]!;
682
749
 
683
750
  expect(resolveViewCountingEligibility).toHaveBeenCalledTimes(1);
684
751
  expect(record).not.toHaveProperty('activeObjectRef');
@@ -708,8 +775,9 @@ describe('share-link owner route boundary', () => {
708
775
 
709
776
  const result = await caller.list({ limit: 25 });
710
777
 
778
+ expect(result.records).toHaveLength(1);
711
779
  expect(result.records[0]).not.toHaveProperty('viewCount');
712
- expect(result.records[0].lastViewedAt).toBeNull();
780
+ expect(result.records[0]!.lastViewedAt).toBeNull();
713
781
  });
714
782
 
715
783
  it('maps a repository failure to a fixed safe error without leaking the cause', async () => {
@@ -42,13 +42,24 @@ describe('share-link policy decision table', () => {
42
42
  expect(composeShareLinkPolicy('unknown', false)).toEqual(DEFAULT_SHARE_LINK_POLICY);
43
43
  expect(composeShareLinkPolicy('unknown', true)).toEqual(DEFAULT_SHARE_LINK_POLICY);
44
44
  });
45
+
46
+ it.each(['unknown', 'minor'] as const)(
47
+ 'allows service tracking with %s age without changing the expiry default',
48
+ age => {
49
+ expect(composeShareLinkPolicy(age, true, true)).toEqual({
50
+ isMinor: false,
51
+ policyResolved: true,
52
+ defaultExpiryDays: 30,
53
+ viewCountingEnabled: true,
54
+ });
55
+ }
56
+ );
45
57
  });
46
58
 
47
59
  describe('createShareLinkPolicyResolver', () => {
48
60
  it('fails closed to the conservative default when a source throws', async () => {
49
61
  const resolver = createShareLinkPolicyResolver({
50
- resolveOwnerAge: vi.fn().mockRejectedValue(new Error('age source down')),
51
- isManaged: vi.fn().mockResolvedValue(false),
62
+ resolveOwner: vi.fn().mockRejectedValue(new Error('profile source down')),
52
63
  });
53
64
 
54
65
  await expect(resolver.resolve('owner-1')).resolves.toEqual(DEFAULT_SHARE_LINK_POLICY);
@@ -56,8 +67,11 @@ describe('createShareLinkPolicyResolver', () => {
56
67
 
57
68
  it('never returns an adult policy when the age source is unknown', async () => {
58
69
  const resolver = createShareLinkPolicyResolver({
59
- resolveOwnerAge: vi.fn().mockResolvedValue('unknown'),
60
- isManaged: vi.fn().mockResolvedValue(false),
70
+ resolveOwner: vi.fn().mockResolvedValue({
71
+ age: 'unknown',
72
+ isManaged: false,
73
+ isServiceProfile: false,
74
+ }),
61
75
  });
62
76
 
63
77
  const policy = await resolver.resolve('owner-1');
@@ -1,9 +1,11 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  vi.mock('@accesslayer/profile/read', () => ({ getProfileByProfileId: vi.fn() }));
4
4
  vi.mock('@accesslayer/profile/relationships/read', () => ({ isProfileManaged: vi.fn() }));
5
5
  vi.mock('@helpers/profile.helpers', () => ({
6
6
  transformProfileId: (id: string) => id.toLowerCase(),
7
+ isServiceProfileExemptFromGuardianship: (isServiceProfile: unknown, type: unknown) =>
8
+ isServiceProfile === true && type !== 'child',
7
9
  }));
8
10
 
9
11
  import {
@@ -15,6 +17,7 @@ import {
15
17
  import { getProfileByProfileId } from '@accesslayer/profile/read';
16
18
  import { isProfileManaged } from '@accesslayer/profile/relationships/read';
17
19
  import type { ShareLinkTransaction } from '@accesslayer/share-link/transaction';
20
+ import { createShareLinkPolicyResolver } from '@helpers/share-link-policy/resolver';
18
21
 
19
22
  const NOW = new Date('2026-09-25T12:00:00.000Z');
20
23
 
@@ -22,13 +25,27 @@ const transactionWith = (profile?: {
22
25
  dob: unknown;
23
26
  type: unknown;
24
27
  isManaged: unknown;
28
+ isServiceProfile?: unknown;
25
29
  }): ShareLinkTransaction => ({
26
30
  run: vi.fn(async () => ({
27
- records: profile ? [{ get: (key: string) => profile[key as keyof typeof profile] }] : [],
31
+ records: profile
32
+ ? [
33
+ {
34
+ get: (key: string) =>
35
+ key === 'profileType'
36
+ ? profile.type
37
+ : profile[key as keyof typeof profile],
38
+ },
39
+ ]
40
+ : [],
28
41
  })),
29
42
  });
30
43
 
31
44
  describe('production share-link age policy', () => {
45
+ beforeEach(() => {
46
+ vi.resetAllMocks();
47
+ });
48
+
32
49
  it('requires a valid birthdate and an eighteenth birthday before allowing adulthood', () => {
33
50
  expect(ageFromPersistedProfile({ dob: '2008-09-25' }, NOW)).toBe('adult');
34
51
  expect(ageFromPersistedProfile({ dob: '2008-09-26' }, NOW)).toBe('minor');
@@ -43,16 +60,48 @@ describe('production share-link age policy', () => {
43
60
  vi.mocked(getProfileByProfileId).mockResolvedValueOnce({ dob: '1990-01-01' } as never);
44
61
  vi.mocked(isProfileManaged).mockResolvedValueOnce(false);
45
62
 
46
- const source = createProductionShareLinkPolicySource();
47
- expect(await source.resolveOwnerAge('owner')).toBe('adult');
48
- expect(await source.isManaged('owner')).toBe(false);
49
- expect(getProfileByProfileId).toHaveBeenCalledWith('owner');
63
+ const resolver = createShareLinkPolicyResolver(createProductionShareLinkPolicySource());
64
+ expect((await resolver.resolve('owner')).viewCountingEnabled).toBe(true);
50
65
  });
66
+ it.each([
67
+ { dob: undefined, type: undefined, isServiceProfile: true, eligible: true },
68
+ { dob: 'not-a-date', type: undefined, isServiceProfile: true, eligible: true },
69
+ { dob: '2020-01-01', type: undefined, isServiceProfile: true, eligible: true },
70
+ { dob: '1990-01-01', type: undefined, isServiceProfile: true, eligible: true },
71
+ { dob: undefined, type: 'child', isServiceProfile: true, eligible: false },
72
+ { dob: undefined, type: undefined, isServiceProfile: 'true', eligible: false },
73
+ ])(
74
+ 'uses persisted service classification in preflight and locked policy: $dob / $type / $isServiceProfile',
75
+ async ({ dob, type, isServiceProfile, eligible }) => {
76
+ vi.mocked(getProfileByProfileId).mockResolvedValue({
77
+ dob,
78
+ type,
79
+ isServiceProfile,
80
+ } as never);
81
+ vi.mocked(isProfileManaged).mockResolvedValue(true);
82
+ const resolver = createShareLinkPolicyResolver(createProductionShareLinkPolicySource());
83
+ const preflight = await resolver.resolve('owner');
84
+ const locked = await resolveCurrentShareLinkPolicy(
85
+ transactionWith({ dob, type, isServiceProfile, isManaged: true }),
86
+ 'owner',
87
+ NOW
88
+ );
89
+ expect(preflight.viewCountingEnabled).toBe(eligible);
90
+ expect(locked).toEqual({ ...preflight, isServiceProfile: eligible });
91
+ expect(locked.defaultExpiryDays).toBe(30);
92
+ }
93
+ );
51
94
 
52
95
  it('rechecks persisted age and management inside the receipt transaction', async () => {
53
96
  const adult = transactionWith({ dob: '1990-01-01', type: null, isManaged: false });
54
97
  const managed = transactionWith({ dob: '1990-01-01', type: null, isManaged: true });
55
98
  const unknown = transactionWith();
99
+ const managedService = transactionWith({
100
+ dob: null,
101
+ type: null,
102
+ isManaged: true,
103
+ isServiceProfile: true,
104
+ });
56
105
  const input = {
57
106
  namespace: 'test',
58
107
  ownerProfileId: 'OWNER',
@@ -67,9 +116,9 @@ describe('production share-link age policy', () => {
67
116
  expect(await productionShareViewEligibilitySource.isEligible(adult, input)).toBe(true);
68
117
  expect(await productionShareViewEligibilitySource.isEligible(managed, input)).toBe(false);
69
118
  expect(await productionShareViewEligibilitySource.isEligible(unknown, input)).toBe(false);
70
- expect((adult.run as ReturnType<typeof vi.fn>).mock.calls[0]?.[1]).toEqual({
71
- profileId: 'owner',
72
- });
119
+ expect(await productionShareViewEligibilitySource.isEligible(managedService, input)).toBe(
120
+ true
121
+ );
73
122
  expect(
74
123
  (await resolveCurrentShareLinkPolicy(unknown, 'owner', NOW)).viewCountingEnabled
75
124
  ).toBe(false);