@friggframework/devtools 2.0.0-next.28 → 2.0.0-next.29

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 (34) hide show
  1. package/frigg-cli/build-command/index.js +4 -2
  2. package/frigg-cli/deploy-command/index.js +5 -2
  3. package/frigg-cli/generate-iam-command.js +115 -0
  4. package/frigg-cli/index.js +11 -1
  5. package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +245 -0
  6. package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +596 -0
  7. package/infrastructure/DEPLOYMENT-INSTRUCTIONS.md +268 -0
  8. package/infrastructure/GENERATE-IAM-DOCS.md +253 -0
  9. package/infrastructure/IAM-POLICY-TEMPLATES.md +176 -0
  10. package/infrastructure/README-TESTING.md +332 -0
  11. package/infrastructure/README.md +421 -0
  12. package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -0
  13. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  14. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  15. package/infrastructure/aws-discovery.js +568 -0
  16. package/infrastructure/aws-discovery.test.js +373 -0
  17. package/infrastructure/build-time-discovery.js +206 -0
  18. package/infrastructure/build-time-discovery.test.js +375 -0
  19. package/infrastructure/create-frigg-infrastructure.js +2 -2
  20. package/infrastructure/frigg-deployment-iam-stack.yaml +379 -0
  21. package/infrastructure/iam-generator.js +687 -0
  22. package/infrastructure/iam-generator.test.js +169 -0
  23. package/infrastructure/iam-policy-basic.json +212 -0
  24. package/infrastructure/iam-policy-full.json +282 -0
  25. package/infrastructure/integration.test.js +383 -0
  26. package/infrastructure/run-discovery.js +110 -0
  27. package/infrastructure/serverless-template.js +514 -167
  28. package/infrastructure/serverless-template.test.js +541 -0
  29. package/management-ui/dist/assets/FriggLogo-B7Xx8ZW1.svg +1 -0
  30. package/management-ui/dist/assets/index-BA21WgFa.js +1221 -0
  31. package/management-ui/dist/assets/index-CbM64Oba.js +1221 -0
  32. package/management-ui/dist/assets/index-CkvseXTC.css +1 -0
  33. package/management-ui/dist/index.html +14 -0
  34. package/package.json +9 -5
@@ -0,0 +1,541 @@
1
+ const { composeServerlessDefinition } = require('./serverless-template');
2
+
3
+ // Mock AWS Discovery to prevent actual AWS calls
4
+ jest.mock('./aws-discovery', () => {
5
+ return {
6
+ AWSDiscovery: jest.fn().mockImplementation(() => {
7
+ return {
8
+ discoverResources: jest.fn().mockResolvedValue({
9
+ defaultVpcId: 'vpc-123456',
10
+ defaultSecurityGroupId: 'sg-123456',
11
+ privateSubnetId1: 'subnet-123456',
12
+ privateSubnetId2: 'subnet-789012',
13
+ publicSubnetId: 'subnet-public',
14
+ defaultRouteTableId: 'rtb-123456',
15
+ defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
16
+ })
17
+ };
18
+ })
19
+ };
20
+ });
21
+
22
+ describe('composeServerlessDefinition', () => {
23
+ let mockIntegration;
24
+
25
+ beforeEach(() => {
26
+ mockIntegration = {
27
+ Definition: {
28
+ name: 'testIntegration'
29
+ }
30
+ };
31
+
32
+ // Mock process.argv to avoid offline mode during tests
33
+ process.argv = ['node', 'test'];
34
+
35
+ // Clear AWS_REGION for tests
36
+ delete process.env.AWS_REGION;
37
+ });
38
+
39
+ afterEach(() => {
40
+ jest.restoreAllMocks();
41
+ // Restore env
42
+ delete process.env.AWS_REGION;
43
+ });
44
+
45
+ describe('Basic Configuration', () => {
46
+ it('should create basic serverless definition with minimal app definition', async () => {
47
+ const appDefinition = {
48
+ name: 'test-app',
49
+ integrations: []
50
+ };
51
+
52
+ const result = await composeServerlessDefinition(appDefinition);
53
+
54
+ expect(result.service).toBe('test-app');
55
+ expect(result.provider.name).toBe('aws');
56
+ expect(result.provider.runtime).toBe('nodejs20.x');
57
+ expect(result.provider.region).toBe('us-east-1');
58
+ expect(result.provider.stage).toBe('${opt:stage}');
59
+ expect(result.frameworkVersion).toBe('>=3.17.0');
60
+ });
61
+
62
+ it('should use default service name when name not provided', async () => {
63
+ const appDefinition = {
64
+ integrations: []
65
+ };
66
+
67
+ const result = await composeServerlessDefinition(appDefinition);
68
+
69
+ expect(result.service).toBe('create-frigg-app');
70
+ });
71
+
72
+ it('should use custom provider when specified', async () => {
73
+ const appDefinition = {
74
+ provider: 'custom-provider',
75
+ integrations: []
76
+ };
77
+
78
+ const result = await composeServerlessDefinition(appDefinition);
79
+
80
+ expect(result.provider.name).toBe('custom-provider');
81
+ });
82
+
83
+ it('should use AWS_REGION environment variable when set', async () => {
84
+ process.env.AWS_REGION = 'eu-west-1';
85
+
86
+ const appDefinition = {
87
+ name: 'test-app',
88
+ integrations: []
89
+ };
90
+
91
+ const result = await composeServerlessDefinition(appDefinition);
92
+
93
+ expect(result.provider.region).toBe('eu-west-1');
94
+ expect(result.custom['serverless-offline-sqs'].region).toBe('eu-west-1');
95
+ });
96
+
97
+ it('should default to us-east-1 when AWS_REGION is not set', async () => {
98
+ delete process.env.AWS_REGION;
99
+
100
+ const appDefinition = {
101
+ name: 'test-app',
102
+ integrations: []
103
+ };
104
+
105
+ const result = await composeServerlessDefinition(appDefinition);
106
+
107
+ expect(result.provider.region).toBe('us-east-1');
108
+ expect(result.custom['serverless-offline-sqs'].region).toBe('us-east-1');
109
+ });
110
+ });
111
+
112
+ describe('VPC Configuration', () => {
113
+ it('should add VPC configuration when vpc.enable is true', async () => {
114
+ const appDefinition = {
115
+ vpc: { enable: true },
116
+ integrations: []
117
+ };
118
+
119
+ const result = await composeServerlessDefinition(appDefinition);
120
+
121
+ expect(result.provider.vpc).toBe('${self:custom.vpc.${self:provider.stage}}');
122
+ expect(result.custom.vpc).toEqual({
123
+ '${self:provider.stage}': {
124
+ securityGroupIds: ['${env:AWS_DISCOVERY_SECURITY_GROUP_ID}'],
125
+ subnetIds: [
126
+ '${env:AWS_DISCOVERY_SUBNET_ID_1}',
127
+ '${env:AWS_DISCOVERY_SUBNET_ID_2}'
128
+ ]
129
+ }
130
+ });
131
+ });
132
+
133
+ it('should add VPC endpoint for S3 when VPC is enabled', async () => {
134
+ const appDefinition = {
135
+ vpc: { enable: true },
136
+ integrations: []
137
+ };
138
+
139
+ const result = await composeServerlessDefinition(appDefinition);
140
+
141
+ expect(result.resources.Resources.VPCEndpointS3).toEqual({
142
+ Type: 'AWS::EC2::VPCEndpoint',
143
+ Properties: {
144
+ VpcId: '${env:AWS_DISCOVERY_VPC_ID}',
145
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
146
+ VpcEndpointType: 'Gateway',
147
+ RouteTableIds: ['${env:AWS_DISCOVERY_ROUTE_TABLE_ID}']
148
+ }
149
+ });
150
+ });
151
+
152
+ it('should not add VPC configuration when vpc.enable is false', async () => {
153
+ const appDefinition = {
154
+ vpc: { enable: false },
155
+ integrations: []
156
+ };
157
+
158
+ const result = await composeServerlessDefinition(appDefinition);
159
+
160
+ expect(result.provider.vpc).toBeUndefined();
161
+ expect(result.custom.vpc).toBeUndefined();
162
+ expect(result.resources.Resources.VPCEndpointS3).toBeUndefined();
163
+ });
164
+
165
+ it('should not add VPC configuration when vpc is not defined', async () => {
166
+ const appDefinition = {
167
+ integrations: []
168
+ };
169
+
170
+ const result = await composeServerlessDefinition(appDefinition);
171
+
172
+ expect(result.provider.vpc).toBeUndefined();
173
+ expect(result.custom.vpc).toBeUndefined();
174
+ });
175
+ });
176
+
177
+ describe('KMS Configuration', () => {
178
+ it('should add KMS configuration when encryption is enabled', async () => {
179
+ const appDefinition = {
180
+ encryption: { useDefaultKMSForFieldLevelEncryption: true },
181
+ integrations: []
182
+ };
183
+
184
+ const result = await composeServerlessDefinition(appDefinition);
185
+
186
+ // Check IAM permissions
187
+ const kmsPermission = result.provider.iamRoleStatements.find(
188
+ statement => statement.Action.includes('kms:GenerateDataKey')
189
+ );
190
+ expect(kmsPermission).toEqual({
191
+ Effect: 'Allow',
192
+ Action: [
193
+ 'kms:GenerateDataKey',
194
+ 'kms:Decrypt'
195
+ ],
196
+ Resource: ['${self:custom.kmsGrants.kmsKeyId}']
197
+ });
198
+
199
+ // Check environment variable
200
+ expect(result.provider.environment.KMS_KEY_ARN).toBe('${self:custom.kmsGrants.kmsKeyId}');
201
+
202
+ // Check plugin
203
+ expect(result.plugins).toContain('serverless-kms-grants');
204
+
205
+ // Check custom configuration
206
+ expect(result.custom.kmsGrants).toEqual({
207
+ kmsKeyId: '${env:AWS_DISCOVERY_KMS_KEY_ID}'
208
+ });
209
+ });
210
+
211
+ it('should not add KMS configuration when encryption is disabled', async () => {
212
+ const appDefinition = {
213
+ encryption: { useDefaultKMSForFieldLevelEncryption: false },
214
+ integrations: []
215
+ };
216
+
217
+ const result = await composeServerlessDefinition(appDefinition);
218
+
219
+ const kmsPermission = result.provider.iamRoleStatements.find(
220
+ statement => statement.Action && statement.Action.includes('kms:GenerateDataKey')
221
+ );
222
+ expect(kmsPermission).toBeUndefined();
223
+ expect(result.provider.environment.KMS_KEY_ARN).toBeUndefined();
224
+ expect(result.plugins).not.toContain('serverless-kms-grants');
225
+ expect(result.custom.kmsGrants).toBeUndefined();
226
+ });
227
+
228
+ it('should not add KMS configuration when encryption is not defined', async () => {
229
+ const appDefinition = {
230
+ integrations: []
231
+ };
232
+
233
+ const result = await composeServerlessDefinition(appDefinition);
234
+
235
+ const kmsPermission = result.provider.iamRoleStatements.find(
236
+ statement => statement.Action && statement.Action.includes('kms:GenerateDataKey')
237
+ );
238
+ expect(kmsPermission).toBeUndefined();
239
+ expect(result.custom.kmsGrants).toBeUndefined();
240
+ });
241
+ });
242
+
243
+ describe('SSM Configuration', () => {
244
+ it('should add SSM configuration when ssm.enable is true', async () => {
245
+ const appDefinition = {
246
+ ssm: { enable: true },
247
+ integrations: []
248
+ };
249
+
250
+ const result = await composeServerlessDefinition(appDefinition);
251
+
252
+ // Check lambda layers
253
+ expect(result.provider.layers).toEqual([
254
+ 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11'
255
+ ]);
256
+
257
+ // Check IAM permissions
258
+ const ssmPermission = result.provider.iamRoleStatements.find(
259
+ statement => statement.Action.includes('ssm:GetParameter')
260
+ );
261
+ expect(ssmPermission).toEqual({
262
+ Effect: 'Allow',
263
+ Action: [
264
+ 'ssm:GetParameter',
265
+ 'ssm:GetParameters',
266
+ 'ssm:GetParametersByPath'
267
+ ],
268
+ Resource: [
269
+ 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'
270
+ ]
271
+ });
272
+
273
+ // Check environment variable
274
+ expect(result.provider.environment.SSM_PARAMETER_PREFIX).toBe('/${self:service}/${self:provider.stage}');
275
+ });
276
+
277
+ it('should not add SSM configuration when ssm.enable is false', async () => {
278
+ const appDefinition = {
279
+ ssm: { enable: false },
280
+ integrations: []
281
+ };
282
+
283
+ const result = await composeServerlessDefinition(appDefinition);
284
+
285
+ expect(result.provider.layers).toBeUndefined();
286
+
287
+ const ssmPermission = result.provider.iamRoleStatements.find(
288
+ statement => statement.Action && statement.Action.includes('ssm:GetParameter')
289
+ );
290
+ expect(ssmPermission).toBeUndefined();
291
+ expect(result.provider.environment.SSM_PARAMETER_PREFIX).toBeUndefined();
292
+ });
293
+
294
+ it('should not add SSM configuration when ssm is not defined', async () => {
295
+ const appDefinition = {
296
+ integrations: []
297
+ };
298
+
299
+ const result = await composeServerlessDefinition(appDefinition);
300
+
301
+ expect(result.provider.layers).toBeUndefined();
302
+ expect(result.provider.environment.SSM_PARAMETER_PREFIX).toBeUndefined();
303
+ });
304
+ });
305
+
306
+ describe('Integration Configuration', () => {
307
+ it('should add integration-specific resources and functions', async () => {
308
+ const appDefinition = {
309
+ integrations: [mockIntegration]
310
+ };
311
+
312
+ const result = await composeServerlessDefinition(appDefinition);
313
+
314
+ // Check integration function
315
+ expect(result.functions.testIntegration).toEqual({
316
+ handler: 'node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.testIntegration.handler',
317
+ events: [{
318
+ http: {
319
+ path: '/api/testIntegration-integration/{proxy+}',
320
+ method: 'ANY',
321
+ cors: true
322
+ }
323
+ }]
324
+ });
325
+
326
+ // Check SQS Queue
327
+ expect(result.resources.Resources.TestIntegrationQueue).toEqual({
328
+ Type: 'AWS::SQS::Queue',
329
+ Properties: {
330
+ QueueName: '${self:custom.TestIntegrationQueue}',
331
+ MessageRetentionPeriod: 60,
332
+ VisibilityTimeout: 1800,
333
+ RedrivePolicy: {
334
+ maxReceiveCount: 1,
335
+ deadLetterTargetArn: {
336
+ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn']
337
+ }
338
+ }
339
+ }
340
+ });
341
+
342
+ // Check Queue Worker
343
+ expect(result.functions.testIntegrationQueueWorker).toEqual({
344
+ handler: 'node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.testIntegration.queueWorker',
345
+ reservedConcurrency: 5,
346
+ events: [{
347
+ sqs: {
348
+ arn: {
349
+ 'Fn::GetAtt': ['TestIntegrationQueue', 'Arn']
350
+ },
351
+ batchSize: 1
352
+ }
353
+ }],
354
+ timeout: 600
355
+ });
356
+
357
+ // Check environment variable
358
+ expect(result.provider.environment.TESTINTEGRATION_QUEUE_URL).toEqual({
359
+ Ref: 'TestIntegrationQueue'
360
+ });
361
+
362
+ // Check custom queue name
363
+ expect(result.custom.TestIntegrationQueue).toBe('${self:service}--${self:provider.stage}-TestIntegrationQueue');
364
+ });
365
+
366
+ it('should handle multiple integrations', async () => {
367
+ const secondIntegration = {
368
+ Definition: {
369
+ name: 'secondIntegration'
370
+ }
371
+ };
372
+
373
+ const appDefinition = {
374
+ integrations: [mockIntegration, secondIntegration]
375
+ };
376
+
377
+ const result = await composeServerlessDefinition(appDefinition);
378
+
379
+ expect(result.functions.testIntegration).toBeDefined();
380
+ expect(result.functions.secondIntegration).toBeDefined();
381
+ expect(result.functions.testIntegrationQueueWorker).toBeDefined();
382
+ expect(result.functions.secondIntegrationQueueWorker).toBeDefined();
383
+ expect(result.resources.Resources.TestIntegrationQueue).toBeDefined();
384
+ expect(result.resources.Resources.SecondIntegrationQueue).toBeDefined();
385
+ });
386
+ });
387
+
388
+ describe('Combined Configurations', () => {
389
+ it('should combine VPC, KMS, and SSM configurations', async () => {
390
+ const appDefinition = {
391
+ vpc: { enable: true },
392
+ encryption: { useDefaultKMSForFieldLevelEncryption: true },
393
+ ssm: { enable: true },
394
+ integrations: [mockIntegration]
395
+ };
396
+
397
+ const result = await composeServerlessDefinition(appDefinition);
398
+
399
+ // VPC
400
+ expect(result.provider.vpc).toBeDefined();
401
+ expect(result.custom.vpc).toBeDefined();
402
+ expect(result.resources.Resources.VPCEndpointS3).toBeDefined();
403
+
404
+ // KMS
405
+ expect(result.plugins).toContain('serverless-kms-grants');
406
+ expect(result.provider.environment.KMS_KEY_ARN).toBeDefined();
407
+ expect(result.custom.kmsGrants).toBeDefined();
408
+
409
+ // SSM
410
+ expect(result.provider.layers).toBeDefined();
411
+ expect(result.provider.environment.SSM_PARAMETER_PREFIX).toBeDefined();
412
+
413
+ // Integration
414
+ expect(result.functions.testIntegration).toBeDefined();
415
+ expect(result.resources.Resources.TestIntegrationQueue).toBeDefined();
416
+
417
+ // All plugins should be present
418
+ expect(result.plugins).toEqual([
419
+ 'serverless-jetpack',
420
+ 'serverless-dotenv-plugin',
421
+ 'serverless-offline-sqs',
422
+ 'serverless-offline',
423
+ '@friggframework/serverless-plugin',
424
+ 'serverless-kms-grants'
425
+ ]);
426
+ });
427
+
428
+ it('should handle partial configuration combinations', async () => {
429
+ const appDefinition = {
430
+ vpc: { enable: true },
431
+ encryption: { useDefaultKMSForFieldLevelEncryption: true },
432
+ integrations: []
433
+ };
434
+
435
+ const result = await composeServerlessDefinition(appDefinition);
436
+
437
+ // VPC and KMS should be present
438
+ expect(result.provider.vpc).toBeDefined();
439
+ expect(result.custom.kmsGrants).toBeDefined();
440
+
441
+ // SSM should not be present
442
+ expect(result.provider.layers).toBeUndefined();
443
+ expect(result.provider.environment.SSM_PARAMETER_PREFIX).toBeUndefined();
444
+ });
445
+ });
446
+
447
+ describe('Default Resources', () => {
448
+ it('should always include default resources', async () => {
449
+ const appDefinition = {
450
+ integrations: []
451
+ };
452
+
453
+ const result = await composeServerlessDefinition(appDefinition);
454
+
455
+ // Check default resources are always present
456
+ expect(result.resources.Resources.InternalErrorQueue).toBeDefined();
457
+ expect(result.resources.Resources.InternalErrorBridgeTopic).toBeDefined();
458
+ expect(result.resources.Resources.InternalErrorBridgePolicy).toBeDefined();
459
+ expect(result.resources.Resources.ApiGatewayAlarm5xx).toBeDefined();
460
+
461
+ // Check default functions
462
+ expect(result.functions.defaultWebsocket).toBeDefined();
463
+ expect(result.functions.auth).toBeDefined();
464
+ expect(result.functions.user).toBeDefined();
465
+
466
+ // Check default plugins
467
+ expect(result.plugins).toContain('serverless-jetpack');
468
+ expect(result.plugins).toContain('serverless-dotenv-plugin');
469
+ expect(result.plugins).toContain('@friggframework/serverless-plugin');
470
+ });
471
+
472
+ it('should always include default IAM permissions', async () => {
473
+ const appDefinition = {
474
+ integrations: []
475
+ };
476
+
477
+ const result = await composeServerlessDefinition(appDefinition);
478
+
479
+ // Check SNS publish permission
480
+ const snsPermission = result.provider.iamRoleStatements.find(
481
+ statement => statement.Action.includes('sns:Publish')
482
+ );
483
+ expect(snsPermission).toBeDefined();
484
+
485
+ // Check SQS permissions
486
+ const sqsPermission = result.provider.iamRoleStatements.find(
487
+ statement => statement.Action.includes('sqs:SendMessage')
488
+ );
489
+ expect(sqsPermission).toBeDefined();
490
+ });
491
+
492
+ it('should include default environment variables', async () => {
493
+ const appDefinition = {
494
+ integrations: []
495
+ };
496
+
497
+ const result = await composeServerlessDefinition(appDefinition);
498
+
499
+ expect(result.provider.environment.STAGE).toBe('${opt:stage}');
500
+ expect(result.provider.environment.AWS_NODEJS_CONNECTION_REUSE_ENABLED).toBe(1);
501
+ });
502
+ });
503
+
504
+ describe('Edge Cases', () => {
505
+ it('should handle empty app definition', async () => {
506
+ const appDefinition = {};
507
+
508
+ await expect(composeServerlessDefinition(appDefinition)).resolves.not.toThrow();
509
+ const result = await composeServerlessDefinition(appDefinition);
510
+ expect(result.service).toBe('create-frigg-app');
511
+ });
512
+
513
+ it('should handle null/undefined integrations', async () => {
514
+ const appDefinition = {
515
+ integrations: null
516
+ };
517
+
518
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
519
+ });
520
+
521
+ it('should handle integration with missing Definition', async () => {
522
+ const invalidIntegration = {};
523
+ const appDefinition = {
524
+ integrations: [invalidIntegration]
525
+ };
526
+
527
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
528
+ });
529
+
530
+ it('should handle integration with missing name', async () => {
531
+ const invalidIntegration = {
532
+ Definition: {}
533
+ };
534
+ const appDefinition = {
535
+ integrations: [invalidIntegration]
536
+ };
537
+
538
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
539
+ });
540
+ });
541
+ });
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 201.65"><defs><style>.cls-1,.cls-2{fill:#71a087;}.cls-3{letter-spacing:.02em;}.cls-3,.cls-4,.cls-5{font-family:DINCondensed-Bold, 'DIN Condensed';}.cls-3,.cls-5{fill:#fff;font-size:56.17px;}.cls-4{font-size:114.37px;}.cls-4,.cls-6{stroke:#616061;stroke-miterlimit:10;}.cls-4,.cls-6,.cls-7{fill:#565859;}.cls-5{letter-spacing:.03em;}.cls-7,.cls-2{fill-rule:evenodd;}</style></defs><g id="BADGE"><rect class="cls-1" x="0" y="121.65" width="400" height="80" rx="13" ry="13"/><g><text class="cls-5" transform="translate(132.49 179.9)"><tspan x="0" y="0">LEFT</tspan></text><text class="cls-5" transform="translate(235.62 179.14)"><tspan x="0" y="0">HOOK</tspan></text><text class="cls-3" transform="translate(64.35 179.9)"><tspan x="0" y="0">BY</tspan></text></g></g><g id="FRIGG"><path class="cls-6" d="M147.74,16.97h35.79v11.06h-23.78v24.89h20.72v11.06h-20.72v35.04h-12.01V16.97Z"/><path class="cls-6" d="M193.68,16.97h19.31c14.13,0,21.19,8.03,21.19,24.09,0,4.76-.77,8.86-2.3,12.27-1.53,3.42-4.22,6.17-8.06,8.24l12.95,37.46h-12.71l-11.18-35.04h-7.18v35.04h-12.01V16.97Zm12.01,36.65h6.83c2.12,0,3.81-.29,5.06-.87,1.25-.58,2.22-1.4,2.88-2.48,.67-1.08,1.12-2.4,1.35-3.98,.23-1.57,.35-3.4,.35-5.47s-.12-3.9-.35-5.47c-.24-1.57-.73-2.92-1.47-4.03-.75-1.11-1.79-1.94-3.12-2.48-1.34-.54-3.1-.81-5.3-.81h-6.24v25.59Z"/><path class="cls-6" d="M278.29,36.69c0-3.25,.58-6.12,1.74-8.61,1.16-2.49,2.68-4.55,4.57-6.18,1.89-1.62,4.01-2.85,6.37-3.68,2.35-.83,4.73-1.25,7.12-1.25s4.76,.42,7.12,1.25c2.35,.83,4.48,2.06,6.37,3.68,1.89,1.63,3.41,3.68,4.57,6.18,1.16,2.49,1.74,5.37,1.74,8.61v4.08h-11.81v-4.08c0-2.79-.79-4.85-2.37-6.18-1.58-1.32-3.45-1.98-5.61-1.98s-4.03,.66-5.61,1.98c-1.58,1.32-2.37,3.38-2.37,6.18v42.61c0,2.8,.79,4.86,2.37,6.18,1.58,1.32,3.45,1.98,5.61,1.98s4.03-.66,5.61-1.98c1.58-1.32,2.37-3.38,2.37-6.18v-15.19h-9.38v-10.2h21.19v25.39c0,3.33-.58,6.22-1.74,8.67-1.16,2.46-2.68,4.5-4.57,6.12-1.89,1.63-4.01,2.85-6.37,3.68-2.35,.83-4.73,1.25-7.12,1.25s-4.77-.42-7.12-1.25c-2.35-.83-4.48-2.06-6.37-3.68-1.89-1.62-3.41-3.66-4.57-6.12-1.16-2.45-1.74-5.34-1.74-8.67V36.69Z"/><path class="cls-6" d="M329.06,36.69c0-3.25,.58-6.12,1.74-8.61s2.68-4.55,4.57-6.18c1.89-1.62,4.01-2.85,6.37-3.68,2.35-.83,4.73-1.25,7.12-1.25s4.76,.42,7.12,1.25c2.35,.83,4.48,2.06,6.37,3.68,1.89,1.63,3.41,3.68,4.57,6.18,1.16,2.49,1.74,5.37,1.74,8.61v4.08h-11.81v-4.08c0-2.79-.79-4.85-2.37-6.18-1.58-1.32-3.45-1.98-5.61-1.98s-4.03,.66-5.61,1.98c-1.58,1.32-2.37,3.38-2.37,6.18v42.61c0,2.8,.79,4.86,2.37,6.18,1.58,1.32,3.45,1.98,5.61,1.98s4.03-.66,5.61-1.98c1.58-1.32,2.37-3.38,2.37-6.18v-15.19h-9.38v-10.2h21.19v25.39c0,3.33-.58,6.22-1.74,8.67-1.16,2.46-2.68,4.5-4.57,6.12-1.89,1.63-4.01,2.85-6.37,3.68-2.35,.83-4.73,1.25-7.12,1.25s-4.77-.42-7.12-1.25c-2.35-.83-4.48-2.06-6.37-3.68-1.89-1.62-3.41-3.66-4.57-6.12-1.16-2.45-1.74-5.34-1.74-8.67V36.69Z"/><text class="cls-4" transform="translate(243.51 98.4) scale(1.02 1)"><tspan x="0" y="0">I</tspan></text></g><g id="ICON"><path class="cls-2" d="M93.72,73.79c7.86-7.18,14-7.02,20.8,.72,2.94,4.35,.48,8.46-1.78,10.48-.02,.02-.04,.03-.05,.05l-2.28,1.92c2.32-4.48-1.91-5.1-3.52-4.05-.12,.08-.23,.16-.34,.26l-.03,.03-34.94,30.92c-.18,.15-.45,.13-.6-.04l-10.23-11.58c-.16-.18-.14-.45,.04-.61l32.94-28.1Z"/><path class="cls-7" d="M125.17,11.41l.32-8.7c.04-1.23-.41-1.26-1.85-.05l-42.08,35.62,7.98,7.85,33.42-29.03c1.42-1.25,2.16-4.16,2.21-5.69h0Z"/><path class="cls-2" d="M124.79,32.92l.32-8.7c.04-1.23-.41-1.26-1.85-.05l-30.28,25.64,8.05,7.51,21.54-18.71c1.42-1.25,2.16-4.16,2.21-5.69h0Z"/><path class="cls-7" d="M124.96,54.26l.32-8.7c.04-1.23-.41-1.26-1.85-.05l-18.01,15.24,7.88,7.37,9.43-8.17c1.42-1.25,2.16-4.16,2.23-5.69h0Z"/><path class="cls-2" d="M88.65,69.04L35.29,21.17c-3.14-2.81-3.26-5.55-3.12-8.58l.38-9.17L97.11,61.67l-8.46,7.37Z"/><path class="cls-7" d="M76.09,79.78L34.75,42.73c-1.65-1.49-2.15-6.15-2.08-7.85l.65-8.58,51.14,46.26-8.37,7.23Z"/><path class="cls-2" d="M62.39,91.85l-27.96-26.7c-1.93-1.84-3.17-4.71-3.07-6.67l.56-9.77,39.27,35.47-8.79,7.66Z"/></g></svg>