@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
@@ -768,8 +768,10 @@ export const inboxRouter = t.router({
768
768
  claimed: z.number(),
769
769
  errors: z.number(),
770
770
  guardianPending: z.number(),
771
- verifiableCredentials: z.array(VCValidator),
772
- deliveries: z.array(z.object({ id: z.string(), credential: VCValidator })),
771
+ verifiableCredentials: z.array(VCValidator.or(JWEValidator)),
772
+ deliveries: z.array(
773
+ z.object({ id: z.string(), credential: VCValidator.or(JWEValidator) })
774
+ ),
773
775
  })
774
776
  )
775
777
  .mutation(async ({ ctx }) => {
@@ -23,6 +23,7 @@ import {
23
23
  } from '@accesslayer/profile/relationships/read';
24
24
  import { getDidWeb } from '@helpers/did.helpers';
25
25
  import { getEmptyLearnCard, isServersDidWebDID } from '@helpers/learnCard.helpers';
26
+ import { isServiceProfileExemptFromGuardianship } from '@helpers/profile.helpers';
26
27
  import { invalidateChallengeForDid, isChallengeValidForDid } from '@cache/challenges';
27
28
  import { ProfileType } from 'types/profile';
28
29
  import { getProfileManagerById } from '@accesslayer/profile-manager/read';
@@ -555,7 +556,9 @@ export const guardianGatedRoute = profileRoute.use(async ({ ctx, next }) => {
555
556
  const { profile } = ctx.user;
556
557
  const guardianApprovalToken = ctx._guardianApprovalToken;
557
558
  // Service profiles (orgs) are managed for admin purposes, not guardianship.
558
- const isChildAccount = !profile.isServiceProfile && (await isProfileManaged(profile.profileId));
559
+ const isChildAccount =
560
+ !isServiceProfileExemptFromGuardianship(profile.isServiceProfile, profile.type) &&
561
+ (await isProfileManaged(profile.profileId));
559
562
  let guardianIdentity: { profileId: string; did: string } | undefined;
560
563
 
561
564
  if (isChildAccount && guardianApprovalToken) {
@@ -29,6 +29,7 @@ import {
29
29
  import { getProfilesThatManageAProfile } from '@accesslayer/profile/relationships/read';
30
30
  import { updateProfileManager } from '@accesslayer/profile-manager/update';
31
31
  import { getProfileManagerById } from '@accesslayer/profile-manager/read';
32
+ import { ProfileManager } from '@models';
32
33
 
33
34
  export const profileManagersRouter = t.router({
34
35
  createProfileManager: profileRoute
@@ -126,6 +127,24 @@ export const profileManagersRouter = t.router({
126
127
  )
127
128
  .output(z.string())
128
129
  .mutation(async ({ input, ctx }) => {
130
+ // Org onboarding also uses this API. Only the persisted Family parent
131
+ // (or an explicit child type), not a client service flag, defines a child path.
132
+ if (input.isServiceProfile) {
133
+ const parents = await ProfileManager.findRelationships({
134
+ alias: 'childOf',
135
+ where: { source: { id: ctx.user.manager.id } },
136
+ });
137
+ if (
138
+ input.type === 'child' ||
139
+ parents.some(({ target }) => target.category === 'Family')
140
+ ) {
141
+ throw new TRPCError({
142
+ code: 'BAD_REQUEST',
143
+ message: 'Child profiles cannot be service profiles.',
144
+ });
145
+ }
146
+ }
147
+
129
148
  const profileExists = await checkIfProfileExists(input);
130
149
 
131
150
  if (profileExists) {
@@ -574,6 +574,18 @@ export const profilesRouter = t.router({
574
574
  .output(z.boolean())
575
575
  .mutation(async ({ input, ctx }) => {
576
576
  const { profile } = ctx.user;
577
+ if (
578
+ profile.isServiceProfile === true &&
579
+ profile.type === 'child' &&
580
+ typeof input.type === 'string' &&
581
+ input.type !== 'child'
582
+ ) {
583
+ throw new TRPCError({
584
+ code: 'BAD_REQUEST',
585
+ message:
586
+ 'A service profile explicitly typed as a child cannot change profile type.',
587
+ });
588
+ }
577
589
 
578
590
  const {
579
591
  profileId,
@@ -10,6 +10,9 @@ import {
10
10
  ShareEnvelopeValidator,
11
11
  ShareLinkPublicContentViewValidator,
12
12
  ShareLinkPublicStateValidator,
13
+ GetShareLinkAttachmentChunkInputValidator,
14
+ ShareLinkAttachmentChunkOutputValidator,
15
+ ShareLinkAttachmentChunkEnvelopeValidator,
13
16
  } from '@learncard/types';
14
17
  import { LCNNotificationTypeEnumValidator } from '@learncard/types';
15
18
 
@@ -32,6 +35,11 @@ import type {
32
35
  ShareViewEligibilitySource,
33
36
  } from '@accesslayer/share-link';
34
37
  import type { ShareLinkRecord } from '../models/ShareLink';
38
+ import { shareAttachmentTuple } from '@accesslayer/share-link/attachment';
39
+ import {
40
+ mintShareAttachmentAccessToken,
41
+ verifyShareAttachmentAccessToken,
42
+ } from '@helpers/share-link-attachment-access';
35
43
 
36
44
  /**
37
45
  * LC-2187 public (anonymous) share-link API: resolve, guarded content and
@@ -67,6 +75,11 @@ export type PublicShareLinkRouterDependencies = {
67
75
  }) => Promise<
68
76
  { ok: true; value: ShareContentContentProjection } | { ok: false; error: string }
69
77
  >;
78
+ fetchAttachmentChunk?: (
79
+ input: import('@helpers/share-content-client/types').ShareContentTuple
80
+ ) => Promise<
81
+ { ok: true; value: ShareContentContentProjection } | { ok: false; error: string }
82
+ >;
70
83
  };
71
84
  readonly receipts: {
72
85
  persist: (input: PersistShareViewReceiptInput) => Promise<boolean>;
@@ -219,6 +232,161 @@ export const createPublicShareLinksRouter = (
219
232
  };
220
233
 
221
234
  return t.router({
235
+ attachmentChunk: openRouteWithoutInputCapture
236
+ .meta({
237
+ openapi: openapi(
238
+ 'POST',
239
+ '/public/share-links/attachment-chunk',
240
+ 'Fetch guarded encrypted attachment chunk'
241
+ ),
242
+ })
243
+ .input(GetShareLinkAttachmentChunkInputValidator)
244
+ .output(ShareLinkAttachmentChunkOutputValidator)
245
+ .mutation(async ({ ctx, input }) => {
246
+ const dependencies = await resolve();
247
+ if (!dependencies.repository.fetchAttachmentChunk) notFound();
248
+ try {
249
+ await dependencies.enforceRateLimit({
250
+ key: rateLimitKey('attachment', dependencies.namespace, ctx.sourceIp),
251
+ limit: 240,
252
+ windowSeconds: 60,
253
+ description: 'public share-link attachment',
254
+ });
255
+ } catch (error) {
256
+ if (error instanceof TRPCError && error.code === 'TOO_MANY_REQUESTS')
257
+ throw new TRPCError({
258
+ code: 'TOO_MANY_REQUESTS',
259
+ message: 'try again later',
260
+ });
261
+ throw new TRPCError({
262
+ code: 'SERVICE_UNAVAILABLE',
263
+ message: 'share-link content is unavailable',
264
+ });
265
+ }
266
+ const read = async (): Promise<ShareLinkRecord> => {
267
+ let record: ShareLinkRecord | null;
268
+ try {
269
+ record = await dependencies.repository.getShareLink({
270
+ shareId: input.id,
271
+ namespace: dependencies.namespace,
272
+ });
273
+ } catch {
274
+ throw new TRPCError({
275
+ code: 'SERVICE_UNAVAILABLE',
276
+ message: 'share-link content is unavailable',
277
+ });
278
+ }
279
+ const state = classifyPublicShare(record, dependencies.now());
280
+ if (
281
+ state.state !== 'active' ||
282
+ state.record.namespace !== dependencies.namespace ||
283
+ state.record.contentVersion !== input.contentVersion ||
284
+ state.record.attachmentId !== input.attachmentId ||
285
+ !state.record.attachmentChunkCount ||
286
+ input.chunkIndex >= state.record.attachmentChunkCount
287
+ )
288
+ notFound();
289
+ return state.record;
290
+ };
291
+ const first = await read();
292
+ const grantAccepted =
293
+ input.accessToken &&
294
+ verifyShareAttachmentAccessToken(
295
+ input.accessToken,
296
+ first,
297
+ dependencies.namespace,
298
+ ctx.sourceIp,
299
+ dependencies.now()
300
+ );
301
+ const access = grantAccepted
302
+ ? 'accepted'
303
+ : await passcodeAccepted(first, input.passcode, dependencies, ctx.sourceIp);
304
+ if (access === 'try_later')
305
+ throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: 'try again later' });
306
+ if (access !== 'accepted')
307
+ throw new TRPCError({
308
+ code: 'UNAUTHORIZED',
309
+ message: 'share-link passcode required',
310
+ });
311
+ const tuple = shareAttachmentTuple(
312
+ {
313
+ namespace: dependencies.namespace,
314
+ ownerProfileId: first.ownerProfileId,
315
+ shareId: first.id,
316
+ contentVersion: first.contentVersion,
317
+ attachmentId: input.attachmentId,
318
+ chunkCount: first.attachmentChunkCount!,
319
+ },
320
+ input.chunkIndex
321
+ );
322
+ let fetched;
323
+ try {
324
+ fetched = await dependencies.repository.fetchAttachmentChunk(tuple);
325
+ } catch {
326
+ throw new TRPCError({
327
+ code: 'SERVICE_UNAVAILABLE',
328
+ message: 'share-link content is unavailable',
329
+ });
330
+ }
331
+ if (!fetched.ok) {
332
+ if (fetched.error === 'NOT_FOUND' || fetched.error === 'CONFLICT') notFound();
333
+ throw new TRPCError({
334
+ code: 'SERVICE_UNAVAILABLE',
335
+ message: 'share-link content is unavailable',
336
+ });
337
+ }
338
+ if (
339
+ Object.entries(tuple).some(
340
+ ([key, value]) => fetched.value[key as keyof typeof fetched.value] !== value
341
+ )
342
+ )
343
+ notFound();
344
+ const final = await read();
345
+ if (
346
+ final.version !== first.version ||
347
+ final.ownerProfileId !== first.ownerProfileId ||
348
+ final.activeObjectRef !== first.activeObjectRef ||
349
+ final.passcodeHash !== first.passcodeHash
350
+ )
351
+ notFound();
352
+ if (
353
+ grantAccepted &&
354
+ !verifyShareAttachmentAccessToken(
355
+ input.accessToken!,
356
+ final,
357
+ dependencies.namespace,
358
+ ctx.sourceIp,
359
+ dependencies.now()
360
+ )
361
+ ) {
362
+ throw new TRPCError({
363
+ code: 'UNAUTHORIZED',
364
+ message: 'share-link passcode required',
365
+ });
366
+ }
367
+ const envelope = ShareLinkAttachmentChunkEnvelopeValidator.safeParse(
368
+ fetched.value.envelope
369
+ );
370
+ if (!envelope.success) notFound();
371
+ return {
372
+ id: input.id,
373
+ contentVersion: input.contentVersion,
374
+ attachmentId: input.attachmentId,
375
+ chunkIndex: input.chunkIndex,
376
+ envelope: envelope.data,
377
+ ...(final.passcodeHash
378
+ ? {
379
+ // Renew only after the chunk and its current policy pass every check.
380
+ accessToken: mintShareAttachmentAccessToken(
381
+ final,
382
+ dependencies.namespace,
383
+ ctx.sourceIp,
384
+ dependencies.now()
385
+ ),
386
+ }
387
+ : {}),
388
+ };
389
+ }),
222
390
  resolve: openRouteWithoutInputCapture
223
391
  .meta({
224
392
  openapi: openapi('POST', PUBLIC_RESOLVE_PATH, 'Resolve public share metadata'),
@@ -735,6 +903,7 @@ const buildProductionDependencies = async (
735
903
  return {
736
904
  namespace: config.namespace,
737
905
  repository: {
906
+ fetchAttachmentChunk: input => client.get(input),
738
907
  getShareLink: input => getShareLink(input),
739
908
  fetchContent: async input => {
740
909
  const result = await coordinator.fetchShareContent(input.shareId, {
@@ -11,6 +11,10 @@ import {
11
11
  ShareLinkOperationKeyInputValidator,
12
12
  ShareOwnerRecoveryValidator,
13
13
  UpdateShareLinkInputValidator,
14
+ PutShareLinkAttachmentChunkInputValidator,
15
+ PutShareLinkAttachmentChunkOutputValidator,
16
+ DeleteShareLinkAttachmentChunksInputValidator,
17
+ DeleteShareLinkAttachmentChunksOutputValidator,
14
18
  } from '@learncard/types';
15
19
 
16
20
  import { t, profileRouteWithoutInputCapture } from '@routes';
@@ -37,6 +41,7 @@ import type {
37
41
  ShareLinkCoordinatorErrorCode,
38
42
  } from '@helpers/share-link-coordinator';
39
43
  import { decodeShareLinkListCursor } from '@accesslayer/share-link/list';
44
+ import type { ShareLinkAttachmentOwnerApi } from '@helpers/share-link-attachment';
40
45
  import type {
41
46
  ListShareLinksInput,
42
47
  ListShareLinksResult,
@@ -57,6 +62,8 @@ export type ShareLinkRouterDependencies = {
57
62
  readonly namespace: string;
58
63
  readonly coordinator: ShareLinkCoordinator;
59
64
  readonly recovery: RecoveryRunnerDependencies;
65
+ readonly attachments?: ShareLinkAttachmentOwnerApi;
66
+ readonly enforceAttachmentWriteRateLimit?: (ownerProfileId: string) => Promise<void>;
60
67
  /**
61
68
  * Bounded, namespace+owner-scoped keyset listing over the immutable
62
69
  * `(createdAt, id)` key. Receives only trusted scope; the route decodes and
@@ -206,6 +213,56 @@ export const createShareLinksRouter = (
206
213
  };
207
214
 
208
215
  return t.router({
216
+ putAttachmentChunk: profileRouteWithoutInputCapture
217
+ .meta({
218
+ openapi: openapi(
219
+ 'POST',
220
+ '/share-links/attachment-chunk/put',
221
+ 'Stage encrypted share attachment chunk'
222
+ ),
223
+ requiredScope: AUTH_GRANT_SHARE_LINKS_WRITE_SCOPE,
224
+ })
225
+ .input(PutShareLinkAttachmentChunkInputValidator)
226
+ .output(PutShareLinkAttachmentChunkOutputValidator)
227
+ .mutation(async ({ ctx, input }) => {
228
+ const dependencies = await resolve();
229
+ if (!dependencies.attachments) notFound();
230
+ await (
231
+ dependencies.enforceAttachmentWriteRateLimit ??
232
+ dependencies.enforceOwnerWriteRateLimit
233
+ )(ctx.user.profile.profileId);
234
+ return runCoordinated(() =>
235
+ dependencies.attachments!.put(input, {
236
+ namespace: dependencies.namespace,
237
+ ownerProfileId: ctx.user.profile.profileId,
238
+ })
239
+ );
240
+ }),
241
+ deleteAttachmentChunks: profileRouteWithoutInputCapture
242
+ .meta({
243
+ openapi: openapi(
244
+ 'POST',
245
+ '/share-links/attachment-chunks/delete',
246
+ 'Delete unreferenced encrypted attachment'
247
+ ),
248
+ requiredScope: AUTH_GRANT_SHARE_LINKS_WRITE_SCOPE,
249
+ })
250
+ .input(DeleteShareLinkAttachmentChunksInputValidator)
251
+ .output(DeleteShareLinkAttachmentChunksOutputValidator)
252
+ .mutation(async ({ ctx, input }) => {
253
+ const dependencies = await resolve();
254
+ if (!dependencies.attachments) notFound();
255
+ await (
256
+ dependencies.enforceAttachmentWriteRateLimit ??
257
+ dependencies.enforceOwnerWriteRateLimit
258
+ )(ctx.user.profile.profileId);
259
+ return runCoordinated(() =>
260
+ dependencies.attachments!.delete(input, {
261
+ namespace: dependencies.namespace,
262
+ ownerProfileId: ctx.user.profile.profileId,
263
+ })
264
+ );
265
+ }),
209
266
  create: profileRouteWithoutInputCapture
210
267
  .meta({
211
268
  openapi: openapi('POST', '/share-links/create', 'Create an owner share link'),
@@ -629,6 +686,18 @@ const buildProductionDependencies = async (
629
686
  namespace: config.namespace,
630
687
  coordinator,
631
688
  recovery,
689
+ attachments: (
690
+ await import('@helpers/share-link-attachment')
691
+ ).createShareLinkAttachmentOwnerApi(client),
692
+ enforceAttachmentWriteRateLimit: ownerProfileId =>
693
+ enforceRateLimits([
694
+ {
695
+ key: `share-link-attachment-write:${config.namespace}:${ownerProfileId}`,
696
+ limit: 120,
697
+ windowSeconds: 60 * 60,
698
+ description: 'owner share-link attachment writes',
699
+ },
700
+ ]),
632
701
  listShareLinks: listRepository.listShareLinks,
633
702
  enforceOwnerWriteRateLimit: ownerProfileId =>
634
703
  enforceOwnerShareWriteRateLimit(config.namespace, ownerProfileId),
@@ -409,6 +409,7 @@ async function handlePresentationForClaim(
409
409
  // Inject OBv3 skill alignments based on boost's framework/skills
410
410
  await injectObv3AlignmentsIntoCredentialForBoost(boostCredential, boost, domain);
411
411
 
412
+ // encrypt=false on purpose: interoperable claim wallets may not decrypt LearnCard JWEs. See LC-2201.
412
413
  const vc = (
413
414
  await issueCredentialWithSigningAuthority(
414
415
  { type: 'profile', profile: saOwnerProfile },
@@ -679,14 +680,14 @@ async function handleInboxClaimPresentation(
679
680
  // Set issuer from signing authority
680
681
  unsignedCredential.issuer = signingAuthorityForUser.relationship.did;
681
682
 
682
- // Sign the credential
683
+ // encrypt=false on purpose: interoperable inbox claim wallets may not decrypt LearnCard JWEs. See LC-2201.
683
684
  finalCredential = (
684
685
  await issueCredentialWithSigningAuthority(
685
686
  { type: 'profile', profile: issuerProfile },
686
687
  unsignedCredential,
687
688
  signingAuthorityForUser,
688
689
  ctx.domain,
689
- false // don't encrypt
690
+ false
690
691
  )
691
692
  ).credential as VC;
692
693
  }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ AppManifestValidator,
5
+ AppManifestVersionValidator,
6
+ AppManifestDiffValidator,
7
+ } from '@learncard/types';
8
+
9
+ export const AppManifestTypeValidator = AppManifestValidator;
10
+ export type AppManifestType = z.infer<typeof AppManifestTypeValidator>;
11
+
12
+ export const AppManifestVersionTypeValidator = AppManifestVersionValidator;
13
+ export type AppManifestVersionType = z.infer<typeof AppManifestVersionTypeValidator>;
14
+
15
+ export const FlatAppManifestVersionValidator = z.object({
16
+ id: z.string(),
17
+ version: z.number().int().min(1),
18
+ manifestHash: z.string(),
19
+ manifestJson: z.string(),
20
+ status: z.enum(['draft', 'active', 'superseded']),
21
+ createdAt: z.string(),
22
+ activatedAt: z.string().optional(),
23
+ });
24
+
25
+ export type FlatAppManifestVersionType = z.infer<typeof FlatAppManifestVersionValidator>;
26
+
27
+ export const AppManifestDiffTypeValidator = AppManifestDiffValidator;
28
+ export type AppManifestDiffType = z.infer<typeof AppManifestDiffTypeValidator>;
@@ -19,6 +19,16 @@ export type PromotionLevelEnum = z.infer<typeof PromotionLevel>;
19
19
  export const AgeRating = z.enum(['4+', '9+', '12+', '17+']);
20
20
  export type AgeRatingEnum = z.infer<typeof AgeRating>;
21
21
 
22
+ export const PendingListingUpdateStatus = z.enum(['DRAFT', 'PENDING_REVIEW']);
23
+ export type PendingListingUpdateStatusEnum = z.infer<typeof PendingListingUpdateStatus>;
24
+
25
+ /** Changes to a LISTED app held back until review, stored as listing storage fields. */
26
+ export const StoredPendingListingUpdateValidator = z.object({
27
+ changes: z.record(z.string(), z.unknown()).default({}),
28
+ manifestVersion: z.number().int().min(1).optional(),
29
+ });
30
+ export type StoredPendingListingUpdate = z.infer<typeof StoredPendingListingUpdateValidator>;
31
+
22
32
  export const AppStoreListingValidator = z.object({
23
33
  listing_id: z.string(),
24
34
  slug: z.string().optional(),
@@ -43,6 +53,9 @@ export const AppStoreListingValidator = z.object({
43
53
  age_rating: AgeRating.optional(),
44
54
  submitted_at: z.string().optional(),
45
55
  contact_email: z.string().email().optional(),
56
+ pending_update_json: z.string().optional(),
57
+ pending_update_status: PendingListingUpdateStatus.optional(),
58
+ pending_update_submitted_at: z.string().optional(),
46
59
  });
47
60
  export type AppStoreListingType = z.infer<typeof AppStoreListingValidator>;
48
61
 
@@ -4,6 +4,7 @@ import {
4
4
  ConsentFlowTermsStatusValidator,
5
5
  ConsentFlowTransactionActionValidator,
6
6
  ConsentFlowGuardianApprovalValidator,
7
+ ConsentFlowReferralValidator,
7
8
  } from '@learncard/types';
8
9
  import { z } from 'zod';
9
10
 
@@ -15,6 +16,10 @@ type FlatGuardianApproval = {
15
16
  ]?: string;
16
17
  };
17
18
 
19
+ type FlatReferral = {
20
+ [Key in keyof z.infer<typeof ConsentFlowReferralValidator> as `referral.${Key}`]?: string;
21
+ };
22
+
18
23
  type FlatConsentTerms = Partial<Record<`terms.${string}`, string | boolean | string[]>>;
19
24
 
20
25
  export const DbContractValidator = z.object({
@@ -31,6 +36,8 @@ export const DbContractValidator = z.object({
31
36
  createdAt: z.string(),
32
37
  updatedAt: z.string(),
33
38
  expiresAt: z.string().optional(),
39
+ audienceVersion: z.coerce.number().int().nonnegative().optional(),
40
+ hasConsented: z.boolean().optional(),
34
41
  });
35
42
 
36
43
  export type DbContractType = z.infer<typeof DbContractValidator>;
@@ -41,22 +48,35 @@ export const DbTransactionValidator = z.object({
41
48
  action: ConsentFlowTransactionActionValidator,
42
49
  terms: ConsentFlowTermsValidator,
43
50
  guardianApproval: ConsentFlowGuardianApprovalValidator.optional(),
51
+ referral: ConsentFlowReferralValidator.optional(),
44
52
  date: z.string(),
45
53
  expiresAt: z.string().optional(),
46
54
  oneTime: z.boolean().optional(),
47
55
  });
48
56
 
49
57
  export type DbTransactionType = z.infer<typeof DbTransactionValidator>;
50
- export type FlatDbTransactionType = Omit<DbTransactionType, 'guardianApproval' | 'terms'> &
58
+ export type FlatDbTransactionType = Omit<
59
+ DbTransactionType,
60
+ 'guardianApproval' | 'terms' | 'referral'
61
+ > &
51
62
  FlatConsentTerms &
52
- FlatGuardianApproval;
63
+ FlatGuardianApproval &
64
+ FlatReferral;
53
65
 
54
66
  export const DbTermsValidator = z.object({
67
+ smartResumeFingerprint: z.string().optional(),
68
+ smartResumePublicationStatus: z.enum(['pending', 'sending', 'failed', 'succeeded']).optional(),
69
+ smartResumeLeaseId: z.string().optional(),
70
+ smartResumeLeaseUntil: z.coerce.number().optional(),
71
+ smartResumeMutationVersion: z.coerce.number().optional(),
72
+ smartResumeRedirectUrl: z.string().optional(),
73
+ mutationVersion: z.coerce.number().int().nonnegative().optional(),
55
74
  id: z.string(),
56
75
  status: ConsentFlowTermsStatusValidator,
57
76
  // Neo4j has no properties to store for entirely empty permission objects.
58
77
  terms: ConsentFlowTermsValidator.prefault({}),
59
78
  guardianApproval: ConsentFlowGuardianApprovalValidator.optional(),
79
+ referral: ConsentFlowReferralValidator.optional(),
60
80
  createdAt: z.string().optional(),
61
81
  updatedAt: z.string().optional(),
62
82
  expiresAt: z.string().optional(),
@@ -64,6 +84,7 @@ export const DbTermsValidator = z.object({
64
84
  });
65
85
 
66
86
  export type DbTermsType = z.infer<typeof DbTermsValidator>;
67
- export type FlatDbTermsType = Omit<DbTermsType, 'guardianApproval' | 'terms'> &
87
+ export type FlatDbTermsType = Omit<DbTermsType, 'guardianApproval' | 'terms' | 'referral'> &
68
88
  FlatConsentTerms &
69
- FlatGuardianApproval;
89
+ FlatGuardianApproval &
90
+ FlatReferral;