@zigrivers/scaffold 3.15.0 → 3.17.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 (381) hide show
  1. package/README.md +47 -12
  2. package/content/knowledge/backend/backend-fintech-broker-integration.md +244 -0
  3. package/content/knowledge/backend/backend-fintech-compliance.md +181 -0
  4. package/content/knowledge/backend/backend-fintech-data-modeling.md +210 -0
  5. package/content/knowledge/backend/backend-fintech-ledger.md +226 -0
  6. package/content/knowledge/backend/backend-fintech-observability.md +151 -0
  7. package/content/knowledge/backend/backend-fintech-order-lifecycle.md +213 -0
  8. package/content/knowledge/backend/backend-fintech-risk-management.md +150 -0
  9. package/content/knowledge/backend/backend-fintech-testing.md +197 -0
  10. package/content/knowledge/core/automated-review-tooling.md +31 -26
  11. package/content/knowledge/core/multi-model-review-dispatch.md +30 -55
  12. package/content/knowledge/core/multi-service-api-contracts.md +634 -0
  13. package/content/knowledge/core/multi-service-architecture.md +492 -0
  14. package/content/knowledge/core/multi-service-auth.md +706 -0
  15. package/content/knowledge/core/multi-service-data-ownership.md +539 -0
  16. package/content/knowledge/core/multi-service-observability.md +545 -0
  17. package/content/knowledge/core/multi-service-resilience.md +710 -0
  18. package/content/knowledge/core/multi-service-task-decomposition.md +615 -0
  19. package/content/knowledge/core/multi-service-testing.md +728 -0
  20. package/content/methodology/backend-fintech.yml +46 -0
  21. package/content/methodology/custom-defaults.yml +6 -0
  22. package/content/methodology/deep.yml +6 -0
  23. package/content/methodology/multi-service-overlay.yml +103 -0
  24. package/content/methodology/mvp.yml +6 -0
  25. package/content/pipeline/architecture/service-ownership-map.md +83 -0
  26. package/content/pipeline/quality/cross-service-auth.md +96 -0
  27. package/content/pipeline/quality/cross-service-observability.md +104 -0
  28. package/content/pipeline/quality/integration-test-plan.md +106 -0
  29. package/content/pipeline/specification/inter-service-contracts.md +95 -0
  30. package/content/tools/post-implementation-review.md +36 -7
  31. package/content/tools/review-code.md +33 -8
  32. package/content/tools/review-pr.md +79 -95
  33. package/dist/cli/commands/adopt.cli-flags.test.js +20 -0
  34. package/dist/cli/commands/adopt.cli-flags.test.js.map +1 -1
  35. package/dist/cli/commands/adopt.d.ts.map +1 -1
  36. package/dist/cli/commands/adopt.js +11 -3
  37. package/dist/cli/commands/adopt.js.map +1 -1
  38. package/dist/cli/commands/complete.d.ts +1 -0
  39. package/dist/cli/commands/complete.d.ts.map +1 -1
  40. package/dist/cli/commands/complete.js +26 -8
  41. package/dist/cli/commands/complete.js.map +1 -1
  42. package/dist/cli/commands/dashboard.d.ts +1 -0
  43. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  44. package/dist/cli/commands/dashboard.js +19 -6
  45. package/dist/cli/commands/dashboard.js.map +1 -1
  46. package/dist/cli/commands/decisions.d.ts +1 -0
  47. package/dist/cli/commands/decisions.d.ts.map +1 -1
  48. package/dist/cli/commands/decisions.js +18 -4
  49. package/dist/cli/commands/decisions.js.map +1 -1
  50. package/dist/cli/commands/info.d.ts +1 -0
  51. package/dist/cli/commands/info.d.ts.map +1 -1
  52. package/dist/cli/commands/info.js +25 -3
  53. package/dist/cli/commands/info.js.map +1 -1
  54. package/dist/cli/commands/init-from.test.d.ts +2 -0
  55. package/dist/cli/commands/init-from.test.d.ts.map +1 -0
  56. package/dist/cli/commands/init-from.test.js +315 -0
  57. package/dist/cli/commands/init-from.test.js.map +1 -0
  58. package/dist/cli/commands/init.d.ts +3 -0
  59. package/dist/cli/commands/init.d.ts.map +1 -1
  60. package/dist/cli/commands/init.js +239 -129
  61. package/dist/cli/commands/init.js.map +1 -1
  62. package/dist/cli/commands/init.test.js +20 -0
  63. package/dist/cli/commands/init.test.js.map +1 -1
  64. package/dist/cli/commands/next.d.ts +1 -0
  65. package/dist/cli/commands/next.d.ts.map +1 -1
  66. package/dist/cli/commands/next.js +40 -4
  67. package/dist/cli/commands/next.js.map +1 -1
  68. package/dist/cli/commands/next.test.js +151 -0
  69. package/dist/cli/commands/next.test.js.map +1 -1
  70. package/dist/cli/commands/reset.d.ts +1 -0
  71. package/dist/cli/commands/reset.d.ts.map +1 -1
  72. package/dist/cli/commands/reset.js +77 -29
  73. package/dist/cli/commands/reset.js.map +1 -1
  74. package/dist/cli/commands/rework.d.ts +1 -0
  75. package/dist/cli/commands/rework.d.ts.map +1 -1
  76. package/dist/cli/commands/rework.js +16 -2
  77. package/dist/cli/commands/rework.js.map +1 -1
  78. package/dist/cli/commands/run.d.ts +1 -0
  79. package/dist/cli/commands/run.d.ts.map +1 -1
  80. package/dist/cli/commands/run.js +65 -13
  81. package/dist/cli/commands/run.js.map +1 -1
  82. package/dist/cli/commands/run.test.js +192 -3
  83. package/dist/cli/commands/run.test.js.map +1 -1
  84. package/dist/cli/commands/skip.d.ts +1 -0
  85. package/dist/cli/commands/skip.d.ts.map +1 -1
  86. package/dist/cli/commands/skip.js +24 -7
  87. package/dist/cli/commands/skip.js.map +1 -1
  88. package/dist/cli/commands/status.d.ts +1 -0
  89. package/dist/cli/commands/status.d.ts.map +1 -1
  90. package/dist/cli/commands/status.js +51 -4
  91. package/dist/cli/commands/status.js.map +1 -1
  92. package/dist/cli/commands/status.test.js +128 -0
  93. package/dist/cli/commands/status.test.js.map +1 -1
  94. package/dist/cli/guards-coverage.test.d.ts +2 -0
  95. package/dist/cli/guards-coverage.test.d.ts.map +1 -0
  96. package/dist/cli/guards-coverage.test.js +26 -0
  97. package/dist/cli/guards-coverage.test.js.map +1 -0
  98. package/dist/cli/guards-integration.test.d.ts +2 -0
  99. package/dist/cli/guards-integration.test.d.ts.map +1 -0
  100. package/dist/cli/guards-integration.test.js +178 -0
  101. package/dist/cli/guards-integration.test.js.map +1 -0
  102. package/dist/cli/guards.d.ts +13 -0
  103. package/dist/cli/guards.d.ts.map +1 -0
  104. package/dist/cli/guards.js +70 -0
  105. package/dist/cli/guards.js.map +1 -0
  106. package/dist/cli/guards.test.d.ts +2 -0
  107. package/dist/cli/guards.test.d.ts.map +1 -0
  108. package/dist/cli/guards.test.js +136 -0
  109. package/dist/cli/guards.test.js.map +1 -0
  110. package/dist/cli/init-flag-families.d.ts +1 -1
  111. package/dist/cli/init-flag-families.d.ts.map +1 -1
  112. package/dist/cli/init-flag-families.js +4 -1
  113. package/dist/cli/init-flag-families.js.map +1 -1
  114. package/dist/cli/init-flag-families.test.js +10 -0
  115. package/dist/cli/init-flag-families.test.js.map +1 -1
  116. package/dist/cli/shutdown.d.ts +2 -3
  117. package/dist/cli/shutdown.d.ts.map +1 -1
  118. package/dist/cli/shutdown.js +14 -11
  119. package/dist/cli/shutdown.js.map +1 -1
  120. package/dist/cli/shutdown.test.js +2 -4
  121. package/dist/cli/shutdown.test.js.map +1 -1
  122. package/dist/config/schema.d.ts +12122 -288
  123. package/dist/config/schema.d.ts.map +1 -1
  124. package/dist/config/schema.js +74 -79
  125. package/dist/config/schema.js.map +1 -1
  126. package/dist/config/schema.test.js +230 -1
  127. package/dist/config/schema.test.js.map +1 -1
  128. package/dist/config/validators/backend.d.ts +4 -0
  129. package/dist/config/validators/backend.d.ts.map +1 -0
  130. package/dist/config/validators/backend.js +14 -0
  131. package/dist/config/validators/backend.js.map +1 -0
  132. package/dist/config/validators/browser-extension.d.ts +4 -0
  133. package/dist/config/validators/browser-extension.d.ts.map +1 -0
  134. package/dist/config/validators/browser-extension.js +24 -0
  135. package/dist/config/validators/browser-extension.js.map +1 -0
  136. package/dist/config/validators/cli.d.ts +4 -0
  137. package/dist/config/validators/cli.d.ts.map +1 -0
  138. package/dist/config/validators/cli.js +14 -0
  139. package/dist/config/validators/cli.js.map +1 -0
  140. package/dist/config/validators/data-pipeline.d.ts +4 -0
  141. package/dist/config/validators/data-pipeline.d.ts.map +1 -0
  142. package/dist/config/validators/data-pipeline.js +14 -0
  143. package/dist/config/validators/data-pipeline.js.map +1 -0
  144. package/dist/config/validators/game.d.ts +4 -0
  145. package/dist/config/validators/game.d.ts.map +1 -0
  146. package/dist/config/validators/game.js +14 -0
  147. package/dist/config/validators/game.js.map +1 -0
  148. package/dist/config/validators/index.d.ts +7 -0
  149. package/dist/config/validators/index.d.ts.map +1 -0
  150. package/dist/config/validators/index.js +27 -0
  151. package/dist/config/validators/index.js.map +1 -0
  152. package/dist/config/validators/library.d.ts +4 -0
  153. package/dist/config/validators/library.d.ts.map +1 -0
  154. package/dist/config/validators/library.js +25 -0
  155. package/dist/config/validators/library.js.map +1 -0
  156. package/dist/config/validators/ml.d.ts +4 -0
  157. package/dist/config/validators/ml.d.ts.map +1 -0
  158. package/dist/config/validators/ml.js +31 -0
  159. package/dist/config/validators/ml.js.map +1 -0
  160. package/dist/config/validators/mobile-app.d.ts +4 -0
  161. package/dist/config/validators/mobile-app.d.ts.map +1 -0
  162. package/dist/config/validators/mobile-app.js +14 -0
  163. package/dist/config/validators/mobile-app.js.map +1 -0
  164. package/dist/config/validators/registry.test.d.ts +2 -0
  165. package/dist/config/validators/registry.test.d.ts.map +1 -0
  166. package/dist/config/validators/registry.test.js +26 -0
  167. package/dist/config/validators/registry.test.js.map +1 -0
  168. package/dist/config/validators/research.d.ts +4 -0
  169. package/dist/config/validators/research.d.ts.map +1 -0
  170. package/dist/config/validators/research.js +24 -0
  171. package/dist/config/validators/research.js.map +1 -0
  172. package/dist/config/validators/research.test.d.ts +2 -0
  173. package/dist/config/validators/research.test.d.ts.map +1 -0
  174. package/dist/config/validators/research.test.js +44 -0
  175. package/dist/config/validators/research.test.js.map +1 -0
  176. package/dist/config/validators/types.d.ts +19 -0
  177. package/dist/config/validators/types.d.ts.map +1 -0
  178. package/dist/config/validators/types.js +2 -0
  179. package/dist/config/validators/types.js.map +1 -0
  180. package/dist/config/validators/validators.test.d.ts +2 -0
  181. package/dist/config/validators/validators.test.d.ts.map +1 -0
  182. package/dist/config/validators/validators.test.js +25 -0
  183. package/dist/config/validators/validators.test.js.map +1 -0
  184. package/dist/config/validators/web-app.d.ts +4 -0
  185. package/dist/config/validators/web-app.d.ts.map +1 -0
  186. package/dist/config/validators/web-app.js +31 -0
  187. package/dist/config/validators/web-app.js.map +1 -0
  188. package/dist/core/assembly/context-gatherer.d.ts.map +1 -1
  189. package/dist/core/assembly/context-gatherer.js +4 -2
  190. package/dist/core/assembly/context-gatherer.js.map +1 -1
  191. package/dist/core/assembly/cross-reads.d.ts +58 -0
  192. package/dist/core/assembly/cross-reads.d.ts.map +1 -0
  193. package/dist/core/assembly/cross-reads.js +185 -0
  194. package/dist/core/assembly/cross-reads.js.map +1 -0
  195. package/dist/core/assembly/cross-reads.test.d.ts +2 -0
  196. package/dist/core/assembly/cross-reads.test.d.ts.map +1 -0
  197. package/dist/core/assembly/cross-reads.test.js +383 -0
  198. package/dist/core/assembly/cross-reads.test.js.map +1 -0
  199. package/dist/core/assembly/overlay-loader-structural.test.d.ts +2 -0
  200. package/dist/core/assembly/overlay-loader-structural.test.d.ts.map +1 -0
  201. package/dist/core/assembly/overlay-loader-structural.test.js +114 -0
  202. package/dist/core/assembly/overlay-loader-structural.test.js.map +1 -0
  203. package/dist/core/assembly/overlay-loader.d.ts +17 -3
  204. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  205. package/dist/core/assembly/overlay-loader.js +75 -0
  206. package/dist/core/assembly/overlay-loader.js.map +1 -1
  207. package/dist/core/assembly/overlay-resolver.d.ts +2 -2
  208. package/dist/core/assembly/overlay-resolver.d.ts.map +1 -1
  209. package/dist/core/assembly/overlay-resolver.js.map +1 -1
  210. package/dist/core/assembly/overlay-resolver.test.js.map +1 -1
  211. package/dist/core/assembly/overlay-state-resolver.d.ts +5 -0
  212. package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
  213. package/dist/core/assembly/overlay-state-resolver.js +41 -1
  214. package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
  215. package/dist/core/assembly/overlay-state-resolver.test.js +262 -0
  216. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  217. package/dist/core/assembly/update-mode.d.ts +1 -0
  218. package/dist/core/assembly/update-mode.d.ts.map +1 -1
  219. package/dist/core/assembly/update-mode.js +17 -9
  220. package/dist/core/assembly/update-mode.js.map +1 -1
  221. package/dist/core/dependency/eligibility.d.ts +10 -1
  222. package/dist/core/dependency/eligibility.d.ts.map +1 -1
  223. package/dist/core/dependency/eligibility.js +19 -1
  224. package/dist/core/dependency/eligibility.js.map +1 -1
  225. package/dist/core/dependency/eligibility.test.js +82 -0
  226. package/dist/core/dependency/eligibility.test.js.map +1 -1
  227. package/dist/core/dependency/graph.d.ts +4 -1
  228. package/dist/core/dependency/graph.d.ts.map +1 -1
  229. package/dist/core/dependency/graph.js +7 -1
  230. package/dist/core/dependency/graph.js.map +1 -1
  231. package/dist/core/dependency/graph.test.js +29 -0
  232. package/dist/core/dependency/graph.test.js.map +1 -1
  233. package/dist/core/pipeline/global-steps.d.ts +7 -0
  234. package/dist/core/pipeline/global-steps.d.ts.map +1 -0
  235. package/dist/core/pipeline/global-steps.js +18 -0
  236. package/dist/core/pipeline/global-steps.js.map +1 -0
  237. package/dist/core/pipeline/resolver.d.ts +1 -0
  238. package/dist/core/pipeline/resolver.d.ts.map +1 -1
  239. package/dist/core/pipeline/resolver.js +51 -6
  240. package/dist/core/pipeline/resolver.js.map +1 -1
  241. package/dist/core/pipeline/types.d.ts +5 -1
  242. package/dist/core/pipeline/types.d.ts.map +1 -1
  243. package/dist/e2e/cross-service-references.test.d.ts +22 -0
  244. package/dist/e2e/cross-service-references.test.d.ts.map +1 -0
  245. package/dist/e2e/cross-service-references.test.js +188 -0
  246. package/dist/e2e/cross-service-references.test.js.map +1 -0
  247. package/dist/e2e/multi-service-pipeline.test.d.ts +10 -0
  248. package/dist/e2e/multi-service-pipeline.test.d.ts.map +1 -0
  249. package/dist/e2e/multi-service-pipeline.test.js +185 -0
  250. package/dist/e2e/multi-service-pipeline.test.js.map +1 -0
  251. package/dist/e2e/project-type-overlays.test.js +68 -0
  252. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  253. package/dist/e2e/service-execution.test.d.ts +15 -0
  254. package/dist/e2e/service-execution.test.d.ts.map +1 -0
  255. package/dist/e2e/service-execution.test.js +219 -0
  256. package/dist/e2e/service-execution.test.js.map +1 -0
  257. package/dist/e2e/service-manifest.test.d.ts +19 -0
  258. package/dist/e2e/service-manifest.test.d.ts.map +1 -0
  259. package/dist/e2e/service-manifest.test.js +166 -0
  260. package/dist/e2e/service-manifest.test.js.map +1 -0
  261. package/dist/project/__frozen-schemas__/schema-v3.9.2.d.ts +224 -224
  262. package/dist/project/frontmatter.d.ts.map +1 -1
  263. package/dist/project/frontmatter.js +11 -0
  264. package/dist/project/frontmatter.js.map +1 -1
  265. package/dist/project/frontmatter.test.js +71 -0
  266. package/dist/project/frontmatter.test.js.map +1 -1
  267. package/dist/state/completion.d.ts +1 -1
  268. package/dist/state/completion.d.ts.map +1 -1
  269. package/dist/state/completion.js +10 -8
  270. package/dist/state/completion.js.map +1 -1
  271. package/dist/state/decision-logger.d.ts +3 -2
  272. package/dist/state/decision-logger.d.ts.map +1 -1
  273. package/dist/state/decision-logger.js +12 -11
  274. package/dist/state/decision-logger.js.map +1 -1
  275. package/dist/state/ensure-v3-migration.d.ts +9 -0
  276. package/dist/state/ensure-v3-migration.d.ts.map +1 -0
  277. package/dist/state/ensure-v3-migration.js +35 -0
  278. package/dist/state/ensure-v3-migration.js.map +1 -0
  279. package/dist/state/lock-manager.d.ts +5 -4
  280. package/dist/state/lock-manager.d.ts.map +1 -1
  281. package/dist/state/lock-manager.js +11 -11
  282. package/dist/state/lock-manager.js.map +1 -1
  283. package/dist/state/rework-manager.d.ts +1 -2
  284. package/dist/state/rework-manager.d.ts.map +1 -1
  285. package/dist/state/rework-manager.js +4 -5
  286. package/dist/state/rework-manager.js.map +1 -1
  287. package/dist/state/state-manager.d.ts +25 -1
  288. package/dist/state/state-manager.d.ts.map +1 -1
  289. package/dist/state/state-manager.js +86 -12
  290. package/dist/state/state-manager.js.map +1 -1
  291. package/dist/state/state-manager.test.js +278 -0
  292. package/dist/state/state-manager.test.js.map +1 -1
  293. package/dist/state/state-migration-v3.d.ts +22 -0
  294. package/dist/state/state-migration-v3.d.ts.map +1 -0
  295. package/dist/state/state-migration-v3.js +82 -0
  296. package/dist/state/state-migration-v3.js.map +1 -0
  297. package/dist/state/state-migration-v3.test.d.ts +2 -0
  298. package/dist/state/state-migration-v3.test.d.ts.map +1 -0
  299. package/dist/state/state-migration-v3.test.js +196 -0
  300. package/dist/state/state-migration-v3.test.js.map +1 -0
  301. package/dist/state/state-migration.d.ts.map +1 -1
  302. package/dist/state/state-migration.js +11 -6
  303. package/dist/state/state-migration.js.map +1 -1
  304. package/dist/state/state-migration.test.js +47 -2
  305. package/dist/state/state-migration.test.js.map +1 -1
  306. package/dist/state/state-path-resolver.d.ts +23 -0
  307. package/dist/state/state-path-resolver.d.ts.map +1 -0
  308. package/dist/state/state-path-resolver.js +36 -0
  309. package/dist/state/state-path-resolver.js.map +1 -0
  310. package/dist/state/state-path-resolver.test.d.ts +2 -0
  311. package/dist/state/state-path-resolver.test.d.ts.map +1 -0
  312. package/dist/state/state-path-resolver.test.js +78 -0
  313. package/dist/state/state-path-resolver.test.js.map +1 -0
  314. package/dist/state/state-version-dispatch.d.ts +17 -0
  315. package/dist/state/state-version-dispatch.d.ts.map +1 -0
  316. package/dist/state/state-version-dispatch.js +27 -0
  317. package/dist/state/state-version-dispatch.js.map +1 -0
  318. package/dist/state/state-version-dispatch.test.d.ts +2 -0
  319. package/dist/state/state-version-dispatch.test.d.ts.map +1 -0
  320. package/dist/state/state-version-dispatch.test.js +40 -0
  321. package/dist/state/state-version-dispatch.test.js.map +1 -0
  322. package/dist/types/config.d.ts +25 -3
  323. package/dist/types/config.d.ts.map +1 -1
  324. package/dist/types/config.test.js +13 -1
  325. package/dist/types/config.test.js.map +1 -1
  326. package/dist/types/dependency.d.ts +5 -0
  327. package/dist/types/dependency.d.ts.map +1 -1
  328. package/dist/types/frontmatter.d.ts +5 -0
  329. package/dist/types/frontmatter.d.ts.map +1 -1
  330. package/dist/types/lock.d.ts +1 -1
  331. package/dist/types/lock.d.ts.map +1 -1
  332. package/dist/types/state.d.ts +1 -1
  333. package/dist/types/state.d.ts.map +1 -1
  334. package/dist/utils/artifact-path.d.ts +19 -0
  335. package/dist/utils/artifact-path.d.ts.map +1 -0
  336. package/dist/utils/artifact-path.js +95 -0
  337. package/dist/utils/artifact-path.js.map +1 -0
  338. package/dist/utils/artifact-path.test.d.ts +2 -0
  339. package/dist/utils/artifact-path.test.d.ts.map +1 -0
  340. package/dist/utils/artifact-path.test.js +138 -0
  341. package/dist/utils/artifact-path.test.js.map +1 -0
  342. package/dist/utils/errors.d.ts +1 -1
  343. package/dist/utils/errors.d.ts.map +1 -1
  344. package/dist/utils/errors.js +5 -2
  345. package/dist/utils/errors.js.map +1 -1
  346. package/dist/utils/user-errors.d.ts +46 -0
  347. package/dist/utils/user-errors.d.ts.map +1 -0
  348. package/dist/utils/user-errors.js +76 -0
  349. package/dist/utils/user-errors.js.map +1 -0
  350. package/dist/utils/user-errors.test.d.ts +2 -0
  351. package/dist/utils/user-errors.test.d.ts.map +1 -0
  352. package/dist/utils/user-errors.test.js +74 -0
  353. package/dist/utils/user-errors.test.js.map +1 -0
  354. package/dist/validation/index.d.ts.map +1 -1
  355. package/dist/validation/index.js +16 -0
  356. package/dist/validation/index.js.map +1 -1
  357. package/dist/validation/index.test.js +48 -0
  358. package/dist/validation/index.test.js.map +1 -1
  359. package/dist/validation/state-validator.d.ts +5 -2
  360. package/dist/validation/state-validator.d.ts.map +1 -1
  361. package/dist/validation/state-validator.js +18 -20
  362. package/dist/validation/state-validator.js.map +1 -1
  363. package/dist/validation/state-validator.test.js +31 -2
  364. package/dist/validation/state-validator.test.js.map +1 -1
  365. package/dist/wizard/copy/backend.d.ts.map +1 -1
  366. package/dist/wizard/copy/backend.js +12 -0
  367. package/dist/wizard/copy/backend.js.map +1 -1
  368. package/dist/wizard/flags.d.ts +1 -0
  369. package/dist/wizard/flags.d.ts.map +1 -1
  370. package/dist/wizard/questions.d.ts.map +1 -1
  371. package/dist/wizard/questions.js +5 -1
  372. package/dist/wizard/questions.js.map +1 -1
  373. package/dist/wizard/questions.test.js +45 -2
  374. package/dist/wizard/questions.test.js.map +1 -1
  375. package/dist/wizard/wizard.d.ts +23 -0
  376. package/dist/wizard/wizard.d.ts.map +1 -1
  377. package/dist/wizard/wizard.js +85 -47
  378. package/dist/wizard/wizard.js.map +1 -1
  379. package/dist/wizard/wizard.test.js +186 -1
  380. package/dist/wizard/wizard.test.js.map +1 -1
  381. package/package.json +1 -1
@@ -0,0 +1,706 @@
1
+ ---
2
+ name: multi-service-auth
3
+ description: Mutual TLS, service tokens, zero-trust architecture, and audience scoping
4
+ topics: [mtls, service-tokens, zero-trust, audience-scoping, token-rotation]
5
+ ---
6
+
7
+ ## Summary
8
+
9
+ Service-to-service authentication is distinct from user authentication. Services are long-running workloads with machine identities, not human users. Relying on network perimeter security ("inside the firewall means trusted") is insufficient for modern multi-service architectures.
10
+
11
+ **Zero-trust principles:** Every service-to-service call must be authenticated and authorized regardless of source. All traffic is encrypted. Services request only minimum required permissions. Assume any service may be compromised and limit lateral movement.
12
+
13
+ **Two complementary layers:**
14
+ - **mTLS (transport layer):** Both services present certificates signed by a trusted internal CA. Provides encryption and cryptographic identity verification. A service mesh (Istio/Linkerd) can handle mTLS transparently without application code changes.
15
+ - **Service JWTs (application layer):** Short-lived tokens (5 min TTL) carrying claims: `iss` (caller), `sub` (caller identity), `aud` (target service), `scope` (authorized operations), and optional propagated user context.
16
+
17
+ **Audience scoping:** Tokens must include the target service's identifier in the `aud` claim. Without this, a stolen token can be replayed against any service.
18
+
19
+ **SPIFFE/SPIRE:** Standard workload identity framework for multi-cloud/multi-cluster environments where a service mesh is insufficient.
20
+
21
+ **Secret rotation:** mTLS certificates every 24–90 days; JWT signing keys every 30–90 days; database passwords every 30–90 days. Use zero-downtime rotation via JWKS endpoints that serve both old and new public keys during the transition window.
22
+
23
+ ## Deep Guidance
24
+
25
+ ## Zero-Trust Architecture Principles
26
+
27
+ ### Never Trust the Network
28
+
29
+ The traditional security perimeter model assumes that traffic inside the network is trusted. Zero-trust inverts this: every request must be authenticated and authorized regardless of its source.
30
+
31
+ **Core zero-trust rules:**
32
+ 1. Every service-to-service call is authenticated — no implicit trust based on network location.
33
+ 2. Every service-to-service call is authorized — the caller must have explicit permission to call the specific endpoint.
34
+ 3. All traffic is encrypted — no plaintext HTTP between services, even within a private VPC.
35
+ 4. Least privilege — services request only the permissions they need, scoped to the minimum audience.
36
+ 5. Assume breach — design for the case where one service is compromised. Lateral movement must be limited.
37
+
38
+ **Trade-offs (zero-trust):**
39
+ - (+) Compromised service cannot access all other services — blast radius is limited by explicit authorization.
40
+ - (+) Insider threat mitigation — a rogue actor who gains network access cannot impersonate services.
41
+ - (+) Audit trail — every service call has authenticated identity attached, enabling forensic analysis.
42
+ - (-) Operational complexity — certificates, tokens, and rotation must be managed as infrastructure.
43
+ - (-) Latency overhead — mTLS handshake and token validation add a few milliseconds per request.
44
+ - (-) Requires consistent adoption — a single service using HTTP without auth is a gap in the model.
45
+
46
+ ### Identity for Workloads
47
+
48
+ Every service must have a cryptographic identity that other services can verify. The two primary models:
49
+
50
+ **Certificate-based identity (mTLS):** The service presents a TLS client certificate. The certificate's Subject or SAN identifies the service. Verification is done at the TLS layer.
51
+
52
+ **Token-based identity (JWT):** The service presents a signed token in the `Authorization` header. The token contains claims identifying the service. Verification is done at the application layer.
53
+
54
+ Use mTLS and JWTs together: mTLS at the transport layer for encryption and certificate-based authentication; JWTs at the application layer for fine-grained authorization claims. A service mesh (Istio, Linkerd) can handle mTLS transparently, leaving application-layer JWT for business authorization logic.
55
+
56
+ ## Mutual TLS (mTLS)
57
+
58
+ ### How mTLS Works
59
+
60
+ In standard TLS, the client verifies the server's certificate. In mutual TLS, both sides present certificates:
61
+
62
+ ```
63
+ Client (Service A) Server (Service B)
64
+ | |
65
+ |--- ClientHello -----------------------> |
66
+ |<-- ServerHello + Server Cert ----------- |
67
+ |--- Client Cert + ClientKeyExchange ----> | (A proves identity to B)
68
+ |<-- Finished (session established) ------ |
69
+ |--- Encrypted request ------------------> | (B verifies A's cert)
70
+ |<-- Encrypted response ------------------ |
71
+ ```
72
+
73
+ Both sides verify that the certificate was signed by a trusted Certificate Authority (CA). The CA can be internal (managed by your organization) or a public CA.
74
+
75
+ ### Certificate Management with Internal CA
76
+
77
+ For service meshes and internal APIs, an internal CA is the standard approach:
78
+
79
+ ```bash
80
+ # Create internal CA using OpenSSL
81
+ openssl genrsa -out ca-key.pem 4096
82
+ openssl req -new -x509 -days 3650 -key ca-key.pem -out ca-cert.pem \
83
+ -subj "/C=US/O=Acme Corp/CN=Acme Internal CA"
84
+
85
+ # Generate service certificate (for order-service)
86
+ openssl genrsa -out order-service-key.pem 2048
87
+ openssl req -new -key order-service-key.pem -out order-service.csr \
88
+ -subj "/C=US/O=Acme Corp/CN=order-service"
89
+
90
+ # Sign with internal CA — certificate expires in 90 days (rotate frequently)
91
+ openssl x509 -req -days 90 -in order-service.csr \
92
+ -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial \
93
+ -out order-service-cert.pem \
94
+ -extfile <(printf "subjectAltName=DNS:order-service,DNS:order-service.production.svc.cluster.local")
95
+ ```
96
+
97
+ ### mTLS Configuration in Node.js
98
+
99
+ ```typescript
100
+ import https from 'https';
101
+ import fs from 'fs';
102
+ import tls from 'tls';
103
+
104
+ // Server: require client certificates
105
+ const serverOptions: https.ServerOptions = {
106
+ key: fs.readFileSync('/etc/certs/order-service-key.pem'),
107
+ cert: fs.readFileSync('/etc/certs/order-service-cert.pem'),
108
+ ca: fs.readFileSync('/etc/certs/ca-cert.pem'),
109
+ requestCert: true, // Request client certificate
110
+ rejectUnauthorized: true, // Reject if client cert is not trusted by CA
111
+ };
112
+
113
+ const server = https.createServer(serverOptions, (req, res) => {
114
+ const clientCert = (req.socket as tls.TLSSocket).getPeerCertificate();
115
+
116
+ if (!clientCert || !clientCert.subject) {
117
+ res.writeHead(401);
118
+ res.end('Client certificate required');
119
+ return;
120
+ }
121
+
122
+ // Extract service identity from certificate subject
123
+ const callerServiceName = clientCert.subject.CN;
124
+
125
+ // Authorize: check if this service is allowed to call this endpoint
126
+ if (!isAuthorized(callerServiceName, req.method, req.url)) {
127
+ res.writeHead(403);
128
+ res.end(`Service ${callerServiceName} not authorized for this endpoint`);
129
+ return;
130
+ }
131
+
132
+ // Attach caller identity to request context for audit logging
133
+ (req as any).callerService = callerServiceName;
134
+ handleRequest(req, res);
135
+ });
136
+
137
+ // Client: present certificate when calling other services
138
+ const clientOptions: https.RequestOptions = {
139
+ key: fs.readFileSync('/etc/certs/order-service-key.pem'),
140
+ cert: fs.readFileSync('/etc/certs/order-service-cert.pem'),
141
+ ca: fs.readFileSync('/etc/certs/ca-cert.pem'), // Trust internal CA only
142
+ rejectUnauthorized: true,
143
+ };
144
+
145
+ async function callInventoryService(path: string): Promise<unknown> {
146
+ return new Promise((resolve, reject) => {
147
+ const req = https.request(
148
+ { hostname: 'inventory-service', port: 443, path, ...clientOptions },
149
+ (res) => {
150
+ let data = '';
151
+ res.on('data', chunk => data += chunk);
152
+ res.on('end', () => resolve(JSON.parse(data)));
153
+ }
154
+ );
155
+ req.on('error', reject);
156
+ req.end();
157
+ });
158
+ }
159
+ ```
160
+
161
+ ### mTLS in Kubernetes with Istio
162
+
163
+ If using a service mesh, mTLS is handled transparently at the sidecar layer — application code does not need to manage certificates:
164
+
165
+ ```yaml
166
+ # Istio PeerAuthentication — require mTLS for all services in namespace
167
+ apiVersion: security.istio.io/v1beta1
168
+ kind: PeerAuthentication
169
+ metadata:
170
+ name: default
171
+ namespace: production
172
+ spec:
173
+ mtls:
174
+ mode: STRICT # STRICT = require mTLS; PERMISSIVE = allow both (for migration)
175
+ ---
176
+ # Istio AuthorizationPolicy — control which services can call order-service
177
+ apiVersion: security.istio.io/v1beta1
178
+ kind: AuthorizationPolicy
179
+ metadata:
180
+ name: order-service-authz
181
+ namespace: production
182
+ spec:
183
+ selector:
184
+ matchLabels:
185
+ app: order-service
186
+ rules:
187
+ - from:
188
+ - source:
189
+ principals:
190
+ # Only allow calls from these service identities (SPIFFE URIs)
191
+ - "cluster.local/ns/production/sa/checkout-service"
192
+ - "cluster.local/ns/production/sa/fulfillment-service"
193
+ to:
194
+ - operation:
195
+ methods: ["GET", "POST"]
196
+ paths: ["/orders", "/orders/*"]
197
+ ```
198
+
199
+ **Trade-offs (mTLS):**
200
+ - (+) Encryption and mutual authentication in one mechanism — transport and identity together.
201
+ - (+) Certificate-based identity is cryptographically strong. Cannot be forged without the private key.
202
+ - (+) Service mesh handles certificate rotation automatically (Istio rotates every 24 hours by default).
203
+ - (-) Certificate management complexity — requires a CA, cert distribution, rotation automation.
204
+ - (-) TLS handshake adds latency (~1-5ms for the initial connection).
205
+ - (-) Debugging TLS issues requires familiarity with certificate tooling (openssl, istioctl).
206
+
207
+ ## Service-to-Service JWT Patterns
208
+
209
+ ### Why JWTs for Inter-Service Auth
210
+
211
+ While mTLS provides transport-level identity, JWTs provide application-level claims that carry richer context:
212
+
213
+ - Which service is calling (subject)
214
+ - Which service is the intended recipient (audience)
215
+ - What permissions the caller has (scopes)
216
+ - The user context being acted on behalf of (propagated user identity)
217
+
218
+ This enables fine-grained authorization at the application layer, independent of network topology.
219
+
220
+ ### Token Issuance
221
+
222
+ An internal token issuer (auth service or a shared library) signs JWTs with a private key. Other services verify signatures using the corresponding public key.
223
+
224
+ ```typescript
225
+ import jwt from 'jsonwebtoken';
226
+ import fs from 'fs';
227
+
228
+ const PRIVATE_KEY = fs.readFileSync('/etc/secrets/service-signing-key.pem');
229
+ const TOKEN_TTL_SECONDS = 300; // 5 minutes — short-lived for internal tokens
230
+
231
+ interface ServiceTokenClaims {
232
+ iss: string; // Issuer: which service issued the token
233
+ sub: string; // Subject: the calling service's identity
234
+ aud: string; // Audience: the specific target service
235
+ iat: number; // Issued at
236
+ exp: number; // Expiration
237
+ jti: string; // JWT ID — unique token identifier (for replay prevention)
238
+ scope: string[]; // Authorized scopes for this token
239
+ // Optional: propagate user context
240
+ user_id?: string;
241
+ user_roles?: string[];
242
+ }
243
+
244
+ function issueServiceToken(options: {
245
+ callerService: string;
246
+ targetService: string;
247
+ scopes: string[];
248
+ userContext?: { userId: string; roles: string[] };
249
+ }): string {
250
+ const claims: ServiceTokenClaims = {
251
+ iss: options.callerService,
252
+ sub: options.callerService,
253
+ aud: options.targetService,
254
+ iat: Math.floor(Date.now() / 1000),
255
+ exp: Math.floor(Date.now() / 1000) + TOKEN_TTL_SECONDS,
256
+ jti: crypto.randomUUID(),
257
+ scope: options.scopes,
258
+ ...(options.userContext && {
259
+ user_id: options.userContext.userId,
260
+ user_roles: options.userContext.roles,
261
+ }),
262
+ };
263
+
264
+ return jwt.sign(claims, PRIVATE_KEY, { algorithm: 'RS256' });
265
+ }
266
+ ```
267
+
268
+ ### Token Validation
269
+
270
+ ```typescript
271
+ import jwt, { JwtPayload } from 'jsonwebtoken';
272
+
273
+ // Load public keys from a JWKS endpoint or static file
274
+ // In production: use a JWKS endpoint so keys can rotate without redeployment
275
+ const PUBLIC_KEYS = loadPublicKeys('/etc/certs/service-signing-pub.pem');
276
+
277
+ interface ValidationOptions {
278
+ expectedAudience: string; // This service's identity
279
+ allowedIssuers: string[]; // Which services are authorized to call
280
+ requiredScopes?: string[]; // Scopes required for this endpoint
281
+ }
282
+
283
+ async function validateServiceToken(
284
+ token: string,
285
+ options: ValidationOptions,
286
+ ): Promise<ServiceTokenClaims> {
287
+ let claims: JwtPayload;
288
+
289
+ try {
290
+ claims = jwt.verify(token, PUBLIC_KEYS, {
291
+ algorithms: ['RS256'],
292
+ audience: options.expectedAudience,
293
+ clockTolerance: 5, // Allow 5 seconds of clock skew
294
+ }) as JwtPayload;
295
+ } catch (error) {
296
+ if (error instanceof jwt.TokenExpiredError) {
297
+ throw new AuthError('TOKEN_EXPIRED', 'Service token has expired');
298
+ }
299
+ if (error instanceof jwt.JsonWebTokenError) {
300
+ throw new AuthError('TOKEN_INVALID', 'Service token signature is invalid');
301
+ }
302
+ throw error;
303
+ }
304
+
305
+ // Verify issuer is an authorized caller
306
+ if (!options.allowedIssuers.includes(claims.iss as string)) {
307
+ throw new AuthError('UNAUTHORIZED_ISSUER', `Service ${claims.iss} is not authorized`);
308
+ }
309
+
310
+ // Verify audience matches this service (prevents token reuse across services)
311
+ if (claims.aud !== options.expectedAudience) {
312
+ throw new AuthError('AUDIENCE_MISMATCH', 'Token was not issued for this service');
313
+ }
314
+
315
+ // Verify required scopes are present
316
+ if (options.requiredScopes) {
317
+ const tokenScopes = new Set<string>(claims.scope as string[]);
318
+ const missingScopes = options.requiredScopes.filter(s => !tokenScopes.has(s));
319
+ if (missingScopes.length > 0) {
320
+ throw new AuthError('INSUFFICIENT_SCOPE', `Missing scopes: ${missingScopes.join(', ')}`);
321
+ }
322
+ }
323
+
324
+ return claims as unknown as ServiceTokenClaims;
325
+ }
326
+
327
+ // Express middleware
328
+ function requireServiceAuth(requiredScopes?: string[]) {
329
+ return async (req: Request, res: Response, next: NextFunction) => {
330
+ const authHeader = req.headers.authorization;
331
+ if (!authHeader?.startsWith('Bearer ')) {
332
+ return res.status(401).json({ error: { code: 'MISSING_TOKEN' } });
333
+ }
334
+
335
+ const token = authHeader.slice(7);
336
+
337
+ try {
338
+ const claims = await validateServiceToken(token, {
339
+ expectedAudience: 'order-service',
340
+ allowedIssuers: ['checkout-service', 'fulfillment-service', 'admin-service'],
341
+ requiredScopes,
342
+ });
343
+
344
+ req.callerService = claims.sub;
345
+ req.callerScopes = claims.scope;
346
+ if (claims.user_id) req.propagatedUserId = claims.user_id;
347
+
348
+ next();
349
+ } catch (error) {
350
+ if (error instanceof AuthError) {
351
+ return res.status(401).json({ error: { code: error.code, message: error.message } });
352
+ }
353
+ next(error);
354
+ }
355
+ };
356
+ }
357
+ ```
358
+
359
+ **Trade-offs (service JWTs):**
360
+ - (+) Rich authorization claims — scopes, propagated user context, audience constraints.
361
+ - (+) Stateless validation — verifying a JWT requires only the public key, no database lookup.
362
+ - (+) Short TTL limits the window for a stolen token to be used.
363
+ - (-) Cannot revoke individual tokens before expiry (use short TTLs to limit damage).
364
+ - (-) Public key distribution requires a JWKS endpoint or coordinated rotation.
365
+ - (-) Token forwarding risk — a compromised service could forward its tokens to other services.
366
+
367
+ ## Audience Scoping
368
+
369
+ ### Why Audience Scoping Matters
370
+
371
+ Without audience scoping, a token issued for service A can be used to call service B. If an attacker steals a token from service A's request, they can replay it against any service.
372
+
373
+ **The attack without audience scoping:**
374
+ ```
375
+ Attacker intercepts token from A → B request
376
+ Attacker replays token to call C (which A has no business calling)
377
+ C accepts the token because it's validly signed
378
+ ```
379
+
380
+ **With audience scoping:**
381
+ ```
382
+ Token for A → B includes aud: "service-B"
383
+ Attacker replays token to call C
384
+ C rejects: aud "service-B" does not match "service-C"
385
+ ```
386
+
387
+ ### Audience Scoping Patterns
388
+
389
+ **Per-service audiences:** Each service has a unique audience identifier. Tokens are issued with a specific target service's audience.
390
+
391
+ ```typescript
392
+ // Token issued by checkout-service to call order-service
393
+ const token = issueServiceToken({
394
+ callerService: 'checkout-service',
395
+ targetService: 'order-service', // aud: "order-service"
396
+ scopes: ['orders:create', 'orders:read'],
397
+ });
398
+
399
+ // This token cannot be used to call inventory-service
400
+ // inventory-service expects aud: "inventory-service"
401
+ ```
402
+
403
+ **Per-operation audiences:** For even tighter scoping, include the operation in the audience:
404
+
405
+ ```typescript
406
+ // Token scoped to a specific endpoint
407
+ const token = issueServiceToken({
408
+ callerService: 'billing-service',
409
+ targetService: 'order-service:GET:/orders/:id',
410
+ scopes: ['orders:read'],
411
+ });
412
+ ```
413
+
414
+ **Trade-offs (audience scoping):**
415
+ - (+) Stolen tokens cannot be replayed against arbitrary services.
416
+ - (+) Defense-in-depth: limits blast radius if one service is compromised.
417
+ - (-) More tokens to manage — each service pair requires separate token issuance.
418
+ - (-) Token caching becomes harder — a token cached for A→B cannot be reused for A→C.
419
+
420
+ ### Token Caching for Performance
421
+
422
+ Short-lived tokens (5 minutes) require frequent issuance. Cache tokens with a safety margin:
423
+
424
+ ```typescript
425
+ class ServiceTokenCache {
426
+ private cache = new Map<string, { token: string; expiresAt: number }>();
427
+ private readonly SAFETY_MARGIN_MS = 30_000; // Refresh 30s before expiry
428
+
429
+ async getToken(callerService: string, targetService: string, scopes: string[]): Promise<string> {
430
+ const cacheKey = `${callerService}:${targetService}:${scopes.sort().join(',')}`;
431
+ const cached = this.cache.get(cacheKey);
432
+
433
+ if (cached && cached.expiresAt - Date.now() > this.SAFETY_MARGIN_MS) {
434
+ return cached.token;
435
+ }
436
+
437
+ const token = await issueServiceToken({ callerService, targetService, scopes });
438
+ const decoded = jwt.decode(token) as JwtPayload;
439
+
440
+ this.cache.set(cacheKey, {
441
+ token,
442
+ expiresAt: (decoded.exp ?? 0) * 1000,
443
+ });
444
+
445
+ return token;
446
+ }
447
+ }
448
+ ```
449
+
450
+ ## SPIFFE/SPIRE Identity Framework
451
+
452
+ ### What SPIFFE Provides
453
+
454
+ SPIFFE (Secure Production Identity Framework for Everyone) is an open standard for workload identity in multi-cloud and multi-cluster environments. SPIRE is its reference implementation.
455
+
456
+ **Core concept:** Every workload gets a SPIFFE Verifiable Identity Document (SVID) — a short-lived X.509 certificate or JWT containing a SPIFFE ID:
457
+
458
+ ```
459
+ spiffe://trust-domain/path/to/workload
460
+
461
+ # Examples:
462
+ spiffe://acme.com/ns/production/sa/order-service
463
+ spiffe://acme.com/ns/production/sa/checkout-service
464
+ ```
465
+
466
+ ### SPIRE Architecture
467
+
468
+ ```
469
+ SPIRE Server (runs once per cluster/region)
470
+ ↓ issues SVIDs via workload API
471
+ SPIRE Agent (runs on every node as DaemonSet)
472
+ ↓ attests workload identity
473
+ Workload (your service)
474
+ ↓ presents SVID for mTLS or JWT auth
475
+ ```
476
+
477
+ **SPIRE agent configuration:**
478
+
479
+ ```hcl
480
+ # spire-agent.conf
481
+ agent {
482
+ data_dir = "/var/lib/spire/agent"
483
+ log_level = "INFO"
484
+ trust_domain = "acme.com"
485
+ server_address = "spire-server.spire.svc.cluster.local"
486
+ server_port = 8081
487
+ }
488
+
489
+ plugins {
490
+ KeyManager "disk" {
491
+ plugin_data {
492
+ directory = "/var/lib/spire/agent/keys"
493
+ }
494
+ }
495
+
496
+ NodeAttestor "k8s_psat" {
497
+ plugin_data {
498
+ cluster = "production-cluster"
499
+ }
500
+ }
501
+
502
+ WorkloadAttestor "k8s" {
503
+ plugin_data {
504
+ skip_kubelet_verification = false
505
+ }
506
+ }
507
+ }
508
+ ```
509
+
510
+ **SPIRE registration entry (maps workload to SPIFFE ID):**
511
+
512
+ ```bash
513
+ # Register order-service workload
514
+ spire-server entry create \
515
+ -spiffeID spiffe://acme.com/ns/production/sa/order-service \
516
+ -parentID spiffe://acme.com/spire/agent/k8s_psat/production-cluster/node1 \
517
+ -selector k8s:ns:production \
518
+ -selector k8s:sa:order-service \
519
+ -ttl 3600
520
+ ```
521
+
522
+ **Trade-offs (SPIFFE/SPIRE):**
523
+ - (+) Automated workload identity — no manual certificate management per service.
524
+ - (+) Short-lived SVIDs (hours, not years) — compromise window is narrow.
525
+ - (+) Works across clouds, clusters, and on-premises — not tied to one provider.
526
+ - (-) Operational overhead — SPIRE Server and Agent fleet must be highly available.
527
+ - (-) Learning curve — SPIFFE concepts and SPIRE configuration are non-trivial.
528
+ - (-) Not necessary for single-cluster deployments where Istio/Linkerd already provide workload identity.
529
+
530
+ **Use SPIFFE/SPIRE when:** Multi-cloud or multi-cluster architectures where workload identity must span infrastructure boundaries, or when you need a provider-agnostic identity standard.
531
+
532
+ ## Secret Rotation Strategies
533
+
534
+ ### Rotation Principles
535
+
536
+ Secrets that never rotate are a liability: a leaked secret is valid forever. All credentials — certificates, tokens, API keys, database passwords — should be rotated on a schedule shorter than your expected breach detection time.
537
+
538
+ **Target rotation windows:**
539
+ - mTLS certificates: 24–90 days (Istio default: 24 hours)
540
+ - Service JWT signing keys: 30–90 days
541
+ - Database passwords: 30–90 days
542
+ - API keys (third-party): 90–365 days (limited by provider)
543
+ - Secrets in Kubernetes: sync with source on every deployment
544
+
545
+ ### Zero-Downtime Key Rotation
546
+
547
+ Rotating JWT signing keys requires a transition period where both old and new keys are valid:
548
+
549
+ ```typescript
550
+ // JWKS endpoint — serves multiple active keys
551
+ // Tokens signed with old key are still valid until they expire
552
+ // New tokens are signed with the new key
553
+ app.get('/.well-known/jwks.json', (req, res) => {
554
+ res.json({
555
+ keys: [
556
+ // New key (primary — used for signing new tokens)
557
+ {
558
+ kid: 'key-2026-04',
559
+ kty: 'RSA',
560
+ use: 'sig',
561
+ alg: 'RS256',
562
+ n: newKeyPublicN,
563
+ e: 'AQAB',
564
+ },
565
+ // Old key (secondary — only for verifying tokens signed before rotation)
566
+ // Remove this entry after all old tokens have expired (after max TTL)
567
+ {
568
+ kid: 'key-2026-01',
569
+ kty: 'RSA',
570
+ use: 'sig',
571
+ alg: 'RS256',
572
+ n: oldKeyPublicN,
573
+ e: 'AQAB',
574
+ },
575
+ ],
576
+ });
577
+ });
578
+
579
+ // Validator: try all active public keys — succeeds if any key verifies the token
580
+ async function verifyWithAnyActiveKey(token: string): Promise<JwtPayload> {
581
+ const header = jwt.decode(token, { complete: true })?.header;
582
+ const kid = header?.kid;
583
+
584
+ // Try the specific key if kid is present
585
+ if (kid) {
586
+ const key = getKeyById(kid);
587
+ if (!key) throw new AuthError('UNKNOWN_KEY_ID', `Key ${kid} not found`);
588
+ return jwt.verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
589
+ }
590
+
591
+ // No kid — try all active keys
592
+ for (const key of getActivePublicKeys()) {
593
+ try {
594
+ return jwt.verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
595
+ } catch {
596
+ continue;
597
+ }
598
+ }
599
+
600
+ throw new AuthError('TOKEN_INVALID', 'No active key could verify this token');
601
+ }
602
+ ```
603
+
604
+ ### Secret Management Infrastructure
605
+
606
+ ```typescript
607
+ // Kubernetes Secret with rotation via external-secrets-operator
608
+ // Syncs from AWS Secrets Manager / HashiCorp Vault on a schedule
609
+ ```
610
+
611
+ ```yaml
612
+ # external-secrets-operator ExternalSecret
613
+ apiVersion: external-secrets.io/v1beta1
614
+ kind: ExternalSecret
615
+ metadata:
616
+ name: order-service-secrets
617
+ namespace: production
618
+ spec:
619
+ refreshInterval: 1h # Re-sync from source every hour
620
+ secretStoreRef:
621
+ name: aws-secretsmanager
622
+ kind: ClusterSecretStore
623
+ target:
624
+ name: order-service-secrets # Kubernetes Secret name
625
+ creationPolicy: Owner
626
+ template:
627
+ engineVersion: v2
628
+ data:
629
+ DATABASE_URL: "{{ .db_url }}"
630
+ JWT_SIGNING_KEY: "{{ .jwt_private_key }}"
631
+ data:
632
+ - secretKey: db_url
633
+ remoteRef:
634
+ key: production/order-service/database
635
+ property: url
636
+ - secretKey: jwt_private_key
637
+ remoteRef:
638
+ key: production/order-service/jwt
639
+ property: private_key
640
+ ```
641
+
642
+ **Trade-offs (rotation):**
643
+ - (+) Limits the damage window if a secret is leaked — the leaked value expires.
644
+ - (+) Forces secret hygiene — stale secrets are cleaned up on a schedule.
645
+ - (-) Requires automation — manual rotation at scale is error-prone and skipped.
646
+ - (-) Rotation bugs cause outages — test rotation in staging before enabling in production.
647
+
648
+ ## Propagating User Context
649
+
650
+ When a user's request causes service A to call service B, service B may need to know who the original user is (for authorization, audit logging, or personalization). The user context must be propagated securely.
651
+
652
+ ```typescript
653
+ // Service A: propagate user context in the outgoing service token
654
+ const token = issueServiceToken({
655
+ callerService: 'checkout-service',
656
+ targetService: 'order-service',
657
+ scopes: ['orders:create'],
658
+ userContext: {
659
+ userId: req.user.id,
660
+ roles: req.user.roles,
661
+ },
662
+ });
663
+
664
+ // Service B: extract and use the propagated user context
665
+ app.post('/orders', requireServiceAuth(['orders:create']), async (req, res) => {
666
+ const userId = req.propagatedUserId; // Set by auth middleware from JWT claims
667
+
668
+ // Use for authorization: can this user create an order?
669
+ const user = await userService.getUser(userId);
670
+ if (!user.canPlaceOrders()) {
671
+ return res.status(403).json({ error: { code: 'USER_NOT_AUTHORIZED' } });
672
+ }
673
+
674
+ // Use for audit logging: attribute the action to the user, not the service
675
+ logger.info({ userId, callerService: req.callerService }, 'Creating order on behalf of user');
676
+
677
+ const order = await createOrder({ userId, ...req.body });
678
+ res.status(201).json(order);
679
+ });
680
+ ```
681
+
682
+ **Trade-offs:**
683
+ - (+) Service B has full context for authorization and audit — the user identity is not lost in the call chain.
684
+ - (-) Never use the propagated user context without also verifying the caller service is authorized. A malicious caller could forge user context claims if the token itself is valid.
685
+
686
+ ## Common Pitfalls
687
+
688
+ **Network trust without mTLS.** Assuming that traffic inside a VPC or Kubernetes cluster is trusted. A compromised pod can make requests to any other pod. Fix: enforce mTLS between all services. Use a service mesh with STRICT mTLS mode.
689
+
690
+ **Long-lived service credentials.** API keys or static tokens that never expire. A leaked credential is valid indefinitely. Fix: use short-lived JWTs (5-15 minutes) and automate rotation of all longer-lived credentials.
691
+
692
+ **No audience validation.** Tokens are validated for signature but not audience. A token issued for service A can be replayed against service B. Fix: always validate the `aud` claim matches the expected audience for this service.
693
+
694
+ **Broad token scopes.** Issuing tokens with `scope: ["*"]` or all permissions. A compromised caller can do anything its target service allows. Fix: request and grant minimum required scopes per operation.
695
+
696
+ **Secret sprawl.** Secrets hard-coded in environment variables, config files, or source code. Rotation requires redeployment. Fix: use a secrets manager (Vault, AWS Secrets Manager) and inject secrets at runtime via external-secrets-operator or equivalent.
697
+
698
+ **Missing token replay prevention.** The same JWT can be presented multiple times within its validity window. Fix: for high-value operations, maintain a token revocation list keyed by `jti` and reject tokens whose `jti` has been seen within the validity window.
699
+
700
+ **Certificate pinning without rotation plan.** Hard-coding a certificate's fingerprint in a service for "extra security." When the certificate rotates, the pinned service breaks. Fix: pin the CA certificate, not the leaf certificate — or use the JWKS endpoint pattern for key distribution.
701
+
702
+ ## See Also
703
+
704
+ - [multi-service-architecture](./multi-service-architecture.md) — Service discovery and networking topology
705
+ - [multi-service-api-contracts](./multi-service-api-contracts.md) — API versioning, retries, and idempotency
706
+ - [security-best-practices](./security-best-practices.md) — OWASP Top 10, secrets management, and threat modeling