@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
@@ -0,0 +1,440 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ import { computeShareContentRequestBodyHash } from './canonical';
4
+ import {
5
+ parseShareContentContentProjection,
6
+ parseShareContentDeleteValue,
7
+ parseShareContentPutValue,
8
+ parseShareContentRecoveryProjection,
9
+ parseShareContentResponseEnvelope,
10
+ parseShareContentStatValue,
11
+ } from './responses';
12
+ import {
13
+ MAX_SHARE_CONTENT_RETRY_BACKOFF_MS,
14
+ MAX_SHARE_CONTENT_TOKEN_TTL_SECONDS,
15
+ SHARE_CONTENT_AUTH_PURPOSE,
16
+ isTransientShareContentError,
17
+ type ShareContentAuthorizationClaims,
18
+ type ShareContentClient,
19
+ type ShareContentClientConfig,
20
+ type ShareContentClientErrorCode,
21
+ type ShareContentClientResult,
22
+ type ShareContentContentProjection,
23
+ type ShareContentDeleteValue,
24
+ type ShareContentEnvelope,
25
+ type ShareContentOperation,
26
+ type ShareContentPutRequest,
27
+ type ShareContentPutValue,
28
+ type ShareContentRecoveryProjection,
29
+ type ShareContentStatValue,
30
+ type ShareContentTuple,
31
+ } from './types';
32
+
33
+ const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
34
+
35
+ const isOpaqueIdentifier = (value: unknown): value is string =>
36
+ typeof value === 'string' &&
37
+ value.length >= 1 &&
38
+ value.length <= 128 &&
39
+ OPAQUE_IDENTIFIER_RE.test(value);
40
+
41
+ const isSafeVersion = (value: unknown): value is number =>
42
+ typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 && value <= 2 ** 31 - 1;
43
+
44
+ const isValidTuple = (tuple: ShareContentTuple): boolean =>
45
+ isOpaqueIdentifier(tuple.namespace) &&
46
+ isOpaqueIdentifier(tuple.ownerProfileId) &&
47
+ isOpaqueIdentifier(tuple.shareId) &&
48
+ isSafeVersion(tuple.contentVersion) &&
49
+ isOpaqueIdentifier(tuple.objectId) &&
50
+ isOpaqueIdentifier(tuple.operationId);
51
+
52
+ const isValidEnvelope = (envelope: unknown): envelope is ShareContentEnvelope => {
53
+ if (typeof envelope !== 'object' || envelope === null || Array.isArray(envelope)) return false;
54
+
55
+ const candidate = envelope as Record<string, unknown>;
56
+
57
+ return (
58
+ candidate.v === 1 &&
59
+ candidate.alg === 'A256GCM' &&
60
+ typeof candidate.iv === 'string' &&
61
+ candidate.iv.length > 0 &&
62
+ typeof candidate.ct === 'string' &&
63
+ candidate.ct.length > 0
64
+ );
65
+ };
66
+
67
+ /** Fresh CSPRNG nonce for every transport attempt (never reused across retries). */
68
+ const generateJti = (): string => randomBytes(24).toString('base64url');
69
+
70
+ const buildRequestBody = (
71
+ request: ShareContentPutRequest | ShareContentTuple
72
+ ): Record<string, unknown> => {
73
+ const body: Record<string, unknown> = {
74
+ namespace: request.namespace,
75
+ ownerProfileId: request.ownerProfileId,
76
+ shareId: request.shareId,
77
+ contentVersion: request.contentVersion,
78
+ objectId: request.objectId,
79
+ operationId: request.operationId,
80
+ };
81
+
82
+ if ('envelope' in request && 'ownerEncryptedRecovery' in request) {
83
+ body.envelope = request.envelope;
84
+ body.ownerEncryptedRecovery = request.ownerEncryptedRecovery;
85
+ }
86
+
87
+ return body;
88
+ };
89
+
90
+ const buildUrl = (config: ShareContentClientConfig, op: ShareContentOperation): string =>
91
+ `${config.origin}/internal/share-content/${op}`;
92
+
93
+ const mapStatusToError = (status: number): ShareContentClientErrorCode => {
94
+ switch (status) {
95
+ case 400:
96
+ return 'INVALID_INPUT';
97
+ case 401:
98
+ return 'UNAUTHORIZED';
99
+ case 404:
100
+ return 'NOT_FOUND';
101
+ case 409:
102
+ return 'CONFLICT';
103
+ case 413:
104
+ return 'PAYLOAD_TOO_LARGE';
105
+ case 503:
106
+ return 'UNAVAILABLE';
107
+ default:
108
+ return 'UNEXPECTED_STATUS';
109
+ }
110
+ };
111
+
112
+ const responseTupleMatches = (
113
+ value: {
114
+ namespace: string;
115
+ ownerProfileId: string;
116
+ shareId: string;
117
+ contentVersion: number;
118
+ objectId: string;
119
+ operationId: string;
120
+ },
121
+ tuple: ShareContentTuple
122
+ ): boolean =>
123
+ value.namespace === tuple.namespace &&
124
+ value.ownerProfileId === tuple.ownerProfileId &&
125
+ value.shareId === tuple.shareId &&
126
+ value.contentVersion === tuple.contentVersion &&
127
+ value.objectId === tuple.objectId &&
128
+ value.operationId === tuple.operationId;
129
+
130
+ /** Decode the JOSE header and require it binds exactly the configured signer. */
131
+ const validateSignedTokenHeader = (token: string, signerDid: string): boolean => {
132
+ if (typeof token !== 'string') return false;
133
+
134
+ const parts = token.split('.');
135
+ if (parts.length !== 3) return false;
136
+
137
+ const [headerPart, payloadPart, signaturePart] = parts;
138
+ if (!headerPart || !payloadPart || !signaturePart) return false;
139
+
140
+ let header: unknown;
141
+
142
+ try {
143
+ header = JSON.parse(Buffer.from(headerPart, 'base64url').toString('utf8'));
144
+ } catch {
145
+ return false;
146
+ }
147
+
148
+ if (typeof header !== 'object' || header === null) return false;
149
+
150
+ const candidate = header as Record<string, unknown>;
151
+
152
+ return (
153
+ candidate.alg === 'EdDSA' &&
154
+ typeof candidate.kid === 'string' &&
155
+ candidate.kid.startsWith(`${signerDid}#`)
156
+ );
157
+ };
158
+
159
+ export const createShareContentClient = (config: ShareContentClientConfig): ShareContentClient => {
160
+ const readBoundedBody = async (
161
+ response: Response,
162
+ maxBytes: number
163
+ ): Promise<{ ok: true; text: string } | { ok: false; error: ShareContentClientErrorCode }> => {
164
+ const body = response.body;
165
+
166
+ if (!body || typeof body.getReader !== 'function') {
167
+ try {
168
+ const text = await response.text();
169
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) {
170
+ return { ok: false, error: 'RESPONSE_TOO_LARGE' };
171
+ }
172
+
173
+ return { ok: true, text };
174
+ } catch {
175
+ return { ok: false, error: 'NETWORK_ERROR' };
176
+ }
177
+ }
178
+
179
+ const reader = body.getReader();
180
+ const chunks: Uint8Array[] = [];
181
+ let total = 0;
182
+
183
+ try {
184
+ for (;;) {
185
+ const { done, value } = await reader.read();
186
+ if (done) break;
187
+ if (!value) continue;
188
+
189
+ total += value.byteLength;
190
+
191
+ if (total > maxBytes) {
192
+ await reader.cancel().catch(() => undefined);
193
+
194
+ return { ok: false, error: 'RESPONSE_TOO_LARGE' };
195
+ }
196
+
197
+ chunks.push(value);
198
+ }
199
+ } catch {
200
+ return { ok: false, error: 'NETWORK_ERROR' };
201
+ }
202
+
203
+ return {
204
+ ok: true,
205
+ text: Buffer.concat(chunks.map(chunk => Buffer.from(chunk))).toString('utf8'),
206
+ };
207
+ };
208
+
209
+ const attemptOnce = async <T>(
210
+ op: ShareContentOperation,
211
+ bodyText: string,
212
+ requestHash: string,
213
+ expectedTuple: ShareContentTuple,
214
+ parseValue: (value: unknown) => T | null,
215
+ selectTuple: (value: T) => ShareContentTuple
216
+ ): Promise<ShareContentClientResult<T>> => {
217
+ const nowSeconds = Math.floor(config.now() / 1000);
218
+ const claims: ShareContentAuthorizationClaims = {
219
+ iss: config.signerDid,
220
+ aud: config.audience,
221
+ purpose: SHARE_CONTENT_AUTH_PURPOSE,
222
+ namespace: expectedTuple.namespace,
223
+ op,
224
+ shareId: expectedTuple.shareId,
225
+ contentVersion: expectedTuple.contentVersion,
226
+ ownerProfileId: expectedTuple.ownerProfileId,
227
+ objectId: expectedTuple.objectId,
228
+ operationId: expectedTuple.operationId,
229
+ requestHash,
230
+ iat: nowSeconds,
231
+ exp: nowSeconds + MAX_SHARE_CONTENT_TOKEN_TTL_SECONDS,
232
+ jti: generateJti(),
233
+ };
234
+
235
+ let token: string;
236
+
237
+ try {
238
+ token = await config.signer(claims);
239
+ } catch {
240
+ return { ok: false, error: 'SIGNING_FAILED' };
241
+ }
242
+
243
+ if (!validateSignedTokenHeader(token, config.signerDid)) {
244
+ return { ok: false, error: 'SIGNING_FAILED' };
245
+ }
246
+
247
+ const controller = new AbortController();
248
+ const timer = setTimeout(() => controller.abort(), config.requestTimeoutMs);
249
+
250
+ try {
251
+ let response: Response;
252
+
253
+ try {
254
+ response = await config.fetchImpl(buildUrl(config, op), {
255
+ method: 'POST',
256
+ headers: {
257
+ authorization: `Bearer ${token}`,
258
+ 'content-type': 'application/json',
259
+ accept: 'application/json',
260
+ 'cache-control': 'no-store',
261
+ },
262
+ body: bodyText,
263
+ redirect: 'manual',
264
+ signal: controller.signal,
265
+ });
266
+ } catch {
267
+ return {
268
+ ok: false,
269
+ error: controller.signal.aborted ? 'TIMEOUT' : 'NETWORK_ERROR',
270
+ };
271
+ }
272
+
273
+ if (response.status >= 300 && response.status < 400) {
274
+ return { ok: false, error: 'REDIRECT_REJECTED' };
275
+ }
276
+
277
+ if (response.status !== 200) {
278
+ // Drain a bounded amount so the socket can be reused; the body is
279
+ // discarded and never surfaces in the returned error.
280
+ await readBoundedBody(response, config.maxResponseBytes).catch(() => undefined);
281
+
282
+ return { ok: false, error: mapStatusToError(response.status) };
283
+ }
284
+
285
+ const read = await readBoundedBody(response, config.maxResponseBytes);
286
+
287
+ if (!read.ok) {
288
+ if (read.error === 'NETWORK_ERROR' && controller.signal.aborted) {
289
+ return { ok: false, error: 'TIMEOUT' };
290
+ }
291
+
292
+ return { ok: false, error: read.error };
293
+ }
294
+
295
+ let parsed: unknown;
296
+
297
+ try {
298
+ parsed = JSON.parse(read.text);
299
+ } catch {
300
+ return { ok: false, error: 'MALFORMED_RESPONSE' };
301
+ }
302
+
303
+ const envelope = parseShareContentResponseEnvelope(parsed);
304
+
305
+ if (!envelope || !envelope.ok) return { ok: false, error: 'MALFORMED_RESPONSE' };
306
+
307
+ const value = parseValue(envelope.value);
308
+
309
+ if (!value) return { ok: false, error: 'MALFORMED_RESPONSE' };
310
+
311
+ if (!responseTupleMatches(selectTuple(value), expectedTuple)) {
312
+ return { ok: false, error: 'RESPONSE_TUPLE_MISMATCH' };
313
+ }
314
+
315
+ return { ok: true, value };
316
+ } finally {
317
+ clearTimeout(timer);
318
+ }
319
+ };
320
+
321
+ const perform = async <T>(
322
+ op: ShareContentOperation,
323
+ body: Record<string, unknown>,
324
+ expectedTuple: ShareContentTuple,
325
+ parseValue: (value: unknown) => T | null,
326
+ selectTuple: (value: T) => ShareContentTuple
327
+ ): Promise<ShareContentClientResult<T>> => {
328
+ if (!config.enabled) return { ok: false, error: 'DISABLED' };
329
+ if (!isValidTuple(expectedTuple)) return { ok: false, error: 'INVALID_INPUT' };
330
+
331
+ let bodyText: string;
332
+
333
+ try {
334
+ bodyText = JSON.stringify(body);
335
+ } catch {
336
+ return { ok: false, error: 'INVALID_INPUT' };
337
+ }
338
+
339
+ if (typeof bodyText !== 'string') return { ok: false, error: 'INVALID_INPUT' };
340
+ if (Buffer.byteLength(bodyText, 'utf8') > config.maxRequestBytes) {
341
+ return { ok: false, error: 'PAYLOAD_TOO_LARGE' };
342
+ }
343
+
344
+ let requestHash: string;
345
+
346
+ try {
347
+ requestHash = computeShareContentRequestBodyHash(body, {
348
+ maxBytes: config.maxRequestBytes,
349
+ });
350
+ } catch {
351
+ return { ok: false, error: 'INVALID_INPUT' };
352
+ }
353
+
354
+ let lastError: ShareContentClientErrorCode = 'NETWORK_ERROR';
355
+
356
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
357
+ const result = await attemptOnce(
358
+ op,
359
+ bodyText,
360
+ requestHash,
361
+ expectedTuple,
362
+ parseValue,
363
+ selectTuple
364
+ );
365
+
366
+ if (result.ok) return result;
367
+
368
+ lastError = result.error;
369
+
370
+ if (!isTransientShareContentError(result.error) || attempt === config.maxAttempts) {
371
+ return result;
372
+ }
373
+
374
+ const backoff = Math.min(
375
+ config.retryBackoffMs * 2 ** (attempt - 1),
376
+ MAX_SHARE_CONTENT_RETRY_BACKOFF_MS
377
+ );
378
+
379
+ if (backoff > 0) await config.sleep(backoff);
380
+ }
381
+
382
+ return { ok: false, error: lastError };
383
+ };
384
+
385
+ return {
386
+ config,
387
+ put: async (request: ShareContentPutRequest) => {
388
+ if (!isValidEnvelope(request.envelope)) return { ok: false, error: 'INVALID_INPUT' };
389
+
390
+ return perform(
391
+ 'put',
392
+ buildRequestBody(request),
393
+ request,
394
+ parseShareContentPutValue,
395
+ value => value.record
396
+ );
397
+ },
398
+ get: async (request: ShareContentTuple) =>
399
+ perform(
400
+ 'get',
401
+ buildRequestBody(request),
402
+ request,
403
+ parseShareContentContentProjection,
404
+ value => value
405
+ ),
406
+ stat: async (request: ShareContentTuple) =>
407
+ perform(
408
+ 'stat',
409
+ buildRequestBody(request),
410
+ request,
411
+ parseShareContentStatValue,
412
+ value => value
413
+ ),
414
+ delete: async (request: ShareContentTuple) =>
415
+ perform(
416
+ 'delete',
417
+ buildRequestBody(request),
418
+ request,
419
+ parseShareContentDeleteValue,
420
+ value => value
421
+ ),
422
+ readRecovery: async (request: ShareContentTuple) =>
423
+ perform(
424
+ 'readRecovery',
425
+ buildRequestBody(request),
426
+ request,
427
+ parseShareContentRecoveryProjection,
428
+ value => value
429
+ ),
430
+ };
431
+ };
432
+
433
+ export type { ShareContentClient, ShareContentClientConfig };
434
+ export type {
435
+ ShareContentContentProjection,
436
+ ShareContentDeleteValue,
437
+ ShareContentPutValue,
438
+ ShareContentRecoveryProjection,
439
+ ShareContentStatValue,
440
+ };
@@ -0,0 +1,189 @@
1
+ import {
2
+ DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES,
3
+ DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES,
4
+ DEFAULT_SHARE_CONTENT_MAX_ATTEMPTS,
5
+ DEFAULT_SHARE_CONTENT_REQUEST_TIMEOUT_MS,
6
+ DEFAULT_SHARE_CONTENT_RETRY_BACKOFF_MS,
7
+ MAX_SHARE_CONTENT_RETRY_BACKOFF_MS,
8
+ ShareContentClientConfigurationError,
9
+ type ShareContentClientConfig,
10
+ type ShareContentTokenSigner,
11
+ } from './types';
12
+
13
+ /**
14
+ * Explicit, fail-closed client configuration. Nothing here reads environment
15
+ * variables or secrets: the wiring batch constructs it deliberately.
16
+ *
17
+ * A missing/invalid origin, namespace, audience, signer DID or signer function
18
+ * collapses to the disabled configuration, so a partially configured deployment
19
+ * never performs a signed request. There is deliberately no offline or unsigned
20
+ * bypass.
21
+ */
22
+
23
+ const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
24
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
25
+
26
+ const disabledSigner: ShareContentTokenSigner = async () => {
27
+ throw new ShareContentClientConfigurationError('share-content client is disabled');
28
+ };
29
+
30
+ export const DISABLED_SHARE_CONTENT_CLIENT_CONFIG: ShareContentClientConfig = Object.freeze({
31
+ enabled: false,
32
+ origin: '',
33
+ namespace: '',
34
+ audience: '',
35
+ signerDid: '',
36
+ allowInsecureLoopback: false,
37
+ signer: disabledSigner,
38
+ requestTimeoutMs: DEFAULT_SHARE_CONTENT_REQUEST_TIMEOUT_MS,
39
+ maxAttempts: 1,
40
+ retryBackoffMs: 0,
41
+ maxRequestBytes: DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES,
42
+ maxResponseBytes: DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES,
43
+ now: () => Date.now(),
44
+ sleep: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)),
45
+ fetchImpl: ((...args: Parameters<typeof fetch>) => fetch(...args)) as typeof fetch,
46
+ }) as ShareContentClientConfig;
47
+
48
+ const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
49
+ typeof value === 'string' &&
50
+ value.length >= 1 &&
51
+ value.length <= maxLength &&
52
+ OPAQUE_IDENTIFIER_RE.test(value);
53
+
54
+ const isBoundedInteger = (value: unknown, minimum: number, maximum: number): value is number =>
55
+ typeof value === 'number' &&
56
+ Number.isSafeInteger(value) &&
57
+ value >= minimum &&
58
+ value <= maximum;
59
+
60
+ /**
61
+ * Validate a configured origin. Only an `https:` origin is trusted; a loopback
62
+ * `http:` origin is accepted only when `allowInsecureLoopback` is explicitly set
63
+ * for local tests/development, and the cryptographic auth path is unchanged.
64
+ */
65
+ export const validateShareContentOrigin = (
66
+ origin: unknown,
67
+ allowInsecureLoopback: boolean
68
+ ): { ok: true; origin: string } | { ok: false; reason: string } => {
69
+ if (typeof origin !== 'string' || origin.trim().length === 0) {
70
+ return { ok: false, reason: 'origin must be a non-empty string' };
71
+ }
72
+
73
+ let url: URL;
74
+
75
+ try {
76
+ url = new URL(origin);
77
+ } catch {
78
+ return { ok: false, reason: 'origin must be a valid absolute URL' };
79
+ }
80
+
81
+ if (url.username || url.password) {
82
+ return { ok: false, reason: 'origin must not contain userinfo' };
83
+ }
84
+ if (url.search || url.hash) {
85
+ return { ok: false, reason: 'origin must not contain a query or fragment' };
86
+ }
87
+ if (url.pathname !== '/' && url.pathname !== '') {
88
+ return { ok: false, reason: 'origin must not contain a path' };
89
+ }
90
+
91
+ if (url.protocol === 'https:') {
92
+ return { ok: true, origin: `${url.protocol}//${url.host}` };
93
+ }
94
+
95
+ if (url.protocol === 'http:') {
96
+ if (!allowInsecureLoopback) {
97
+ return { ok: false, reason: 'insecure http origin requires allowInsecureLoopback' };
98
+ }
99
+ if (!LOOPBACK_HOSTS.has(url.hostname)) {
100
+ return { ok: false, reason: 'insecure http origin must be loopback' };
101
+ }
102
+
103
+ return { ok: true, origin: `${url.protocol}//${url.host}` };
104
+ }
105
+
106
+ return { ok: false, reason: 'origin must use https (or loopback http for local tests)' };
107
+ };
108
+
109
+ /** Build a validated, frozen client configuration, or the disabled config. */
110
+ export const resolveShareContentClientConfig = (raw: unknown): ShareContentClientConfig => {
111
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
112
+ return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
113
+ }
114
+
115
+ const source = raw as Record<string, unknown>;
116
+
117
+ if (source.enabled !== true) return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
118
+
119
+ const allowInsecureLoopback = source.allowInsecureLoopback === true;
120
+ const originResult = validateShareContentOrigin(source.origin, allowInsecureLoopback);
121
+
122
+ if (!originResult.ok) return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
123
+
124
+ if (!isOpaqueIdentifier(source.namespace)) return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
125
+ if (typeof source.audience !== 'string' || source.audience.trim().length === 0) {
126
+ return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
127
+ }
128
+ if (typeof source.signerDid !== 'string' || !source.signerDid.startsWith('did:')) {
129
+ return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
130
+ }
131
+ if (typeof source.signer !== 'function') return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
132
+
133
+ const signer = source.signer as ShareContentTokenSigner;
134
+ const requestTimeoutMs = isBoundedInteger(source.requestTimeoutMs, 1, 120_000)
135
+ ? source.requestTimeoutMs
136
+ : DEFAULT_SHARE_CONTENT_REQUEST_TIMEOUT_MS;
137
+ const maxAttempts = isBoundedInteger(source.maxAttempts, 1, 10)
138
+ ? source.maxAttempts
139
+ : DEFAULT_SHARE_CONTENT_MAX_ATTEMPTS;
140
+ const retryBackoffMs = isBoundedInteger(
141
+ source.retryBackoffMs,
142
+ 0,
143
+ MAX_SHARE_CONTENT_RETRY_BACKOFF_MS
144
+ )
145
+ ? source.retryBackoffMs
146
+ : DEFAULT_SHARE_CONTENT_RETRY_BACKOFF_MS;
147
+ const maxRequestBytes = isBoundedInteger(
148
+ source.maxRequestBytes,
149
+ 1024,
150
+ DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES
151
+ )
152
+ ? source.maxRequestBytes
153
+ : DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES;
154
+ const maxResponseBytes = isBoundedInteger(
155
+ source.maxResponseBytes,
156
+ 1024,
157
+ DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES
158
+ )
159
+ ? source.maxResponseBytes
160
+ : DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES;
161
+
162
+ const now = typeof source.now === 'function' ? (source.now as () => number) : () => Date.now();
163
+ const sleep =
164
+ typeof source.sleep === 'function'
165
+ ? (source.sleep as (ms: number) => Promise<void>)
166
+ : (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
167
+ const fetchImpl =
168
+ typeof source.fetchImpl === 'function'
169
+ ? (source.fetchImpl as typeof fetch)
170
+ : (((...args: Parameters<typeof fetch>) => fetch(...args)) as typeof fetch);
171
+
172
+ return Object.freeze({
173
+ enabled: true,
174
+ origin: originResult.origin,
175
+ namespace: source.namespace,
176
+ audience: source.audience,
177
+ signerDid: source.signerDid,
178
+ allowInsecureLoopback,
179
+ signer,
180
+ requestTimeoutMs,
181
+ maxAttempts,
182
+ retryBackoffMs,
183
+ maxRequestBytes,
184
+ maxResponseBytes,
185
+ now,
186
+ sleep,
187
+ fetchImpl,
188
+ }) as ShareContentClientConfig;
189
+ };
@@ -0,0 +1,27 @@
1
+ export * from './types';
2
+ export {
3
+ canonicalizeShareContentRequestBody,
4
+ computeShareContentRequestBodyHash,
5
+ isSha256Hex,
6
+ ShareContentCanonicalizationError,
7
+ } from './canonical';
8
+ export {
9
+ DISABLED_SHARE_CONTENT_CLIENT_CONFIG,
10
+ resolveShareContentClientConfig,
11
+ validateShareContentOrigin,
12
+ } from './config';
13
+ export { createShareContentClient } from './client';
14
+ export {
15
+ parseShareContentActiveSummary,
16
+ parseShareContentContentProjection,
17
+ parseShareContentDeleteValue,
18
+ parseShareContentPutValue,
19
+ parseShareContentRecoveryProjection,
20
+ parseShareContentResponseEnvelope,
21
+ parseShareContentStatValue,
22
+ parseShareContentTombstoneSummary,
23
+ } from './responses';
24
+
25
+ // The runtime did:web signing adapter deliberately lives outside this barrel:
26
+ // import `@helpers/share-content-client/adapters` from the wiring layer so unit
27
+ // tests of the transport never load the LearnCard/environment graph.