@ibgib/node-gib 0.0.14 → 0.0.17

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 (218) hide show
  1. package/data/identity_index/email/hashes/32/13/49/e64f32730f01a61ada211f29eaf0a210fc2386b668ad04d7e8a9b16766.json +4 -0
  2. package/dist/email/email-helpers.d.mts.map +1 -1
  3. package/dist/email/email-helpers.mjs.map +1 -1
  4. package/dist/email/index.d.mts +3 -0
  5. package/dist/email/index.d.mts.map +1 -1
  6. package/dist/email/index.mjs +3 -0
  7. package/dist/email/index.mjs.map +1 -1
  8. package/dist/email/verify/email-verify-constants.d.mts +3 -0
  9. package/dist/email/verify/email-verify-constants.d.mts.map +1 -0
  10. package/dist/email/verify/email-verify-constants.mjs +3 -0
  11. package/dist/email/verify/email-verify-constants.mjs.map +1 -0
  12. package/dist/email/verify/email-verify-helpers.d.mts +107 -0
  13. package/dist/email/verify/email-verify-helpers.d.mts.map +1 -0
  14. package/dist/email/verify/email-verify-helpers.mjs +357 -0
  15. package/dist/email/verify/email-verify-helpers.mjs.map +1 -0
  16. package/dist/email/verify/email-verify-types.d.mts +41 -0
  17. package/dist/email/verify/email-verify-types.d.mts.map +1 -0
  18. package/dist/email/verify/email-verify-types.mjs +2 -0
  19. package/dist/email/verify/email-verify-types.mjs.map +1 -0
  20. package/dist/identity/sso/custodian-auth-helpers.d.mts +45 -0
  21. package/dist/identity/sso/custodian-auth-helpers.d.mts.map +1 -0
  22. package/dist/identity/sso/custodian-auth-helpers.mjs +108 -0
  23. package/dist/identity/sso/custodian-auth-helpers.mjs.map +1 -0
  24. package/dist/identity/sso/sso-config-helper.d.mts +7 -0
  25. package/dist/identity/sso/sso-config-helper.d.mts.map +1 -0
  26. package/dist/identity/sso/sso-config-helper.mjs +42 -0
  27. package/dist/identity/sso/sso-config-helper.mjs.map +1 -0
  28. package/dist/identity/sso/sso-custodian-service.d.mts +139 -0
  29. package/dist/identity/sso/sso-custodian-service.d.mts.map +1 -0
  30. package/dist/identity/sso/sso-custodian-service.mjs +471 -0
  31. package/dist/identity/sso/sso-custodian-service.mjs.map +1 -0
  32. package/dist/identity/sso/sso-types.d.mts +71 -0
  33. package/dist/identity/sso/sso-types.d.mts.map +1 -0
  34. package/dist/identity/sso/sso-types.mjs +7 -0
  35. package/dist/identity/sso/sso-types.mjs.map +1 -0
  36. package/dist/index.d.mts +5 -1
  37. package/dist/index.d.mts.map +1 -1
  38. package/dist/index.mjs +5 -1
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/serve-gib/handlers/api/email/email-outbound.handler.d.mts.map +1 -1
  41. package/dist/serve-gib/handlers/api/email/email-outbound.handler.mjs +9 -2
  42. package/dist/serve-gib/handlers/api/email/email-outbound.handler.mjs.map +1 -1
  43. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.d.mts +29 -0
  44. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.d.mts.map +1 -0
  45. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs +149 -0
  46. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs.map +1 -0
  47. package/dist/serve-gib/handlers/api/email/email-verify-code-onetime.handler.d.mts +24 -0
  48. package/dist/serve-gib/handlers/api/email/email-verify-code-onetime.handler.d.mts.map +1 -0
  49. package/dist/serve-gib/handlers/api/email/email-verify-code-onetime.handler.mjs +212 -0
  50. package/dist/serve-gib/handlers/api/email/email-verify-code-onetime.handler.mjs.map +1 -0
  51. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.d.mts +37 -0
  52. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.d.mts.map +1 -0
  53. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mjs +192 -0
  54. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mjs.map +1 -0
  55. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.d.mts +33 -0
  56. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.d.mts.map +1 -0
  57. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs +173 -0
  58. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs.map +1 -0
  59. package/dist/serve-gib/handlers/api/keystone/keystone-check-email.handler.d.mts.map +1 -1
  60. package/dist/serve-gib/handlers/api/keystone/keystone-check-email.handler.mjs +2 -3
  61. package/dist/serve-gib/handlers/api/keystone/keystone-check-email.handler.mjs.map +1 -1
  62. package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.d.mts.map +1 -1
  63. package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.mjs +38 -3
  64. package/dist/serve-gib/handlers/api/keystone/keystone-evolve.handler.mjs.map +1 -1
  65. package/dist/serve-gib/handlers/api/keystone/keystone-genesis.handler.d.mts.map +1 -1
  66. package/dist/serve-gib/handlers/api/keystone/keystone-genesis.handler.mjs +54 -24
  67. package/dist/serve-gib/handlers/api/keystone/keystone-genesis.handler.mjs.map +1 -1
  68. package/dist/serve-gib/handlers/api/keystone/keystone-lookup.handler.d.mts.map +1 -1
  69. package/dist/serve-gib/handlers/api/keystone/keystone-lookup.handler.mjs +13 -13
  70. package/dist/serve-gib/handlers/api/keystone/keystone-lookup.handler.mjs.map +1 -1
  71. package/dist/serve-gib/handlers/api/keystone/keystone-post.handler.mjs +1 -1
  72. package/dist/serve-gib/handlers/api/keystone/keystone-post.handler.mjs.map +1 -1
  73. package/dist/serve-gib/handlers/api/keystone/sso-config.handler.mjs +3 -3
  74. package/dist/serve-gib/handlers/api/keystone/sso-config.handler.mjs.map +1 -1
  75. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.d.mts.map +1 -1
  76. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs +28 -15
  77. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs.map +1 -1
  78. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.d.mts +29 -15
  79. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.d.mts.map +1 -1
  80. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs +105 -108
  81. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs.map +1 -1
  82. package/dist/serve-gib/identity-index/identity-index-constants.d.mts +3 -2
  83. package/dist/serve-gib/identity-index/identity-index-constants.d.mts.map +1 -1
  84. package/dist/serve-gib/identity-index/identity-index-constants.mjs +4 -2
  85. package/dist/serve-gib/identity-index/identity-index-constants.mjs.map +1 -1
  86. package/dist/serve-gib/identity-index/identity-index-helpers.d.mts.map +1 -1
  87. package/dist/serve-gib/identity-index/identity-index-helpers.mjs +28 -11
  88. package/dist/serve-gib/identity-index/identity-index-helpers.mjs.map +1 -1
  89. package/dist/serve-gib/identity-index/identity-index-service.d.mts +20 -3
  90. package/dist/serve-gib/identity-index/identity-index-service.d.mts.map +1 -1
  91. package/dist/serve-gib/identity-index/identity-index-service.mjs +134 -18
  92. package/dist/serve-gib/identity-index/identity-index-service.mjs.map +1 -1
  93. package/dist/serve-gib/identity-index/identity-index-types.d.mts +4 -2
  94. package/dist/serve-gib/identity-index/identity-index-types.d.mts.map +1 -1
  95. package/dist/serve-gib/index.d.mts +4 -0
  96. package/dist/serve-gib/index.d.mts.map +1 -1
  97. package/dist/serve-gib/index.mjs +12 -0
  98. package/dist/serve-gib/index.mjs.map +1 -1
  99. package/dist/serve-gib/path-constants.d.mts +17 -0
  100. package/dist/serve-gib/path-constants.d.mts.map +1 -1
  101. package/dist/serve-gib/path-constants.mjs +17 -0
  102. package/dist/serve-gib/path-constants.mjs.map +1 -1
  103. package/e2e/identity-custodian-lifecycle.e2e.md +93 -0
  104. package/e2e/identity-sso-lifecycle.e2e.md +132 -0
  105. package/package.json +5 -5
  106. package/src/email/email-helpers.mts +0 -1
  107. package/src/email/index.mts +3 -0
  108. package/src/email/verify/email-verify-constants.mts +3 -0
  109. package/src/email/verify/email-verify-helpers.mts +466 -0
  110. package/src/email/verify/email-verify-types.mts +47 -0
  111. package/src/identity/sso/custodian-auth-helpers.mts +146 -0
  112. package/src/identity/sso/custodian-auth-helpers.respec.mts +97 -0
  113. package/src/identity/sso/sso-config-helper.mts +52 -0
  114. package/src/identity/sso/sso-custodian-service.mts +566 -0
  115. package/src/identity/sso/sso-custodian-service.respec.mts +297 -0
  116. package/src/identity/sso/sso-types.mts +80 -0
  117. package/src/index.mts +14 -0
  118. package/src/serve-gib/handlers/api/email/email-outbound.handler.mts +8 -2
  119. package/src/serve-gib/handlers/api/email/email-verification-code.handler.mts +163 -0
  120. package/src/serve-gib/handlers/api/email/email-verify-code-onetime.handler.mts +231 -0
  121. package/src/serve-gib/handlers/api/email/email-verify.respec.mts +183 -0
  122. package/src/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mts +233 -0
  123. package/src/serve-gib/handlers/api/identity/keystone-custodian-genesis.respec.mts +93 -0
  124. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mts +210 -0
  125. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.respec.mts +112 -0
  126. package/src/serve-gib/handlers/api/keystone/keystone-check-email.handler.mts +2 -4
  127. package/src/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +42 -3
  128. package/src/serve-gib/handlers/api/keystone/keystone-genesis.handler.mts +46 -23
  129. package/src/serve-gib/handlers/api/keystone/keystone-lookup.handler.mts +11 -15
  130. package/src/serve-gib/handlers/api/keystone/keystone-post.handler.mts +1 -1
  131. package/src/serve-gib/handlers/api/keystone/sso-config.handler.mts +3 -3
  132. package/src/serve-gib/handlers/api/keystone/sso-link.handler.mts +29 -17
  133. package/src/serve-gib/handlers/api/keystone/sso-login.handler.mts +132 -125
  134. package/src/serve-gib/identity-index/identity-index-constants.mts +5 -2
  135. package/src/serve-gib/identity-index/identity-index-helpers.mts +26 -12
  136. package/src/serve-gib/identity-index/identity-index-service.mts +133 -16
  137. package/src/serve-gib/identity-index/identity-index-service.respec.mts +1 -1
  138. package/src/serve-gib/identity-index/identity-index-types.mts +4 -2
  139. package/src/serve-gib/index.mts +12 -0
  140. package/src/serve-gib/path-constants.mts +20 -0
  141. package/tsconfig.json +2 -2
  142. package/tsconfig.test.json +1 -1
  143. package/dist/assumptions.respec.d.mts +0 -2
  144. package/dist/assumptions.respec.d.mts.map +0 -1
  145. package/dist/assumptions.respec.mjs +0 -41
  146. package/dist/assumptions.respec.mjs.map +0 -1
  147. package/dist/email/email-service.respec.d.mts +0 -2
  148. package/dist/email/email-service.respec.d.mts.map +0 -1
  149. package/dist/email/email-service.respec.mjs +0 -115
  150. package/dist/email/email-service.respec.mjs.map +0 -1
  151. package/dist/serve-gib/bootstrap-helper.respec.d.mts +0 -2
  152. package/dist/serve-gib/bootstrap-helper.respec.d.mts.map +0 -1
  153. package/dist/serve-gib/bootstrap-helper.respec.mjs +0 -85
  154. package/dist/serve-gib/bootstrap-helper.respec.mjs.map +0 -1
  155. package/dist/serve-gib/handlers/api/health.respec.d.mts +0 -2
  156. package/dist/serve-gib/handlers/api/health.respec.d.mts.map +0 -1
  157. package/dist/serve-gib/handlers/api/health.respec.mjs +0 -47
  158. package/dist/serve-gib/handlers/api/health.respec.mjs.map +0 -1
  159. package/dist/serve-gib/handlers/api/keystone/keystone-get.respec.d.mts +0 -2
  160. package/dist/serve-gib/handlers/api/keystone/keystone-get.respec.d.mts.map +0 -1
  161. package/dist/serve-gib/handlers/api/keystone/keystone-get.respec.mjs +0 -98
  162. package/dist/serve-gib/handlers/api/keystone/keystone-get.respec.mjs.map +0 -1
  163. package/dist/serve-gib/handlers/api/keystone/keystone-post.respec.d.mts +0 -2
  164. package/dist/serve-gib/handlers/api/keystone/keystone-post.respec.d.mts.map +0 -1
  165. package/dist/serve-gib/handlers/api/keystone/keystone-post.respec.mjs +0 -131
  166. package/dist/serve-gib/handlers/api/keystone/keystone-post.respec.mjs.map +0 -1
  167. package/dist/serve-gib/identity-index/identity-index-service.respec.d.mts +0 -2
  168. package/dist/serve-gib/identity-index/identity-index-service.respec.d.mts.map +0 -1
  169. package/dist/serve-gib/identity-index/identity-index-service.respec.mjs +0 -109
  170. package/dist/serve-gib/identity-index/identity-index-service.respec.mjs.map +0 -1
  171. package/dist/serve-gib/keystone-schema-helpers.respec.d.mts +0 -2
  172. package/dist/serve-gib/keystone-schema-helpers.respec.d.mts.map +0 -1
  173. package/dist/serve-gib/keystone-schema-helpers.respec.mjs +0 -68
  174. package/dist/serve-gib/keystone-schema-helpers.respec.mjs.map +0 -1
  175. package/dist/serve-gib/path-helper.respec.d.mts +0 -2
  176. package/dist/serve-gib/path-helper.respec.d.mts.map +0 -1
  177. package/dist/serve-gib/path-helper.respec.mjs +0 -83
  178. package/dist/serve-gib/path-helper.respec.mjs.map +0 -1
  179. package/dist/serve-gib/serve-gib.respec.d.mts +0 -2
  180. package/dist/serve-gib/serve-gib.respec.d.mts.map +0 -1
  181. package/dist/serve-gib/serve-gib.respec.mjs +0 -85
  182. package/dist/serve-gib/serve-gib.respec.mjs.map +0 -1
  183. package/dist/spec-helper.node.respec.d.mts +0 -13
  184. package/dist/spec-helper.node.respec.d.mts.map +0 -1
  185. package/dist/spec-helper.node.respec.mjs +0 -50
  186. package/dist/spec-helper.node.respec.mjs.map +0 -1
  187. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_createAndInit.node-indexed-filesystem-space-v1.respec.d.mts +0 -2
  188. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_createAndInit.node-indexed-filesystem-space-v1.respec.d.mts.map +0 -1
  189. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_createAndInit.node-indexed-filesystem-space-v1.respec.mjs +0 -52
  190. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_createAndInit.node-indexed-filesystem-space-v1.respec.mjs.map +0 -1
  191. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_persistTransformResult.node-indexed-filesystem-space-v1.respec.d.mts +0 -2
  192. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_persistTransformResult.node-indexed-filesystem-space-v1.respec.d.mts.map +0 -1
  193. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_persistTransformResult.node-indexed-filesystem-space-v1.respec.mjs +0 -53
  194. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_persistTransformResult.node-indexed-filesystem-space-v1.respec.mjs.map +0 -1
  195. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_putGetDelete.node-indexed-filesystem-space-v1.respec.d.mts +0 -2
  196. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_putGetDelete.node-indexed-filesystem-space-v1.respec.d.mts.map +0 -1
  197. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_putGetDelete.node-indexed-filesystem-space-v1.respec.mjs +0 -59
  198. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_putGetDelete.node-indexed-filesystem-space-v1.respec.mjs.map +0 -1
  199. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-indexed-filesystem-space-v1.respec.d.mts +0 -2
  200. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-indexed-filesystem-space-v1.respec.d.mts.map +0 -1
  201. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-indexed-filesystem-space-v1.respec.mjs +0 -55
  202. package/dist/witness/space/node-filesystem-space/node-indexed-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-indexed-filesystem-space-v1.respec.mjs.map +0 -1
  203. package/dist/witness/space/node-filesystem-space/respec/testSpace_createAndInit.node-filesystem-space-v1.respec.d.mts +0 -2
  204. package/dist/witness/space/node-filesystem-space/respec/testSpace_createAndInit.node-filesystem-space-v1.respec.d.mts.map +0 -1
  205. package/dist/witness/space/node-filesystem-space/respec/testSpace_createAndInit.node-filesystem-space-v1.respec.mjs +0 -67
  206. package/dist/witness/space/node-filesystem-space/respec/testSpace_createAndInit.node-filesystem-space-v1.respec.mjs.map +0 -1
  207. package/dist/witness/space/node-filesystem-space/respec/testSpace_persistTransformResult.node-filesystem-space-v1.respec.d.mts +0 -2
  208. package/dist/witness/space/node-filesystem-space/respec/testSpace_persistTransformResult.node-filesystem-space-v1.respec.d.mts.map +0 -1
  209. package/dist/witness/space/node-filesystem-space/respec/testSpace_persistTransformResult.node-filesystem-space-v1.respec.mjs +0 -67
  210. package/dist/witness/space/node-filesystem-space/respec/testSpace_persistTransformResult.node-filesystem-space-v1.respec.mjs.map +0 -1
  211. package/dist/witness/space/node-filesystem-space/respec/testSpace_putGetDelete.node-filesystem-space-v1.respec.d.mts +0 -2
  212. package/dist/witness/space/node-filesystem-space/respec/testSpace_putGetDelete.node-filesystem-space-v1.respec.d.mts.map +0 -1
  213. package/dist/witness/space/node-filesystem-space/respec/testSpace_putGetDelete.node-filesystem-space-v1.respec.mjs +0 -68
  214. package/dist/witness/space/node-filesystem-space/respec/testSpace_putGetDelete.node-filesystem-space-v1.respec.mjs.map +0 -1
  215. package/dist/witness/space/node-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-filesystem-space-v1.respec.d.mts +0 -2
  216. package/dist/witness/space/node-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-filesystem-space-v1.respec.d.mts.map +0 -1
  217. package/dist/witness/space/node-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-filesystem-space-v1.respec.mjs +0 -69
  218. package/dist/witness/space/node-filesystem-space/respec/testSpace_registerNewIbGib_GetLatest.node-filesystem-space-v1.respec.mjs.map +0 -1
@@ -20,7 +20,6 @@ export function validateEmailAddress(email: string): boolean {
20
20
  return validateEmail(email);
21
21
  }
22
22
 
23
-
24
23
  /**
25
24
  * Sanitizes header values by stripping carriage return (\r) and newline (\n) characters
26
25
  * to prevent MIME header injection.
@@ -5,6 +5,9 @@
5
5
  export * from './email-constants.mjs';
6
6
  export * from './email-types.mjs';
7
7
  export * from './email-helpers.mjs';
8
+ export * from './verify/email-verify-constants.mjs';
9
+ export * from './verify/email-verify-types.mjs';
10
+ export * from './verify/email-verify-helpers.mjs';
8
11
  export * from './email-sender.mjs';
9
12
  export * from './email-receiver.mjs';
10
13
  export * from './aws/index.mjs';
@@ -0,0 +1,3 @@
1
+
2
+ /** Default TTL for magic link tokens: 15 minutes in ms. */
3
+ export const DEFAULT_MAGIC_LINK_TTL_MS = 15 * 60 * 1000;
@@ -0,0 +1,466 @@
1
+ import { createHmac, timingSafeEqual, randomUUID } from 'node:crypto';
2
+ import * as fs from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { extractErrorMsg, hash } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
6
+ import { EmailSender, OutboundEmail } from '../email-types.mjs';
7
+ import { EmailSenderAws } from '../aws/email-sender-aws.mjs';
8
+ import { EmailSenderMock } from '../mock/email-sender-mock.mjs';
9
+ import { validateEmail } from '../email-helpers.mjs';
10
+ import { normalizeEmail } from '../../serve-gib/identity-index/identity-index-helpers.mjs';
11
+ import { DEFAULT_IDENTITY_INDEX_SALT, DEFAULT_IDENTITY_INDEX_DIR_NAME } from '../../serve-gib/identity-index/identity-index-constants.mjs';
12
+ import { DEFAULT_MAGIC_LINK_TTL_MS } from './email-verify-constants.mjs';
13
+ import { CreateMagicLinkTokenOptions, MagicLinkTokenPayload, PendingStagingDirInfo, SavePendingStagingKeystoneOptions, SavePendingStagingKeystoneResult, VerifyMagicLinkTokenOptions, VerifyMagicLinkTokenResult } from './email-verify-types.mjs';
14
+
15
+ /** In-memory store for tracking single-use nonces mapped to expiration timestamps. */
16
+ const consumedNonces = new Map<string, number>();
17
+
18
+ /**
19
+ * Gets the secret key used for signing and verifying magic link tokens.
20
+ */
21
+ export function getMagicLinkSecret(): string {
22
+ return process.env.MAGIC_LINK_SECRET || process.env.IDENTITY_INDEX_SALT || DEFAULT_IDENTITY_INDEX_SALT;
23
+ }
24
+
25
+ /**
26
+ * Generates a signed, single-use 15-minute verification token containing email and primaryTjpAddr.
27
+ */
28
+ export function createMagicLinkToken({
29
+ email,
30
+ primaryTjpAddr,
31
+ ttlMs = DEFAULT_MAGIC_LINK_TTL_MS,
32
+ secret,
33
+ nonce,
34
+ }: CreateMagicLinkTokenOptions): string {
35
+ if (!email || !validateEmail(email)) {
36
+ throw new Error(`Invalid email address provided for magic link token creation: ${email} (E: a9b8c7d6e5f43210123456789abcdef0)`);
37
+ }
38
+ if (!primaryTjpAddr) {
39
+ throw new Error(`primaryTjpAddr is required for magic link token creation (E: b8c7d6e5f43210123456789abcdef01)`);
40
+ }
41
+
42
+ const exp = Date.now() + ttlMs;
43
+ const tokenNonce = nonce || randomUUID();
44
+
45
+ const payload: MagicLinkTokenPayload = {
46
+ email,
47
+ primaryTjpAddr,
48
+ exp,
49
+ nonce: tokenNonce,
50
+ };
51
+
52
+ const payloadJson = JSON.stringify(payload);
53
+ const payloadBase64 = Buffer.from(payloadJson, 'utf-8').toString('base64url');
54
+
55
+ const signingSecret = secret || getMagicLinkSecret();
56
+ const signature = createHmac('sha256', signingSecret)
57
+ .update(payloadBase64)
58
+ .digest('base64url');
59
+
60
+ return `${payloadBase64}.${signature}`;
61
+ }
62
+
63
+ /**
64
+ * Verifies a magic link token's HMAC signature, expiration time, and single-use status.
65
+ */
66
+ export function verifyMagicLinkToken({
67
+ token,
68
+ secret,
69
+ }: VerifyMagicLinkTokenOptions): VerifyMagicLinkTokenResult {
70
+ try {
71
+ if (!token || typeof token !== 'string') {
72
+ return { valid: false, error: 'Token must be a non-empty string. (E: c7d6e5f43210123456789abcdef012)' };
73
+ }
74
+
75
+ const parts = token.split('.');
76
+ if (parts.length !== 2) {
77
+ return { valid: false, error: 'Malformed token structure. Expected two dot-separated parts. (E: d6e5f43210123456789abcdef0123)' };
78
+ }
79
+
80
+ const [payloadBase64, providedSignature] = parts;
81
+ const signingSecret = secret || getMagicLinkSecret();
82
+
83
+ const expectedSignature = createHmac('sha256', signingSecret)
84
+ .update(payloadBase64)
85
+ .digest('base64url');
86
+
87
+ // Timing-safe signature comparison
88
+ const sigBufA = Buffer.from(providedSignature, 'utf-8');
89
+ const sigBufB = Buffer.from(expectedSignature, 'utf-8');
90
+
91
+ if (sigBufA.length !== sigBufB.length || !timingSafeEqual(sigBufA, sigBufB)) {
92
+ return { valid: false, error: 'Invalid token signature. (E: e5f43210123456789abcdef01234)' };
93
+ }
94
+
95
+ const payloadJson = Buffer.from(payloadBase64, 'base64url').toString('utf-8');
96
+ const payload: MagicLinkTokenPayload = JSON.parse(payloadJson);
97
+
98
+ if (!payload.email || !payload.primaryTjpAddr || !payload.exp || !payload.nonce) {
99
+ return { valid: false, error: 'Missing required fields in token payload. (E: f43210123456789abcdef012345)' };
100
+ }
101
+
102
+ if (Date.now() > payload.exp) {
103
+ return { valid: false, error: 'Token has expired. (E: 43210123456789abcdef0123456)' };
104
+ }
105
+
106
+ if (isNonceUsed(payload.nonce)) {
107
+ return { valid: false, error: 'Token has already been used. (E: 3210123456789abcdef01234567)' };
108
+ }
109
+
110
+ return { valid: true, payload };
111
+ } catch (error) {
112
+ return { valid: false, error: `Failed to verify token: ${extractErrorMsg(error)} (E: 210123456789abcdef012345678)` };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Checks if a token nonce has already been consumed.
118
+ */
119
+ export function isNonceUsed(nonce: string): boolean {
120
+ pruneExpiredNonces();
121
+ return consumedNonces.has(nonce);
122
+ }
123
+
124
+ /**
125
+ * Marks a token nonce as consumed, preventing replay.
126
+ */
127
+ export function markNonceUsed(nonce: string, exp?: number): void {
128
+ const expiresAt = exp || Date.now() + DEFAULT_MAGIC_LINK_TTL_MS;
129
+ consumedNonces.set(nonce, expiresAt);
130
+ }
131
+
132
+ /**
133
+ * Helper for tests to reset consumed nonces state.
134
+ */
135
+ export function clearConsumedNonces(): void {
136
+ consumedNonces.clear();
137
+ }
138
+
139
+ /**
140
+ * Internal helper to clean up expired nonces from memory.
141
+ */
142
+ function pruneExpiredNonces(): void {
143
+ const now = Date.now();
144
+ for (const [nonce, exp] of consumedNonces.entries()) {
145
+ if (now > exp) {
146
+ consumedNonces.delete(nonce);
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Saves or updates a pending staging directory for an unverified keystone and its associated emails.
153
+ *
154
+ * Folder structure created:
155
+ * `<dataDir>/identity_index/email/pending/<expiryTicks>_<tjpHash>/`
156
+ * ├── keystone.json (serialized keystone DTO if provided)
157
+ * └── emails/
158
+ * └── <emailHash>.json ({ email, verified: false, verifiedAt: null })
159
+ */
160
+ export async function savePendingStagingKeystone({
161
+ keystone,
162
+ emails,
163
+ dataDir,
164
+ ttlMs = DEFAULT_MAGIC_LINK_TTL_MS,
165
+ primaryTjpAddr: inputPrimaryTjpAddr,
166
+ }: SavePendingStagingKeystoneOptions): Promise<SavePendingStagingKeystoneResult> {
167
+ if (!dataDir) {
168
+ throw new Error('dataDir is required for savePendingStagingKeystone (E: 87123456789abcdef0123456789a0123)');
169
+ }
170
+ if (!emails || !Array.isArray(emails) || emails.length === 0) {
171
+ throw new Error('emails array must be a non-empty array of strings (E: 98123456789abcdef0123456789a0124)');
172
+ }
173
+
174
+ const targetTjpAddr = inputPrimaryTjpAddr || keystone?.data?.tjpAddr || keystone?.ib;
175
+ if (!targetTjpAddr) {
176
+ throw new Error('Primary identity TJP address could not be derived from options or keystone (E: 09123456789abcdef0123456789a0125)');
177
+ }
178
+
179
+ const tjpHash = await hash({ s: `emailindex${targetTjpAddr}` });
180
+ const expiryTicks = Date.now() + ttlMs;
181
+
182
+ const pendingBaseDir = path.join(dataDir, DEFAULT_IDENTITY_INDEX_DIR_NAME, 'email', 'pending');
183
+
184
+ // Remove any existing pending directory for this tjpHash to maintain a single active staging entry
185
+ try {
186
+ if (existsSync(pendingBaseDir)) {
187
+ const entries = await fs.readdir(pendingBaseDir, { withFileTypes: true });
188
+ for (const entry of entries) {
189
+ if (entry.isDirectory() && entry.name.endsWith(`_${tjpHash}`)) {
190
+ const oldDir = path.join(pendingBaseDir, entry.name);
191
+ await fs.rm(oldDir, { recursive: true, force: true });
192
+ }
193
+ }
194
+ }
195
+ } catch { /* ignore directory reading errors */ }
196
+
197
+ const pendingDir = path.join(pendingBaseDir, `${expiryTicks}_${tjpHash}`);
198
+ const emailsDir = path.join(pendingDir, 'emails');
199
+
200
+ await fs.mkdir(emailsDir, { recursive: true });
201
+
202
+ if (keystone) {
203
+ const keystonePath = path.join(pendingDir, 'keystone.json');
204
+ await fs.writeFile(keystonePath, JSON.stringify(keystone, null, 2), 'utf-8');
205
+ }
206
+
207
+ for (const email of emails) {
208
+ const normEmail = normalizeEmail(email);
209
+ const emailHash = await hash({ s: `emailindex${normEmail}` });
210
+ const emailMetaPath = path.join(emailsDir, `${emailHash}.json`);
211
+ const meta = {
212
+ email: normEmail,
213
+ verified: false,
214
+ verifiedAt: null,
215
+ };
216
+ await fs.writeFile(emailMetaPath, JSON.stringify(meta, null, 2), 'utf-8');
217
+ }
218
+
219
+ return {
220
+ pendingDir,
221
+ expiryTicks,
222
+ tjpHash,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Generates a 6-digit numeric verification code string (e.g. "482901").
228
+ */
229
+ export function generateVerificationCode(): string {
230
+ const codeNum = Math.floor(100000 + Math.random() * 900000);
231
+ return codeNum.toString();
232
+ }
233
+
234
+ export interface SavePendingCodeRecordOptions {
235
+ pendingDir: string;
236
+ code: string;
237
+ maxAttempts?: number;
238
+ ttlMs?: number;
239
+ }
240
+
241
+ export interface PendingCodeRecord {
242
+ codeHash: string;
243
+ attempts: number;
244
+ maxAttempts: number;
245
+ expiresAt: number;
246
+ }
247
+
248
+ /**
249
+ * Saves a hashed 6-digit verification code record to `code.json` in the pending staging directory.
250
+ */
251
+ export async function savePendingCodeRecord({
252
+ pendingDir,
253
+ code,
254
+ maxAttempts = 5,
255
+ ttlMs = DEFAULT_MAGIC_LINK_TTL_MS,
256
+ }: SavePendingCodeRecordOptions): Promise<void> {
257
+ const codeHash = await hash({ s: `verificationcode${code}` });
258
+ const record: PendingCodeRecord = {
259
+ codeHash,
260
+ attempts: 0,
261
+ maxAttempts,
262
+ expiresAt: Date.now() + ttlMs,
263
+ };
264
+ const codeFilePath = path.join(pendingDir, 'code.json');
265
+ await fs.writeFile(codeFilePath, JSON.stringify(record, null, 2), 'utf-8');
266
+ }
267
+
268
+ /**
269
+ * Creates a default EmailSender instance based on process.env.EMAIL_MODE and NODE_ENV.
270
+ */
271
+ export function createDefaultEmailSender(): EmailSender {
272
+ const isProd = process.env.NODE_ENV === 'production';
273
+ const mode = (process.env.EMAIL_MODE || (isProd ? 'aws' : 'mock')).toLowerCase();
274
+ if (mode === 'aws') {
275
+ return new EmailSenderAws({
276
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
277
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
278
+ region: process.env.AWS_REGION || 'us-east-1',
279
+ });
280
+ } else if (mode === 'mock') {
281
+ if (isProd) {
282
+ throw new Error('EMAIL_MODE=mock is prohibited when NODE_ENV=production. (E: 897a6b5c4d3e2f10987654321fedcba0)');
283
+ }
284
+ return new EmailSenderMock();
285
+ } else {
286
+ throw new Error(`Unsupported EMAIL_MODE '${mode}'. Must be 'aws' or 'mock'. (E: 987a6b5c4d3e2f10987654321fedcba1)`);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Generates a 6-digit OTP code, writes `code.json` to the pending staging directory,
292
+ * and dispatches an email via the active `EmailSender` driver.
293
+ */
294
+ export async function dispatchVerificationCode({
295
+ email,
296
+ primaryTjpAddr,
297
+ pendingDir,
298
+ sender,
299
+ }: {
300
+ email: string;
301
+ primaryTjpAddr: string;
302
+ pendingDir: string;
303
+ sender?: EmailSender;
304
+ }): Promise<{ success: boolean; code?: string; messageId?: string; error?: string }> {
305
+ const code = generateVerificationCode();
306
+ await savePendingCodeRecord({ pendingDir, code });
307
+
308
+ const activeSender = sender || createDefaultEmailSender();
309
+ const outboundEmail: OutboundEmail = {
310
+ from: process.env.AWS_SES_FROM_EMAIL || 'noreply@ibgib.com',
311
+ to: [email],
312
+ subject: 'Verify your Ibgib Identity Email',
313
+ textBody: `Hello,\n\nYour 6-digit verification code for your Ibgib identity is:\n\n${code}\n\nThis code will expire in 15 minutes.\n\nIf you did not request this, please ignore this email.`,
314
+ htmlBody: `<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px;">
315
+ <h2>Verify your Ibgib Identity Email</h2>
316
+ <p>Your 6-digit verification code is:</p>
317
+ <h1 style="font-family: monospace; font-size: 36px; letter-spacing: 6px; color: #4f46e5; margin: 24px 0;">${code}</h1>
318
+ <p style="font-size: 12px; color: #999;">This code will expire in 15 minutes.</p>
319
+ </div>`,
320
+ };
321
+
322
+ const sendResult = await activeSender.sendEmail(outboundEmail);
323
+
324
+ const isProd = process.env.NODE_ENV === 'production';
325
+ const mode = (process.env.EMAIL_MODE || (isProd ? 'aws' : 'mock')).toLowerCase();
326
+ if (sendResult.success && (mode === 'mock' || !isProd)) {
327
+ console.log(`✉️ [EmailSenderMock] Verification Code for ${email}: ${code} (I: c123456789abcdef0123456789abcdef)`);
328
+ }
329
+
330
+ return {
331
+ success: sendResult.success,
332
+ code,
333
+ messageId: sendResult.messageId,
334
+ error: sendResult.error,
335
+ };
336
+ }
337
+
338
+ export interface VerifyPendingCodeOptions {
339
+ pendingDir: string;
340
+ code: string;
341
+ }
342
+
343
+ export interface VerifyPendingCodeResult {
344
+ valid: boolean;
345
+ error?: string;
346
+ reason?: 'expired' | 'too_many_attempts' | 'invalid_code';
347
+ }
348
+
349
+ /**
350
+ * Validates a 6-digit verification code against `code.json` in the pending staging directory.
351
+ * Increments `attempts`. Enforces 5-attempt rate limit.
352
+ */
353
+ export async function verifyPendingCode({
354
+ pendingDir,
355
+ code,
356
+ }: VerifyPendingCodeOptions): Promise<VerifyPendingCodeResult> {
357
+ const codeFilePath = path.join(pendingDir, 'code.json');
358
+ if (!existsSync(codeFilePath)) {
359
+ return { valid: false, error: 'Verification code record not found. Please click "Resend Code" to request a new code.', reason: 'invalid_code' };
360
+ }
361
+
362
+ const raw = await fs.readFile(codeFilePath, 'utf-8');
363
+ const record: PendingCodeRecord = JSON.parse(raw);
364
+
365
+ if (Date.now() > record.expiresAt) {
366
+ return { valid: false, error: 'Verification code has expired. Please click "Resend Code" for a new code.', reason: 'expired' };
367
+ }
368
+
369
+ if (record.attempts >= record.maxAttempts) {
370
+ return { valid: false, error: 'Too many incorrect attempts. Please click "Resend Code" for a new code.', reason: 'too_many_attempts' };
371
+ }
372
+
373
+ record.attempts += 1;
374
+ await fs.writeFile(codeFilePath, JSON.stringify(record, null, 2), 'utf-8');
375
+
376
+ const inputHash = await hash({ s: `verificationcode${code.trim()}` });
377
+ if (inputHash !== record.codeHash) {
378
+ const remaining = record.maxAttempts - record.attempts;
379
+ if (remaining <= 0) {
380
+ return { valid: false, error: 'Too many incorrect attempts. Please click "Resend Code" for a new code.', reason: 'too_many_attempts' };
381
+ }
382
+ return { valid: false, error: `Incorrect verification code. (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`, reason: 'invalid_code' };
383
+ }
384
+
385
+ return { valid: true };
386
+ }
387
+
388
+ /**
389
+ * Helper to derive consistent tjpHash for email staging quarantine directories.
390
+ */
391
+ export async function getPendingTjpHash(primaryTjpAddr: string): Promise<string> {
392
+ return await hash({ s: `emailindex${primaryTjpAddr}` });
393
+ }
394
+
395
+ /**
396
+ * Locates the pending staging directory for a given tjpHash under `<dataDir>/identity_index/email/pending/`.
397
+ */
398
+ export async function getPendingStagingDir({
399
+ dataDir,
400
+ tjpHash,
401
+ }: {
402
+ dataDir: string;
403
+ tjpHash: string;
404
+ }): Promise<PendingStagingDirInfo | undefined> {
405
+ const pendingBaseDir = path.join(dataDir, DEFAULT_IDENTITY_INDEX_DIR_NAME, 'email', 'pending');
406
+ try {
407
+ const entries = await fs.readdir(pendingBaseDir, { withFileTypes: true });
408
+ for (const entry of entries) {
409
+ if (entry.isDirectory() && entry.name.endsWith(`_${tjpHash}`)) {
410
+ const parts = entry.name.split('_');
411
+ const expiryTicks = parseInt(parts[0], 10);
412
+ const pendingDir = path.join(pendingBaseDir, entry.name);
413
+ const isExpired = Date.now() > expiryTicks;
414
+ return { pendingDir, expiryTicks, tjpHash, isExpired };
415
+ }
416
+ }
417
+ return undefined;
418
+ } catch (err: any) {
419
+ if (err.code === 'ENOENT') {
420
+ return undefined;
421
+ }
422
+ throw err;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Executes garbage collection on expired staging directories using an OS atomic lockfile (`.gc.lock` with `flag: 'wx'`).
428
+ */
429
+ export async function performPendingGc({ dataDir }: { dataDir: string }): Promise<number> {
430
+ const pendingBaseDir = path.join(dataDir, DEFAULT_IDENTITY_INDEX_DIR_NAME, 'email', 'pending');
431
+ const lockFile = path.join(pendingBaseDir, '.gc.lock');
432
+ let lockAcquired = false;
433
+
434
+ try {
435
+ await fs.mkdir(pendingBaseDir, { recursive: true });
436
+ await fs.writeFile(lockFile, JSON.stringify({ pid: process.pid, lockedAt: Date.now() }), { flag: 'wx' });
437
+ lockAcquired = true;
438
+
439
+ const entries = await fs.readdir(pendingBaseDir, { withFileTypes: true });
440
+ const now = Date.now();
441
+ let cleanedCount = 0;
442
+
443
+ for (const entry of entries) {
444
+ if (entry.isDirectory() && entry.name.includes('_')) {
445
+ const parts = entry.name.split('_');
446
+ const expiryTicks = parseInt(parts[0], 10);
447
+ if (!isNaN(expiryTicks) && now > expiryTicks) {
448
+ const dirToRemove = path.join(pendingBaseDir, entry.name);
449
+ await fs.rm(dirToRemove, { recursive: true, force: true });
450
+ cleanedCount++;
451
+ }
452
+ }
453
+ }
454
+ return cleanedCount;
455
+ } catch (err: any) {
456
+ if (err.code === 'EEXIST') {
457
+ // Lockfile already exists, another process is executing GC
458
+ return 0;
459
+ }
460
+ throw err;
461
+ } finally {
462
+ if (lockAcquired) {
463
+ try { await fs.unlink(lockFile); } catch { /* ignore */ }
464
+ }
465
+ }
466
+ }
@@ -0,0 +1,47 @@
1
+
2
+ export interface MagicLinkTokenPayload {
3
+ email: string;
4
+ primaryTjpAddr: string;
5
+ exp: number;
6
+ nonce: string;
7
+ }
8
+
9
+ export interface CreateMagicLinkTokenOptions {
10
+ email: string;
11
+ primaryTjpAddr: string;
12
+ ttlMs?: number;
13
+ secret?: string;
14
+ nonce?: string;
15
+ }
16
+
17
+ export interface VerifyMagicLinkTokenOptions {
18
+ token: string;
19
+ secret?: string;
20
+ }
21
+
22
+ export interface VerifyMagicLinkTokenResult {
23
+ valid: boolean;
24
+ error?: string;
25
+ payload?: MagicLinkTokenPayload;
26
+ }
27
+
28
+ export interface SavePendingStagingKeystoneOptions {
29
+ keystone?: any;
30
+ emails: string[];
31
+ dataDir: string;
32
+ ttlMs?: number;
33
+ primaryTjpAddr?: string;
34
+ }
35
+
36
+ export interface SavePendingStagingKeystoneResult {
37
+ pendingDir: string;
38
+ expiryTicks: number;
39
+ tjpHash: string;
40
+ }
41
+
42
+ export interface PendingStagingDirInfo {
43
+ pendingDir: string;
44
+ expiryTicks: number;
45
+ tjpHash: string;
46
+ isExpired: boolean;
47
+ }
@@ -0,0 +1,146 @@
1
+ import { getUUID, hash, HashAlgorithm, extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
2
+ import { kdf_recursiveSaltWrap } from '@ibgib/core-gib/dist/keystone/kdf/kdf-helpers.mjs';
3
+ import { KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
4
+
5
+ import { CustodianRequirements } from './sso-types.mjs';
6
+
7
+ /**
8
+ * Derives a deterministic single-turn authorization code using the server's private KDF secret
9
+ * and the JSON-stringified `custodianReqs` (excluding `authCode` itself) as salt.
10
+ *
11
+ * @param custodianReqsOmitAuthCode The custodian requirements object omitting the `authCode` property.
12
+ * @param serverSecret The server's private KDF secret (`SSO_SERVER_KDF_SECRET`).
13
+ */
14
+ export async function deriveCustodianAuthCode({
15
+ custodianReqsOmitAuthCode,
16
+ serverSecret,
17
+ }: {
18
+ custodianReqsOmitAuthCode: Omit<CustodianRequirements, 'authCode'>;
19
+ serverSecret: string;
20
+ }): Promise<string> {
21
+ const lc = `[${deriveCustodianAuthCode.name}]`;
22
+ try {
23
+ if (!serverSecret) {
24
+ throw new Error('serverSecret required for custodian auth code derivation (E: a1b2c3d4e5f67890123456789abcdef0)');
25
+ }
26
+
27
+ const salt = await hash({
28
+ s: JSON.stringify(custodianReqsOmitAuthCode),
29
+ algorithm: HashAlgorithm.sha_512
30
+ });
31
+
32
+ return await kdf_recursiveSaltWrap({
33
+ masterSecret: serverSecret,
34
+ salt,
35
+ rounds: 1000,
36
+ algorithm: HashAlgorithm.sha_512
37
+ });
38
+ } catch (error) {
39
+ console.error(`${lc} ${extractErrorMsg(error)}`);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Generates a full `CustodianRequirements` object containing single-turn nonce,
46
+ * expiration timestamp, permitted verbs, and derived auth code.
47
+ *
48
+ * @param primaryTjpAddr Primary identity keystone TJP address (`ib^gib`).
49
+ * @param ttlMs Time-to-live duration in milliseconds. Defaults to 15 minutes.
50
+ * @param allowedVerbs Permitted keystone verbs. Defaults to `['sync', 'connect']`.
51
+ * @param serverSecret The server's private KDF secret (`SSO_SERVER_KDF_SECRET`).
52
+ */
53
+ export async function generateCustodianRequirements({
54
+ primaryTjpAddr,
55
+ ttlMs = 15 * 60 * 1000,
56
+ allowedVerbs = [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
57
+ serverSecret,
58
+ }: {
59
+ primaryTjpAddr: string;
60
+ ttlMs?: number;
61
+ allowedVerbs?: string[];
62
+ serverSecret: string;
63
+ }): Promise<CustodianRequirements> {
64
+ const lc = `[${generateCustodianRequirements.name}]`;
65
+ try {
66
+ if (!primaryTjpAddr) {
67
+ throw new Error('primaryTjpAddr is required to generate custodian requirements (E: b2c3d4e5f67890123456789abcdef01)');
68
+ }
69
+ if (!serverSecret) {
70
+ throw new Error('serverSecret is required to generate custodian requirements (E: c3d4e5f67890123456789abcdef012)');
71
+ }
72
+
73
+ const nonce = await getUUID();
74
+ const expiresAt = Date.now() + ttlMs;
75
+
76
+ const custodianReqsOmitAuthCode: Omit<CustodianRequirements, 'authCode'> = {
77
+ primaryTjpAddr,
78
+ nonce,
79
+ expiresAt,
80
+ allowedVerbs,
81
+ };
82
+
83
+ const authCode = await deriveCustodianAuthCode({
84
+ custodianReqsOmitAuthCode,
85
+ serverSecret
86
+ });
87
+
88
+ return {
89
+ ...custodianReqsOmitAuthCode,
90
+ authCode
91
+ };
92
+ } catch (error) {
93
+ console.error(`${lc} ${extractErrorMsg(error)}`);
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Computes the deterministic index hash (`authCustodianIndex`) over the full `CustodianRequirements` object
100
+ * used for atomic file reservation tracking on the server filesystem.
101
+ */
102
+ export async function getAuthCustodianIndex({
103
+ custodianReqs,
104
+ }: {
105
+ custodianReqs: CustodianRequirements;
106
+ }): Promise<string> {
107
+ return await hash({
108
+ s: JSON.stringify(custodianReqs),
109
+ algorithm: HashAlgorithm.sha_256
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Cryptographically verifies a `CustodianRequirements` object against expiration and server KDF authCode match.
115
+ */
116
+ export async function verifyCustodianRequirements({
117
+ custodianReqs,
118
+ serverSecret,
119
+ }: {
120
+ custodianReqs: CustodianRequirements;
121
+ serverSecret: string;
122
+ }): Promise<{ valid: boolean; error?: string }> {
123
+ try {
124
+ if (!custodianReqs || !custodianReqs.authCode || !custodianReqs.nonce || !custodianReqs.expiresAt || !custodianReqs.primaryTjpAddr) {
125
+ return { valid: false, error: 'Malformed custodian requirements structure. (E: d4e5f67890123456789abcdef0123)' };
126
+ }
127
+
128
+ if (Date.now() > custodianReqs.expiresAt) {
129
+ return { valid: false, error: 'Custodian requirements have expired. (E: e5f67890123456789abcdef01234)' };
130
+ }
131
+
132
+ const { authCode, ...custodianReqsOmitAuthCode } = custodianReqs;
133
+ const expectedAuthCode = await deriveCustodianAuthCode({
134
+ custodianReqsOmitAuthCode,
135
+ serverSecret
136
+ });
137
+
138
+ if (authCode !== expectedAuthCode) {
139
+ return { valid: false, error: 'Cryptographic authCode mismatch. Requirement payload tampered or invalid. (E: f67890123456789abcdef012345)' };
140
+ }
141
+
142
+ return { valid: true };
143
+ } catch (error) {
144
+ return { valid: false, error: `Verification failed: ${extractErrorMsg(error)} (E: 7890123456789abcdef0123456)` };
145
+ }
146
+ }