@friggframework/devtools 2.0.0--canary.653.8213092.0 → 2.0.0--canary.654.be44e66.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' },
681
+ logging: { level: 'info', retentionInDays: 30 },
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.40.1",
29
+ "osls": "^3.58.0",
30
30
  "semver": "^7.6.0",
31
31
  "validate-npm-package-name": "^5.0.0"
32
32
  },
@@ -32,10 +32,6 @@ const {
32
32
  const {
33
33
  nestedNodeModulesExcludes,
34
34
  } = require('../shared/utilities/nested-node-modules');
35
- const {
36
- INTEGRATION_QUEUE_MAX_RECEIVE_COUNT,
37
- QUEUE_MAX_RECEIVE_COUNT_ENV,
38
- } = require('@friggframework/core/queues/queue-delivery');
39
35
 
40
36
  class IntegrationBuilder extends InfrastructureBuilder {
41
37
  constructor() {
@@ -573,7 +569,7 @@ class IntegrationBuilder extends InfrastructureBuilder {
573
569
  MessageRetentionPeriod: 345600, // 4 days (SQS default)
574
570
  VisibilityTimeout: 1800,
575
571
  RedrivePolicy: {
576
- maxReceiveCount: INTEGRATION_QUEUE_MAX_RECEIVE_COUNT,
572
+ maxReceiveCount: 3,
577
573
  deadLetterTargetArn: {
578
574
  'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
579
575
  },
@@ -581,14 +577,6 @@ class IntegrationBuilder extends InfrastructureBuilder {
581
577
  },
582
578
  };
583
579
 
584
- const queueWorker = result.functions[`${integrationName}QueueWorker`];
585
- queueWorker.environment = {
586
- ...queueWorker.environment,
587
- [QUEUE_MAX_RECEIVE_COUNT_ENV]: String(
588
- INTEGRATION_QUEUE_MAX_RECEIVE_COUNT
589
- ),
590
- };
591
-
592
580
  // Add queue URL to environment
593
581
  this.setQueueUrlEnvironment(
594
582
  integrationName,
@@ -4,10 +4,6 @@
4
4
  * Tests integration-specific Lambda functions and SQS queues
5
5
  */
6
6
 
7
- const {
8
- INTEGRATION_QUEUE_MAX_RECEIVE_COUNT,
9
- QUEUE_MAX_RECEIVE_COUNT_ENV,
10
- } = require('@friggframework/core/queues/queue-delivery');
11
7
  const { IntegrationBuilder } = require('./integration-builder');
12
8
  const { ValidationResult } = require('../shared/base-builder');
13
9
 
@@ -211,54 +207,6 @@ describe('IntegrationBuilder', () => {
211
207
  });
212
208
  });
213
209
 
214
- it('takes the redrive max receive count from the core runtime contract', async () => {
215
- const result = await integrationBuilder.build(
216
- { integrations: [{ Definition: { name: 'test' } }] },
217
- {}
218
- );
219
-
220
- expect(
221
- result.resources.TestQueue.Properties.RedrivePolicy
222
- .maxReceiveCount
223
- ).toBe(INTEGRATION_QUEUE_MAX_RECEIVE_COUNT);
224
- });
225
-
226
- it('tells the queue worker the max receive count of the queue it owns', async () => {
227
- const result = await integrationBuilder.build(
228
- { integrations: [{ Definition: { name: 'test' } }] },
229
- {}
230
- );
231
-
232
- expect(
233
- result.functions.testQueueWorker.environment[
234
- QUEUE_MAX_RECEIVE_COUNT_ENV
235
- ]
236
- ).toBe('3');
237
- });
238
-
239
- it('leaves the max receive count unset for an external queue', async () => {
240
- const result = await integrationBuilder.build(
241
- {
242
- integrations: [
243
- {
244
- Definition: { name: 'test' },
245
- ownership: { queue: 'external' },
246
- queue: {
247
- url: 'https://sqs.us-east-1.amazonaws.com/123456789012/test-queue',
248
- },
249
- },
250
- ],
251
- },
252
- {}
253
- );
254
-
255
- expect(
256
- result.functions.testQueueWorker.environment?.[
257
- QUEUE_MAX_RECEIVE_COUNT_ENV
258
- ]
259
- ).toBeUndefined();
260
- });
261
-
262
210
  it('should create queue worker function', async () => {
263
211
  const appDefinition = {
264
212
  integrations: [
@@ -53,6 +53,13 @@ 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',
56
63
  // Reserved AWS Lambda runtime variables
57
64
  '_HANDLER',
58
65
  '_X_AMZN_TRACE_ID',
@@ -190,4 +190,17 @@ 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
+ });
193
206
  });
@@ -467,6 +467,7 @@ function generateIAMCloudFormation(options = {}) {
467
467
  'logs:FilterLogEvents',
468
468
  'logs:PutLogEvents',
469
469
  'logs:PutRetentionPolicy',
470
+ 'logs:DeleteRetentionPolicy',
470
471
  ],
471
472
  Resource: [
472
473
  {
@@ -1,4 +1,11 @@
1
- const { generateIAMCloudFormation, getFeatureSummary } = require('./iam-generator');
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {
4
+ generateIAMCloudFormation,
5
+ getFeatureSummary,
6
+ generateBasicIAMPolicy,
7
+ generateFullIAMPolicy,
8
+ } = require('./iam-generator');
2
9
 
3
10
  describe('IAM Generator', () => {
4
11
  describe('getFeatureSummary', () => {
@@ -250,6 +257,28 @@ describe('IAM Generator', () => {
250
257
  expect(yaml).toContain('iam:ListPolicyVersions');
251
258
  });
252
259
 
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
+
253
282
  it('should include internal-error-queue pattern in SQS resources', () => {
254
283
  const appDefinition = {
255
284
  name: 'test-app',
@@ -238,6 +238,7 @@ Resources:
238
238
  - 'logs:FilterLogEvents'
239
239
  - 'logs:PutLogEvents'
240
240
  - 'logs:PutRetentionPolicy'
241
+ - 'logs:DeleteRetentionPolicy'
241
242
  Resource:
242
243
  - !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/lambda/*frigg*'
243
244
  - !Sub 'arn:aws:logs:*:${AWS::AccountId}:log-group:/aws/lambda/*frigg*:*'
@@ -183,7 +183,8 @@
183
183
  "logs:DescribeLogStreams",
184
184
  "logs:FilterLogEvents",
185
185
  "logs:PutLogEvents",
186
- "logs:PutRetentionPolicy"
186
+ "logs:PutRetentionPolicy",
187
+ "logs:DeleteRetentionPolicy"
187
188
  ],
188
189
  "Resource": [
189
190
  "arn:aws:logs:*:*:log-group:/aws/lambda/*frigg*",
@@ -183,7 +183,8 @@
183
183
  "logs:DescribeLogStreams",
184
184
  "logs:FilterLogEvents",
185
185
  "logs:PutLogEvents",
186
- "logs:PutRetentionPolicy"
186
+ "logs:PutRetentionPolicy",
187
+ "logs:DeleteRetentionPolicy"
187
188
  ],
188
189
  "Resource": [
189
190
  "arn:aws:logs:*:*:log-group:/aws/lambda/*frigg*",
@@ -13,6 +13,7 @@ const {
13
13
  isSsmOffloadActive,
14
14
  getOffloadedKeys,
15
15
  } = require('../parameters/offload-utils');
16
+ const { normalizeLogLevel } = require('./utilities/logging-config');
16
17
 
17
18
  // OTLP-family exporters read their endpoint/headers from these standard env
18
19
  // vars (ADR-011). When such an exporter is configured we auto-register them as
@@ -44,6 +45,20 @@ function addTelemetryEnvPassthrough(appDefinition, envVars) {
44
45
  }
45
46
  }
46
47
 
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
+
47
62
  /**
48
63
  * Get environment variables from AppDefinition
49
64
  *
@@ -120,6 +135,8 @@ function getAppEnvironmentVars(appDefinition) {
120
135
  envKeys.push(key);
121
136
  }
122
137
 
138
+ addLoggingEnv(appDefinition, envVars);
139
+
123
140
  if (envKeys.length > 0) {
124
141
  console.log(
125
142
  ` Found ${envKeys.length} environment variables: ${envKeys.join(
@@ -147,6 +147,67 @@ 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
+
150
211
  describe("getAppEnvironmentVars() - 'ssm' offload", () => {
151
212
  const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
152
213
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { buildEnvironment } = require('../environment-builder');
11
11
  const { nestedNodeModulesExcludes } = require('./nested-node-modules');
12
+ const { buildLoggingProviderConfig } = require('./logging-config');
12
13
 
13
14
  /**
14
15
  * Create base serverless definition with core functions and resources
@@ -33,6 +34,7 @@ function createBaseDefinition(
33
34
  usePrismaLayer = true
34
35
  ) {
35
36
  const region = process.env.AWS_REGION || 'us-east-1';
37
+ const loggingConfig = buildLoggingProviderConfig(AppDefinition.logging);
36
38
 
37
39
  // Package config for handlers that skip esbuild (need node_modules dependencies)
38
40
  // Include backend src/ and index.js since handlers load the app definition
@@ -164,7 +166,7 @@ function createBaseDefinition(
164
166
  };
165
167
 
166
168
  return {
167
- frameworkVersion: '>=3.17.0',
169
+ frameworkVersion: loggingConfig.frameworkVersion || '>=3.17.0',
168
170
  service: AppDefinition.name || 'create-frigg-app',
169
171
  package: {
170
172
  individually: true,
@@ -218,6 +220,7 @@ function createBaseDefinition(
218
220
  name: '${opt:stage, "dev"}-${self:service}',
219
221
  disableDefaultEndpoint: false,
220
222
  },
223
+ ...loggingConfig.provider,
221
224
  },
222
225
  plugins: [
223
226
  'serverless-esbuild',
@@ -317,3 +317,73 @@ 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('maps logging.level to provider.logs.lambda and sets frameworkVersion >=3.58.0', () => {
334
+ const result = build({ level: 'warn' });
335
+
336
+ expect(result.provider.logs).toEqual({
337
+ lambda: {
338
+ logFormat: 'JSON',
339
+ applicationLogLevel: 'WARN',
340
+ systemLogLevel: 'INFO',
341
+ },
342
+ });
343
+ expect(result.provider.logRetentionInDays).toBeUndefined();
344
+ expect(result.frameworkVersion).toBe('>=3.58.0');
345
+ });
346
+
347
+ it.each(['info', 'INFO', 'Info', 'fatal', 'TRACE'])(
348
+ 'accepts level "%s" in any case',
349
+ (level) => {
350
+ const result = build({ level });
351
+
352
+ expect(result.provider.logs.lambda.applicationLogLevel).toBe(
353
+ level.toUpperCase()
354
+ );
355
+ }
356
+ );
357
+
358
+ it.each(['verbose', 'warning', '', 3])('throws on unknown level %p', (level) => {
359
+ expect(() => build({ level })).toThrow(/logging\.level/);
360
+ });
361
+
362
+ it('maps retentionInDays to provider.logRetentionInDays without a logs block', () => {
363
+ const result = build({ retentionInDays: 30 });
364
+
365
+ expect(result.provider.logRetentionInDays).toBe(30);
366
+ expect(result.provider.logs).toBeUndefined();
367
+ expect(result.frameworkVersion).toBe('>=3.17.0');
368
+ });
369
+
370
+ it('accepts 1096, which CloudWatch allows but the osls schema omits', () => {
371
+ expect(build({ retentionInDays: 1096 }).provider.logRetentionInDays).toBe(1096);
372
+ });
373
+
374
+ it.each([10, 0, -1, 30.5, '30'])(
375
+ 'throws on retentionInDays %p, which CloudWatch rejects',
376
+ (retentionInDays) => {
377
+ expect(() => build({ retentionInDays })).toThrow(
378
+ /logging\.retentionInDays/
379
+ );
380
+ }
381
+ );
382
+
383
+ it('sets both level and retention together', () => {
384
+ const result = build({ level: 'debug', retentionInDays: 14 });
385
+
386
+ expect(result.provider.logs.lambda.applicationLogLevel).toBe('DEBUG');
387
+ expect(result.provider.logRetentionInDays).toBe(14);
388
+ });
389
+ });
@@ -0,0 +1,77 @@
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 (Phase 0 spike). Set to false to leave Lambda on
13
+ // its default Text format; FRIGG_LOG_LEVEL then filters in process alone.
14
+ const EMIT_LAMBDA_JSON_LOGGING_CONFIG = true;
15
+
16
+ function normalizeLogLevel(level) {
17
+ const normalized = typeof level === 'string' ? level.toUpperCase() : level;
18
+ if (!LOG_LEVELS.includes(normalized)) {
19
+ throw new Error(
20
+ `Invalid logging.level ${JSON.stringify(level)}. Use one of: ${LOG_LEVELS.join(', ')} (any case).`
21
+ );
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ function validateRetentionInDays(retentionInDays) {
27
+ if (!RETENTION_DAYS.includes(retentionInDays)) {
28
+ throw new Error(
29
+ `Invalid logging.retentionInDays ${JSON.stringify(retentionInDays)}. CloudWatch accepts: ${RETENTION_DAYS.join(', ')}.`
30
+ );
31
+ }
32
+ return retentionInDays;
33
+ }
34
+
35
+ /**
36
+ * Map appDefinition.logging to provider-level serverless settings.
37
+ * Returns an empty object when logging is absent (ADR-048 §12: no change).
38
+ *
39
+ * @param {Object} [logging] - appDefinition.logging
40
+ * @returns {{ frameworkVersion?: string, provider?: Object }}
41
+ */
42
+ function buildLoggingProviderConfig(logging) {
43
+ if (!logging) return {};
44
+
45
+ const provider = {};
46
+ const result = {};
47
+
48
+ if (logging.level !== undefined) {
49
+ const level = normalizeLogLevel(logging.level);
50
+ result.frameworkVersion = LOGGING_CONFIG_FRAMEWORK_VERSION;
51
+ if (EMIT_LAMBDA_JSON_LOGGING_CONFIG) {
52
+ provider.logs = {
53
+ lambda: {
54
+ logFormat: 'JSON',
55
+ applicationLogLevel: level,
56
+ systemLogLevel: 'INFO',
57
+ },
58
+ };
59
+ }
60
+ }
61
+
62
+ if (logging.retentionInDays !== undefined) {
63
+ provider.logRetentionInDays = validateRetentionInDays(
64
+ logging.retentionInDays
65
+ );
66
+ }
67
+
68
+ if (Object.keys(provider).length > 0) result.provider = provider;
69
+ return result;
70
+ }
71
+
72
+ module.exports = {
73
+ LOG_LEVELS,
74
+ RETENTION_DAYS,
75
+ normalizeLogLevel,
76
+ buildLoggingProviderConfig,
77
+ };
@@ -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('nodejs20.x');
153
+ expect(result.provider.runtime).toBe('nodejs22.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,6 +238,54 @@ describe('composeServerlessDefinition', () => {
238
238
  });
239
239
  });
240
240
 
241
+ describe('Logging (ADR-048)', () => {
242
+ it('carries logging into provider.logs, FRIGG_LOG_LEVEL and logRetentionInDays', async () => {
243
+ const result = await composeServerlessDefinition({
244
+ integrations: [mockIntegration],
245
+ logging: { level: 'debug', retentionInDays: 30 },
246
+ });
247
+
248
+ expect(result.frameworkVersion).toBe('>=3.58.0');
249
+ expect(result.provider.logs.lambda).toEqual({
250
+ logFormat: 'JSON',
251
+ applicationLogLevel: 'DEBUG',
252
+ systemLogLevel: 'INFO',
253
+ });
254
+ expect(result.provider.logRetentionInDays).toBe(30);
255
+ expect(result.provider.environment.FRIGG_LOG_LEVEL).toBe('DEBUG');
256
+ });
257
+
258
+ it('sets no function-level logs, which would replace the provider block', async () => {
259
+ const result = await composeServerlessDefinition({
260
+ integrations: [mockIntegration],
261
+ logging: { level: 'warn' },
262
+ });
263
+
264
+ const functions = Object.values(result.functions);
265
+ expect(functions.length).toBeGreaterThan(0);
266
+ expect(functions.every((fn) => fn.logs === undefined)).toBe(true);
267
+ });
268
+
269
+ it('lets logging.level win over environment: { FRIGG_LOG_LEVEL: true }', async () => {
270
+ const result = await composeServerlessDefinition({
271
+ integrations: [],
272
+ environment: { FRIGG_LOG_LEVEL: true },
273
+ logging: { level: 'error' },
274
+ });
275
+
276
+ expect(result.provider.environment.FRIGG_LOG_LEVEL).toBe('ERROR');
277
+ });
278
+
279
+ it('changes nothing when logging is absent', async () => {
280
+ const result = await composeServerlessDefinition({ integrations: [] });
281
+
282
+ expect(result.frameworkVersion).toBe('>=3.17.0');
283
+ expect(result.provider.logs).toBeUndefined();
284
+ expect(result.provider.logRetentionInDays).toBeUndefined();
285
+ expect(result.provider.environment).not.toHaveProperty('FRIGG_LOG_LEVEL');
286
+ });
287
+ });
288
+
241
289
  describe('VPC Configuration', () => {
242
290
  it('should add VPC configuration when vpc.enable is true with discover mode', async () => {
243
291
  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.653.8213092.0",
4
+ "version": "2.0.0--canary.654.be44e66.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.653.8213092.0",
30
- "@friggframework/schemas": "2.0.0--canary.653.8213092.0",
31
- "@friggframework/test": "2.0.0--canary.653.8213092.0",
29
+ "@friggframework/core": "2.0.0--canary.654.be44e66.0",
30
+ "@friggframework/schemas": "2.0.0--canary.654.be44e66.0",
31
+ "@friggframework/test": "2.0.0--canary.654.be44e66.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.653.8213092.0",
60
- "@friggframework/prettier-config": "2.0.0--canary.653.8213092.0",
59
+ "@friggframework/eslint-config": "2.0.0--canary.654.be44e66.0",
60
+ "@friggframework/prettier-config": "2.0.0--canary.654.be44e66.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.40.1",
64
+ "osls": "^3.58.0",
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": "82130925554847233af5021a9cbed628446c45f7"
92
+ "gitHead": "be44e66226ec1db051c264a5f8cf02998f3aa14c"
93
93
  }
@@ -1,338 +0,0 @@
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
-