@friggframework/devtools 2.0.0--canary.654.3cd4278.0 → 2.0.0--canary.651.dadf4d6.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.
@@ -678,7 +678,7 @@ To integrate Frigg into your production application:
678
678
  credentials: true
679
679
  }
680
680
  },
681
- logging: { level: 'info', retentionInDays: 30 },
681
+ logging: { level: 'info' },
682
682
  custom: {
683
683
  appName: 'My Frigg Application',
684
684
  version: '1.0.0',
@@ -26,7 +26,7 @@
26
26
  "lodash": "4.18.1",
27
27
  "node-cache": "^5.1.2",
28
28
  "open": "^8.4.2",
29
- "osls": "^3.58.0",
29
+ "osls": "^3.40.1",
30
30
  "semver": "^7.6.0",
31
31
  "validate-npm-package-name": "^5.0.0"
32
32
  },
@@ -53,13 +53,6 @@ const FRAMEWORK_ENV_BLOCKLIST = new Set([
53
53
  'FRIGG_SSM_OFFLOADED_KEYS',
54
54
  'FRIGG_SSM_CACHE_TTL',
55
55
  'WORKER_FUNCTION_NAME',
56
- // Read by the logger, telemetry flush and Prisma at INIT, before SSM loads
57
- 'FRIGG_LOG_LEVEL',
58
- 'AWS_LAMBDA_LOG_LEVEL',
59
- 'AWS_LAMBDA_LOG_FORMAT',
60
- 'OTEL_FLUSH_TIMEOUT_MS',
61
- 'DEBUG_VERBOSE',
62
- 'PRISMA_LOG_LEVEL',
63
56
  // Reserved AWS Lambda runtime variables
64
57
  '_HANDLER',
65
58
  '_X_AMZN_TRACE_ID',
@@ -190,17 +190,4 @@ describe('offload-utils', () => {
190
190
  expect(FRAMEWORK_ENV_BLOCKLIST.has(key)).toBe(true);
191
191
  }
192
192
  });
193
-
194
- it('blocklist covers the logger keys read at INIT (ADR-048)', () => {
195
- for (const key of [
196
- 'FRIGG_LOG_LEVEL',
197
- 'AWS_LAMBDA_LOG_LEVEL',
198
- 'AWS_LAMBDA_LOG_FORMAT',
199
- 'OTEL_FLUSH_TIMEOUT_MS',
200
- 'DEBUG_VERBOSE',
201
- 'PRISMA_LOG_LEVEL',
202
- ]) {
203
- expect(FRAMEWORK_ENV_BLOCKLIST.has(key)).toBe(true);
204
- }
205
- });
206
193
  });
@@ -467,7 +467,6 @@ function generateIAMCloudFormation(options = {}) {
467
467
  'logs:FilterLogEvents',
468
468
  'logs:PutLogEvents',
469
469
  'logs:PutRetentionPolicy',
470
- 'logs:DeleteRetentionPolicy',
471
470
  ],
472
471
  Resource: [
473
472
  {
@@ -1,11 +1,4 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const {
4
- generateIAMCloudFormation,
5
- getFeatureSummary,
6
- generateBasicIAMPolicy,
7
- generateFullIAMPolicy,
8
- } = require('./iam-generator');
1
+ const { generateIAMCloudFormation, getFeatureSummary } = require('./iam-generator');
9
2
 
10
3
  describe('IAM Generator', () => {
11
4
  describe('getFeatureSummary', () => {
@@ -257,28 +250,6 @@ describe('IAM Generator', () => {
257
250
  expect(yaml).toContain('iam:ListPolicyVersions');
258
251
  });
259
252
 
260
- it('should grant logs:DeleteRetentionPolicy so logging.retentionInDays can be removed', () => {
261
- const yaml = generateIAMCloudFormation({
262
- appName: 'test-app',
263
- features: getFeatureSummary({ name: 'test-app' }).features,
264
- });
265
-
266
- expect(yaml).toContain('logs:PutRetentionPolicy');
267
- expect(yaml).toContain('logs:DeleteRetentionPolicy');
268
- });
269
-
270
- it('should grant logs:DeleteRetentionPolicy in the static policy templates', () => {
271
- for (const policy of [generateBasicIAMPolicy(), generateFullIAMPolicy()]) {
272
- const actions = policy.Statement.flatMap((statement) => statement.Action);
273
- expect(actions).toContain('logs:DeleteRetentionPolicy');
274
- }
275
- const stackYaml = fs.readFileSync(
276
- path.join(__dirname, 'templates/frigg-deployment-iam-stack.yaml'),
277
- 'utf8'
278
- );
279
- expect(stackYaml).toContain("'logs:DeleteRetentionPolicy'");
280
- });
281
-
282
253
  it('should include internal-error-queue pattern in SQS resources', () => {
283
254
  const appDefinition = {
284
255
  name: 'test-app',
@@ -238,7 +238,6 @@ Resources:
238
238
  - 'logs:FilterLogEvents'
239
239
  - 'logs:PutLogEvents'
240
240
  - 'logs:PutRetentionPolicy'
241
- - 'logs:DeleteRetentionPolicy'
242
241
  Resource:
243
242
  - !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/lambda/*frigg*'
244
243
  - !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/lambda/*frigg*:*'
@@ -183,8 +183,7 @@
183
183
  "logs:DescribeLogStreams",
184
184
  "logs:FilterLogEvents",
185
185
  "logs:PutLogEvents",
186
- "logs:PutRetentionPolicy",
187
- "logs:DeleteRetentionPolicy"
186
+ "logs:PutRetentionPolicy"
188
187
  ],
189
188
  "Resource": [
190
189
  "arn:aws:logs:*:*:log-group:/aws/lambda/*frigg*",
@@ -183,8 +183,7 @@
183
183
  "logs:DescribeLogStreams",
184
184
  "logs:FilterLogEvents",
185
185
  "logs:PutLogEvents",
186
- "logs:PutRetentionPolicy",
187
- "logs:DeleteRetentionPolicy"
186
+ "logs:PutRetentionPolicy"
188
187
  ],
189
188
  "Resource": [
190
189
  "arn:aws:logs:*:*:log-group:/aws/lambda/*frigg*",
@@ -13,7 +13,6 @@ const {
13
13
  isSsmOffloadActive,
14
14
  getOffloadedKeys,
15
15
  } = require('../parameters/offload-utils');
16
- const { normalizeLogLevel } = require('./utilities/logging-config');
17
16
 
18
17
  // OTLP-family exporters read their endpoint/headers from these standard env
19
18
  // vars (ADR-011). When such an exporter is configured we auto-register them as
@@ -45,20 +44,6 @@ function addTelemetryEnvPassthrough(appDefinition, envVars) {
45
44
  }
46
45
  }
47
46
 
48
- // ADR-048 §12: logging.level wins over an `environment` passthrough. INFO is
49
- // the core default, so it emits no env var.
50
- function addLoggingEnv(appDefinition, envVars) {
51
- const level = appDefinition?.logging?.level;
52
- if (level === undefined) return;
53
-
54
- const normalized = normalizeLogLevel(level);
55
- if (normalized === 'INFO') {
56
- delete envVars.FRIGG_LOG_LEVEL;
57
- return;
58
- }
59
- envVars.FRIGG_LOG_LEVEL = normalized;
60
- }
61
-
62
47
  /**
63
48
  * Get environment variables from AppDefinition
64
49
  *
@@ -135,8 +120,6 @@ function getAppEnvironmentVars(appDefinition) {
135
120
  envKeys.push(key);
136
121
  }
137
122
 
138
- addLoggingEnv(appDefinition, envVars);
139
-
140
123
  if (envKeys.length > 0) {
141
124
  console.log(
142
125
  ` Found ${envKeys.length} environment variables: ${envKeys.join(
@@ -147,67 +147,6 @@ describe('Environment Builder', () => {
147
147
  });
148
148
  });
149
149
 
150
- describe('logging env (ADR-048)', () => {
151
- it.each(['debug', 'WARN', 'Error'])(
152
- 'emits literal upper-case FRIGG_LOG_LEVEL for level "%s"',
153
- (level) => {
154
- const result = getAppEnvironmentVars({ logging: { level } });
155
-
156
- expect(result.FRIGG_LOG_LEVEL).toBe(level.toUpperCase());
157
- }
158
- );
159
-
160
- it.each(['info', 'INFO'])(
161
- 'emits nothing for the default level "%s"',
162
- (level) => {
163
- const result = getAppEnvironmentVars({ logging: { level } });
164
-
165
- expect(result).not.toHaveProperty('FRIGG_LOG_LEVEL');
166
- }
167
- );
168
-
169
- it('emits nothing when logging or logging.level is absent', () => {
170
- expect(getAppEnvironmentVars({})).not.toHaveProperty(
171
- 'FRIGG_LOG_LEVEL'
172
- );
173
- expect(
174
- getAppEnvironmentVars({ logging: { retentionInDays: 30 } })
175
- ).not.toHaveProperty('FRIGG_LOG_LEVEL');
176
- });
177
-
178
- it('lets logging.level win over an environment passthrough', () => {
179
- const result = getAppEnvironmentVars({
180
- environment: { FRIGG_LOG_LEVEL: true },
181
- logging: { level: 'error' },
182
- });
183
-
184
- expect(result.FRIGG_LOG_LEVEL).toBe('ERROR');
185
- });
186
-
187
- it('drops an environment passthrough when logging.level is INFO', () => {
188
- const result = getAppEnvironmentVars({
189
- environment: { FRIGG_LOG_LEVEL: true },
190
- logging: { level: 'info' },
191
- });
192
-
193
- expect(result).not.toHaveProperty('FRIGG_LOG_LEVEL');
194
- });
195
-
196
- it('keeps an environment passthrough when logging.level is absent', () => {
197
- const result = getAppEnvironmentVars({
198
- environment: { FRIGG_LOG_LEVEL: true },
199
- });
200
-
201
- expect(result.FRIGG_LOG_LEVEL).toBe("${env:FRIGG_LOG_LEVEL, ''}");
202
- });
203
-
204
- it('throws on an unknown level', () => {
205
- expect(() =>
206
- getAppEnvironmentVars({ logging: { level: 'verbose' } })
207
- ).toThrow(/logging\.level/);
208
- });
209
- });
210
-
211
150
  describe("getAppEnvironmentVars() - 'ssm' offload", () => {
212
151
  const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
213
152
 
@@ -9,7 +9,6 @@
9
9
 
10
10
  const { buildEnvironment } = require('../environment-builder');
11
11
  const { nestedNodeModulesExcludes } = require('./nested-node-modules');
12
- const { buildLoggingProviderConfig } = require('./logging-config');
13
12
 
14
13
  /**
15
14
  * Create base serverless definition with core functions and resources
@@ -34,7 +33,6 @@ function createBaseDefinition(
34
33
  usePrismaLayer = true
35
34
  ) {
36
35
  const region = process.env.AWS_REGION || 'us-east-1';
37
- const loggingConfig = buildLoggingProviderConfig(AppDefinition.logging);
38
36
 
39
37
  // Package config for handlers that skip esbuild (need node_modules dependencies)
40
38
  // Include backend src/ and index.js since handlers load the app definition
@@ -166,7 +164,7 @@ function createBaseDefinition(
166
164
  };
167
165
 
168
166
  return {
169
- frameworkVersion: loggingConfig.frameworkVersion || '>=3.17.0',
167
+ frameworkVersion: '>=3.17.0',
170
168
  service: AppDefinition.name || 'create-frigg-app',
171
169
  package: {
172
170
  individually: true,
@@ -220,7 +218,6 @@ function createBaseDefinition(
220
218
  name: '${opt:stage, "dev"}-${self:service}',
221
219
  disableDefaultEndpoint: false,
222
220
  },
223
- ...loggingConfig.provider,
224
221
  },
225
222
  plugins: [
226
223
  'serverless-esbuild',
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Base Serverless Definition Factory
3
+ *
4
+ * Utility Layer - Hexagonal Architecture
5
+ *
6
+ * Creates the base serverless.yml configuration with core functions,
7
+ * resources, plugins, and provider settings.
8
+ */
9
+
10
+ const { buildEnvironment } = require('../environment-builder');
11
+
12
+ /**
13
+ * Create base serverless definition with core functions and resources
14
+ *
15
+ * This creates the foundation serverless configuration that all
16
+ * Frigg applications need, including:
17
+ * - Core Lambda functions (auth, user, health, dbMigrate)
18
+ * - Error handling infrastructure (SQS, SNS, CloudWatch)
19
+ * - Prisma Lambda Layer
20
+ * - Base plugins and esbuild configuration
21
+ *
22
+ * @param {Object} AppDefinition - Application definition
23
+ * @param {Object} appEnvironmentVars - Environment variables from app definition
24
+ * @param {Object} discoveredResources - AWS resources discovered during build
25
+ * @returns {Object} Base serverless definition
26
+ */
27
+ function createBaseDefinition(
28
+ AppDefinition,
29
+ appEnvironmentVars,
30
+ discoveredResources
31
+ ) {
32
+ const region = process.env.AWS_REGION || 'us-east-1';
33
+
34
+ // Function-level package config to exclude Prisma and AWS SDK
35
+ const functionPackageConfig = {
36
+ exclude: [
37
+ // Exclude AWS SDK (already in Lambda runtime or externalized by esbuild)
38
+ 'node_modules/aws-sdk/**',
39
+ 'node_modules/@aws-sdk/**',
40
+
41
+ // Exclude Prisma (provided via Lambda Layer)
42
+ 'node_modules/@prisma/**',
43
+ 'node_modules/.prisma/**',
44
+ 'node_modules/prisma/**',
45
+ 'node_modules/@friggframework/core/generated/**',
46
+
47
+ // Exclude nested node_modules from symlinked frigg packages (for npm link development)
48
+ 'node_modules/@friggframework/core/node_modules/**',
49
+ 'node_modules/@friggframework/devtools/node_modules/**',
50
+
51
+ // Exclude development/test files from backend project
52
+ 'coverage/**',
53
+ 'test/**',
54
+ 'src/**',
55
+ 'layers/**',
56
+ '**/*.test.js',
57
+ '**/*.spec.js',
58
+ '.git/**',
59
+ '.github/**',
60
+ ],
61
+ };
62
+
63
+ return {
64
+ frameworkVersion: '>=3.17.0',
65
+ service: AppDefinition.name || 'create-frigg-app',
66
+ package: {
67
+ individually: true,
68
+ },
69
+ useDotenv: true,
70
+ provider: {
71
+ name: AppDefinition.provider || 'aws',
72
+ ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
73
+ runtime: 'nodejs22.x', // Node.js 22.x (latest Lambda runtime with AWS SDK v3)
74
+ timeout: 29, // Set to 29s to give buffer before API Gateway's 30s timeout
75
+ region,
76
+ stage: '${opt:stage}',
77
+ environment: buildEnvironment(appEnvironmentVars, discoveredResources),
78
+ iamRoleStatements: [
79
+ {
80
+ Effect: 'Allow',
81
+ Action: ['sns:Publish'],
82
+ Resource: { Ref: 'InternalErrorBridgeTopic' },
83
+ },
84
+ {
85
+ Effect: 'Allow',
86
+ Action: [
87
+ 'sqs:SendMessage',
88
+ 'sqs:SendMessageBatch',
89
+ 'sqs:GetQueueUrl',
90
+ 'sqs:GetQueueAttributes',
91
+ ],
92
+ Resource: [
93
+ { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
94
+ {
95
+ 'Fn::Join': [
96
+ ':',
97
+ [
98
+ 'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
99
+ ],
100
+ ],
101
+ },
102
+ ],
103
+ },
104
+ ],
105
+ httpApi: {
106
+ payload: '2.0',
107
+ cors: {
108
+ allowedOrigins: ['*'],
109
+ allowedHeaders: ['*'],
110
+ allowedMethods: ['*'],
111
+ allowCredentials: false,
112
+ },
113
+ name: '${opt:stage, "dev"}-${self:service}',
114
+ disableDefaultEndpoint: false,
115
+ },
116
+ },
117
+ plugins: [
118
+ 'serverless-esbuild',
119
+ 'serverless-dotenv-plugin',
120
+ 'serverless-offline-sqs',
121
+ 'serverless-offline',
122
+ '@friggframework/serverless-plugin',
123
+ ],
124
+ custom: {
125
+ esbuild: {
126
+ bundle: true,
127
+ minify: true,
128
+ sourcemap: true,
129
+ target: 'node22',
130
+ platform: 'node',
131
+ format: 'cjs',
132
+ external: [
133
+ '@aws-sdk/*',
134
+ 'aws-sdk',
135
+ '@prisma/client',
136
+ 'prisma',
137
+ '.prisma/*',
138
+ ],
139
+ packager: 'npm',
140
+ keepNames: true,
141
+ keepOutputDirectory: false, // Clean up .esbuild directory after packaging
142
+ exclude: [
143
+ 'aws-sdk',
144
+ '@aws-sdk/*',
145
+ '@prisma/client',
146
+ 'prisma',
147
+ ],
148
+ },
149
+ 'serverless-offline': {
150
+ httpPort: 3001,
151
+ lambdaPort: 4001,
152
+ websocketPort: 3002,
153
+ location: '.', // Set base directory for handler resolution to current directory
154
+ skipCacheInvalidation: false,
155
+ },
156
+ 'serverless-offline-sqs': {
157
+ autoCreate: false,
158
+ apiVersion: '2012-11-05',
159
+ endpoint: 'http://localhost:4566',
160
+ region,
161
+ accessKeyId: 'root',
162
+ secretAccessKey: 'root',
163
+ skipCacheInvalidation: false,
164
+ },
165
+ },
166
+ functions: {
167
+ auth: {
168
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
169
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
170
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
171
+ package: functionPackageConfig,
172
+ events: [
173
+ { httpApi: { path: '/api/integrations', method: 'ANY' } },
174
+ {
175
+ httpApi: {
176
+ path: '/api/integrations/{proxy+}',
177
+ method: 'ANY',
178
+ },
179
+ },
180
+ { httpApi: { path: '/api/authorize', method: 'ANY' } },
181
+ ],
182
+ },
183
+ user: {
184
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
185
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
186
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
187
+ package: functionPackageConfig,
188
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
189
+ },
190
+ health: {
191
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
192
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
193
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
194
+ package: functionPackageConfig,
195
+ events: [
196
+ { httpApi: { path: '/health', method: 'GET' } },
197
+ { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
198
+ ],
199
+ },
200
+ dbMigrate: {
201
+ handler: 'node_modules/@friggframework/core/handlers/database-migration-handler.handler',
202
+ // DO NOT use Prisma layer - this function includes Prisma CLI separately
203
+ skipEsbuild: true, // Handlers in node_modules don't need bundling
204
+ timeout: 300, // 5 minutes for long-running migrations
205
+ memorySize: 1024, // Extra memory for Prisma CLI and migration operations
206
+ reservedConcurrency: 1, // Prevent concurrent migrations (CRITICAL for data safety)
207
+ description: 'Runs database migrations via Prisma CLI (invoke manually from CI/CD or triggers). Prisma CLI bundled separately.',
208
+ package: {
209
+ individually: true,
210
+ patterns: [
211
+ // Include handler
212
+ 'node_modules/@friggframework/core/handlers/database-migration-handler.js',
213
+
214
+ // Include ONLY PostgreSQL Prisma client (exclude MongoDB)
215
+ 'node_modules/@friggframework/core/generated/prisma-postgresql/**',
216
+ '!node_modules/@friggframework/core/generated/prisma-mongodb/**', // Exclude MongoDB client entirely
217
+
218
+ // Include Prisma runtime
219
+ 'node_modules/@prisma/client/**',
220
+ 'node_modules/.prisma/**',
221
+ 'node_modules/prisma/**', // Prisma CLI
222
+
223
+ // Exclude unnecessary engines and files
224
+ '!node_modules/prisma/node_modules/**',
225
+ '!**/query-engine-darwin*', // Exclude macOS binaries (keep rhel for Lambda)
226
+ '!**/runtime/*.wasm', // WASM engines
227
+ '!**/*.md',
228
+ '!**/*.map',
229
+ '!**/LICENSE*',
230
+ '!**/*.d.ts',
231
+ '!**/*.d.mts',
232
+ ],
233
+ },
234
+ maximumEventAge: 60,
235
+ maximumRetryAttempts: 0,
236
+ tags: {
237
+ Purpose: 'DatabaseMigration',
238
+ ManagedBy: 'Frigg',
239
+ },
240
+ environment: {
241
+ CI: '1',
242
+ PRISMA_HIDE_UPDATE_MESSAGE: '1',
243
+ PRISMA_MIGRATE_SKIP_SEED: '1',
244
+ },
245
+ },
246
+ },
247
+ layers: {
248
+ prisma: {
249
+ path: 'layers/prisma',
250
+ name: '${self:service}-prisma-${sls:stage}',
251
+ description: 'Prisma runtime client only (NO CLI) with rhel-openssl-3.0.x binaries (~10-15MB). CLI packaged separately in dbMigrate function.',
252
+ compatibleRuntimes: ['nodejs20.x', 'nodejs22.x'],
253
+ retain: false,
254
+ },
255
+ },
256
+ resources: {
257
+ Resources: {
258
+ InternalErrorQueue: {
259
+ Type: 'AWS::SQS::Queue',
260
+ Properties: {
261
+ QueueName:
262
+ '${self:service}-internal-error-queue-${self:provider.stage}',
263
+ MessageRetentionPeriod: 300,
264
+ },
265
+ },
266
+ InternalErrorBridgeTopic: {
267
+ Type: 'AWS::SNS::Topic',
268
+ Properties: {
269
+ Subscription: [
270
+ {
271
+ Protocol: 'sqs',
272
+ Endpoint: {
273
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
274
+ },
275
+ },
276
+ ],
277
+ },
278
+ },
279
+ InternalErrorBridgePolicy: {
280
+ Type: 'AWS::SQS::QueuePolicy',
281
+ Properties: {
282
+ Queues: [{ Ref: 'InternalErrorQueue' }],
283
+ PolicyDocument: {
284
+ Version: '2012-10-17',
285
+ Statement: [
286
+ {
287
+ Sid: 'Allow Dead Letter SNS to publish to SQS',
288
+ Effect: 'Allow',
289
+ Principal: { Service: 'sns.amazonaws.com' },
290
+ Resource: {
291
+ 'Fn::GetAtt': [
292
+ 'InternalErrorQueue',
293
+ 'Arn',
294
+ ],
295
+ },
296
+ Action: [
297
+ 'SQS:SendMessage',
298
+ 'SQS:SendMessageBatch',
299
+ ],
300
+ Condition: {
301
+ ArnEquals: {
302
+ 'aws:SourceArn': {
303
+ Ref: 'InternalErrorBridgeTopic',
304
+ },
305
+ },
306
+ },
307
+ },
308
+ ],
309
+ },
310
+ },
311
+ },
312
+ ApiGatewayAlarm5xx: {
313
+ Type: 'AWS::CloudWatch::Alarm',
314
+ Properties: {
315
+ AlarmDescription: 'API Gateway 5xx Errors',
316
+ Namespace: 'AWS/ApiGateway',
317
+ MetricName: '5XXError',
318
+ Statistic: 'Sum',
319
+ Threshold: 0,
320
+ ComparisonOperator: 'GreaterThanThreshold',
321
+ EvaluationPeriods: 1,
322
+ Period: 60,
323
+ AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
324
+ Dimensions: [
325
+ { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
326
+ { Name: 'Stage', Value: '${self:provider.stage}' },
327
+ ],
328
+ },
329
+ },
330
+ },
331
+ },
332
+ };
333
+ }
334
+
335
+ module.exports = {
336
+ createBaseDefinition,
337
+ };
338
+
@@ -317,54 +317,3 @@ describe('nested node_modules (lambda.keepNestedNodeModules)', () => {
317
317
  }
318
318
  });
319
319
  });
320
-
321
- describe('logging (ADR-048)', () => {
322
- const build = (logging) =>
323
- createBaseDefinition({ name: 'test-app', ...(logging && { logging }) }, {}, {});
324
-
325
- it('emits no logs, logRetentionInDays or frameworkVersion change when logging is absent', () => {
326
- const result = build();
327
-
328
- expect(result.provider.logs).toBeUndefined();
329
- expect(result.provider.logRetentionInDays).toBeUndefined();
330
- expect(result.frameworkVersion).toBe('>=3.17.0');
331
- });
332
-
333
- it('validates logging.level but leaves the Lambda log format and frameworkVersion alone by default', () => {
334
- const result = build({ level: 'warn' });
335
-
336
- expect(result.provider.logs).toBeUndefined();
337
- expect(result.frameworkVersion).toBe('>=3.17.0');
338
- });
339
-
340
- it.each(['verbose', 'warning', '', 3])('throws on unknown level %p', (level) => {
341
- expect(() => build({ level })).toThrow(/logging\.level/);
342
- });
343
-
344
- it('maps retentionInDays to provider.logRetentionInDays without a logs block', () => {
345
- const result = build({ retentionInDays: 30 });
346
-
347
- expect(result.provider.logRetentionInDays).toBe(30);
348
- expect(result.provider.logs).toBeUndefined();
349
- expect(result.frameworkVersion).toBe('>=3.17.0');
350
- });
351
-
352
- it('accepts 1096, which CloudWatch allows but the osls schema omits', () => {
353
- expect(build({ retentionInDays: 1096 }).provider.logRetentionInDays).toBe(1096);
354
- });
355
-
356
- it.each([10, 0, -1, 30.5, '30'])(
357
- 'throws on retentionInDays %p, which CloudWatch rejects',
358
- (retentionInDays) => {
359
- expect(() => build({ retentionInDays })).toThrow(
360
- /logging\.retentionInDays/
361
- );
362
- }
363
- );
364
-
365
- it('sets retention when level is also set', () => {
366
- const result = build({ level: 'debug', retentionInDays: 14 });
367
-
368
- expect(result.provider.logRetentionInDays).toBe(14);
369
- });
370
- });
@@ -150,7 +150,7 @@ describe('composeServerlessDefinition', () => {
150
150
 
151
151
  expect(result.service).toBe('test-app');
152
152
  expect(result.provider.name).toBe('aws');
153
- expect(result.provider.runtime).toBe('nodejs22.x');
153
+ expect(result.provider.runtime).toBe('nodejs20.x');
154
154
  expect(result.provider.region).toBe('us-east-1');
155
155
  expect(result.provider.stage).toBe('${opt:stage}');
156
156
  expect(result.frameworkVersion).toBe('>=3.17.0');
@@ -238,50 +238,6 @@ describe('composeServerlessDefinition', () => {
238
238
  });
239
239
  });
240
240
 
241
- describe('Logging (ADR-048)', () => {
242
- it('carries logging into FRIGG_LOG_LEVEL and logRetentionInDays, with no Lambda LoggingConfig by default', async () => {
243
- const result = await composeServerlessDefinition({
244
- integrations: [mockIntegration],
245
- logging: { level: 'debug', retentionInDays: 30 },
246
- });
247
-
248
- expect(result.frameworkVersion).toBe('>=3.17.0');
249
- expect(result.provider.logs).toBeUndefined();
250
- expect(result.provider.logRetentionInDays).toBe(30);
251
- expect(result.provider.environment.FRIGG_LOG_LEVEL).toBe('DEBUG');
252
- });
253
-
254
- it('sets no function-level logs, which would replace the provider block', async () => {
255
- const result = await composeServerlessDefinition({
256
- integrations: [mockIntegration],
257
- logging: { level: 'warn' },
258
- });
259
-
260
- const functions = Object.values(result.functions);
261
- expect(functions.length).toBeGreaterThan(0);
262
- expect(functions.every((fn) => fn.logs === undefined)).toBe(true);
263
- });
264
-
265
- it('lets logging.level win over environment: { FRIGG_LOG_LEVEL: true }', async () => {
266
- const result = await composeServerlessDefinition({
267
- integrations: [],
268
- environment: { FRIGG_LOG_LEVEL: true },
269
- logging: { level: 'error' },
270
- });
271
-
272
- expect(result.provider.environment.FRIGG_LOG_LEVEL).toBe('ERROR');
273
- });
274
-
275
- it('changes nothing when logging is absent', async () => {
276
- const result = await composeServerlessDefinition({ integrations: [] });
277
-
278
- expect(result.frameworkVersion).toBe('>=3.17.0');
279
- expect(result.provider.logs).toBeUndefined();
280
- expect(result.provider.logRetentionInDays).toBeUndefined();
281
- expect(result.provider.environment).not.toHaveProperty('FRIGG_LOG_LEVEL');
282
- });
283
- });
284
-
285
241
  describe('VPC Configuration', () => {
286
242
  it('should add VPC configuration when vpc.enable is true with discover mode', async () => {
287
243
  const appDefinition = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.654.3cd4278.0",
4
+ "version": "2.0.0--canary.651.dadf4d6.0",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -26,9 +26,9 @@
26
26
  "@babel/eslint-parser": "^7.18.9",
27
27
  "@babel/parser": "^7.25.3",
28
28
  "@babel/traverse": "^7.25.3",
29
- "@friggframework/core": "2.0.0--canary.654.3cd4278.0",
30
- "@friggframework/schemas": "2.0.0--canary.654.3cd4278.0",
31
- "@friggframework/test": "2.0.0--canary.654.3cd4278.0",
29
+ "@friggframework/core": "2.0.0--canary.651.dadf4d6.0",
30
+ "@friggframework/schemas": "2.0.0--canary.651.dadf4d6.0",
31
+ "@friggframework/test": "2.0.0--canary.651.dadf4d6.0",
32
32
  "@hapi/boom": "^10.0.1",
33
33
  "@inquirer/prompts": "^5.3.8",
34
34
  "axios": "^1.18.0",
@@ -56,12 +56,12 @@
56
56
  "validate-npm-package-name": "^5.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@friggframework/eslint-config": "2.0.0--canary.654.3cd4278.0",
60
- "@friggframework/prettier-config": "2.0.0--canary.654.3cd4278.0",
59
+ "@friggframework/eslint-config": "2.0.0--canary.651.dadf4d6.0",
60
+ "@friggframework/prettier-config": "2.0.0--canary.651.dadf4d6.0",
61
61
  "aws-sdk-client-mock": "^4.1.0",
62
62
  "aws-sdk-client-mock-jest": "^4.1.0",
63
63
  "jest": "^30.1.3",
64
- "osls": "^3.58.0",
64
+ "osls": "^3.40.1",
65
65
  "prettier": "^2.7.1",
66
66
  "serverless-dotenv-plugin": "^6.0.0",
67
67
  "serverless-esbuild": "^1.54.3",
@@ -89,5 +89,5 @@
89
89
  "publishConfig": {
90
90
  "access": "public"
91
91
  },
92
- "gitHead": "3cd42784313ae866c499d7c7549b7c28e39d3045"
92
+ "gitHead": "dadf4d6a1a064d6ab883b486f2f851a58d093cf8"
93
93
  }
@@ -1,82 +0,0 @@
1
- const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
2
-
3
- // CloudWatch Logs PutRetentionPolicy values. The osls schema copy lacks 1096.
4
- const RETENTION_DAYS = [
5
- 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827,
6
- 2192, 2557, 2922, 3288, 3653,
7
- ];
8
-
9
- // First osls version that compiles provider.logs.lambda into LoggingConfig.
10
- const LOGGING_CONFIG_FRAMEWORK_VERSION = '>=3.58.0';
11
-
12
- // ADR-048 Open question 3: off until the Phase 0 spike proves Lambda JSON mode
13
- // keeps direct fd 1 lines intact. Until then Lambda stays on Text and
14
- // FRIGG_LOG_LEVEL filters in process.
15
- const EMIT_LAMBDA_JSON_LOGGING_CONFIG = false;
16
-
17
- function normalizeLogLevel(level) {
18
- const normalized = typeof level === 'string' ? level.toUpperCase() : level;
19
- if (!LOG_LEVELS.includes(normalized)) {
20
- throw new Error(
21
- `Invalid logging.level ${JSON.stringify(level)}. Use one of: ${LOG_LEVELS.join(', ')} (any case).`
22
- );
23
- }
24
- return normalized;
25
- }
26
-
27
- function validateRetentionInDays(retentionInDays) {
28
- if (!RETENTION_DAYS.includes(retentionInDays)) {
29
- throw new Error(
30
- `Invalid logging.retentionInDays ${JSON.stringify(retentionInDays)}. CloudWatch accepts: ${RETENTION_DAYS.join(', ')}.`
31
- );
32
- }
33
- return retentionInDays;
34
- }
35
-
36
- /**
37
- * Map appDefinition.logging to provider-level serverless settings.
38
- * Returns an empty object when logging is absent (ADR-048 §12: no change).
39
- *
40
- * @param {Object} [logging] - appDefinition.logging
41
- * @param {{ lambdaJson?: boolean }} [options]
42
- * @returns {{ frameworkVersion?: string, provider?: Object }}
43
- */
44
- function buildLoggingProviderConfig(
45
- logging,
46
- { lambdaJson = EMIT_LAMBDA_JSON_LOGGING_CONFIG } = {}
47
- ) {
48
- if (!logging) return {};
49
-
50
- const provider = {};
51
- const result = {};
52
-
53
- if (logging.level !== undefined) {
54
- const level = normalizeLogLevel(logging.level);
55
- if (lambdaJson) {
56
- result.frameworkVersion = LOGGING_CONFIG_FRAMEWORK_VERSION;
57
- provider.logs = {
58
- lambda: {
59
- logFormat: 'JSON',
60
- applicationLogLevel: level,
61
- systemLogLevel: 'INFO',
62
- },
63
- };
64
- }
65
- }
66
-
67
- if (logging.retentionInDays !== undefined) {
68
- provider.logRetentionInDays = validateRetentionInDays(
69
- logging.retentionInDays
70
- );
71
- }
72
-
73
- if (Object.keys(provider).length > 0) result.provider = provider;
74
- return result;
75
- }
76
-
77
- module.exports = {
78
- LOG_LEVELS,
79
- RETENTION_DAYS,
80
- normalizeLogLevel,
81
- buildLoggingProviderConfig,
82
- };
@@ -1,52 +0,0 @@
1
- const { buildLoggingProviderConfig } = require('./logging-config');
2
-
3
- describe('buildLoggingProviderConfig (ADR-048)', () => {
4
- it('emits no Lambda LoggingConfig and no osls floor by default, until the Phase 0 spike passes', () => {
5
- const result = buildLoggingProviderConfig({ level: 'warn' });
6
-
7
- expect(result.provider).toBeUndefined();
8
- expect(result.frameworkVersion).toBeUndefined();
9
- });
10
-
11
- it('still validates the level when the LoggingConfig is off', () => {
12
- expect(() => buildLoggingProviderConfig({ level: 'verbose' })).toThrow(
13
- /logging\.level/
14
- );
15
- });
16
-
17
- describe('with lambdaJson', () => {
18
- const build = (logging) =>
19
- buildLoggingProviderConfig(logging, { lambdaJson: true });
20
-
21
- it('maps level to provider.logs.lambda and sets frameworkVersion >=3.58.0', () => {
22
- expect(build({ level: 'warn' })).toEqual({
23
- frameworkVersion: '>=3.58.0',
24
- provider: {
25
- logs: {
26
- lambda: {
27
- logFormat: 'JSON',
28
- applicationLogLevel: 'WARN',
29
- systemLogLevel: 'INFO',
30
- },
31
- },
32
- },
33
- });
34
- });
35
-
36
- it.each(['info', 'INFO', 'Info', 'fatal', 'TRACE'])(
37
- 'accepts level "%s" in any case',
38
- (level) => {
39
- expect(
40
- build({ level }).provider.logs.lambda.applicationLogLevel
41
- ).toBe(level.toUpperCase());
42
- }
43
- );
44
-
45
- it('sets both level and retention together', () => {
46
- const result = build({ level: 'debug', retentionInDays: 14 });
47
-
48
- expect(result.provider.logs.lambda.applicationLogLevel).toBe('DEBUG');
49
- expect(result.provider.logRetentionInDays).toBe(14);
50
- });
51
- });
52
- });