@friggframework/devtools 2.0.0-next.11 → 2.0.0-next.110

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 (247) hide show
  1. package/frigg-cli/README.md +1289 -0
  2. package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
  3. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +649 -0
  4. package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
  5. package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
  6. package/frigg-cli/__tests__/unit/commands/generate-iam.test.js +97 -0
  7. package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
  8. package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
  9. package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
  10. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +397 -0
  11. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +345 -0
  12. package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
  13. package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
  14. package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
  15. package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
  16. package/frigg-cli/__tests__/utils/test-setup.js +287 -0
  17. package/frigg-cli/auth-command/CLAUDE.md +293 -0
  18. package/frigg-cli/auth-command/README.md +450 -0
  19. package/frigg-cli/auth-command/api-key-flow.js +153 -0
  20. package/frigg-cli/auth-command/auth-tester.js +344 -0
  21. package/frigg-cli/auth-command/credential-storage.js +182 -0
  22. package/frigg-cli/auth-command/index.js +256 -0
  23. package/frigg-cli/auth-command/json-schema-form.js +67 -0
  24. package/frigg-cli/auth-command/module-loader.js +172 -0
  25. package/frigg-cli/auth-command/oauth-callback-server.js +431 -0
  26. package/frigg-cli/auth-command/oauth-flow.js +195 -0
  27. package/frigg-cli/auth-command/utils/browser.js +30 -0
  28. package/frigg-cli/build-command/index.js +45 -12
  29. package/frigg-cli/db-setup-command/index.js +246 -0
  30. package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
  31. package/frigg-cli/deploy-command/index.js +339 -23
  32. package/frigg-cli/doctor-command/index.js +335 -0
  33. package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
  34. package/frigg-cli/generate-command/azure-generator.js +43 -0
  35. package/frigg-cli/generate-command/gcp-generator.js +47 -0
  36. package/frigg-cli/generate-command/index.js +333 -0
  37. package/frigg-cli/generate-command/terraform-generator.js +555 -0
  38. package/frigg-cli/generate-iam-command.js +119 -0
  39. package/frigg-cli/index.js +189 -1
  40. package/frigg-cli/index.test.js +1 -4
  41. package/frigg-cli/init-command/backend-first-handler.js +756 -0
  42. package/frigg-cli/init-command/index.js +93 -0
  43. package/frigg-cli/init-command/template-handler.js +143 -0
  44. package/frigg-cli/install-command/index.js +1 -4
  45. package/frigg-cli/jest.config.js +124 -0
  46. package/frigg-cli/package.json +63 -0
  47. package/frigg-cli/repair-command/index.js +564 -0
  48. package/frigg-cli/ssm-command/index.js +308 -0
  49. package/frigg-cli/ssm-command/index.test.js +318 -0
  50. package/frigg-cli/start-command/index.js +118 -5
  51. package/frigg-cli/start-command/start-command.test.js +297 -0
  52. package/frigg-cli/test/init-command.test.js +180 -0
  53. package/frigg-cli/test/npm-registry.test.js +319 -0
  54. package/frigg-cli/ui-command/index.js +154 -0
  55. package/frigg-cli/utils/app-resolver.js +319 -0
  56. package/frigg-cli/utils/backend-path.js +16 -17
  57. package/frigg-cli/utils/database-validator.js +167 -0
  58. package/frigg-cli/utils/error-messages.js +329 -0
  59. package/frigg-cli/utils/npm-registry.js +167 -0
  60. package/frigg-cli/utils/process-manager.js +199 -0
  61. package/frigg-cli/utils/repo-detection.js +405 -0
  62. package/infrastructure/ARCHITECTURE.md +487 -0
  63. package/infrastructure/CLAUDE.md +481 -0
  64. package/infrastructure/HEALTH.md +468 -0
  65. package/infrastructure/README.md +540 -0
  66. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  67. package/infrastructure/__tests__/helpers/test-utils.js +275 -0
  68. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  69. package/infrastructure/__tests__/scoped-environment.test.js +126 -0
  70. package/infrastructure/__tests__/ssm-preload-node-options.test.js +79 -0
  71. package/infrastructure/__tests__/template-generation.test.js +687 -0
  72. package/infrastructure/create-frigg-infrastructure.js +129 -20
  73. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  74. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
  75. package/infrastructure/docs/WEBSOCKET-CONFIGURATION.md +105 -0
  76. package/infrastructure/docs/deployment-instructions.md +268 -0
  77. package/infrastructure/docs/generate-iam-command.md +278 -0
  78. package/infrastructure/docs/iam-policy-templates.md +193 -0
  79. package/infrastructure/domains/admin-scripts/admin-script-builder.js +567 -0
  80. package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +1017 -0
  81. package/infrastructure/domains/admin-scripts/index.js +5 -0
  82. package/infrastructure/domains/database/aurora-builder.js +857 -0
  83. package/infrastructure/domains/database/aurora-builder.test.js +960 -0
  84. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  85. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  86. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  87. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  88. package/infrastructure/domains/database/migration-builder.js +757 -0
  89. package/infrastructure/domains/database/migration-builder.test.js +454 -0
  90. package/infrastructure/domains/database/migration-resolver.js +163 -0
  91. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  92. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  93. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  94. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  95. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  96. package/infrastructure/domains/health/application/ports/index.js +26 -0
  97. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  98. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  99. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  100. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  101. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  102. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  103. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  104. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  105. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  106. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  107. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  108. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  109. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  110. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  111. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  112. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  113. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  114. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  115. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  116. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  117. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  118. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  119. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  120. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  121. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  122. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  123. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  124. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  125. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  126. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  127. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  128. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  129. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  130. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  131. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  132. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  133. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  134. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  135. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  136. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  137. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  138. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  139. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  140. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  141. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  142. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  143. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  144. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  145. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  146. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  147. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  148. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  149. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  150. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  151. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  152. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  153. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  154. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  155. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  156. package/infrastructure/domains/integration/integration-builder.js +656 -0
  157. package/infrastructure/domains/integration/integration-builder.test.js +1064 -0
  158. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  159. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  160. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  161. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  162. package/infrastructure/domains/networking/vpc-builder.js +2101 -0
  163. package/infrastructure/domains/networking/vpc-builder.test.js +2001 -0
  164. package/infrastructure/domains/networking/vpc-discovery.js +184 -0
  165. package/infrastructure/domains/networking/vpc-discovery.test.js +368 -0
  166. package/infrastructure/domains/networking/vpc-resolver.js +514 -0
  167. package/infrastructure/domains/networking/vpc-resolver.test.js +841 -0
  168. package/infrastructure/domains/parameters/offload-utils.js +190 -0
  169. package/infrastructure/domains/parameters/offload-utils.test.js +193 -0
  170. package/infrastructure/domains/parameters/ssm-builder.js +132 -0
  171. package/infrastructure/domains/parameters/ssm-builder.test.js +346 -0
  172. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  173. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  174. package/infrastructure/domains/scheduler/scheduler-builder.js +247 -0
  175. package/infrastructure/domains/scheduler/scheduler-builder.test.js +118 -0
  176. package/infrastructure/domains/security/iam-generator.js +851 -0
  177. package/infrastructure/domains/security/iam-generator.test.js +287 -0
  178. package/infrastructure/domains/security/kms-builder.js +415 -0
  179. package/infrastructure/domains/security/kms-builder.test.js +392 -0
  180. package/infrastructure/domains/security/kms-discovery.js +80 -0
  181. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  182. package/infrastructure/domains/security/kms-resolver.js +96 -0
  183. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  184. package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +418 -0
  185. package/infrastructure/domains/security/templates/iam-policy-basic.json +218 -0
  186. package/infrastructure/domains/security/templates/iam-policy-full.json +293 -0
  187. package/infrastructure/domains/shared/base-builder.js +112 -0
  188. package/infrastructure/domains/shared/base-resolver.js +186 -0
  189. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  190. package/infrastructure/domains/shared/builder-orchestrator.js +226 -0
  191. package/infrastructure/domains/shared/builder-orchestrator.test.js +258 -0
  192. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  193. package/infrastructure/domains/shared/cloudformation-discovery.js +681 -0
  194. package/infrastructure/domains/shared/cloudformation-discovery.test.js +1320 -0
  195. package/infrastructure/domains/shared/environment-builder.js +192 -0
  196. package/infrastructure/domains/shared/environment-builder.test.js +529 -0
  197. package/infrastructure/domains/shared/function-environments.js +97 -0
  198. package/infrastructure/domains/shared/function-environments.test.js +146 -0
  199. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +579 -0
  200. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +416 -0
  201. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  202. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  203. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  204. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  205. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  206. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
  207. package/infrastructure/domains/shared/resource-discovery.js +256 -0
  208. package/infrastructure/domains/shared/resource-discovery.test.js +757 -0
  209. package/infrastructure/domains/shared/types/app-definition.js +225 -0
  210. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  211. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  212. package/infrastructure/domains/shared/types/index.js +46 -0
  213. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  214. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  215. package/infrastructure/domains/shared/utilities/base-definition-factory.js +409 -0
  216. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  217. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +319 -0
  218. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  219. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  220. package/infrastructure/domains/shared/utilities/nested-node-modules.js +53 -0
  221. package/infrastructure/domains/shared/utilities/nested-node-modules.test.js +61 -0
  222. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +159 -0
  223. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +444 -0
  224. package/infrastructure/domains/shared/validation/env-validator.js +78 -0
  225. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  226. package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
  227. package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
  228. package/infrastructure/esbuild.config.js +53 -0
  229. package/infrastructure/infrastructure-composer.js +168 -0
  230. package/infrastructure/infrastructure-composer.test.js +2042 -0
  231. package/infrastructure/integration.test.js +381 -0
  232. package/infrastructure/scripts/build-prisma-layer.js +701 -0
  233. package/infrastructure/scripts/build-prisma-layer.test.js +170 -0
  234. package/infrastructure/scripts/build-time-discovery.js +238 -0
  235. package/infrastructure/scripts/build-time-discovery.test.js +379 -0
  236. package/infrastructure/scripts/run-discovery.js +110 -0
  237. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  238. package/management-ui/README.md +203 -0
  239. package/package.json +48 -16
  240. package/test/index.js +2 -4
  241. package/test/mock-api.js +1 -3
  242. package/test/mock-integration.js +4 -14
  243. package/.eslintrc.json +0 -3
  244. package/CHANGELOG.md +0 -132
  245. package/infrastructure/serverless-template.js +0 -283
  246. package/infrastructure/webpack.config.js +0 -20
  247. package/test/auther-definition-tester.js +0 -125
@@ -0,0 +1,308 @@
1
+ /**
2
+ * frigg ssm push
3
+ *
4
+ * Writes SSM-offloaded environment values (see ADR-027) from the CLI
5
+ * process environment into Parameter Store. Runs automatically before
6
+ * `frigg deploy` so parameters exist before new code cold-starts; also
7
+ * available standalone to rotate values without deploying.
8
+ */
9
+
10
+ const path = require('path');
11
+ const dotenv = require('dotenv');
12
+ const {
13
+ getOffloadedKeys,
14
+ resolveParameterPrefix,
15
+ validateOffloadConfig,
16
+ } = require('../../infrastructure/domains/parameters/offload-utils');
17
+
18
+ const TIER_LIMITS = {
19
+ standard: 4096,
20
+ advanced: 8192,
21
+ };
22
+
23
+ const THROTTLE_BACKOFF_MS = [100, 200, 400];
24
+
25
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
26
+
27
+ /**
28
+ * Send a PutParameterCommand, retrying ThrottlingException with short
29
+ * exponential backoff (mirrors sendWithRetry in core/parameters-to-env.js).
30
+ */
31
+ async function putParameterWithRetry(client, PutParameterCommand, input) {
32
+ let attempt = 0;
33
+ for (;;) {
34
+ try {
35
+ return await client.send(new PutParameterCommand(input));
36
+ } catch (error) {
37
+ if (
38
+ error.name === 'ThrottlingException' &&
39
+ attempt < THROTTLE_BACKOFF_MS.length
40
+ ) {
41
+ await sleep(THROTTLE_BACKOFF_MS[attempt]);
42
+ attempt += 1;
43
+ continue;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolve the tier for a single offloaded key. An explicit `--tier` CLI
52
+ * option overrides every key; otherwise each key uses its own
53
+ * ssm.parameters[KEY].tier, defaulting to 'standard'.
54
+ */
55
+ function resolveKeyTier(appDefinition, key, options = {}) {
56
+ const raw =
57
+ options.tier ||
58
+ appDefinition.ssm?.parameters?.[key]?.tier ||
59
+ 'standard';
60
+ const tier = String(raw).toLowerCase();
61
+ if (!TIER_LIMITS[tier]) {
62
+ throw new Error(
63
+ `Unknown parameter tier '${raw}' for ${key}. Use 'standard' or 'advanced'.`
64
+ );
65
+ }
66
+ return tier;
67
+ }
68
+
69
+ /**
70
+ * Build the PutParameter specs for every offloaded key, reading values
71
+ * from process.env. Throws on invalid config, missing values (unless
72
+ * allowEmpty), or values that exceed their own tier limit.
73
+ */
74
+ function collectParameterSpecs(appDefinition, stage, options = {}) {
75
+ const { errors: configErrors } = validateOffloadConfig(appDefinition);
76
+ if (configErrors.length > 0) {
77
+ throw new Error(
78
+ `Invalid SSM offload configuration:\n - ${configErrors.join(
79
+ '\n - '
80
+ )}`
81
+ );
82
+ }
83
+
84
+ const keys = getOffloadedKeys(appDefinition);
85
+ if (keys.length === 0) {
86
+ return { specs: [], skipped: [] };
87
+ }
88
+
89
+ const prefix = resolveParameterPrefix(appDefinition, stage);
90
+ const specs = [];
91
+ const skipped = [];
92
+ const missing = [];
93
+ const oversized = [];
94
+
95
+ for (const key of keys) {
96
+ const value = process.env[key];
97
+
98
+ if (value === undefined || value === '') {
99
+ if (options.allowEmpty) {
100
+ skipped.push(key);
101
+ } else {
102
+ missing.push(key);
103
+ }
104
+ continue;
105
+ }
106
+
107
+ const tier = resolveKeyTier(appDefinition, key, options);
108
+ const maxBytes = TIER_LIMITS[tier];
109
+ const bytes = Buffer.byteLength(value, 'utf8');
110
+ if (bytes > maxBytes) {
111
+ oversized.push(
112
+ `${key} (${bytes} bytes > ${tier} tier limit ${maxBytes})`
113
+ );
114
+ continue;
115
+ }
116
+
117
+ specs.push({
118
+ key,
119
+ name: `${prefix}/${key}`,
120
+ value,
121
+ type: appDefinition.ssm?.parameters?.[key]?.type || 'String',
122
+ tier,
123
+ });
124
+ }
125
+
126
+ if (missing.length > 0) {
127
+ throw new Error(
128
+ `Missing or empty values for offloaded keys: ${missing.join(
129
+ ', '
130
+ )}. Set them in the environment (or .env) before pushing, or pass --allow-empty to skip them.`
131
+ );
132
+ }
133
+
134
+ if (oversized.length > 0) {
135
+ throw new Error(
136
+ `Offloaded values exceed their parameter tier limit: ${oversized.join(
137
+ ', '
138
+ )}. Values over ${TIER_LIMITS.standard} bytes need the advanced tier (up to ${TIER_LIMITS.advanced} bytes, billed by AWS): set ssm.parameters.<KEY>.tier: 'advanced' in the app definition, or pass --tier advanced to 'frigg ssm push'.`
139
+ );
140
+ }
141
+
142
+ return { specs, skipped };
143
+ }
144
+
145
+ /**
146
+ * Resolve the region parameters are pushed to. An explicit `--region`
147
+ * option wins; otherwise fall back to AWS_REGION and finally to the same
148
+ * default the composed stack uses ('us-east-1'), so pushes and cold-start
149
+ * reads always target the same region.
150
+ */
151
+ function resolvePushRegion(options = {}) {
152
+ return options.region || process.env.AWS_REGION || 'us-east-1';
153
+ }
154
+
155
+ /**
156
+ * Push all offloaded parameters for an app definition. Silent no-op when
157
+ * the offload set is empty. Returns { pushed, skipped }.
158
+ */
159
+ async function pushOffloadedParameters(appDefinition, stage, options = {}) {
160
+ const { specs, skipped } = collectParameterSpecs(
161
+ appDefinition,
162
+ stage,
163
+ options
164
+ );
165
+
166
+ if (specs.length === 0 && skipped.length === 0) {
167
+ return { pushed: [], skipped };
168
+ }
169
+
170
+ const {
171
+ SSMClient,
172
+ PutParameterCommand,
173
+ GetParametersCommand,
174
+ } = require('@aws-sdk/client-ssm');
175
+ const client = new SSMClient({
176
+ region: resolvePushRegion(options),
177
+ });
178
+
179
+ // --allow-empty only rotates keys that already exist. A skipped key still
180
+ // ships in FRIGG_SSM_OFFLOADED_KEYS, so if its parameter does not exist the
181
+ // runtime fails fast at cold start on every function — a green deploy that
182
+ // bricks the fleet. Verify existence and abort instead.
183
+ if (skipped.length > 0) {
184
+ const prefix = resolveParameterPrefix(appDefinition, stage);
185
+ const names = skipped.map((key) => `${prefix}/${key}`);
186
+ const existing = new Set();
187
+ for (let i = 0; i < names.length; i += 10) {
188
+ const { Parameters = [] } = await client.send(
189
+ new GetParametersCommand({ Names: names.slice(i, i + 10) })
190
+ );
191
+ for (const param of Parameters) {
192
+ existing.add(param.Name);
193
+ }
194
+ }
195
+ const orphaned = skipped.filter(
196
+ (key) => !existing.has(`${prefix}/${key}`)
197
+ );
198
+ if (orphaned.length > 0) {
199
+ throw new Error(
200
+ `--allow-empty skipped ${orphaned.join(
201
+ ', '
202
+ )}, but no such parameter exists in Parameter Store. --allow-empty ` +
203
+ `only rotates keys that already have a value; a skipped key with no ` +
204
+ `parameter stays in FRIGG_SSM_OFFLOADED_KEYS and fails every function ` +
205
+ `at cold start. Set a value and push without --allow-empty.`
206
+ );
207
+ }
208
+ for (const key of skipped) {
209
+ console.warn(
210
+ `⚠️ Skipping ${key}: no value in the environment; keeping the existing Parameter Store value (--allow-empty)`
211
+ );
212
+ }
213
+ }
214
+
215
+ if (specs.length === 0) {
216
+ return { pushed: [], skipped };
217
+ }
218
+
219
+ const pushed = [];
220
+ for (const spec of specs) {
221
+ const input = {
222
+ Name: spec.name,
223
+ Value: spec.value,
224
+ Type: spec.type,
225
+ Overwrite: true,
226
+ };
227
+ if (spec.tier === 'advanced') {
228
+ input.Tier = 'Advanced';
229
+ }
230
+ if (spec.type === 'SecureString' && appDefinition.ssm?.kmsKeyArn) {
231
+ input.KeyId = appDefinition.ssm.kmsKeyArn;
232
+ }
233
+
234
+ try {
235
+ const result = await putParameterWithRetry(
236
+ client,
237
+ PutParameterCommand,
238
+ input
239
+ );
240
+ pushed.push({ name: spec.name, version: result.Version });
241
+ console.log(
242
+ ` ✅ ${spec.name} (${spec.type}, v${result.Version})`
243
+ );
244
+ } catch (error) {
245
+ const failure = new Error(
246
+ `Failed to push parameter ${spec.name} (${spec.key}): ${error.message}`
247
+ );
248
+ failure.pushed = pushed;
249
+ throw failure;
250
+ }
251
+ }
252
+
253
+ return { pushed, skipped };
254
+ }
255
+
256
+ function loadAppDefinition() {
257
+ const appDefPath = path.join(process.cwd(), 'index.js');
258
+ const { Definition } = require(appDefPath);
259
+ return Definition;
260
+ }
261
+
262
+ /**
263
+ * `frigg ssm push` command action.
264
+ */
265
+ async function ssmPushCommand(options) {
266
+ dotenv.config();
267
+
268
+ let appDefinition;
269
+ try {
270
+ appDefinition = loadAppDefinition();
271
+ } catch (error) {
272
+ console.error(
273
+ `✗ Could not load the app definition from ${process.cwd()}/index.js: ${error.message}`
274
+ );
275
+ process.exit(1);
276
+ }
277
+
278
+ const keys = getOffloadedKeys(appDefinition);
279
+ if (keys.length === 0) {
280
+ console.log(
281
+ 'No environment variables are marked for SSM offload — nothing to push.'
282
+ );
283
+ return;
284
+ }
285
+
286
+ console.log(
287
+ `🔒 Pushing ${keys.length} offloaded parameter(s) for stage '${options.stage}'...`
288
+ );
289
+
290
+ try {
291
+ const { pushed } = await pushOffloadedParameters(
292
+ appDefinition,
293
+ options.stage,
294
+ options
295
+ );
296
+ console.log(`✓ Pushed ${pushed.length} parameter(s)`);
297
+ } catch (error) {
298
+ console.error(`✗ ${error.message}`);
299
+ process.exit(1);
300
+ }
301
+ }
302
+
303
+ module.exports = {
304
+ ssmPushCommand,
305
+ pushOffloadedParameters,
306
+ collectParameterSpecs,
307
+ resolvePushRegion,
308
+ };
@@ -0,0 +1,318 @@
1
+ const { mockClient } = require('aws-sdk-client-mock');
2
+ const {
3
+ SSMClient,
4
+ PutParameterCommand,
5
+ GetParametersCommand,
6
+ } = require('@aws-sdk/client-ssm');
7
+ const { pushOffloadedParameters, resolvePushRegion } = require('./index');
8
+
9
+ describe('ssm-command pushOffloadedParameters', () => {
10
+ let ssmMock;
11
+ const originalEnv = process.env;
12
+
13
+ const appDefinition = {
14
+ name: 'my-app',
15
+ ssm: {
16
+ enable: true,
17
+ parameters: {
18
+ MY_SECRET: { type: 'SecureString' },
19
+ },
20
+ },
21
+ environment: {
22
+ MY_CONFIG: 'ssm',
23
+ PLAIN: true,
24
+ },
25
+ };
26
+
27
+ beforeEach(() => {
28
+ ssmMock = mockClient(SSMClient);
29
+ ssmMock.on(PutParameterCommand).resolves({ Version: 1 });
30
+ process.env = {
31
+ ...originalEnv,
32
+ MY_SECRET: 'shh',
33
+ MY_CONFIG: 'value-1',
34
+ };
35
+ });
36
+
37
+ afterEach(() => {
38
+ ssmMock.restore();
39
+ process.env = originalEnv;
40
+ });
41
+
42
+ it('pushes each offloaded key with resolved name, type, and overwrite', async () => {
43
+ const result = await pushOffloadedParameters(appDefinition, 'prod');
44
+
45
+ const calls = ssmMock.commandCalls(PutParameterCommand);
46
+ expect(calls).toHaveLength(2);
47
+
48
+ const byName = Object.fromEntries(
49
+ calls.map((c) => [c.args[0].input.Name, c.args[0].input])
50
+ );
51
+ expect(byName['/frigg/my-app/prod/MY_SECRET']).toMatchObject({
52
+ Value: 'shh',
53
+ Type: 'SecureString',
54
+ Overwrite: true,
55
+ });
56
+ expect(byName['/frigg/my-app/prod/MY_CONFIG']).toMatchObject({
57
+ Value: 'value-1',
58
+ Type: 'String',
59
+ Overwrite: true,
60
+ });
61
+ expect(result.pushed).toHaveLength(2);
62
+ });
63
+
64
+ it('passes KeyId for SecureString when ssm.kmsKeyArn is set', async () => {
65
+ const withKey = {
66
+ ...appDefinition,
67
+ ssm: {
68
+ ...appDefinition.ssm,
69
+ kmsKeyArn: 'arn:aws:kms:us-east-1:1:key/k',
70
+ },
71
+ };
72
+ await pushOffloadedParameters(withKey, 'dev');
73
+
74
+ const inputs = ssmMock
75
+ .commandCalls(PutParameterCommand)
76
+ .map((c) => c.args[0].input);
77
+ const secure = inputs.find((i) => i.Type === 'SecureString');
78
+ const plain = inputs.find((i) => i.Type === 'String');
79
+ expect(secure.KeyId).toBe('arn:aws:kms:us-east-1:1:key/k');
80
+ expect(plain.KeyId).toBeUndefined();
81
+ });
82
+
83
+ it('is a silent no-op when nothing is offloaded', async () => {
84
+ const result = await pushOffloadedParameters(
85
+ { name: 'x', environment: { PLAIN: true } },
86
+ 'dev'
87
+ );
88
+ expect(result.pushed).toHaveLength(0);
89
+ expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
90
+ });
91
+
92
+ it('throws listing keys with missing or empty values', async () => {
93
+ delete process.env.MY_SECRET;
94
+ process.env.MY_CONFIG = '';
95
+
96
+ await expect(
97
+ pushOffloadedParameters(appDefinition, 'dev')
98
+ ).rejects.toThrow(/MY_SECRET.*MY_CONFIG|MY_CONFIG.*MY_SECRET/s);
99
+ expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
100
+ });
101
+
102
+ it('skips missing values with allowEmpty when the parameter already exists', async () => {
103
+ delete process.env.MY_SECRET;
104
+ ssmMock.on(GetParametersCommand).resolves({
105
+ Parameters: [{ Name: '/frigg/my-app/dev/MY_SECRET' }],
106
+ });
107
+
108
+ const result = await pushOffloadedParameters(appDefinition, 'dev', {
109
+ allowEmpty: true,
110
+ });
111
+ expect(result.skipped).toEqual(['MY_SECRET']);
112
+ expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(1);
113
+ });
114
+
115
+ it('aborts when allowEmpty skips a key that does not exist in Parameter Store', async () => {
116
+ delete process.env.MY_SECRET;
117
+ ssmMock.on(GetParametersCommand).resolves({ Parameters: [] });
118
+
119
+ await expect(
120
+ pushOffloadedParameters(appDefinition, 'dev', { allowEmpty: true })
121
+ ).rejects.toThrow(/MY_SECRET.*no such parameter|--allow-empty/s);
122
+ expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
123
+ });
124
+
125
+ it('rejects values over the standard tier limit and suggests advanced', async () => {
126
+ process.env.MY_CONFIG = 'x'.repeat(4097);
127
+
128
+ await expect(
129
+ pushOffloadedParameters(appDefinition, 'dev')
130
+ ).rejects.toThrow(/advanced/i);
131
+ });
132
+
133
+ it('accepts large values with the advanced tier', async () => {
134
+ process.env.MY_CONFIG = 'x'.repeat(4097);
135
+
136
+ await pushOffloadedParameters(appDefinition, 'dev', {
137
+ tier: 'advanced',
138
+ });
139
+ const inputs = ssmMock
140
+ .commandCalls(PutParameterCommand)
141
+ .map((c) => c.args[0].input);
142
+ expect(
143
+ inputs.find((i) => i.Name.endsWith('/MY_CONFIG')).Tier
144
+ ).toBe('Advanced');
145
+ });
146
+
147
+ it('rejects values over the advanced tier limit', async () => {
148
+ process.env.MY_CONFIG = 'x'.repeat(8193);
149
+
150
+ await expect(
151
+ pushOffloadedParameters(appDefinition, 'dev', {
152
+ tier: 'advanced',
153
+ })
154
+ ).rejects.toThrow(/8192|8 ?KB/i);
155
+ });
156
+
157
+ describe('per-key tier resolution', () => {
158
+ const withKeyTier = {
159
+ ...appDefinition,
160
+ ssm: {
161
+ ...appDefinition.ssm,
162
+ parameters: {
163
+ MY_SECRET: { type: 'SecureString' },
164
+ MY_CONFIG: { tier: 'advanced' },
165
+ },
166
+ },
167
+ };
168
+
169
+ it('accepts a large value when the key is configured advanced in the app definition', async () => {
170
+ process.env.MY_CONFIG = 'x'.repeat(4097);
171
+
172
+ await pushOffloadedParameters(withKeyTier, 'dev');
173
+
174
+ const config = ssmMock
175
+ .commandCalls(PutParameterCommand)
176
+ .map((c) => c.args[0].input)
177
+ .find((i) => i.Name.endsWith('/MY_CONFIG'));
178
+ expect(config.Tier).toBe('Advanced');
179
+ });
180
+
181
+ it('validates each key against its own configured tier', async () => {
182
+ // MY_CONFIG (advanced) tolerates 5000 bytes; MY_SECRET (standard) does not
183
+ process.env.MY_CONFIG = 'x'.repeat(5000);
184
+ process.env.MY_SECRET = 'y'.repeat(5000);
185
+
186
+ await expect(
187
+ pushOffloadedParameters(withKeyTier, 'dev')
188
+ ).rejects.toThrow(/MY_SECRET/);
189
+ expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
190
+ });
191
+
192
+ it('does not tag standard-tier keys with an Advanced tier', async () => {
193
+ await pushOffloadedParameters(withKeyTier, 'dev');
194
+
195
+ const secret = ssmMock
196
+ .commandCalls(PutParameterCommand)
197
+ .map((c) => c.args[0].input)
198
+ .find((i) => i.Name.endsWith('/MY_SECRET'));
199
+ expect(secret.Tier).toBeUndefined();
200
+ });
201
+
202
+ it('lets the --tier CLI option override per-key config for all keys', async () => {
203
+ process.env.MY_CONFIG = 'x'.repeat(4097);
204
+
205
+ await expect(
206
+ pushOffloadedParameters(withKeyTier, 'dev', {
207
+ tier: 'standard',
208
+ })
209
+ ).rejects.toThrow(/advanced/i);
210
+ });
211
+ });
212
+
213
+ it('throws on invalid offload config (markers without ssm.enable)', async () => {
214
+ await expect(
215
+ pushOffloadedParameters(
216
+ { name: 'x', environment: { FOO: 'ssm' } },
217
+ 'dev'
218
+ )
219
+ ).rejects.toThrow(/ssm\.enable/);
220
+ });
221
+
222
+ it('wraps AWS errors with the parameter name', async () => {
223
+ ssmMock
224
+ .on(PutParameterCommand, {
225
+ Name: '/frigg/my-app/dev/MY_SECRET',
226
+ })
227
+ .rejects(new Error('boom'));
228
+
229
+ await expect(
230
+ pushOffloadedParameters(appDefinition, 'dev')
231
+ ).rejects.toThrow(/MY_SECRET/);
232
+ });
233
+
234
+ it('retries a throttled PutParameter and succeeds', async () => {
235
+ const throttle = Object.assign(new Error('Rate exceeded'), {
236
+ name: 'ThrottlingException',
237
+ });
238
+ ssmMock
239
+ .on(PutParameterCommand, {
240
+ Name: '/frigg/my-app/dev/MY_SECRET',
241
+ })
242
+ .rejectsOnce(throttle)
243
+ .resolves({ Version: 2 });
244
+
245
+ const result = await pushOffloadedParameters(appDefinition, 'dev');
246
+
247
+ expect(result.pushed).toHaveLength(2);
248
+ const secretCalls = ssmMock
249
+ .commandCalls(PutParameterCommand)
250
+ .filter(
251
+ (c) => c.args[0].input.Name === '/frigg/my-app/dev/MY_SECRET'
252
+ );
253
+ expect(secretCalls).toHaveLength(2);
254
+ });
255
+
256
+ it('exposes keys pushed so far on a mid-batch failure', async () => {
257
+ // MY_CONFIG (sorted before MY_SECRET) succeeds; MY_SECRET fails.
258
+ ssmMock
259
+ .on(PutParameterCommand, {
260
+ Name: '/frigg/my-app/dev/MY_CONFIG',
261
+ })
262
+ .resolves({ Version: 1 });
263
+ ssmMock
264
+ .on(PutParameterCommand, {
265
+ Name: '/frigg/my-app/dev/MY_SECRET',
266
+ })
267
+ .rejects(new Error('boom'));
268
+
269
+ let caught;
270
+ try {
271
+ await pushOffloadedParameters(appDefinition, 'dev');
272
+ } catch (error) {
273
+ caught = error;
274
+ }
275
+
276
+ expect(caught).toBeDefined();
277
+ expect(caught.pushed).toEqual([
278
+ { name: '/frigg/my-app/dev/MY_CONFIG', version: 1 },
279
+ ]);
280
+ });
281
+
282
+ it('honors a custom parameterPrefix', async () => {
283
+ const custom = {
284
+ ...appDefinition,
285
+ ssm: { ...appDefinition.ssm, parameterPrefix: '/team/x' },
286
+ };
287
+ await pushOffloadedParameters(custom, 'dev');
288
+
289
+ const names = ssmMock
290
+ .commandCalls(PutParameterCommand)
291
+ .map((c) => c.args[0].input.Name);
292
+ expect(names).toEqual(
293
+ expect.arrayContaining([
294
+ '/team/x/MY_SECRET',
295
+ '/team/x/MY_CONFIG',
296
+ ])
297
+ );
298
+ });
299
+
300
+ describe('resolvePushRegion', () => {
301
+ it('prefers an explicit --region option over the environment', () => {
302
+ process.env.AWS_REGION = 'us-west-2';
303
+ expect(resolvePushRegion({ region: 'eu-central-1' })).toBe(
304
+ 'eu-central-1'
305
+ );
306
+ });
307
+
308
+ it('falls back to AWS_REGION when no option is given', () => {
309
+ process.env.AWS_REGION = 'us-west-2';
310
+ expect(resolvePushRegion({})).toBe('us-west-2');
311
+ });
312
+
313
+ it('defaults to us-east-1 when neither option nor AWS_REGION is set', () => {
314
+ delete process.env.AWS_REGION;
315
+ expect(resolvePushRegion({})).toBe('us-east-1');
316
+ });
317
+ });
318
+ });