@friggframework/core 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.
- package/CLAUDE.md +702 -0
- package/README.md +999 -50
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-script-execution-repository-interface.js +171 -0
- package/admin-scripts/repositories/admin-script-execution-repository-mongo.js +238 -0
- package/admin-scripts/repositories/admin-script-execution-repository-postgres.js +278 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/README.md +451 -0
- package/application/commands/admin-script-commands.js +359 -0
- package/application/commands/credential-commands.js +262 -0
- package/application/commands/entity-commands.js +336 -0
- package/application/commands/integration-commands.js +356 -0
- package/application/commands/integration-mapping-commands.js +25 -0
- package/application/commands/process-commands.js +135 -0
- package/application/commands/report-commands.js +188 -0
- package/application/commands/scheduler-commands.js +263 -0
- package/application/commands/usage-commands.js +56 -0
- package/application/commands/user-commands.js +312 -0
- package/application/index.js +83 -0
- package/artifacts/repositories/artifact-repository-factory.js +19 -0
- package/artifacts/repositories/artifact-repository-interface.js +27 -0
- package/artifacts/repositories/artifact-repository-local.js +42 -0
- package/artifacts/repositories/artifact-repository-s3.js +61 -0
- package/assertions/index.js +0 -3
- package/core/CLAUDE.md +700 -0
- package/core/Worker.js +60 -24
- package/core/create-handler.js +189 -5
- package/core/parameters-to-env.js +257 -0
- package/core/ssm-preload.mjs +34 -0
- package/credential/repositories/credential-active-type.js +32 -0
- package/credential/repositories/credential-repository-documentdb.js +355 -0
- package/credential/repositories/credential-repository-factory.js +54 -0
- package/credential/repositories/credential-repository-interface.js +113 -0
- package/credential/repositories/credential-repository-mongo.js +294 -0
- package/credential/repositories/credential-repository-postgres.js +312 -0
- package/credential/repositories/credential-repository.js +300 -0
- package/credential/use-cases/get-credential-for-user.js +25 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +154 -0
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +192 -0
- package/database/encryption/README.md +839 -0
- package/database/encryption/documentdb-encryption-service.md +3575 -0
- package/database/encryption/encryption-schema-registry.js +401 -0
- package/database/encryption/field-encryption-service.js +254 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +230 -0
- package/database/index.js +21 -21
- package/database/prisma.js +182 -0
- package/database/repositories/health-check-repository-documentdb.js +138 -0
- package/database/repositories/health-check-repository-factory.js +48 -0
- package/database/repositories/health-check-repository-interface.js +82 -0
- package/database/repositories/health-check-repository-mongodb.js +89 -0
- package/database/repositories/health-check-repository-postgres.js +82 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-health-use-case.js +29 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +83 -0
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/resolve-migration-via-worker-use-case.js +49 -0
- package/database/use-cases/run-database-migration-use-case.js +139 -0
- package/database/use-cases/test-encryption-use-case.js +253 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +94 -0
- package/database/utils/mongodb-schema-init.js +108 -0
- package/database/utils/prisma-runner.js +491 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/encrypt/Cryptor.js +34 -168
- package/encrypt/index.js +1 -2
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +15 -7
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +5 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +5 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +386 -0
- package/generated/prisma-mongodb/index-browser.js +368 -0
- package/generated/prisma-mongodb/index.d.ts +27065 -0
- package/generated/prisma-mongodb/index.js +411 -0
- package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +35 -0
- package/generated/prisma-mongodb/runtime/edge.js +35 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +17 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
- package/generated/prisma-mongodb/runtime/library.js +147 -0
- package/generated/prisma-mongodb/runtime/react-native.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-mongodb/schema.prisma +455 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +393 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +5 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +5 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +407 -0
- package/generated/prisma-postgresql/index-browser.js +389 -0
- package/generated/prisma-postgresql/index.d.ts +29904 -0
- package/generated/prisma-postgresql/index.js +432 -0
- package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +35 -0
- package/generated/prisma-postgresql/runtime/edge.js +35 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +17 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
- package/generated/prisma-postgresql/runtime/library.js +147 -0
- package/generated/prisma-postgresql/runtime/react-native.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +85 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +38 -0
- package/generated/prisma-postgresql/schema.prisma +436 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +5 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +414 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +67 -0
- package/handlers/app-handler-helpers.js +3 -3
- package/handlers/backend-utils.js +271 -42
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +68 -0
- package/handlers/middleware/admin-auth.js +73 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +4 -15
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +329 -0
- package/handlers/routers/health.js +518 -0
- package/handlers/routers/integration-defined-routers.js +85 -10
- package/handlers/routers/integration-webhook-routers.js +80 -0
- package/handlers/routers/user.js +27 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
- package/handlers/workers/db-migration.js +427 -0
- package/handlers/workers/dlq-processor.js +63 -0
- package/handlers/workers/integration-defined-workers.js +13 -7
- package/index.js +102 -46
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
- package/infrastructure/scheduler/index.js +33 -0
- package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
- package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
- package/integrations/EXTENSIONS.md +240 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/extension.js +254 -0
- package/integrations/index.js +20 -10
- package/integrations/integration-base.js +730 -74
- package/integrations/integration-router.js +398 -180
- package/integrations/options.js +1 -1
- package/integrations/repositories/config-patch-shared.js +43 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +303 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +120 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +183 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +255 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-documentdb.js +322 -0
- package/integrations/repositories/integration-repository-factory.js +51 -0
- package/integrations/repositories/integration-repository-interface.js +189 -0
- package/integrations/repositories/integration-repository-mongo.js +450 -0
- package/integrations/repositories/integration-repository-postgres.js +492 -0
- package/integrations/repositories/process-repository-documentdb.js +311 -0
- package/integrations/repositories/process-repository-factory.js +53 -0
- package/integrations/repositories/process-repository-interface.js +136 -0
- package/integrations/repositories/process-repository-mongo.js +262 -0
- package/integrations/repositories/process-repository-postgres.js +380 -0
- package/integrations/repositories/process-update-ops-shared.js +112 -0
- package/integrations/repositories/report-id.js +13 -0
- package/integrations/tests/doubles/config-capturing-integration.js +81 -0
- package/integrations/tests/doubles/dummy-integration-class.js +113 -0
- package/integrations/tests/doubles/test-integration-repository.js +146 -0
- package/integrations/use-cases/create-integration.js +215 -0
- package/integrations/use-cases/create-process.js +130 -0
- package/integrations/use-cases/delete-integration-for-user.js +119 -0
- package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +88 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/get-process.js +89 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/patch-integration-config.js +39 -0
- package/integrations/use-cases/process-errors.js +28 -0
- package/integrations/use-cases/update-integration-config.js +32 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +92 -0
- package/integrations/use-cases/update-process-metrics.js +222 -0
- package/integrations/use-cases/update-process-state.js +163 -0
- package/integrations/utils/map-integration-dto.js +37 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/logs/logger.js +0 -4
- package/{module-plugin → modules}/index.js +0 -10
- package/modules/module-factory.js +56 -0
- package/modules/module.js +307 -0
- package/modules/repositories/module-repository-documentdb.js +350 -0
- package/modules/repositories/module-repository-factory.js +40 -0
- package/modules/repositories/module-repository-interface.js +145 -0
- package/modules/repositories/module-repository-mongo.js +436 -0
- package/modules/repositories/module-repository-postgres.js +481 -0
- package/modules/repositories/module-repository.js +369 -0
- package/modules/requester/api-key.js +52 -0
- package/modules/requester/oauth-2.js +555 -0
- package/modules/requester/requester.js +542 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +71 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +34 -0
- package/modules/use-cases/get-module.js +74 -0
- package/modules/use-cases/process-authorization-callback.js +243 -0
- package/modules/use-cases/refresh-entity-options.js +72 -0
- package/modules/use-cases/test-module-auth.js +72 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +92 -50
- package/prisma-mongodb/schema.prisma +455 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
- package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
- package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
- package/prisma-postgresql/migrations/20260625000000_add_user_organization_id_index/migration.sql +2 -0
- package/prisma-postgresql/migrations/20260703000000_add_integration_status_in_creation_in_deletion/migration.sql +11 -0
- package/prisma-postgresql/migrations/20260703000001_integration_status_default_in_creation/migration.sql +8 -0
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +26 -0
- package/prisma-postgresql/migrations/20260706000000_add_admin_script_execution_and_schedule/migration.sql +61 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +436 -0
- package/queues/queuer-util.js +103 -21
- package/reporting/README.md +154 -0
- package/reporting/builtin-reports.js +6 -0
- package/reporting/index.js +13 -0
- package/reporting/report-base.js +49 -0
- package/reporting/reports/integrations-report.js +221 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +43 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/telemetry/README.md +331 -0
- package/telemetry/bind-telemetry-context.js +73 -0
- package/telemetry/canonical-counters.js +52 -0
- package/telemetry/exporters.js +85 -0
- package/telemetry/index.js +26 -0
- package/telemetry/instrument-handler.js +87 -0
- package/telemetry/no-op-telemetry.js +67 -0
- package/telemetry/north-star.js +103 -0
- package/telemetry/otel-telemetry.js +213 -0
- package/telemetry/plugin-subscribers.js +77 -0
- package/telemetry/telemetry-config.js +120 -0
- package/telemetry/telemetry-context.js +40 -0
- package/telemetry/telemetry-event-bus.js +58 -0
- package/telemetry/telemetry-runtime.js +147 -0
- package/telemetry/telemetry-service.js +51 -0
- package/telemetry/usage-rollup-subscriber.js +116 -0
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +40 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +219 -0
- package/token/repositories/token-repository-postgres.js +264 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/associations/index.d.ts +0 -17
- package/types/core/index.d.ts +12 -4
- package/types/database/index.d.ts +10 -2
- package/types/encrypt/index.d.ts +5 -3
- package/types/integrations/index.d.ts +3 -8
- package/types/module-plugin/index.d.ts +20 -69
- package/types/syncs/index.d.ts +0 -17
- package/usage/README.md +54 -0
- package/usage/index.js +17 -0
- package/usage/repositories/usage-repository-documentdb.js +194 -0
- package/usage/repositories/usage-repository-factory.js +25 -0
- package/usage/repositories/usage-repository-interface.js +37 -0
- package/usage/repositories/usage-repository-prisma.js +146 -0
- package/usage/tracked-metrics.js +38 -0
- package/usage/usage-windows.js +24 -0
- package/user/repositories/user-repository-documentdb.js +458 -0
- package/user/repositories/user-repository-factory.js +52 -0
- package/user/repositories/user-repository-interface.js +214 -0
- package/user/repositories/user-repository-mongo.js +323 -0
- package/user/repositories/user-repository-postgres.js +377 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +125 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
- package/websocket/repositories/websocket-connection-repository.js +161 -0
- package/assertions/is-equal.js +0 -17
- package/associations/model.js +0 -54
- package/database/models/IndividualUser.js +0 -76
- package/database/models/OrganizationUser.js +0 -29
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/models/UserModel.js +0 -7
- package/database/models/WebsocketConnection.js +0 -49
- package/database/mongo.js +0 -45
- package/database/mongoose.js +0 -5
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -132
- package/encrypt/encrypt.test.js +0 -1069
- package/encrypt/test-encrypt.js +0 -107
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/entity.js +0 -46
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/requester/api-key.js +0 -36
- package/module-plugin/requester/oauth-2.js +0 -219
- package/module-plugin/requester/requester.js +0 -165
- package/module-plugin/requester/requester.test.js +0 -28
- package/module-plugin/test/auther.test.js +0 -97
- package/syncs/model.js +0 -62
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
package/core/Worker.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs');
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
const { RequiredPropertyError } = require('../errors');
|
|
4
4
|
const { get } = require('../assertions');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
|
|
6
|
+
const sqs = new SQSClient({ region: process.env.AWS_REGION });
|
|
8
7
|
|
|
9
8
|
class Worker {
|
|
10
9
|
async getQueueURL(params) {
|
|
@@ -12,25 +11,68 @@ class Worker {
|
|
|
12
11
|
// let params = {
|
|
13
12
|
// QueueName: process.env.QueueName
|
|
14
13
|
// };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
reject(err);
|
|
19
|
-
} else {
|
|
20
|
-
resolve(data.QueueUrl);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
});
|
|
14
|
+
const command = new GetQueueUrlCommand(params);
|
|
15
|
+
const data = await sqs.send(command);
|
|
16
|
+
return data.QueueUrl;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
async run(params, context = {}) {
|
|
27
20
|
const records = get(params, 'Records');
|
|
21
|
+
const batchItemFailures = [];
|
|
22
|
+
|
|
23
|
+
console.log(
|
|
24
|
+
`[Worker] run: processing ${records.length} record(s)`
|
|
25
|
+
);
|
|
28
26
|
|
|
29
27
|
for (const record of records) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Log record entry with SQS-provided attributes useful for tracing
|
|
29
|
+
// delivery history (ApproximateReceiveCount for retries, etc.).
|
|
30
|
+
let parsedEvent;
|
|
31
|
+
try {
|
|
32
|
+
parsedEvent = JSON.parse(record.body)?.event;
|
|
33
|
+
} catch {
|
|
34
|
+
parsedEvent = undefined;
|
|
35
|
+
}
|
|
36
|
+
console.log(`[Worker] record begin`, {
|
|
37
|
+
messageId: record.messageId,
|
|
38
|
+
event: parsedEvent,
|
|
39
|
+
receiveCount: record.attributes?.ApproximateReceiveCount,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const runParams = JSON.parse(record.body);
|
|
44
|
+
this._validateParams(runParams);
|
|
45
|
+
await this._run(runParams, context);
|
|
46
|
+
console.log(`[Worker] record success`, {
|
|
47
|
+
messageId: record.messageId,
|
|
48
|
+
event: runParams?.event,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.isHaltError) {
|
|
52
|
+
// HaltError means "discard this message, don't retry".
|
|
53
|
+
// Treat as success so SQS deletes it from the queue.
|
|
54
|
+
// Logged explicitly — silent discards made prod debugging
|
|
55
|
+
// extremely hard; keep this visible.
|
|
56
|
+
console.warn(`[Worker] record halted (discarded, no retry)`, {
|
|
57
|
+
messageId: record.messageId,
|
|
58
|
+
event: parsedEvent,
|
|
59
|
+
reason: error.message,
|
|
60
|
+
statusCode: error.statusCode,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
|
|
65
|
+
batchItemFailures.push({ itemIdentifier: record.messageId });
|
|
66
|
+
}
|
|
33
67
|
}
|
|
68
|
+
|
|
69
|
+
if (batchItemFailures.length > 0) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[Worker] run: returning ${batchItemFailures.length} batchItemFailure(s) of ${records.length}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { batchItemFailures };
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
async _run(params, context = {}) {
|
|
@@ -54,15 +96,9 @@ class Worker {
|
|
|
54
96
|
}
|
|
55
97
|
|
|
56
98
|
async sendAsyncSQSMessage(params) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
reject(err);
|
|
61
|
-
} else {
|
|
62
|
-
resolve(data.MessageId);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
});
|
|
99
|
+
const command = new SendMessageCommand(params);
|
|
100
|
+
const data = await sqs.send(command);
|
|
101
|
+
return data.MessageId;
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
// Throw an exception if the params do not validate
|
package/core/create-handler.js
CHANGED
|
@@ -1,9 +1,133 @@
|
|
|
1
1
|
// This line should be at the top of the webpacked output, so be sure to require createHandler first in any handlers. "Soon" sourcemaps will be built into Node... after that, this package won't be needed.
|
|
2
|
-
|
|
2
|
+
// REMOVING FOR NOW UNTIL WE ADD WEBPACK BACK IN
|
|
3
|
+
// require('source-map-support').install();
|
|
3
4
|
|
|
4
|
-
const { connectToDatabase } = require('../database/mongo');
|
|
5
5
|
const { initDebugLog, flushDebugLog } = require('../logs');
|
|
6
6
|
const { secretsToEnv } = require('./secrets-to-env');
|
|
7
|
+
const { parametersToEnv } = require('./parameters-to-env');
|
|
8
|
+
const {
|
|
9
|
+
getTelemetry,
|
|
10
|
+
getUsageRollupSubscriber,
|
|
11
|
+
getPluginTelemetrySubscribers,
|
|
12
|
+
} = require('../telemetry/telemetry-runtime');
|
|
13
|
+
|
|
14
|
+
// Bounds the tail latency telemetry adds to every warm invocation. Kept low so
|
|
15
|
+
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
16
|
+
// most this, not multiple seconds. Override with OTEL_FLUSH_TIMEOUT_MS.
|
|
17
|
+
const DEFAULT_FLUSH_TIMEOUT_MS =
|
|
18
|
+
Number(process.env.OTEL_FLUSH_TIMEOUT_MS) || 500;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fold the invocation's buffered usage counters into the durable store, then
|
|
22
|
+
* clear the buffer. On an SQS redelivery (ApproximateReceiveCount > 1) we
|
|
23
|
+
* DISCARD rather than flush — the prior delivery already counted, and the usage
|
|
24
|
+
* accuracy contract is "approximate, skip obvious redeliveries". Fully guarded.
|
|
25
|
+
*/
|
|
26
|
+
async function flushUsageRollup(subscriber, eventSummary, shouldUseDatabase) {
|
|
27
|
+
if (!subscriber) return;
|
|
28
|
+
try {
|
|
29
|
+
// Persisting usage requires a DB connection. DB-free handlers (e.g. the
|
|
30
|
+
// webhook-receipt route) never called connectPrisma, so drop the buffer
|
|
31
|
+
// instead of issuing a connectionless Prisma write.
|
|
32
|
+
if (!shouldUseDatabase) {
|
|
33
|
+
subscriber.discard();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Discard only when EVERY record in the batch is a redelivery. The buffer
|
|
37
|
+
// is invocation-scoped (not per-message), so discarding on *any*
|
|
38
|
+
// redelivery would drop the fresh records' counts too (silent
|
|
39
|
+
// under-count). For a mixed batch we flush: preserving fresh counts and
|
|
40
|
+
// at worst re-counting the one redelivered record is strictly better than
|
|
41
|
+
// losing fresh data for an approximate store. (Integration queue workers
|
|
42
|
+
// are batchSize:1 today, so a batch is all-or-nothing; this keeps it
|
|
43
|
+
// correct if batchSize is ever raised.)
|
|
44
|
+
const records = Array.isArray(eventSummary?.records)
|
|
45
|
+
? eventSummary.records
|
|
46
|
+
: [];
|
|
47
|
+
const allRedelivered =
|
|
48
|
+
records.length > 0 &&
|
|
49
|
+
records.every((r) => Number(r.receiveCount) > 1);
|
|
50
|
+
if (allRedelivered) {
|
|
51
|
+
subscriber.discard();
|
|
52
|
+
} else {
|
|
53
|
+
await subscriber.flush();
|
|
54
|
+
}
|
|
55
|
+
} catch (_) {
|
|
56
|
+
// Usage rollup must never break the handler.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Flush telemetry before the Lambda container freezes. Because
|
|
62
|
+
* `callbackWaitsForEmptyEventLoop=false` (below) stops the event loop the moment
|
|
63
|
+
* the handler returns, OTel's timer-driven batch processors would never fire —
|
|
64
|
+
* so spans/metrics must be flushed synchronously here. Bounded by a timeout so a
|
|
65
|
+
* stalled exporter can never block the response, and fully guarded so a flush
|
|
66
|
+
* failure never breaks the handler.
|
|
67
|
+
*/
|
|
68
|
+
async function flushTelemetry(telemetry, timeoutMs) {
|
|
69
|
+
try {
|
|
70
|
+
if (
|
|
71
|
+
!telemetry ||
|
|
72
|
+
typeof telemetry.isEnabled !== 'function' ||
|
|
73
|
+
!telemetry.isEnabled()
|
|
74
|
+
) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let timer;
|
|
78
|
+
const deadline = new Promise((resolve) => {
|
|
79
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
await Promise.race([
|
|
83
|
+
Promise.resolve(telemetry.forceFlush()),
|
|
84
|
+
deadline,
|
|
85
|
+
]);
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
} catch (_) {
|
|
90
|
+
// Telemetry flush must never break the handler.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Best-effort extraction of correlation identifiers from a Lambda event.
|
|
95
|
+
// For SQS: pulls messageIds + parsed event/processId/integrationId from each
|
|
96
|
+
// record body. For HTTP: pulls method+path. Never throws.
|
|
97
|
+
const summarizeLambdaEvent = (event) => {
|
|
98
|
+
if (!event) return {};
|
|
99
|
+
if (Array.isArray(event.Records)) {
|
|
100
|
+
return {
|
|
101
|
+
source: 'sqs',
|
|
102
|
+
records: event.Records.map((r) => {
|
|
103
|
+
let parsed = {};
|
|
104
|
+
try {
|
|
105
|
+
const body = JSON.parse(r.body);
|
|
106
|
+
parsed = {
|
|
107
|
+
event: body?.event,
|
|
108
|
+
processId: body?.data?.processId,
|
|
109
|
+
integrationId: body?.data?.integrationId,
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore unparseable bodies
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
messageId: r.messageId,
|
|
116
|
+
receiveCount: r.attributes?.ApproximateReceiveCount,
|
|
117
|
+
...parsed,
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (event.httpMethod || event.requestContext?.http) {
|
|
123
|
+
return {
|
|
124
|
+
source: 'http',
|
|
125
|
+
method: event.httpMethod || event.requestContext?.http?.method,
|
|
126
|
+
path: event.path || event.rawPath,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { source: 'other' };
|
|
130
|
+
};
|
|
7
131
|
|
|
8
132
|
const createHandler = (optionByName = {}) => {
|
|
9
133
|
const {
|
|
@@ -11,6 +135,9 @@ const createHandler = (optionByName = {}) => {
|
|
|
11
135
|
isUserFacingResponse = true,
|
|
12
136
|
method,
|
|
13
137
|
shouldUseDatabase = true,
|
|
138
|
+
telemetry,
|
|
139
|
+
flushTimeoutMs = DEFAULT_FLUSH_TIMEOUT_MS,
|
|
140
|
+
usageRollup,
|
|
14
141
|
} = optionByName;
|
|
15
142
|
|
|
16
143
|
if (!method) {
|
|
@@ -18,7 +145,25 @@ const createHandler = (optionByName = {}) => {
|
|
|
18
145
|
}
|
|
19
146
|
|
|
20
147
|
return async (event, context) => {
|
|
148
|
+
const eventSummary = summarizeLambdaEvent(event);
|
|
149
|
+
const activeTelemetry = telemetry || getTelemetry();
|
|
150
|
+
const activeUsageRollup =
|
|
151
|
+
usageRollup !== undefined
|
|
152
|
+
? usageRollup
|
|
153
|
+
: getUsageRollupSubscriber();
|
|
154
|
+
|
|
155
|
+
// Wire adopter-declared telemetry subscribers once per cold start.
|
|
156
|
+
// Memoized in the singleton, so this is a cheap
|
|
157
|
+
// no-op after the first invocation.
|
|
158
|
+
getPluginTelemetrySubscribers();
|
|
159
|
+
|
|
21
160
|
try {
|
|
161
|
+
console.info(`[createHandler] ${eventName}: handler entry`, {
|
|
162
|
+
eventName,
|
|
163
|
+
awsRequestId: context?.awsRequestId,
|
|
164
|
+
...eventSummary,
|
|
165
|
+
});
|
|
166
|
+
|
|
22
167
|
initDebugLog(eventName, event);
|
|
23
168
|
|
|
24
169
|
const requestMethod = event.httpMethod;
|
|
@@ -30,13 +175,18 @@ const createHandler = (optionByName = {}) => {
|
|
|
30
175
|
// If enabled (i.e. if SECRET_ARN is set in process.env) Fetch secrets from AWS Secrets Manager, and set them as environment variables.
|
|
31
176
|
await secretsToEnv();
|
|
32
177
|
|
|
33
|
-
//
|
|
34
|
-
|
|
178
|
+
// If enabled (i.e. if SSM_PARAMETER_PREFIX and FRIGG_SSM_OFFLOADED_KEYS are set) fetch offloaded params from SSM Parameter Store into process.env.
|
|
179
|
+
await parametersToEnv();
|
|
35
180
|
|
|
181
|
+
// Lazy-required so DB-free handlers never load the Prisma client.
|
|
36
182
|
if (shouldUseDatabase) {
|
|
37
|
-
|
|
183
|
+
const { connectPrisma } = require('../database/prisma');
|
|
184
|
+
await connectPrisma();
|
|
38
185
|
}
|
|
39
186
|
|
|
187
|
+
// Helps reuse the database connection. Lowers response times.
|
|
188
|
+
context.callbackWaitsForEmptyEventLoop = false;
|
|
189
|
+
|
|
40
190
|
// Run the Lambda
|
|
41
191
|
return await method(event, context);
|
|
42
192
|
} catch (error) {
|
|
@@ -44,6 +194,18 @@ const createHandler = (optionByName = {}) => {
|
|
|
44
194
|
|
|
45
195
|
// Don't leak implementation details to end users.
|
|
46
196
|
if (isUserFacingResponse) {
|
|
197
|
+
// Allow client-safe errors to pass through with their actual message
|
|
198
|
+
if (error.isClientSafe === true) {
|
|
199
|
+
const statusCode = error.statusCode || 400;
|
|
200
|
+
return {
|
|
201
|
+
statusCode,
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
error: error.message,
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Hide other errors with generic message
|
|
47
209
|
return {
|
|
48
210
|
statusCode: 500,
|
|
49
211
|
body: JSON.stringify({
|
|
@@ -55,12 +217,34 @@ const createHandler = (optionByName = {}) => {
|
|
|
55
217
|
// Handle server-to-server responses.
|
|
56
218
|
|
|
57
219
|
// Halt errors are logged but suceed and won't be retried.
|
|
220
|
+
// Log explicitly — silent suppression here previously made stuck
|
|
221
|
+
// messages invisible to observability tooling. Include
|
|
222
|
+
// eventSummary so operators can correlate across concurrent
|
|
223
|
+
// invocations (processId / messageIds / HTTP path).
|
|
58
224
|
if (error.isHaltError === true) {
|
|
225
|
+
console.warn(
|
|
226
|
+
`[createHandler] ${eventName}: halt error suppressed (no retry)`,
|
|
227
|
+
{
|
|
228
|
+
eventName,
|
|
229
|
+
errorName: error.name,
|
|
230
|
+
errorMessage: error.message,
|
|
231
|
+
statusCode: error.statusCode,
|
|
232
|
+
...eventSummary,
|
|
233
|
+
}
|
|
234
|
+
);
|
|
59
235
|
return;
|
|
60
236
|
}
|
|
61
237
|
|
|
62
238
|
// Here we can just rethrow and let AWS build the response.
|
|
63
239
|
throw error;
|
|
240
|
+
} finally {
|
|
241
|
+
// Flush telemetry + usage before the container freezes.
|
|
242
|
+
await flushTelemetry(activeTelemetry, flushTimeoutMs);
|
|
243
|
+
await flushUsageRollup(
|
|
244
|
+
activeUsageRollup,
|
|
245
|
+
eventSummary,
|
|
246
|
+
shouldUseDatabase
|
|
247
|
+
);
|
|
64
248
|
}
|
|
65
249
|
};
|
|
66
250
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Runtime loader that hydrates process.env from SSM Parameter Store. Used to
|
|
2
|
+
// offload env vars that would otherwise blow past Lambda's 4KB env limit.
|
|
3
|
+
// Real process.env always wins over SSM (a documented local-debugging escape
|
|
4
|
+
// hatch); only keys this loader sets are ever refreshed.
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CACHE_TTL_SECONDS = 300;
|
|
7
|
+
const BATCH_SIZE = 10;
|
|
8
|
+
const THROTTLE_BACKOFF_MS = [100, 200, 400];
|
|
9
|
+
const REFRESH_RETRY_MS = 30_000;
|
|
10
|
+
|
|
11
|
+
let client = null;
|
|
12
|
+
let cacheExpiresAt = null;
|
|
13
|
+
let inflight = null;
|
|
14
|
+
const ownedKeys = new Set();
|
|
15
|
+
|
|
16
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
|
|
18
|
+
const chunk = (items, size) => {
|
|
19
|
+
const batches = [];
|
|
20
|
+
for (let i = 0; i < items.length; i += size) {
|
|
21
|
+
batches.push(items.slice(i, i + size));
|
|
22
|
+
}
|
|
23
|
+
return batches;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getCacheTtlSeconds = () => {
|
|
27
|
+
const raw = process.env.FRIGG_SSM_CACHE_TTL;
|
|
28
|
+
if (raw === undefined || raw === '') {
|
|
29
|
+
return DEFAULT_CACHE_TTL_SECONDS;
|
|
30
|
+
}
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
return Number.isNaN(parsed) ? DEFAULT_CACHE_TTL_SECONDS : parsed;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const parseKeys = (raw) =>
|
|
36
|
+
raw
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((key) => key.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
const sendWithRetry = async (ssmClient, command) => {
|
|
42
|
+
let attempt = 0;
|
|
43
|
+
for (;;) {
|
|
44
|
+
try {
|
|
45
|
+
return await ssmClient.send(command);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (
|
|
48
|
+
err.name === 'ThrottlingException' &&
|
|
49
|
+
attempt < THROTTLE_BACKOFF_MS.length
|
|
50
|
+
) {
|
|
51
|
+
await sleep(THROTTLE_BACKOFF_MS[attempt]);
|
|
52
|
+
attempt += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch offloaded parameters and return a plain { key: value } map. Fetch-only:
|
|
62
|
+
* no process.env mutation, no caching, no ownership tracking. Shared by the
|
|
63
|
+
* runtime loader and the INIT-phase preload (ssm-preload.js). Throws (listing
|
|
64
|
+
* every missing name) if any requested key is absent from SSM.
|
|
65
|
+
*/
|
|
66
|
+
const fetchOffloadedParameters = async (prefix, keys, { region } = {}) => {
|
|
67
|
+
const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm');
|
|
68
|
+
const ssmClient = new SSMClient({
|
|
69
|
+
region: region || process.env.AWS_REGION,
|
|
70
|
+
});
|
|
71
|
+
const nameFor = (key) => `${prefix}/${key}`;
|
|
72
|
+
|
|
73
|
+
const found = new Map();
|
|
74
|
+
for (const batch of chunk(keys, BATCH_SIZE)) {
|
|
75
|
+
const { Parameters = [] } = await sendWithRetry(
|
|
76
|
+
ssmClient,
|
|
77
|
+
new GetParametersCommand({
|
|
78
|
+
Names: batch.map(nameFor),
|
|
79
|
+
WithDecryption: true,
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
for (const param of Parameters) {
|
|
83
|
+
found.set(param.Name, param);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const missing = keys.map(nameFor).filter((name) => !found.has(name));
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`missing SSM parameters under ${prefix}: ${missing.join(', ')}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const values = {};
|
|
95
|
+
for (const key of keys) {
|
|
96
|
+
values[key] = found.get(nameFor(key)).Value;
|
|
97
|
+
}
|
|
98
|
+
return values;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const loadParameters = async (prefix, keys) => {
|
|
102
|
+
const { SSMClient, GetParametersCommand } = require('@aws-sdk/client-ssm');
|
|
103
|
+
if (!client) {
|
|
104
|
+
client = new SSMClient({ region: process.env.AWS_REGION });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A key already living in process.env wins over SSM, except keys we set
|
|
108
|
+
// ourselves — those we refresh so later invocations pick up rotations.
|
|
109
|
+
const keysToFetch = keys.filter(
|
|
110
|
+
(key) => ownedKeys.has(key) || process.env[key] === undefined
|
|
111
|
+
);
|
|
112
|
+
const nameFor = (key) => `${prefix}/${key}`;
|
|
113
|
+
|
|
114
|
+
// A refresh means every key already has a served value; a warm container
|
|
115
|
+
// must keep serving stale values through an SSM blip instead of erroring.
|
|
116
|
+
const isRefresh =
|
|
117
|
+
keysToFetch.length > 0 &&
|
|
118
|
+
keysToFetch.every((key) => ownedKeys.has(key));
|
|
119
|
+
|
|
120
|
+
const found = new Map();
|
|
121
|
+
try {
|
|
122
|
+
for (const batch of chunk(keysToFetch, BATCH_SIZE)) {
|
|
123
|
+
const { Parameters = [] } = await sendWithRetry(
|
|
124
|
+
client,
|
|
125
|
+
new GetParametersCommand({
|
|
126
|
+
Names: batch.map(nameFor),
|
|
127
|
+
WithDecryption: true,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
for (const param of Parameters) {
|
|
131
|
+
found.set(param.Name, param);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const missing = keysToFetch
|
|
136
|
+
.map(nameFor)
|
|
137
|
+
.filter((name) => !found.has(name));
|
|
138
|
+
if (missing.length > 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`parametersToEnv: missing SSM parameters under ${prefix}: ${missing.join(
|
|
141
|
+
', '
|
|
142
|
+
)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (!isRefresh) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
console.warn(
|
|
150
|
+
`parametersToEnv: refresh failed, keeping cached values: ${err.message}`
|
|
151
|
+
);
|
|
152
|
+
cacheExpiresAt = Date.now() + REFRESH_RETRY_MS;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const loaded = keysToFetch.map((key) => {
|
|
157
|
+
const param = found.get(nameFor(key));
|
|
158
|
+
process.env[key] = param.Value;
|
|
159
|
+
ownedKeys.add(key);
|
|
160
|
+
return `${param.Name} (v${param.Version})`;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const ttlSeconds = getCacheTtlSeconds();
|
|
164
|
+
cacheExpiresAt =
|
|
165
|
+
ttlSeconds === 0 ? Infinity : Date.now() + ttlSeconds * 1000;
|
|
166
|
+
|
|
167
|
+
if (loaded.length > 0) {
|
|
168
|
+
console.log('parametersToEnv: loaded', loaded);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Hydrate process.env from SSM Parameter Store.
|
|
174
|
+
*
|
|
175
|
+
* No-op unless both SSM_PARAMETER_PREFIX and FRIGG_SSM_OFFLOADED_KEYS are set.
|
|
176
|
+
* Results are cached for FRIGG_SSM_CACHE_TTL seconds (default 300; 0 = forever).
|
|
177
|
+
* A failed fetch is never cached, so the next invocation retries.
|
|
178
|
+
*/
|
|
179
|
+
const parametersToEnv = async () => {
|
|
180
|
+
const prefix = process.env.SSM_PARAMETER_PREFIX;
|
|
181
|
+
const rawKeys = process.env.FRIGG_SSM_OFFLOADED_KEYS;
|
|
182
|
+
if (!prefix || !rawKeys) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const keys = parseKeys(rawKeys);
|
|
186
|
+
if (keys.length === 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (cacheExpiresAt !== null && Date.now() < cacheExpiresAt) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!inflight) {
|
|
195
|
+
inflight = loadParameters(prefix, keys).finally(() => {
|
|
196
|
+
inflight = null;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return inflight;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Adopt keys the INIT preload (ssm-preload.mjs) already populated into
|
|
204
|
+
* process.env, so the handler-time loader treats them as its own: it seeds
|
|
205
|
+
* the cache TTL and marks the keys owned so the TTL-refresh path re-fetches
|
|
206
|
+
* them. The preload runs in-process (NODE_OPTIONS=--import), sharing this
|
|
207
|
+
* module singleton. Pass ONLY the keys the preload actually set (not keys
|
|
208
|
+
* already present as real env vars) so the real-env-wins rule is preserved.
|
|
209
|
+
*/
|
|
210
|
+
const adoptPreloadedKeys = (keys) => {
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
ownedKeys.add(key);
|
|
213
|
+
}
|
|
214
|
+
const ttlSeconds = getCacheTtlSeconds();
|
|
215
|
+
cacheExpiresAt =
|
|
216
|
+
ttlSeconds === 0 ? Infinity : Date.now() + ttlSeconds * 1000;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* INIT-phase preload used by ssm-preload.mjs: fetch offloaded parameters and
|
|
221
|
+
* set them into process.env, then adopt the keys it set so the handler-time
|
|
222
|
+
* loader refreshes them on TTL. Real env wins — a key already present is never
|
|
223
|
+
* fetched or overwritten, so a console override keeps working even if that
|
|
224
|
+
* key's parameter is absent from SSM (fetching it would fail INIT). Returns the
|
|
225
|
+
* keys actually set (empty when every key was already in real env). Throws,
|
|
226
|
+
* listing the names, only for a genuinely-needed parameter that is missing.
|
|
227
|
+
*/
|
|
228
|
+
const preloadOffloadedParameters = async (prefix, keys, options = {}) => {
|
|
229
|
+
const keysToFetch = keys.filter((key) => process.env[key] === undefined);
|
|
230
|
+
if (keysToFetch.length === 0) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const values = await fetchOffloadedParameters(prefix, keysToFetch, options);
|
|
235
|
+
const setKeys = [];
|
|
236
|
+
for (const [key, value] of Object.entries(values)) {
|
|
237
|
+
process.env[key] = value;
|
|
238
|
+
setKeys.push(key);
|
|
239
|
+
}
|
|
240
|
+
adoptPreloadedKeys(setKeys);
|
|
241
|
+
return setKeys;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const _resetCache = () => {
|
|
245
|
+
client = null;
|
|
246
|
+
cacheExpiresAt = null;
|
|
247
|
+
inflight = null;
|
|
248
|
+
ownedKeys.clear();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
parametersToEnv,
|
|
253
|
+
fetchOffloadedParameters,
|
|
254
|
+
preloadOffloadedParameters,
|
|
255
|
+
adoptPreloadedKeys,
|
|
256
|
+
_resetCache,
|
|
257
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// INIT-phase SSM loader (ADR-027).
|
|
2
|
+
//
|
|
3
|
+
// Loaded via NODE_OPTIONS=--import BEFORE the Lambda handler and any api-module
|
|
4
|
+
// is required, so SSM-offloaded values are present in process.env at
|
|
5
|
+
// module-load time — when api-modules capture their OAuth client credentials
|
|
6
|
+
// in a top-level `const Definition = { env: { client_secret: process.env.X } }`.
|
|
7
|
+
// The runtime loader (parameters-to-env.js) runs inside the handler, which is
|
|
8
|
+
// too late for those module-load reads; this preload closes that gap.
|
|
9
|
+
//
|
|
10
|
+
// Top-level await here is awaited by Node before the entry module loads, so the
|
|
11
|
+
// fetch completes first. A fetch failure rejects the preload, failing Lambda
|
|
12
|
+
// INIT loudly (fail-fast) rather than starting with undefined credentials.
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
const prefix = process.env.SSM_PARAMETER_PREFIX;
|
|
19
|
+
const rawKeys = process.env.FRIGG_SSM_OFFLOADED_KEYS;
|
|
20
|
+
|
|
21
|
+
if (prefix && rawKeys) {
|
|
22
|
+
const keys = rawKeys
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((key) => key.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
|
|
27
|
+
if (keys.length > 0) {
|
|
28
|
+
// The fetch/real-env-wins/adopt logic lives in the CJS module (tested
|
|
29
|
+
// there); this preload is a thin INIT-phase shim over it.
|
|
30
|
+
const { preloadOffloadedParameters } = require('./parameters-to-env');
|
|
31
|
+
const setKeys = await preloadOffloadedParameters(prefix, keys);
|
|
32
|
+
console.log(`frigg-ssm-preload: loaded ${setKeys.length} parameter(s) at INIT under ${prefix}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const UNKNOWN_TYPE = 'unknown';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type is derived from the related Entity.moduleName since Credential carries
|
|
5
|
+
* no type column. Counted once per distinct linked moduleName; none → `unknown`.
|
|
6
|
+
* Reads only the non-encrypted projection — never `data`/secrets.
|
|
7
|
+
*/
|
|
8
|
+
function tallyActiveCredentialsByType(credentials = []) {
|
|
9
|
+
const counts = new Map();
|
|
10
|
+
|
|
11
|
+
for (const credential of credentials) {
|
|
12
|
+
const modules = new Set(
|
|
13
|
+
(credential?.entities || [])
|
|
14
|
+
.map((entity) => entity?.moduleName)
|
|
15
|
+
.filter(
|
|
16
|
+
(moduleName) => moduleName != null && moduleName !== ''
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
const types = modules.size > 0 ? [...modules] : [UNKNOWN_TYPE];
|
|
20
|
+
|
|
21
|
+
for (const type of types) {
|
|
22
|
+
counts.set(type, (counts.get(type) || 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [...counts].map(([integrationType, count]) => ({
|
|
27
|
+
integrationType,
|
|
28
|
+
count,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { tallyActiveCredentialsByType, UNKNOWN_TYPE };
|