@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
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
3
|
+
const { Delegate } = require('../../core');
|
|
4
|
+
const { FetchError } = require('../../errors');
|
|
5
|
+
const { get } = require('../../assertions');
|
|
6
|
+
const { getTelemetry } = require('../../telemetry/telemetry-runtime');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
9
|
+
const MAX_AUTH_RETRIES = 3;
|
|
10
|
+
|
|
11
|
+
// This context marks the async call chain that holds the refresh slot, and
|
|
12
|
+
// names the requester that holds it. Token requests re-enter _rawRequest
|
|
13
|
+
// through this._post. A 401 from inside that chain must fail fast: if it
|
|
14
|
+
// joins the refresh in flight, it awaits its own promise and hangs. The
|
|
15
|
+
// identity keeps a second requester used inside the chain out of the check.
|
|
16
|
+
// AsyncLocalStorage reaches the nested calls without help from the subclasses.
|
|
17
|
+
const refreshContext = new AsyncLocalStorage();
|
|
18
|
+
|
|
19
|
+
class Requester extends Delegate {
|
|
20
|
+
constructor(params) {
|
|
21
|
+
super(params);
|
|
22
|
+
this.backOff = get(params, 'backOff', [1, 3, 10, 30, 60, 180]);
|
|
23
|
+
this.isRefreshable = false;
|
|
24
|
+
this.refreshCount = 0;
|
|
25
|
+
this.authGraceRetryCount = 0;
|
|
26
|
+
// Concurrent 401s share one refreshAuth() run. See _refreshAuthOnce().
|
|
27
|
+
this._inFlightRefresh = null;
|
|
28
|
+
// This counter increases when the tokens change. A stale 401 (the
|
|
29
|
+
// token changed already) then retries and does not refresh again.
|
|
30
|
+
this._authGeneration = 0;
|
|
31
|
+
this.DLGT_INVALID_AUTH = 'INVALID_AUTH';
|
|
32
|
+
this.delegateTypes.push(this.DLGT_INVALID_AUTH);
|
|
33
|
+
this.agent = get(params, 'agent', null);
|
|
34
|
+
|
|
35
|
+
// Per-attempt HTTP timeout. Without this the framework called fetch()
|
|
36
|
+
// with no AbortController and no timeout — a silently-hung TCP
|
|
37
|
+
// connection (server accepts but never responds) blocked the calling
|
|
38
|
+
// promise forever, cascading into stalled batches, stalled syncs,
|
|
39
|
+
// and worker-lambda timeouts.
|
|
40
|
+
//
|
|
41
|
+
// Configuration precedence:
|
|
42
|
+
// 1. Instance param: new Requester({ requestTimeoutMs: 30_000 })
|
|
43
|
+
// 2. Class static: static requestTimeoutMs = 30_000
|
|
44
|
+
// 3. Default: DEFAULT_REQUEST_TIMEOUT_MS (60s)
|
|
45
|
+
//
|
|
46
|
+
// Pass 0 (or null) to disable the timeout entirely — reserved for
|
|
47
|
+
// test doubles and documented long-running endpoints.
|
|
48
|
+
// Intentionally NOT using `get(params, ...)` here — the Frigg
|
|
49
|
+
// `get` helper throws RequiredPropertyError if the key is missing
|
|
50
|
+
// and no default is provided, which would collide with the fall-
|
|
51
|
+
// through to the class-level static override.
|
|
52
|
+
const instanceTimeout = params?.requestTimeoutMs;
|
|
53
|
+
this.requestTimeoutMs =
|
|
54
|
+
instanceTimeout !== undefined && instanceTimeout !== null
|
|
55
|
+
? instanceTimeout
|
|
56
|
+
: this.constructor.requestTimeoutMs ??
|
|
57
|
+
DEFAULT_REQUEST_TIMEOUT_MS;
|
|
58
|
+
|
|
59
|
+
// Allow passing in the fetch function
|
|
60
|
+
// Instance methods can use this.fetch without differentiating
|
|
61
|
+
this.fetch = get(params, 'fetch', fetch);
|
|
62
|
+
|
|
63
|
+
// Defaults to the process singleton. Pass an integration's bound
|
|
64
|
+
// `this.telemetry` to attribute out-of-band requests — setup/OAuth calls
|
|
65
|
+
// made before an integration context exists aren't rolled up otherwise.
|
|
66
|
+
this.telemetry = (params && params.telemetry) || getTelemetry();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Redact secrets/PII from a URL before it touches telemetry. Many API
|
|
71
|
+
* modules embed credentials in the query string (?api_key=, ?token=,
|
|
72
|
+
* presigned signatures) or in userinfo — those must never reach a span,
|
|
73
|
+
* the bus, or an exporter. Keep only protocol + host + path (enough for
|
|
74
|
+
* North Star endpoint matching).
|
|
75
|
+
*/
|
|
76
|
+
_sanitizeUrl(url) {
|
|
77
|
+
const raw = String(url);
|
|
78
|
+
try {
|
|
79
|
+
const u = new URL(raw);
|
|
80
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
81
|
+
} catch (_) {
|
|
82
|
+
// Relative/opaque URL: drop the query string at minimum.
|
|
83
|
+
return raw.split('?')[0];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Bounded module label for the apimodule.requests metric. */
|
|
88
|
+
_telemetryModuleLabel() {
|
|
89
|
+
return (
|
|
90
|
+
this.moduleName ||
|
|
91
|
+
this.delegate?.name ||
|
|
92
|
+
this.delegate?.constructor?.name ||
|
|
93
|
+
this.constructor?.name ||
|
|
94
|
+
'unknown'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
parsedBody = async (resp) => {
|
|
99
|
+
const contentType = resp.headers.get('Content-Type') || '';
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
contentType.match(/^application\/json/) ||
|
|
103
|
+
contentType.match(/^application\/vnd.api\+json/) ||
|
|
104
|
+
contentType.match(/^application\/hal\+json/)
|
|
105
|
+
) {
|
|
106
|
+
return resp.json();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return resp.text();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Instrumenting entry point. Wraps the whole logical request —
|
|
114
|
+
* including retry/refresh recursion — in a single span + one
|
|
115
|
+
* `frigg.apimodule.requests` counter, emitted on the `attempt === 0`
|
|
116
|
+
* boundary so retries are never double-counted. The full URL rides the
|
|
117
|
+
* span only; the metric carries bounded labels {module, method, status}
|
|
118
|
+
* — never endpoint.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} url - The request URL, relative or absolute.
|
|
121
|
+
* @param {Object} options - Fetch options (method, headers, body, query,
|
|
122
|
+
* returnFullRes, etc.) built by the `_get`/`_post`/`_patch`/`_put`/
|
|
123
|
+
* `_delete` wrappers.
|
|
124
|
+
* @param {number} attempt - 0-based count of retries already made for
|
|
125
|
+
* this call. Non-zero only if a caller re-enters directly; normal
|
|
126
|
+
* retries recurse through `_rawRequest` instead (see below).
|
|
127
|
+
*/
|
|
128
|
+
async _request(url, options = {}, attempt = 0) {
|
|
129
|
+
if (attempt !== 0) {
|
|
130
|
+
return this._rawRequest(url, options, attempt);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const telemetry = this.telemetry;
|
|
134
|
+
if (!telemetry || typeof telemetry.span !== 'function') {
|
|
135
|
+
return this._rawRequest(url, options, 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const module = this._telemetryModuleLabel();
|
|
139
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
140
|
+
const safeUrl = this._sanitizeUrl(url);
|
|
141
|
+
|
|
142
|
+
return telemetry.span('frigg.apimodule.request', async (span) => {
|
|
143
|
+
if (span && typeof span.setAttributes === 'function') {
|
|
144
|
+
span.setAttributes({
|
|
145
|
+
'frigg.module': module,
|
|
146
|
+
'http.request.method': method,
|
|
147
|
+
// Redacted (no query/userinfo) — never emit raw URLs.
|
|
148
|
+
'url.path': safeUrl,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Redacted url (unbounded) rides the bus-only context for North Star
|
|
152
|
+
// derived-from-trace matching — never a metric label.
|
|
153
|
+
const busContext = { url: safeUrl };
|
|
154
|
+
try {
|
|
155
|
+
const result = await this._rawRequest(url, options, 0);
|
|
156
|
+
telemetry.count(
|
|
157
|
+
'frigg.apimodule.requests',
|
|
158
|
+
1,
|
|
159
|
+
{ module, method, status: 'ok' },
|
|
160
|
+
busContext
|
|
161
|
+
);
|
|
162
|
+
return result;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const code = err?.status ?? err?.statusCode;
|
|
165
|
+
const status = code ? String(code) : 'error';
|
|
166
|
+
if (span && typeof span.setAttribute === 'function' && code) {
|
|
167
|
+
span.setAttribute('http.response.status_code', code);
|
|
168
|
+
}
|
|
169
|
+
telemetry.count(
|
|
170
|
+
'frigg.apimodule.requests',
|
|
171
|
+
1,
|
|
172
|
+
{ module, method, status },
|
|
173
|
+
busContext
|
|
174
|
+
);
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} url - The request URL, relative or absolute.
|
|
182
|
+
* @param {Object} options - Fetch options, as built by `_request`.
|
|
183
|
+
* @param {number} attempt - 0-based count of retries already made for
|
|
184
|
+
* this call. Indexes `this.backOff` for the next delay and is passed
|
|
185
|
+
* back in on each recursive retry.
|
|
186
|
+
*/
|
|
187
|
+
async _rawRequest(url, options, attempt = 0) {
|
|
188
|
+
let encodedUrl = encodeURI(url);
|
|
189
|
+
if (options.query) {
|
|
190
|
+
let queryBuild = '?';
|
|
191
|
+
for (const key in options.query) {
|
|
192
|
+
queryBuild += `${encodeURIComponent(key)}=${encodeURIComponent(
|
|
193
|
+
options.query[key]
|
|
194
|
+
)}&`;
|
|
195
|
+
}
|
|
196
|
+
encodedUrl += queryBuild.slice(0, -1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
options.headers = await this.addAuthHeaders(options.headers);
|
|
200
|
+
|
|
201
|
+
// A 401 that arrives after a concurrent refresh is stale. It is not
|
|
202
|
+
// proof that the new token failed.
|
|
203
|
+
const authGenerationAtDispatch = this._authGeneration;
|
|
204
|
+
|
|
205
|
+
if (this.agent) options.agent = this.agent;
|
|
206
|
+
|
|
207
|
+
// Per-attempt timeout — fresh AbortController per call so the retry
|
|
208
|
+
// recursion (with its own backoff sleeps) always gets a clean
|
|
209
|
+
// signal. Timer is cleared in the finally block regardless of
|
|
210
|
+
// outcome.
|
|
211
|
+
const timeoutMs = this.requestTimeoutMs;
|
|
212
|
+
const controller = timeoutMs > 0 ? new AbortController() : null;
|
|
213
|
+
const timeoutHandle = controller
|
|
214
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
215
|
+
: null;
|
|
216
|
+
const fetchOptions = controller
|
|
217
|
+
? { ...options, signal: controller.signal }
|
|
218
|
+
: options;
|
|
219
|
+
|
|
220
|
+
// Timer must stay active through body consumption. node-fetch v2
|
|
221
|
+
// resolves the fetch() promise when headers arrive, not when the
|
|
222
|
+
// body is fully read — so a server that sends headers and then
|
|
223
|
+
// stalls the body would still hang parsedBody() or
|
|
224
|
+
// FetchError.create()'s response.text() call. We clear the timer
|
|
225
|
+
// only after the body is fully consumed (success path) or
|
|
226
|
+
// deliberately before each recursive retry so the new attempt
|
|
227
|
+
// starts with its own fresh timer.
|
|
228
|
+
let timerCleared = false;
|
|
229
|
+
const clearRequestTimer = () => {
|
|
230
|
+
if (!timerCleared && timeoutHandle) {
|
|
231
|
+
clearTimeout(timeoutHandle);
|
|
232
|
+
timerCleared = true;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
let response;
|
|
238
|
+
try {
|
|
239
|
+
response = await this.fetch(encodedUrl, fetchOptions);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// AbortController fires AbortError (name) / ETIMEDOUT-shaped
|
|
242
|
+
// errors (type on node-fetch) when we hit the timeout. No
|
|
243
|
+
// retry on timeout: a slow endpoint is a downstream problem,
|
|
244
|
+
// and each retry would wait another `timeoutMs` before giving
|
|
245
|
+
// up — amplifying the hang into a per-record multi-minute
|
|
246
|
+
// stall at batch scale.
|
|
247
|
+
const isTimeout =
|
|
248
|
+
e?.name === 'AbortError' || e?.type === 'aborted';
|
|
249
|
+
if (e?.code === 'ECONNRESET' && attempt < this.backOff.length) {
|
|
250
|
+
clearRequestTimer();
|
|
251
|
+
const delay = this.backOff[attempt] * 1000;
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
253
|
+
return this._rawRequest(url, options, attempt + 1);
|
|
254
|
+
}
|
|
255
|
+
const fetchError = await FetchError.create({
|
|
256
|
+
resource: encodedUrl,
|
|
257
|
+
init: options,
|
|
258
|
+
responseBody: isTimeout
|
|
259
|
+
? `Request timed out after ${timeoutMs}ms`
|
|
260
|
+
: e,
|
|
261
|
+
});
|
|
262
|
+
if (isTimeout) {
|
|
263
|
+
// Flag + machine-readable fields so callers can
|
|
264
|
+
// distinguish a timeout from a generic network error
|
|
265
|
+
// without parsing the message (which FetchError
|
|
266
|
+
// sanitizes outside of STAGE=dev).
|
|
267
|
+
fetchError.isTimeout = true;
|
|
268
|
+
fetchError.timeoutMs = timeoutMs;
|
|
269
|
+
}
|
|
270
|
+
throw fetchError;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { status } = response;
|
|
274
|
+
|
|
275
|
+
// If the status is retriable and there are back off requests left, retry the request
|
|
276
|
+
if (
|
|
277
|
+
(status === 429 || status >= 500) &&
|
|
278
|
+
attempt < this.backOff.length
|
|
279
|
+
) {
|
|
280
|
+
clearRequestTimer();
|
|
281
|
+
const delay = this.backOff[attempt] * 1000;
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
283
|
+
return this._rawRequest(url, options, attempt + 1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (status === 401) {
|
|
287
|
+
// A 401 from inside the refresh flow means the provider
|
|
288
|
+
// rejected the credential itself (invalid_client). A new
|
|
289
|
+
// refresh cannot help. A join would await this same call.
|
|
290
|
+
if (this._isInsideRefreshFlow()) {
|
|
291
|
+
throw await this._invalidateAuth(
|
|
292
|
+
encodedUrl,
|
|
293
|
+
options,
|
|
294
|
+
response
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const tokenReplacedWhileInFlight =
|
|
299
|
+
this._authGeneration !== authGenerationAtDispatch;
|
|
300
|
+
if (this.isRefreshable && tokenReplacedWhileInFlight) {
|
|
301
|
+
// This request did not try the current token. Retry with
|
|
302
|
+
// it. Do not spend one more provider-side rotation.
|
|
303
|
+
clearRequestTimer();
|
|
304
|
+
return this._rawRequest(url, options, attempt + 1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!this.isRefreshable) {
|
|
308
|
+
// Up to MAX_AUTH_RETRIES grace retries before invalidating
|
|
309
|
+
// — a 401 alone isn't proof the credential is bad.
|
|
310
|
+
if (
|
|
311
|
+
this.authGraceRetryCount < MAX_AUTH_RETRIES &&
|
|
312
|
+
this.authGraceRetryCount < this.backOff.length
|
|
313
|
+
) {
|
|
314
|
+
const delay =
|
|
315
|
+
this.backOff[this.authGraceRetryCount] * 1000;
|
|
316
|
+
this.authGraceRetryCount++;
|
|
317
|
+
clearRequestTimer();
|
|
318
|
+
await new Promise((resolve) =>
|
|
319
|
+
setTimeout(resolve, delay)
|
|
320
|
+
);
|
|
321
|
+
return this._rawRequest(url, options, attempt + 1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw await this._invalidateAuth(
|
|
325
|
+
encodedUrl,
|
|
326
|
+
options,
|
|
327
|
+
response
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Concurrent 401s share one refresh. Independent refreshes
|
|
332
|
+
// kill each other, because many providers use single-use
|
|
333
|
+
// refresh tokens. Only the initiator spends the retry budget.
|
|
334
|
+
const refreshAlreadyInFlight = Boolean(this._inFlightRefresh);
|
|
335
|
+
|
|
336
|
+
if (
|
|
337
|
+
!refreshAlreadyInFlight &&
|
|
338
|
+
this.refreshCount >= MAX_AUTH_RETRIES
|
|
339
|
+
) {
|
|
340
|
+
throw await this._invalidateAuth(
|
|
341
|
+
encodedUrl,
|
|
342
|
+
options,
|
|
343
|
+
response
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!refreshAlreadyInFlight) {
|
|
348
|
+
this.refreshCount++;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const refreshSucceeded = await this._refreshAuthOnce();
|
|
352
|
+
if (refreshSucceeded) {
|
|
353
|
+
clearRequestTimer();
|
|
354
|
+
return this._rawRequest(url, options, attempt + 1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
throw await this._invalidateAuth(encodedUrl, options, response);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// If the error wasn't retried, throw. FetchError.create reads
|
|
361
|
+
// the response body (response.text()) — timer must still be
|
|
362
|
+
// alive to catch a stalled body stream.
|
|
363
|
+
if (status >= 400) {
|
|
364
|
+
const fetchError = await FetchError.create({
|
|
365
|
+
resource: encodedUrl,
|
|
366
|
+
init: options,
|
|
367
|
+
response,
|
|
368
|
+
});
|
|
369
|
+
throw this._maybeFlagTimeoutDuringBodyRead(
|
|
370
|
+
fetchError,
|
|
371
|
+
timeoutMs
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Successful response: reset the per-instance refresh budget so
|
|
376
|
+
// a later 401 in the same Requester lifetime can attempt refresh
|
|
377
|
+
// again instead of silently falling through.
|
|
378
|
+
this.refreshCount = 0;
|
|
379
|
+
this.authGraceRetryCount = 0;
|
|
380
|
+
|
|
381
|
+
// parsedBody consumes the response body stream. If the server
|
|
382
|
+
// stalls mid-stream the timer (still armed) aborts it.
|
|
383
|
+
return options.returnFullRes
|
|
384
|
+
? response
|
|
385
|
+
: await this.parsedBody(response);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
// If the abort fired during body consumption, node-fetch emits
|
|
388
|
+
// the error as an AbortError on the body stream. Surface the
|
|
389
|
+
// same isTimeout flag callers use for header-phase timeouts.
|
|
390
|
+
throw this._maybeFlagTimeoutDuringBodyRead(e, timeoutMs);
|
|
391
|
+
} finally {
|
|
392
|
+
clearRequestTimer();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async _invalidateAuth(encodedUrl, options, response) {
|
|
397
|
+
const fetchError = await FetchError.create({
|
|
398
|
+
resource: encodedUrl,
|
|
399
|
+
init: options,
|
|
400
|
+
response,
|
|
401
|
+
});
|
|
402
|
+
await this.notify(this.DLGT_INVALID_AUTH, fetchError);
|
|
403
|
+
return fetchError;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
_maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
|
|
407
|
+
if (!err || typeof err !== 'object') return err;
|
|
408
|
+
if (err.isTimeout) return err;
|
|
409
|
+
const isAbort = err.name === 'AbortError' || err.type === 'aborted';
|
|
410
|
+
if (!isAbort) return err;
|
|
411
|
+
err.isTimeout = true;
|
|
412
|
+
err.timeoutMs = timeoutMs;
|
|
413
|
+
return err;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async _get(options) {
|
|
417
|
+
const fetchOptions = {
|
|
418
|
+
method: 'GET',
|
|
419
|
+
credentials: 'include',
|
|
420
|
+
headers: options.headers || {},
|
|
421
|
+
query: options.query || {},
|
|
422
|
+
returnFullRes: options.returnFullRes || false,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const res = await this._request(options.url, fetchOptions);
|
|
426
|
+
return res;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async _post(options, stringify = true) {
|
|
430
|
+
const fetchOptions = {
|
|
431
|
+
method: 'POST',
|
|
432
|
+
credentials: 'include',
|
|
433
|
+
headers: options.headers || {},
|
|
434
|
+
query: options.query || {},
|
|
435
|
+
body: stringify ? JSON.stringify(options.body) : options.body,
|
|
436
|
+
returnFullRes: options.returnFullRes || false,
|
|
437
|
+
};
|
|
438
|
+
const res = await this._request(options.url, fetchOptions);
|
|
439
|
+
return res;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async _patch(options, stringify = true) {
|
|
443
|
+
const fetchOptions = {
|
|
444
|
+
method: 'PATCH',
|
|
445
|
+
credentials: 'include',
|
|
446
|
+
headers: options.headers || {},
|
|
447
|
+
query: options.query || {},
|
|
448
|
+
body: stringify ? JSON.stringify(options.body) : options.body,
|
|
449
|
+
returnFullRes: options.returnFullRes || false,
|
|
450
|
+
};
|
|
451
|
+
const res = await this._request(options.url, fetchOptions);
|
|
452
|
+
return res;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async _put(options, stringify = true) {
|
|
456
|
+
const fetchOptions = {
|
|
457
|
+
method: 'PUT',
|
|
458
|
+
credentials: 'include',
|
|
459
|
+
headers: options.headers || {},
|
|
460
|
+
query: options.query || {},
|
|
461
|
+
body: stringify ? JSON.stringify(options.body) : options.body,
|
|
462
|
+
returnFullRes: options.returnFullRes || false,
|
|
463
|
+
};
|
|
464
|
+
const res = await this._request(options.url, fetchOptions);
|
|
465
|
+
return res;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async _delete(options) {
|
|
469
|
+
const fetchOptions = {
|
|
470
|
+
method: 'DELETE',
|
|
471
|
+
credentials: 'include',
|
|
472
|
+
headers: options.headers || {},
|
|
473
|
+
query: options.query || {},
|
|
474
|
+
returnFullRes: options.returnFullRes || true,
|
|
475
|
+
};
|
|
476
|
+
return this._request(options.url, fetchOptions);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Runs one refreshAuth() at a time. The first caller starts the refresh.
|
|
481
|
+
* Callers that arrive during the refresh await the same promise. The
|
|
482
|
+
* check-and-store step is synchronous. Thus two concurrent callers
|
|
483
|
+
* cannot both start a refresh.
|
|
484
|
+
*
|
|
485
|
+
* @returns {Promise<boolean>} True if the refresh succeeded.
|
|
486
|
+
*/
|
|
487
|
+
_refreshAuthOnce() {
|
|
488
|
+
if (!this._inFlightRefresh) {
|
|
489
|
+
// Keep .finally last. The stored promise must be the promise
|
|
490
|
+
// that clears the slot. Then the slot is free before a waiter
|
|
491
|
+
// resumes.
|
|
492
|
+
this._inFlightRefresh = this._adoptOrRefresh().finally(() => {
|
|
493
|
+
this._inFlightRefresh = null;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return this._inFlightRefresh;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Adopts a newer stored credential, or refreshes. Both steps run inside
|
|
501
|
+
* the marker, because the marker must cover the whole time this instance
|
|
502
|
+
* holds the slot. The adoption lives here, not in refreshAuth(), so a
|
|
503
|
+
* module that overrides refreshAuth() still gets it.
|
|
504
|
+
*
|
|
505
|
+
* @returns {Promise<boolean>} True if the instance holds a usable token.
|
|
506
|
+
*/
|
|
507
|
+
async _adoptOrRefresh() {
|
|
508
|
+
const refreshSucceeded = await refreshContext.run(
|
|
509
|
+
{ requester: this },
|
|
510
|
+
async () => {
|
|
511
|
+
if (await this._adoptNewerCredential()) return true;
|
|
512
|
+
return this.refreshAuth();
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
if (refreshSucceeded) this._authGeneration++;
|
|
516
|
+
return refreshSucceeded;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Hook for requesters that hold a rotating stored credential. Return true
|
|
521
|
+
* when the instance adopted a newer stored credential and needs no
|
|
522
|
+
* refresh. A module backed by a vendor SDK overrides this, calls super,
|
|
523
|
+
* and then copies the adopted tokens into its SDK client.
|
|
524
|
+
*
|
|
525
|
+
* @returns {Promise<boolean>} True if the instance adopted a newer
|
|
526
|
+
* credential.
|
|
527
|
+
*/
|
|
528
|
+
async _adoptNewerCredential() {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** True while this instance runs its own refresh. */
|
|
533
|
+
_isInsideRefreshFlow() {
|
|
534
|
+
return refreshContext.getStore()?.requester === this;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async refreshAuth() {
|
|
538
|
+
throw new Error('refreshAuth not yet defined in child of Requester');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
module.exports = { Requester };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const { get } = require('
|
|
2
|
-
const { OAuth2Requester } = require('
|
|
1
|
+
const { get } = require('../../../assertions');
|
|
2
|
+
const { OAuth2Requester } = require('../..');
|
|
3
3
|
|
|
4
4
|
class Api extends OAuth2Requester {
|
|
5
5
|
constructor(params) {
|
|
@@ -23,7 +23,12 @@ class Api extends OAuth2Requester {
|
|
|
23
23
|
return this.authorizationUri;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
getAuthorizationRequirements() {
|
|
27
|
+
return {
|
|
28
|
+
url: this.getAuthUri(),
|
|
29
|
+
type: 'oauth2',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
module.exports = { Api };
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
require('dotenv').config();
|
|
2
|
-
const {Api} = require('./api');
|
|
3
|
-
const {get} = require('
|
|
4
|
-
const config = {name: 'anapi'}
|
|
2
|
+
const { Api } = require('./api');
|
|
3
|
+
const { get } = require('../../../assertions');
|
|
4
|
+
const config = { name: 'anapi' }
|
|
5
5
|
|
|
6
6
|
const Definition = {
|
|
7
7
|
API: Api,
|
|
8
|
-
|
|
8
|
+
getAuthorizationRequirements: () => ({
|
|
9
|
+
url: 'http://localhost:3000/redirect/anapi',
|
|
10
|
+
type: 'oauth2',
|
|
11
|
+
}),
|
|
12
|
+
getName: function () { return config.name },
|
|
9
13
|
moduleName: config.name,
|
|
10
14
|
modelName: 'AnApi',
|
|
11
15
|
requiredAuthMethods: {
|
|
12
|
-
getToken: async function(api, params){
|
|
16
|
+
getToken: async function (api, params) {
|
|
13
17
|
const code = get(params.data, 'code');
|
|
14
18
|
return api.getTokenFromCode(code);
|
|
15
19
|
},
|
|
16
|
-
getEntityDetails: async function(api, callbackParams, tokenResponse, userId) {
|
|
20
|
+
getEntityDetails: async function (api, callbackParams, tokenResponse, userId) {
|
|
17
21
|
const userDetails = await api.getUserDetails();
|
|
18
22
|
return {
|
|
19
|
-
identifiers: { externalId: userDetails.portalId,
|
|
23
|
+
identifiers: { externalId: userDetails.portalId, userId },
|
|
20
24
|
details: { name: userDetails.hub_domain },
|
|
21
25
|
}
|
|
22
26
|
},
|
|
@@ -26,14 +30,14 @@ const Definition = {
|
|
|
26
30
|
],
|
|
27
31
|
entity: [],
|
|
28
32
|
},
|
|
29
|
-
getCredentialDetails: async function(api, userId) {
|
|
33
|
+
getCredentialDetails: async function (api, userId) {
|
|
30
34
|
const userDetails = await api.getUserDetails();
|
|
31
35
|
return {
|
|
32
|
-
identifiers: { externalId: userDetails.portalId,
|
|
36
|
+
identifiers: { externalId: userDetails.portalId, userId },
|
|
33
37
|
details: {}
|
|
34
38
|
};
|
|
35
39
|
},
|
|
36
|
-
testAuthRequest: async function(api){
|
|
40
|
+
testAuthRequest: async function (api) {
|
|
37
41
|
return api.getUserDetails()
|
|
38
42
|
},
|
|
39
43
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class TestModuleFactory {
|
|
2
|
+
constructor() { }
|
|
3
|
+
|
|
4
|
+
async getModuleInstance(entityId, userId) {
|
|
5
|
+
// return minimal stub module with getName and api property
|
|
6
|
+
return {
|
|
7
|
+
getName() { return 'stubModule'; },
|
|
8
|
+
api: {},
|
|
9
|
+
entityId,
|
|
10
|
+
userId,
|
|
11
|
+
testAuth: async () => true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { TestModuleFactory };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class TestModuleRepository {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.entities = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
addEntity(entity) {
|
|
7
|
+
this.entities.set(entity.id, entity);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async findEntityById(id) {
|
|
11
|
+
return this.entities.get(id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async findEntitiesByIds(ids) {
|
|
15
|
+
return ids.map((id) => this.entities.get(id));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findEntity(filter) {
|
|
19
|
+
if (!filter || typeof filter !== 'object') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (filter.id && this.entities.has(filter.id)) {
|
|
24
|
+
return this.entities.get(filter.id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (filter.externalId) {
|
|
28
|
+
for (const entity of this.entities.values()) {
|
|
29
|
+
if (entity.externalId === filter.externalId) {
|
|
30
|
+
return entity;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { TestModuleRepository };
|