From 4a773768d44fc304118608881bb2c49b0067850e Mon Sep 17 00:00:00 2001 From: "Sean Matthews (via Claude Code)" Date: Wed, 19 Aug 2026 06:24:26 +0000 Subject: [PATCH 1/2] Add five conversation-source API modules (Gong, Fireflies, Fathom, Otter, Quo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable Frigg api-modules for the meeting-intelligence / telephony class, built to the same bar as the Reevo module (canonical spec + spec-sync test + offline tests + README + LICENSE): - gong — Basic auth (Access Key/Secret); calls/extensive for party emails. OpenAPI + spec-sync. 22 tests. - fireflies — GraphQL (Bearer); transcripts/summaries/attendees. Operations manifest + sync test. 24 tests. - fathom — REST (X-Api-Key) + webhooks; meetings/transcripts/summaries. OpenAPI + spec-sync. 25 tests. (Fixed a real bug found during finalize: /team-members → /team_members.) - otter — Otter.ai Enterprise Public API (Bearer); conversations/transcripts. OpenAPI + spec-sync. 25 tests. - quo — Quo/OpenPhone (raw Authorization, NO Bearer); calls/recordings/transcripts. OpenAPI + spec-sync. 19 tests. Every spec is 1:1 with its client (mutation-tested). Root lockfile regenerated so `npm ci` stays in sync (npm ci --dry-run clean). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JDh45c1vm91ySYtvVv9Z65 --- package-lock.json | 2530 +++++++++++++---- packages/v1-ready/fathom/.env.example | 4 + packages/v1-ready/fathom/LICENSE.md | 21 + packages/v1-ready/fathom/README.md | 163 ++ packages/v1-ready/fathom/api.js | 184 ++ packages/v1-ready/fathom/defaultConfig.json | 10 + packages/v1-ready/fathom/definition.js | 93 + packages/v1-ready/fathom/fathom.openapi.yaml | 364 +++ packages/v1-ready/fathom/index.js | 7 + packages/v1-ready/fathom/package.json | 32 + packages/v1-ready/fathom/tests/api.test.js | 159 ++ .../v1-ready/fathom/tests/definition.test.js | 98 + .../v1-ready/fathom/tests/spec-sync.test.js | 42 + packages/v1-ready/fireflies/.env.example | 2 + packages/v1-ready/fireflies/LICENSE.md | 21 + packages/v1-ready/fireflies/README.md | 91 + packages/v1-ready/fireflies/api.js | 276 ++ .../v1-ready/fireflies/defaultConfig.json | 10 + packages/v1-ready/fireflies/definition.js | 76 + .../fireflies/fireflies.operations.json | 183 ++ packages/v1-ready/fireflies/index.js | 7 + packages/v1-ready/fireflies/package.json | 23 + packages/v1-ready/fireflies/tests/api.test.js | 163 ++ .../fireflies/tests/definition.test.js | 104 + .../fireflies/tests/spec-sync.test.js | 136 + packages/v1-ready/gong/.env.example | 10 + packages/v1-ready/gong/LICENSE.md | 16 + packages/v1-ready/gong/README.md | 107 + packages/v1-ready/gong/api.js | 188 ++ packages/v1-ready/gong/defaultConfig.json | 10 + packages/v1-ready/gong/definition.js | 60 + packages/v1-ready/gong/gong.openapi.yaml | 304 ++ packages/v1-ready/gong/index.js | 7 + packages/v1-ready/gong/package.json | 28 + packages/v1-ready/gong/tests/api.test.js | 145 + .../v1-ready/gong/tests/definition.test.js | 105 + .../v1-ready/gong/tests/spec-sync.test.js | 38 + packages/v1-ready/otter/.env.example | 4 + packages/v1-ready/otter/LICENSE.md | 21 + packages/v1-ready/otter/README.md | 84 + packages/v1-ready/otter/api.js | 147 + packages/v1-ready/otter/defaultConfig.json | 10 + packages/v1-ready/otter/definition.js | 58 + packages/v1-ready/otter/index.js | 7 + packages/v1-ready/otter/otter.openapi.yaml | 160 ++ packages/v1-ready/otter/package.json | 25 + packages/v1-ready/otter/tests/api.test.js | 139 + .../v1-ready/otter/tests/definition.test.js | 86 + .../v1-ready/otter/tests/spec-sync.test.js | 39 + packages/v1-ready/quo/.env.example | 8 + packages/v1-ready/quo/LICENSE.md | 21 + packages/v1-ready/quo/README.md | 100 + packages/v1-ready/quo/api.js | 198 ++ packages/v1-ready/quo/defaultConfig.json | 10 + packages/v1-ready/quo/definition.js | 86 + packages/v1-ready/quo/index.js | 7 + packages/v1-ready/quo/jest.config.js | 7 + packages/v1-ready/quo/package.json | 36 + packages/v1-ready/quo/quo.openapi.yaml | 507 ++++ packages/v1-ready/quo/tests/api.test.js | 90 + .../v1-ready/quo/tests/definition.test.js | 51 + packages/v1-ready/quo/tests/spec-sync.test.js | 45 + 62 files changed, 7133 insertions(+), 630 deletions(-) create mode 100644 packages/v1-ready/fathom/.env.example create mode 100644 packages/v1-ready/fathom/LICENSE.md create mode 100644 packages/v1-ready/fathom/README.md create mode 100644 packages/v1-ready/fathom/api.js create mode 100644 packages/v1-ready/fathom/defaultConfig.json create mode 100644 packages/v1-ready/fathom/definition.js create mode 100644 packages/v1-ready/fathom/fathom.openapi.yaml create mode 100644 packages/v1-ready/fathom/index.js create mode 100644 packages/v1-ready/fathom/package.json create mode 100644 packages/v1-ready/fathom/tests/api.test.js create mode 100644 packages/v1-ready/fathom/tests/definition.test.js create mode 100644 packages/v1-ready/fathom/tests/spec-sync.test.js create mode 100644 packages/v1-ready/fireflies/.env.example create mode 100644 packages/v1-ready/fireflies/LICENSE.md create mode 100644 packages/v1-ready/fireflies/README.md create mode 100644 packages/v1-ready/fireflies/api.js create mode 100644 packages/v1-ready/fireflies/defaultConfig.json create mode 100644 packages/v1-ready/fireflies/definition.js create mode 100644 packages/v1-ready/fireflies/fireflies.operations.json create mode 100644 packages/v1-ready/fireflies/index.js create mode 100644 packages/v1-ready/fireflies/package.json create mode 100644 packages/v1-ready/fireflies/tests/api.test.js create mode 100644 packages/v1-ready/fireflies/tests/definition.test.js create mode 100644 packages/v1-ready/fireflies/tests/spec-sync.test.js create mode 100644 packages/v1-ready/gong/.env.example create mode 100644 packages/v1-ready/gong/LICENSE.md create mode 100644 packages/v1-ready/gong/README.md create mode 100644 packages/v1-ready/gong/api.js create mode 100644 packages/v1-ready/gong/defaultConfig.json create mode 100644 packages/v1-ready/gong/definition.js create mode 100644 packages/v1-ready/gong/gong.openapi.yaml create mode 100644 packages/v1-ready/gong/index.js create mode 100644 packages/v1-ready/gong/package.json create mode 100644 packages/v1-ready/gong/tests/api.test.js create mode 100644 packages/v1-ready/gong/tests/definition.test.js create mode 100644 packages/v1-ready/gong/tests/spec-sync.test.js create mode 100644 packages/v1-ready/otter/.env.example create mode 100644 packages/v1-ready/otter/LICENSE.md create mode 100644 packages/v1-ready/otter/README.md create mode 100644 packages/v1-ready/otter/api.js create mode 100644 packages/v1-ready/otter/defaultConfig.json create mode 100644 packages/v1-ready/otter/definition.js create mode 100644 packages/v1-ready/otter/index.js create mode 100644 packages/v1-ready/otter/otter.openapi.yaml create mode 100644 packages/v1-ready/otter/package.json create mode 100644 packages/v1-ready/otter/tests/api.test.js create mode 100644 packages/v1-ready/otter/tests/definition.test.js create mode 100644 packages/v1-ready/otter/tests/spec-sync.test.js create mode 100644 packages/v1-ready/quo/.env.example create mode 100644 packages/v1-ready/quo/LICENSE.md create mode 100644 packages/v1-ready/quo/README.md create mode 100644 packages/v1-ready/quo/api.js create mode 100644 packages/v1-ready/quo/defaultConfig.json create mode 100644 packages/v1-ready/quo/definition.js create mode 100644 packages/v1-ready/quo/index.js create mode 100644 packages/v1-ready/quo/jest.config.js create mode 100644 packages/v1-ready/quo/package.json create mode 100644 packages/v1-ready/quo/quo.openapi.yaml create mode 100644 packages/v1-ready/quo/tests/api.test.js create mode 100644 packages/v1-ready/quo/tests/definition.test.js create mode 100644 packages/v1-ready/quo/tests/spec-sync.test.js diff --git a/package-lock.json b/package-lock.json index 6b72828..6c03c7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -275,7 +275,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -287,14 +286,12 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-crypto/sha1-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -309,7 +306,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -322,7 +318,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -336,7 +331,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -350,7 +344,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-crypto/sha256-browser": { @@ -6517,7 +6510,6 @@ "version": "3.1015.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1015.0.tgz", "integrity": "sha512-yo+Y+/fq5/E684SynTRO+VA3a+98MeE/hs7J52XpNI5SchOCSrLhLtcDKVASlGhHQdNLGLzblRgps1OZaf8sbA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", @@ -6584,7 +6576,6 @@ "version": "3.973.24", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz", "integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6609,7 +6600,6 @@ "version": "3.972.22", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.22.tgz", "integrity": "sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6626,7 +6616,6 @@ "version": "3.972.24", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.24.tgz", "integrity": "sha512-h694K7+tRuepSRJr09wTvQfaEnjzsKZ5s7fbESrVds02GT/QzViJ94/HCNwM7bUfFxqpPXHxulZfL6Cou0dwPg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6648,7 +6637,6 @@ "version": "3.972.24", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.24.tgz", "integrity": "sha512-O46fFmv0RDFWiWEA9/e6oW92BnsyAXuEgTTasxHligjn2RCr9L/DK773m/NoFaL3ZdNAUz8WxgxunleMnHAkeQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6674,7 +6662,6 @@ "version": "3.972.25", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.25.tgz", "integrity": "sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.22", @@ -6698,7 +6685,6 @@ "version": "3.972.22", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.22.tgz", "integrity": "sha512-Os32s8/4gTZjBk5BtoS/cuTILaj+K72d0dVG7TCJX/fC4598cxwLDmf1AEHEpER5oL3K//yETjvFaz0V8oO5Xw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6716,7 +6702,6 @@ "version": "3.972.24", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.24.tgz", "integrity": "sha512-PaFv7snEfypU2yXkpvfyWgddEbDLtgVe51wdZlinhc2doubBjUzJZZpgwuF2Jenl1FBydMhNpMjD6SBUM3qdSA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6736,7 +6721,6 @@ "version": "3.972.24", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.24.tgz", "integrity": "sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6755,7 +6739,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6771,7 +6754,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6786,7 +6768,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6803,7 +6784,6 @@ "version": "3.972.25", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.25.tgz", "integrity": "sha512-QxiMPofvOt8SwSynTOmuZfvvPM1S9QfkESBxB22NMHTRXCJhR5BygLl8IXfC4jELiisQgwsgUby21GtXfX3f/g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6823,7 +6803,6 @@ "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6840,7 +6819,6 @@ "version": "3.1015.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1015.0.tgz", "integrity": "sha512-3OSD4y110nisRhHzFOjoEeHU4GQL4KpzkX9PxzWaiZe0Yg2+thZKM0Pn9DjYwezH5JYfh/K++xK/SE0IHGrmCQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -6859,7 +6837,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -6873,7 +6850,6 @@ "version": "3.996.5", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6890,7 +6866,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -6903,7 +6878,6 @@ "version": "3.973.11", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.11.tgz", "integrity": "sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.25", @@ -6929,7 +6903,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -6943,7 +6916,6 @@ "version": "4.4.13", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -6961,7 +6933,6 @@ "version": "3.23.12", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -6983,7 +6954,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -7000,7 +6970,6 @@ "version": "5.3.15", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -7017,7 +6986,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7033,7 +7001,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7047,7 +7014,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7060,7 +7026,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -7075,7 +7040,6 @@ "version": "4.4.27", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -7095,7 +7059,6 @@ "version": "4.4.44", "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -7116,7 +7079,6 @@ "version": "4.2.15", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -7132,7 +7094,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7146,7 +7107,6 @@ "version": "4.3.12", "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", @@ -7162,7 +7122,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", @@ -7179,7 +7138,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7193,7 +7151,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7207,7 +7164,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7222,7 +7178,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7236,7 +7191,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1" @@ -7249,7 +7203,6 @@ "version": "4.4.7", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7263,7 +7216,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -7283,7 +7235,6 @@ "version": "4.12.7", "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -7302,7 +7253,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7315,7 +7265,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^4.2.12", @@ -7330,7 +7279,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -7345,7 +7293,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7358,7 +7305,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7371,7 +7317,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -7385,7 +7330,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7398,7 +7342,6 @@ "version": "4.3.43", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", @@ -7414,7 +7357,6 @@ "version": "4.2.47", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.13", @@ -7433,7 +7375,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.12", @@ -7448,7 +7389,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7461,7 +7401,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -7475,7 +7414,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/service-error-classification": "^4.2.12", @@ -7490,7 +7428,6 @@ "version": "4.5.20", "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", @@ -7510,7 +7447,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7523,7 +7459,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -7534,6 +7469,319 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@aws-sdk/client-scheduler": { + "version": "3.1113.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-scheduler/-/client-scheduler-3.1113.0.tgz", + "integrity": "sha512-1vdzaO+mSivNNQYTROPML7mSfto3g9cSv+2bIfsww5fzh3K8F15AEQ4fu0hrCDW9O5FrfKcbxQy0rdePmSXTHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/credential-provider-node": "^3.972.80", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/core": { + "version": "3.977.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.8.tgz", + "integrity": "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.4", + "@aws-sdk/xml-builder": "^3.972.39", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.69", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.69.tgz", + "integrity": "sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.71.tgz", + "integrity": "sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.14.tgz", + "integrity": "sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/credential-provider-env": "^3.972.69", + "@aws-sdk/credential-provider-http": "^3.972.71", + "@aws-sdk/credential-provider-login": "^3.972.76", + "@aws-sdk/credential-provider-process": "^3.972.69", + "@aws-sdk/credential-provider-sso": "^3.973.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.75", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.80.tgz", + "integrity": "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.69", + "@aws-sdk/credential-provider-http": "^3.972.71", + "@aws-sdk/credential-provider-ini": "^3.973.14", + "@aws-sdk/credential-provider-process": "^3.972.69", + "@aws-sdk/credential-provider-sso": "^3.973.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.75", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.69", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.69.tgz", + "integrity": "sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.13.tgz", + "integrity": "sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/token-providers": "3.1111.0", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.75", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.75.tgz", + "integrity": "sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/token-providers": { + "version": "3.1111.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1111.0.tgz", + "integrity": "sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws-sdk/types": { + "version": "3.974.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@smithy/core": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@smithy/fetch-http-handler": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz", + "integrity": "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@smithy/node-http-handler": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.11.2.tgz", + "integrity": "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@smithy/signature-v4": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/@smithy/types": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-scheduler/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", @@ -9505,6 +9753,301 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@aws-sdk/client-ssm": { + "version": "3.1113.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1113.0.tgz", + "integrity": "sha512-eR73mgqpslu9H3AU2l3q0x6gGA4ArSvJxeBJbpLbCB2g5ch3K3EZJMqVmY5MOJcEOqIT0lYaTuzckWZLVcdQ7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/credential-provider-node": "^3.972.80", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/core": { + "version": "3.977.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.8.tgz", + "integrity": "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.4", + "@aws-sdk/xml-builder": "^3.972.39", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.69", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.69.tgz", + "integrity": "sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.71.tgz", + "integrity": "sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.14.tgz", + "integrity": "sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/credential-provider-env": "^3.972.69", + "@aws-sdk/credential-provider-http": "^3.972.71", + "@aws-sdk/credential-provider-login": "^3.972.76", + "@aws-sdk/credential-provider-process": "^3.972.69", + "@aws-sdk/credential-provider-sso": "^3.973.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.75", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.80.tgz", + "integrity": "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.69", + "@aws-sdk/credential-provider-http": "^3.972.71", + "@aws-sdk/credential-provider-ini": "^3.973.14", + "@aws-sdk/credential-provider-process": "^3.972.69", + "@aws-sdk/credential-provider-sso": "^3.973.13", + "@aws-sdk/credential-provider-web-identity": "^3.972.75", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.69", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.69.tgz", + "integrity": "sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.13.tgz", + "integrity": "sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/token-providers": "3.1111.0", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.75", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.75.tgz", + "integrity": "sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/token-providers": { + "version": "3.1111.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1111.0.tgz", + "integrity": "sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/types": { + "version": "3.974.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/core": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/fetch-http-handler": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz", + "integrity": "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/node-http-handler": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.11.2.tgz", + "integrity": "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/signature-v4": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/@smithy/types": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@aws-sdk/client-sso": { "version": "3.631.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.631.0.tgz", @@ -9629,7 +10172,6 @@ "integrity": "sha512-LLeyHPEYlo16wfteaAkYKZx8BrvF+ZqSkYSWodLmtk8xxCAONLOkeHyAofOswQrSetz3BWiu+sd9WNXEvucoPg==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -10107,7 +10649,6 @@ "version": "3.972.5", "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10121,7 +10662,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10134,7 +10674,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { @@ -10246,16 +10785,16 @@ "optional": true }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.40", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.40.tgz", - "integrity": "sha512-IEIl+UQnrEjZP53TSl91e8LBephi4i1Mt9WZrMgN8pOg6xPOLZdkN1GhsEzjkMD1TQy4Fp2dwWA/9ToTQFOlLA==", + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.76.tgz", + "integrity": "sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.10", - "@aws-sdk/nested-clients": "^3.997.8", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/nested-clients": "^3.997.43", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -10263,16 +10802,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { - "version": "3.974.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.10.tgz", - "integrity": "sha512-ZGFFlYynBR78Y/F8b/7y4i4sgW/iGwJSjoM7AZo5Et6vyr4/L0bunN+uzKMsvecCZyqcPp4RRK7Rs17l0kMujg==", + "version": "3.977.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.8.tgz", + "integrity": "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.24", - "@smithy/core": "^3.24.1", - "@smithy/signature-v4": "^5.4.1", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.974.4", + "@aws-sdk/xml-builder": "^3.972.39", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { @@ -10280,26 +10821,34 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "version": "3.974.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/core": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.2.tgz", - "integrity": "sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { @@ -10307,13 +10856,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/signature-v4": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.2.tgz", - "integrity": "sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { @@ -10321,9 +10870,9 @@ } }, "node_modules/@aws-sdk/credential-provider-login/node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10536,7 +11085,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -10555,7 +11103,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10569,7 +11116,6 @@ "version": "4.3.12", "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", @@ -10585,7 +11131,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10599,7 +11144,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10613,7 +11157,6 @@ "version": "4.4.7", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10627,7 +11170,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10640,7 +11182,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10653,14 +11194,12 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -10676,7 +11215,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10690,7 +11228,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10704,7 +11241,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10717,14 +11253,12 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/middleware-flexible-checksums": { "version": "3.974.4", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.4.tgz", "integrity": "sha512-fhCbZXPAyy8btnNbnBlR7Cc1nD54cETSvGn2wey71ehsM89AKPO8Dpco9DBAAgvrUdLrdHQepBXcyX4vxC5OwA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", @@ -10750,7 +11284,6 @@ "version": "3.973.24", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz", "integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -10775,7 +11308,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10789,7 +11321,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10803,7 +11334,6 @@ "version": "3.23.12", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -10825,7 +11355,6 @@ "version": "5.3.15", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -10842,7 +11371,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -10855,7 +11383,6 @@ "version": "4.4.27", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -10875,7 +11402,6 @@ "version": "4.2.15", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -10891,7 +11417,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10905,7 +11430,6 @@ "version": "4.3.12", "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", @@ -10921,7 +11445,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", @@ -10938,7 +11461,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10952,7 +11474,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10966,7 +11487,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10981,7 +11501,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -10995,7 +11514,6 @@ "version": "4.4.7", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -11009,7 +11527,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -11029,7 +11546,6 @@ "version": "4.12.7", "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -11048,7 +11564,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11061,7 +11576,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^4.2.12", @@ -11076,7 +11590,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -11091,7 +11604,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11104,7 +11616,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -11118,7 +11629,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11131,7 +11641,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -11145,7 +11654,6 @@ "version": "4.5.20", "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", @@ -11165,7 +11673,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11178,7 +11685,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -11192,7 +11698,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/middleware-host-header": { @@ -11222,7 +11727,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -11237,7 +11741,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -11251,7 +11754,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -11264,7 +11766,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/middleware-logger": { @@ -12170,7 +12671,6 @@ "version": "3.972.24", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.24.tgz", "integrity": "sha512-4sXxVC/enYgMkZefNMOzU6C6KtAXEvwVJLgNcUx1dvROH6GvKB5Sm2RGnGzTp0/PwkibIyMw4kOzF8tbLfaBAQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.24", @@ -12196,7 +12696,6 @@ "version": "3.973.24", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz", "integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -12221,7 +12720,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12235,7 +12733,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12249,7 +12746,6 @@ "version": "3.23.12", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -12271,7 +12767,6 @@ "version": "5.3.15", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.12", @@ -12288,7 +12783,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12301,7 +12795,6 @@ "version": "4.4.27", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -12321,7 +12814,6 @@ "version": "4.2.15", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -12337,7 +12829,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12351,7 +12842,6 @@ "version": "4.3.12", "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.12", @@ -12367,7 +12857,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.12", @@ -12384,7 +12873,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12398,7 +12886,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12412,7 +12899,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12427,7 +12913,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12441,7 +12926,6 @@ "version": "4.4.7", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12455,7 +12939,6 @@ "version": "5.3.12", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -12475,7 +12958,6 @@ "version": "4.12.7", "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.12", @@ -12494,7 +12976,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12507,7 +12988,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^4.2.12", @@ -12522,7 +13002,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -12537,7 +13016,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12550,7 +13028,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -12564,7 +13041,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12577,7 +13053,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12590,7 +13065,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -12604,7 +13078,6 @@ "version": "4.5.20", "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", @@ -12624,7 +13097,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12637,7 +13109,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -12651,7 +13122,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/middleware-sdk-sqs": { @@ -13052,7 +13522,6 @@ "version": "3.972.8", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", @@ -13067,7 +13536,6 @@ "version": "3.973.6", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -13081,7 +13549,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -13094,7 +13561,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/middleware-user-agent": { @@ -13122,28 +13588,18 @@ "optional": true }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.8.tgz", - "integrity": "sha512-/Vw2M27w+0APfMDzDpvv8auA4WiJ4D22+lC61pMS2M8Wk+4IydeRqh5utbrh+A5gQRxgUYd/xz3tdv8nQlmiHg==", + "version": "3.997.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.43.tgz", + "integrity": "sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.10", - "@aws-sdk/middleware-host-header": "^3.972.11", - "@aws-sdk/middleware-logger": "^3.972.10", - "@aws-sdk/middleware-recursion-detection": "^3.972.12", - "@aws-sdk/middleware-user-agent": "^3.972.40", - "@aws-sdk/region-config-resolver": "^3.972.14", - "@aws-sdk/signature-v4-multi-region": "^3.996.26", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.9", - "@aws-sdk/util-user-agent-browser": "^3.972.11", - "@aws-sdk/util-user-agent-node": "^3.973.26", - "@smithy/core": "^3.24.1", - "@smithy/fetch-http-handler": "^5.4.1", - "@smithy/node-http-handler": "^4.7.1", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.45", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -13151,223 +13607,233 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/core": { - "version": "3.974.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.10.tgz", - "integrity": "sha512-ZGFFlYynBR78Y/F8b/7y4i4sgW/iGwJSjoM7AZo5Et6vyr4/L0bunN+uzKMsvecCZyqcPp4RRK7Rs17l0kMujg==", + "version": "3.977.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.8.tgz", + "integrity": "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.24", - "@smithy/core": "^3.24.1", - "@smithy/signature-v4": "^5.4.1", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.974.4", + "@aws-sdk/xml-builder": "^3.972.39", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.11.tgz", - "integrity": "sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==", + "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": { + "version": "3.974.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", - "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "node_modules/@aws-sdk/nested-clients/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.12.tgz", - "integrity": "sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==", + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/core": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.40", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.40.tgz", - "integrity": "sha512-QLpD+HNQtL1Mc49/GRa6RmZvi/TEYBWPevC9F3L+j96IoG3xOSRctdQfbkX0lETb3TX9QQXU1oGYDmAB+YJprA==", + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/fetch-http-handler": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz", + "integrity": "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.10", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-endpoints": "^3.996.9", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.14.tgz", - "integrity": "sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==", + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.11.2.tgz", + "integrity": "sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/signature-v4": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.9.tgz", - "integrity": "sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==", + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/types": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.11.tgz", - "integrity": "sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==", + "node_modules/@aws-sdk/nested-clients/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz", + "integrity": "sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==", "license": "Apache-2.0", + "optional": true, "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "bowser": "^2.11.0", + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.26.tgz", - "integrity": "sha512-9bHR/EERjhrUGyo1qW620ogbGBtCglYB/pEtcm85sVd4/Ah+bwdLI3g1aJf75oNwNwh7+fw+8wOk/OCWHjzVmA==", + "node_modules/@aws-sdk/region-config-resolver/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1113.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1113.0.tgz", + "integrity": "sha512-FdbHboJSscXRHnqOGRLN+MvtcYNlxw1+XWMKcKi72nBPxpSTGQA+/zmxEBTlkCvTdIPtl/g383nNY0RiKYPmWQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.40", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.1", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.977.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.45", + "@aws-sdk/types": "^3.974.4", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/core": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.2.tgz", - "integrity": "sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==", + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/core": { + "version": "3.977.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.8.tgz", + "integrity": "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.974.4", + "@aws-sdk/xml-builder": "^3.972.39", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/fetch-http-handler": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.2.tgz", - "integrity": "sha512-3wF40g8OOCA5BnwQUvwtzZqYBbWWftDjpAlWIUo6Yld3ZzJaMAKqg7MWQBPjE8oLaqvZQUE7tVGlZPsae6A4bQ==", + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/types": { + "version": "3.974.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.2.tgz", - "integrity": "sha512-EdksTZ8UXYxGUgQ4mpIKrHoaj9WVGsp66TpZuixLAz1Jex8YDLnS4RH9ktGED5aOpN0OJlEtrsC9IGt76go1eA==", + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@smithy/core": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/signature-v4": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.2.tgz", - "integrity": "sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==", + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@smithy/signature-v4": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@smithy/types": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -13376,47 +13842,21 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/tslib": { + "node_modules/@aws-sdk/s3-request-presigner/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz", - "integrity": "sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@aws-sdk/types": "3.609.0", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/types": "^3.3.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "license": "0BSD", - "optional": true - }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.26.tgz", - "integrity": "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==", + "version": "3.996.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.45.tgz", + "integrity": "sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.1", - "@smithy/signature-v4": "^5.4.1", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.974.4", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -13424,12 +13864,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "version": "3.974.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.4.tgz", + "integrity": "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -13437,13 +13877,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@smithy/core": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.2.tgz", - "integrity": "sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.1", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { @@ -13451,13 +13890,13 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@smithy/signature-v4": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.2.tgz", - "integrity": "sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { @@ -13465,9 +13904,9 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -13532,7 +13971,6 @@ "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -13545,7 +13983,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@aws-sdk/util-endpoints": { @@ -13719,14 +14156,12 @@ "optional": true }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", - "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.39.tgz", + "integrity": "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA==", "license": "Apache-2.0", "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -13734,9 +14169,9 @@ } }, "node_modules/@aws-sdk/xml-builder/node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -13745,39 +14180,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.7", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@aws-sdk/xml-builder/node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/@aws-sdk/xml-builder/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -14232,7 +14634,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -15482,6 +15883,14 @@ "resolved": "packages/v1-ready/deel", "link": true }, + "node_modules/@friggframework/api-module-fathom": { + "resolved": "packages/v1-ready/fathom", + "link": true + }, + "node_modules/@friggframework/api-module-fireflies": { + "resolved": "packages/v1-ready/fireflies", + "link": true + }, "node_modules/@friggframework/api-module-frigg-scale-test": { "resolved": "packages/v1-ready/frigg-scale-test", "link": true @@ -15490,6 +15899,10 @@ "resolved": "packages/v1-ready/frontify", "link": true }, + "node_modules/@friggframework/api-module-gong": { + "resolved": "packages/v1-ready/gong", + "link": true + }, "node_modules/@friggframework/api-module-google-calendar": { "resolved": "packages/v1-ready/google-calendar", "link": true @@ -15518,10 +15931,18 @@ "resolved": "packages/needs-updating/microsoft-teams", "link": true }, + "node_modules/@friggframework/api-module-otter": { + "resolved": "packages/v1-ready/otter", + "link": true + }, "node_modules/@friggframework/api-module-pipedrive": { "resolved": "packages/v1-ready/pipedrive", "link": true }, + "node_modules/@friggframework/api-module-quo": { + "resolved": "packages/v1-ready/quo", + "link": true + }, "node_modules/@friggframework/api-module-salesforce": { "resolved": "packages/v1-ready/salesforce", "link": true @@ -16560,6 +16981,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" @@ -16575,6 +16997,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -16710,7 +17133,6 @@ "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^28.1.3", @@ -16917,7 +17339,6 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -17682,18 +18103,6 @@ "eslint-scope": "5.1.1" } }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -18531,7 +18940,6 @@ "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -18711,14 +19119,395 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, "engines": { "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz", + "integrity": "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.220.0.tgz", + "integrity": "sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -19253,7 +20042,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19266,7 +20054,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-base64": "^4.3.2", @@ -19280,7 +20067,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19293,7 +20079,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -19308,7 +20093,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -19322,7 +20106,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -19336,14 +20119,12 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@smithy/chunked-blob-reader/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@smithy/config-resolver": { @@ -19618,7 +20399,6 @@ "version": "4.2.13", "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", @@ -19634,7 +20414,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19647,7 +20426,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@smithy/hash-node": { @@ -19677,7 +20455,6 @@ "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.1", @@ -19692,7 +20469,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19705,7 +20481,6 @@ "version": "4.13.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -19718,7 +20493,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", @@ -19732,7 +20506,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.2", @@ -19746,7 +20519,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/@smithy/invalid-dependency": { @@ -21093,7 +21865,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz", "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.18.2" } @@ -21540,7 +22311,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -22143,7 +22913,6 @@ "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/transform": "^28.1.3", "@types/babel__core": "^7.1.14", @@ -22402,23 +23171,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -22702,7 +23471,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -22949,6 +23717,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -23962,9 +24746,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -24028,7 +24812,6 @@ "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -25601,9 +26384,9 @@ "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -25949,7 +26732,6 @@ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -26364,45 +27146,49 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-async-handler": { @@ -26516,22 +27302,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -26724,17 +27494,17 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -26922,16 +27692,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -26951,8 +27721,7 @@ "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz", "integrity": "sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fresh": { "version": "0.5.2", @@ -27590,7 +28359,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.9.0.tgz", "integrity": "sha512-GCOQdvm7XxV1S4U4CGrsdlEN37245eC8P9zaYCMr6K1BG0IPGy5lUwmJsEOGyl1GD6HXjOtl2keCP9asRBwNvA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.x" } @@ -27706,9 +28474,9 @@ "license": "ISC" }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -27797,19 +28565,23 @@ "license": "BSD-2-Clause" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { @@ -29115,7 +29887,6 @@ "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "detect-newline": "^3.0.0" }, @@ -29426,7 +30197,6 @@ "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -31071,7 +31841,6 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -32558,10 +33327,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -33687,7 +34459,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -33984,9 +34755,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -34500,21 +35271,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -34567,9 +35323,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { @@ -35145,12 +35901,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -35215,15 +35972,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -35918,24 +36675,24 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -35972,15 +36729,15 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -36081,15 +36838,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -36505,9 +37316,9 @@ "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -37230,7 +38041,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -37497,7 +38307,6 @@ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -38605,21 +39414,6 @@ "node": ">=12" } }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", @@ -38902,21 +39696,6 @@ "sinon": "^14.0.0" } }, - "packages/needs-updating/slack/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "packages/v1-ready/42matters": { "name": "@friggframework/api-module-42matters", "version": "1.1.4", @@ -44848,6 +45627,197 @@ "node": ">=12" } }, + "packages/v1-ready/fathom": { + "name": "@friggframework/api-module-fathom", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "jest": "^28.1.3", + "js-yaml": "^4.1.0" + } + }, + "packages/v1-ready/fathom/node_modules/@friggframework/core": { + "version": "2.0.0-next.107", + "resolved": "https://registry.npmjs.org/@friggframework/core/-/core-2.0.0-next.107.tgz", + "integrity": "sha512-t3t030L4e7TzoJ0f3BjkiHt4bUohGNQvQw7xVBHBazfwFSyJFmzasu0DzwTjDIQKiTBk64/O1yOZrZwzS9hkJA==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0", + "@aws-sdk/client-kms": "^3.588.0", + "@aws-sdk/client-lambda": "^3.714.0", + "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-sqs": "^3.588.0", + "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/s3-request-presigner": "^3.588.0", + "@hapi/boom": "^10.0.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.5", + "bson": "^4.7.2", + "chalk": "^4.1.2", + "common-tags": "^1.8.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.22.2", + "express-async-handler": "^1.2.0", + "form-data": "^4.0.6", + "fs-extra": "^11.2.0", + "lodash": "4.18.1", + "lodash.get": "^4.4.2", + "node-fetch": "^2.6.7", + "serverless-http": "^2.7.0", + "uuid": "^11.1.1" + }, + "peerDependencies": { + "@prisma/client": "^6.19.3", + "prisma": "^6.19.3" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "packages/v1-ready/fathom/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/v1-ready/fathom/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "packages/v1-ready/fathom/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "packages/v1-ready/fireflies": { + "name": "@friggframework/api-module-fireflies", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "jest": "^28.1.3" + } + }, + "packages/v1-ready/fireflies/node_modules/@friggframework/core": { + "version": "2.0.0-next.107", + "resolved": "https://registry.npmjs.org/@friggframework/core/-/core-2.0.0-next.107.tgz", + "integrity": "sha512-t3t030L4e7TzoJ0f3BjkiHt4bUohGNQvQw7xVBHBazfwFSyJFmzasu0DzwTjDIQKiTBk64/O1yOZrZwzS9hkJA==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0", + "@aws-sdk/client-kms": "^3.588.0", + "@aws-sdk/client-lambda": "^3.714.0", + "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-sqs": "^3.588.0", + "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/s3-request-presigner": "^3.588.0", + "@hapi/boom": "^10.0.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.5", + "bson": "^4.7.2", + "chalk": "^4.1.2", + "common-tags": "^1.8.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.22.2", + "express-async-handler": "^1.2.0", + "form-data": "^4.0.6", + "fs-extra": "^11.2.0", + "lodash": "4.18.1", + "lodash.get": "^4.4.2", + "node-fetch": "^2.6.7", + "serverless-http": "^2.7.0", + "uuid": "^11.1.1" + }, + "peerDependencies": { + "@prisma/client": "^6.19.3", + "prisma": "^6.19.3" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "packages/v1-ready/fireflies/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/v1-ready/fireflies/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "packages/v1-ready/fireflies/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/v1-ready/frigg-scale-test": { "name": "@friggframework/api-module-frigg-scale-test", "version": "0.1.0", @@ -45715,6 +46685,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@sinclair/typebox": "^0.34.0" }, @@ -45906,6 +46877,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", @@ -45925,7 +46897,8 @@ "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "packages/v1-ready/frigg-scale-test/node_modules/@sinonjs/commons": { "version": "3.0.1", @@ -47500,6 +48473,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@jest/types": "30.3.0", "@types/node": "*", @@ -47525,6 +48499,7 @@ ], "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -47536,6 +48511,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -49110,6 +50086,105 @@ "node": ">=12" } }, + "packages/v1-ready/gong": { + "name": "@friggframework/api-module-gong", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "js-yaml": "^4.1.0", + "prettier": "^2.7.1" + } + }, + "packages/v1-ready/gong/node_modules/@friggframework/core": { + "version": "2.0.0-next.107", + "resolved": "https://registry.npmjs.org/@friggframework/core/-/core-2.0.0-next.107.tgz", + "integrity": "sha512-t3t030L4e7TzoJ0f3BjkiHt4bUohGNQvQw7xVBHBazfwFSyJFmzasu0DzwTjDIQKiTBk64/O1yOZrZwzS9hkJA==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0", + "@aws-sdk/client-kms": "^3.588.0", + "@aws-sdk/client-lambda": "^3.714.0", + "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-sqs": "^3.588.0", + "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/s3-request-presigner": "^3.588.0", + "@hapi/boom": "^10.0.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.5", + "bson": "^4.7.2", + "chalk": "^4.1.2", + "common-tags": "^1.8.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.22.2", + "express-async-handler": "^1.2.0", + "form-data": "^4.0.6", + "fs-extra": "^11.2.0", + "lodash": "4.18.1", + "lodash.get": "^4.4.2", + "node-fetch": "^2.6.7", + "serverless-http": "^2.7.0", + "uuid": "^11.1.1" + }, + "peerDependencies": { + "@prisma/client": "^6.19.3", + "prisma": "^6.19.3" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "packages/v1-ready/gong/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/v1-ready/gong/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "packages/v1-ready/gong/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/v1-ready/google-calendar": { "name": "@friggframework/api-module-google-calendar", "version": "1.1.3", @@ -55065,6 +56140,102 @@ "node": ">=12" } }, + "packages/v1-ready/otter": { + "name": "@friggframework/api-module-otter", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "jest": "^28.1.3", + "js-yaml": "^4.1.0" + } + }, + "packages/v1-ready/otter/node_modules/@friggframework/core": { + "version": "2.0.0-next.107", + "resolved": "https://registry.npmjs.org/@friggframework/core/-/core-2.0.0-next.107.tgz", + "integrity": "sha512-t3t030L4e7TzoJ0f3BjkiHt4bUohGNQvQw7xVBHBazfwFSyJFmzasu0DzwTjDIQKiTBk64/O1yOZrZwzS9hkJA==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0", + "@aws-sdk/client-kms": "^3.588.0", + "@aws-sdk/client-lambda": "^3.714.0", + "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-sqs": "^3.588.0", + "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/s3-request-presigner": "^3.588.0", + "@hapi/boom": "^10.0.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.5", + "bson": "^4.7.2", + "chalk": "^4.1.2", + "common-tags": "^1.8.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.22.2", + "express-async-handler": "^1.2.0", + "form-data": "^4.0.6", + "fs-extra": "^11.2.0", + "lodash": "4.18.1", + "lodash.get": "^4.4.2", + "node-fetch": "^2.6.7", + "serverless-http": "^2.7.0", + "uuid": "^11.1.1" + }, + "peerDependencies": { + "@prisma/client": "^6.19.3", + "prisma": "^6.19.3" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "packages/v1-ready/otter/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/v1-ready/otter/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "packages/v1-ready/otter/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/v1-ready/pipedrive": { "name": "@friggframework/api-module-pipedrive", "version": "2.0.0", @@ -55163,6 +56334,105 @@ "uuid": "dist/bin/uuid" } }, + "packages/v1-ready/quo": { + "name": "@friggframework/api-module-quo", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "js-yaml": "^4.1.0", + "prettier": "^2.7.1" + } + }, + "packages/v1-ready/quo/node_modules/@friggframework/core": { + "version": "2.0.0-next.107", + "resolved": "https://registry.npmjs.org/@friggframework/core/-/core-2.0.0-next.107.tgz", + "integrity": "sha512-t3t030L4e7TzoJ0f3BjkiHt4bUohGNQvQw7xVBHBazfwFSyJFmzasu0DzwTjDIQKiTBk64/O1yOZrZwzS9hkJA==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0", + "@aws-sdk/client-kms": "^3.588.0", + "@aws-sdk/client-lambda": "^3.714.0", + "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-sqs": "^3.588.0", + "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/s3-request-presigner": "^3.588.0", + "@hapi/boom": "^10.0.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.5", + "bson": "^4.7.2", + "chalk": "^4.1.2", + "common-tags": "^1.8.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.22.2", + "express-async-handler": "^1.2.0", + "form-data": "^4.0.6", + "fs-extra": "^11.2.0", + "lodash": "4.18.1", + "lodash.get": "^4.4.2", + "node-fetch": "^2.6.7", + "serverless-http": "^2.7.0", + "uuid": "^11.1.1" + }, + "peerDependencies": { + "@prisma/client": "^6.19.3", + "prisma": "^6.19.3" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "packages/v1-ready/quo/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/v1-ready/quo/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "packages/v1-ready/quo/node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/v1-ready/salesforce": { "name": "@friggframework/api-module-salesforce", "version": "1.0.2", diff --git a/packages/v1-ready/fathom/.env.example b/packages/v1-ready/fathom/.env.example new file mode 100644 index 0000000..f1aee37 --- /dev/null +++ b/packages/v1-ready/fathom/.env.example @@ -0,0 +1,4 @@ +# Fathom (fathom.video) API key. +# Generate under Fathom > User Settings > API Access. +# Sent on every request as the `X-Api-Key` header. +FATHOM_API_KEY=your_fathom_api_key_here diff --git a/packages/v1-ready/fathom/LICENSE.md b/packages/v1-ready/fathom/LICENSE.md new file mode 100644 index 0000000..c307ce6 --- /dev/null +++ b/packages/v1-ready/fathom/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Left Hook + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/v1-ready/fathom/README.md b/packages/v1-ready/fathom/README.md new file mode 100644 index 0000000..01f2a23 --- /dev/null +++ b/packages/v1-ready/fathom/README.md @@ -0,0 +1,163 @@ +# Fathom API Module (`@friggframework/api-module-fathom`) + +A [Frigg](https://friggframework.org) API module for [Fathom](https://fathom.video) +— the AI meeting recorder. It wraps Fathom's public REST API so a Frigg +integration can list meetings/recordings, pull transcripts and summaries, read +team members, and register webhooks. + +Integrations consume it the standard Frigg way: + +```javascript +const meetings = await this.fathom.api.listMeetings({ include_summary: true }); +const { transcript } = await this.fathom.api.getTranscript(recordingId); +``` + +## Authentication + +Fathom uses **API-key** authentication. Generate a key in Fathom under +**User Settings → API Access**, and the module sends it on every request as the +`X-Api-Key` header: + +```bash +curl https://api.fathom.ai/external/v1/meetings -H "X-Api-Key: YOUR_API_KEY" +``` + +- **Base URL:** `https://api.fathom.ai/external/v1` +- **Rate limit:** 60 requests/minute across all of an account's API keys. + +Set `FATHOM_API_KEY` in your environment (see `.env.example`). The module also +exposes a JSON-Schema authorization form (`getAuthorizationRequirements`) so the +key can be collected through the Frigg auth UI / `frigg auth` CLI. + +## API Methods + +| Method | HTTP | Purpose | +|---|---|---| +| `listMeetings(params)` | `GET /meetings` | List meetings/recordings (paginated via `next_cursor`). | +| `listAllMeetings(params, opts)` | — | Convenience: follows `next_cursor` and returns a flat array. | +| `getTranscript(recordingId, params)` | `GET /recordings/{id}/transcript` | Transcript segments (or async POST to `destination_url`). | +| `getSummary(recordingId, params)` | `GET /recordings/{id}/summary` | Markdown-formatted call summary. | +| `listTeamMembers(params)` | `GET /team_members` | People on the Fathom account (optional `cursor`, `team`). | +| `createWebhook(data)` | `POST /webhooks` | Register a webhook for new meeting content. | + +### `listMeetings` parameters (all optional) + +`cursor`, `created_after`, `created_before`, `meeting_type`, +`include_transcript`, `include_summary`, `include_action_items`, +`include_highlights`, `include_crm_matches`, `calendar_invitees_domains_type`, +and the array filters `recorded_by[]` (emails), `teams[]`, +`calendar_invitees_domains[]`. Array values are serialized with the `key[]` +repeated-key convention. + +### Meeting shape (fields used by consumers) + +```jsonc +{ + "recording_id": 12345, + "title": "Acme <> Left Hook", + "meeting_title": "Discovery call", + "meeting_type": "external", + "url": "https://fathom.video/calls/12345", + "share_url": "https://fathom.video/share/...", + "created_at": "2026-08-11T18:00:00Z", + "scheduled_start_time": "2026-08-11T18:00:00Z", + "scheduled_end_time": "2026-08-11T18:30:00Z", + "recording_start_time": "2026-08-11T18:01:00Z", + "recording_end_time": "2026-08-11T18:29:00Z", + "calendar_invitees": [ + { "name": "Jane Buyer", "email": "jane@acme.com", + "email_domain": "acme.com", "is_external": true, + "matched_speaker_display_name": "Jane" } + ], + "recorded_by": { "name": "Sean", "email": "sean@lefthook.co", + "email_domain": "lefthook.co", "team": "Left Hook" } +} +``` + +Attendee emails live on `calendar_invitees[].email` (with `is_external` and +`email_domain`), which is what you match against a CRM/Reevo contact. The +recorder is `recorded_by.email`. + +### Transcript shape + +```jsonc +{ + "transcript": [ + { "speaker": { "display_name": "Jane", + "matched_calendar_invitee_email": "jane@acme.com" }, + "text": "...", "timestamp": "00:01:12" } + ] +} +``` + +### Summary shape + +```jsonc +{ "summary": { "template_name": "General", "markdown_formatted": "## ..." } } +``` + +## Webhooks (this integration is webhook-driven) + +Create a webhook with `createWebhook`: + +```javascript +await this.fathom.api.createWebhook({ + destination_url: 'https://your-frigg-app/webhooks/fathom', + triggered_for: ['my_recordings', 'shared_external_recordings'], + include_summary: true, + include_transcript: true, + include_action_items: true, +}); +``` + +- `triggered_for` (required, ≥1): `my_recordings`, + `shared_external_recordings`, `my_shared_with_team_recordings`, + `shared_team_recordings`. +- At least one of `include_transcript`, `include_summary`, + `include_action_items`, `include_crm_matches` must be `true`. +- The response includes a `secret` (`whsec_...`) used to verify delivery + signatures. + +**Event:** `new-meeting-content-ready` — delivered after a meeting is processed. +The payload carries the same meeting fields listed above (recording id, titles, +timestamps, `share_url`, `calendar_invitees[]`, `recorded_by`) plus the opted-in +`summary` / `transcript` / `action_items`. + +**Signature verification:** each delivery carries `webhook-id`, +`webhook-timestamp`, and `webhook-signature` headers. Verify by HMAC-SHA256 over +`{id}.{timestamp}.{rawBody}` using the base64-decoded portion of the webhook +secret after the `whsec_` prefix, comparing in constant time within a 5-minute +timestamp tolerance (Svix-style signing). + +## Documented-endpoint notes + +The public REST API documents **list** meetings only (no single-meeting `GET` +by id) — fetch a specific recording's content via the transcript/summary +endpoints keyed on `recording_id`. There is **no `/me` identity endpoint**, so +the module derives account identity from the first meeting's `recorded_by`, +falling back to a stable label. `list-webhooks` / `delete-webhook` are not +documented at the time of writing, so only `createWebhook` is modeled. + +## Testing + +```bash +npm install +npm test +``` + +Tests are fully offline — HTTP methods are stubbed and assertions are made on the +request options the module builds. No API key or network access is required. + +## Sources + +- Fathom Developer Hub — https://developers.fathom.ai/ +- List meetings — https://developers.fathom.ai/api-reference/meetings/list-meetings +- Get transcript — https://developers.fathom.ai/api-reference/recordings/get-transcript +- Get summary — https://developers.fathom.ai/api-reference/recordings/get-summary +- List team members — https://developers.fathom.ai/api-reference/team-members/list-team-members +- Create a webhook — https://developers.fathom.ai/api-reference/webhooks/create-a-webhook +- Webhooks overview — https://developers.fathom.ai/webhooks + +## License + +MIT — see [LICENSE.md](./LICENSE.md). diff --git a/packages/v1-ready/fathom/api.js b/packages/v1-ready/fathom/api.js new file mode 100644 index 0000000..b86d946 --- /dev/null +++ b/packages/v1-ready/fathom/api.js @@ -0,0 +1,184 @@ +const { ApiKeyRequester, get } = require('@friggframework/core'); + +/** + * Fathom (fathom.video) API client. + * + * Auth: API key sent in the `X-Api-Key` request header. + * curl https://api.fathom.ai/external/v1/meetings -H "X-Api-Key: YOUR_API_KEY" + * + * Base URL: https://api.fathom.ai/external/v1 + * Rate limit: 60 requests/minute across all of an account's API keys. + * + * Docs: https://developers.fathom.ai/ + */ +class Api extends ApiKeyRequester { + constructor(params = {}) { + super(params); + + // ApiKeyRequester puts `headers[this.api_key_name] = this.api_key` + this.api_key_name = 'X-Api-Key'; + this.api_key = + get(params, 'api_key', null) || + get(params, 'access_token', null) || + get(params, 'apiKey', null); + + this.baseUrl = 'https://api.fathom.ai/external/v1'; + + this.URLs = { + meetings: '/meetings', + teamMembers: '/team_members', + transcript: (recordingId) => + `/recordings/${recordingId}/transcript`, + summary: (recordingId) => `/recordings/${recordingId}/summary`, + webhooks: '/webhooks', + }; + } + + // ---- Query helpers ----------------------------------------------------- + + /** + * Build a query string that supports both scalar params and array params + * (Fathom array filters use the `key[]` repeated-key convention). The core + * Requester's built-in query builder can't emit repeated keys, so we build + * the string here and append it to the URL directly. + */ + _buildQuery(params = {}) { + const usp = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue; + if (Array.isArray(value)) { + for (const v of value) { + if (v === undefined || v === null) continue; + usp.append(`${key}[]`, String(v)); + } + } else { + usp.append(key, String(value)); + } + } + const qs = usp.toString(); + return qs ? `?${qs}` : ''; + } + + // ---- Meetings ---------------------------------------------------------- + + /** + * List meetings/recordings. + * GET /meetings + * + * Supported params (all optional): + * cursor, created_after, created_before, meeting_type, + * include_transcript, include_summary, include_action_items, + * include_highlights, include_crm_matches, + * calendar_invitees_domains_type, + * recorded_by[] (array of emails), teams[] (array), + * calendar_invitees_domains[] (array of domains) + * + * Response: { limit, next_cursor, items: [ Meeting ] } + * Each Meeting includes recording_id, title, meeting_title, share_url, + * url, scheduled_start_time, recording_start_time/end_time, + * calendar_invitees[{ name, email, email_domain, is_external }], + * recorded_by{ name, email, email_domain, team }. + */ + async listMeetings(params = {}) { + return this._get({ + url: this.baseUrl + this.URLs.meetings + this._buildQuery(params), + }); + } + + /** + * Convenience: page through every meeting, following `next_cursor`. + * Returns a flat array of meeting items. + */ + async listAllMeetings(params = {}, { maxPages = 50 } = {}) { + const all = []; + let cursor = params.cursor; + let pages = 0; + do { + const page = await this.listMeetings({ ...params, cursor }); + if (Array.isArray(page.items)) all.push(...page.items); + cursor = page.next_cursor; + pages += 1; + } while (cursor && pages < maxPages); + return all; + } + + // ---- Recording content ------------------------------------------------- + + /** + * Get a recording's transcript. + * GET /recordings/{recording_id}/transcript + * + * With no destination_url the transcript is returned directly: + * { transcript: [ { speaker: { display_name, + * matched_calendar_invitee_email }, text, timestamp } ] } + * With destination_url it is POSTed there asynchronously and the + * endpoint returns { destination_url }. + */ + async getTranscript(recordingId, params = {}) { + return this._get({ + url: + this.baseUrl + + this.URLs.transcript(recordingId) + + this._buildQuery(params), + }); + } + + /** + * Get a recording's summary. + * GET /recordings/{recording_id}/summary + * + * Direct response: { summary: { template_name, markdown_formatted } } + * Async (destination_url): { destination_url } + */ + async getSummary(recordingId, params = {}) { + return this._get({ + url: + this.baseUrl + + this.URLs.summary(recordingId) + + this._buildQuery(params), + }); + } + + // ---- Team members ------------------------------------------------------ + + /** + * List team members. + * GET /team_members + * Optional params: cursor, team (filter by team name). + * Response: { limit, next_cursor, items: [ { name, email, created_at } ] }. + */ + async listTeamMembers(params = {}) { + return this._get({ + url: this.baseUrl + this.URLs.teamMembers + this._buildQuery(params), + }); + } + + // ---- Webhooks ---------------------------------------------------------- + + /** + * Create a webhook. + * POST /webhooks + * + * body: { + * destination_url, // required + * triggered_for: [ 'my_recordings' | 'shared_external_recordings' + * | 'my_shared_with_team_recordings' + * | 'shared_team_recordings' ], // required + * include_transcript?, include_summary?, + * include_action_items?, include_crm_matches? // >=1 must be true + * } + * + * Response: { id, url, secret, created_at, triggered_for, include_* } + * The returned `secret` (whsec_...) verifies delivery signatures + * (webhook-id / webhook-timestamp / webhook-signature headers, HMAC-SHA256). + */ + async createWebhook(data) { + return this._post({ + url: this.baseUrl + this.URLs.webhooks, + headers: { 'Content-Type': 'application/json' }, + body: data, + }); + } +} + +module.exports = { Api }; diff --git a/packages/v1-ready/fathom/defaultConfig.json b/packages/v1-ready/fathom/defaultConfig.json new file mode 100644 index 0000000..2b000ec --- /dev/null +++ b/packages/v1-ready/fathom/defaultConfig.json @@ -0,0 +1,10 @@ +{ + "name": "fathom", + "config": { + "apiKey": true, + "batch": { + "concurrency": 3, + "delay": 1000 + } + } +} diff --git a/packages/v1-ready/fathom/definition.js b/packages/v1-ready/fathom/definition.js new file mode 100644 index 0000000..6aa891f --- /dev/null +++ b/packages/v1-ready/fathom/definition.js @@ -0,0 +1,93 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +/** + * Fathom is API-key authenticated (X-Api-Key header). There is no OAuth flow + * and no dedicated "/me" identity endpoint on the public REST API, so identity + * is derived from the first meeting's `recorded_by` where available, falling + * back to a stable label. testAuthRequest simply performs an authenticated + * list call. + */ +async function resolveAccountIdentity(api) { + try { + const result = await api.listMeetings({}); + const first = Array.isArray(result?.items) ? result.items[0] : null; + const recordedBy = first?.recorded_by; + if (recordedBy?.email) { + return { + externalId: recordedBy.email, + name: recordedBy.team || recordedBy.name || recordedBy.email, + }; + } + } catch (e) { + // fall through to a stable default identity + } + return { externalId: 'fathom-account', name: 'Fathom' }; +} + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Fathom', + requiredAuthMethods: { + getAuthorizationRequirements: () => ({ + type: 'apiKey', + data: { + jsonSchema: { + title: 'Fathom Authentication', + type: 'object', + required: ['api_key'], + properties: { + api_key: { + type: 'string', + title: 'API Key', + }, + }, + }, + uiSchema: { + api_key: { + 'ui:widget': 'password', + 'ui:help': + 'Generate an API key in Fathom under User Settings > API Access. Sent as the X-Api-Key header.', + }, + }, + }, + }), + setAuthParams: async (api, params) => { + const apiKey = + get(params, 'api_key', null) || + get(params, 'access_token', null); + api.setApiKey(apiKey); + }, + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const identity = await resolveAccountIdentity(api); + return { + identifiers: { externalId: identity.externalId, userId }, + details: { name: identity.name }, + }; + }, + getCredentialDetails: async (api, userId) => { + const identity = await resolveAccountIdentity(api); + return { + identifiers: { externalId: identity.externalId, userId }, + details: {}, + }; + }, + apiPropertiesToPersist: { + credential: ['api_key'], + entity: [], + }, + testAuthRequest: async (api) => { + // Any authenticated call proves the key works. + return api.listMeetings({}); + }, + }, + env: { + api_key: process.env.FATHOM_API_KEY, + }, +}; + +module.exports = { Definition }; diff --git a/packages/v1-ready/fathom/fathom.openapi.yaml b/packages/v1-ready/fathom/fathom.openapi.yaml new file mode 100644 index 0000000..7839b73 --- /dev/null +++ b/packages/v1-ready/fathom/fathom.openapi.yaml @@ -0,0 +1,364 @@ +openapi: 3.0.3 +info: + title: Fathom Public API + version: "1.0.0" + description: >- + Fathom (fathom.video) is an AI meeting recorder. This is the public REST API + surface documented at https://developers.fathom.ai/. Fathom publishes its own + OpenAPI document (rendered on the Developer Hub); this file is Left Hook's + curated subset covering exactly the endpoints the @friggframework/api-module-fathom + client in api.js implements. It is the source of truth the hand-written client + mirrors 1:1 (one method per operationId). + contact: + name: Left Hook + url: https://lefthook.com +servers: + - url: https://api.fathom.ai/external/v1 + description: Fathom public API +security: + - ApiKeyAuth: [] +tags: + - name: Meetings + - name: Recordings + - name: Team Members + - name: Webhooks +paths: + /meetings: + get: + tags: [Meetings] + operationId: listMeetings + summary: List meetings/recordings + description: >- + Returns a page of meetings. Paginate by passing the previous response's + `next_cursor` as `cursor`. `include_transcript` and `include_summary` + are unavailable to OAuth apps. + parameters: + - in: query + name: cursor + schema: { type: string } + description: Pagination cursor from a previous response's `next_cursor`. + - in: query + name: created_after + schema: { type: string, format: date-time } + description: Only meetings created at/after this ISO 8601 timestamp. + - in: query + name: created_before + schema: { type: string, format: date-time } + description: Only meetings created at/before this ISO 8601 timestamp. + - in: query + name: meeting_type + schema: { type: string } + description: Filter by meeting type name. + - in: query + name: include_transcript + schema: { type: boolean, default: false } + - in: query + name: include_summary + schema: { type: boolean, default: false } + - in: query + name: include_action_items + schema: { type: boolean, default: false } + - in: query + name: include_highlights + schema: { type: boolean, default: false } + - in: query + name: include_crm_matches + schema: { type: boolean, default: false } + - in: query + name: calendar_invitees_domains_type + schema: + type: string + enum: [all, only_internal, one_or_more_external] + - in: query + name: recorded_by + description: >- + Array filter — emails of recorders. Serialized as the repeated + `recorded_by[]` key. + schema: + type: array + items: { type: string, format: email } + style: form + explode: true + - in: query + name: teams + description: Array filter — team names. Serialized as repeated `teams[]`. + schema: + type: array + items: { type: string } + style: form + explode: true + - in: query + name: calendar_invitees_domains + description: >- + Array filter — company domains. Serialized as repeated + `calendar_invitees_domains[]`. + schema: + type: array + items: { type: string } + style: form + explode: true + responses: + "200": { $ref: "#/components/responses/MeetingListResponse" } + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /recordings/{recording_id}/transcript: + parameters: + - $ref: "#/components/parameters/RecordingId" + get: + tags: [Recordings] + operationId: getTranscript + summary: Get a recording's transcript + description: >- + Without `destination_url` the transcript is returned directly. With + `destination_url` it is POSTed there asynchronously and the endpoint + returns `{ destination_url }`. + parameters: + - in: query + name: destination_url + schema: { type: string, format: uri } + description: If provided, the transcript is POSTed here instead of returned. + responses: + "200": { $ref: "#/components/responses/TranscriptResponse" } + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /recordings/{recording_id}/summary: + parameters: + - $ref: "#/components/parameters/RecordingId" + get: + tags: [Recordings] + operationId: getSummary + summary: Get a recording's summary + description: >- + Without `destination_url` the summary is returned directly. With + `destination_url` it is POSTed there asynchronously and the endpoint + returns `{ destination_url }`. + parameters: + - in: query + name: destination_url + schema: { type: string, format: uri } + description: If provided, the summary is POSTed here instead of returned. + responses: + "200": { $ref: "#/components/responses/SummaryResponse" } + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /team_members: + get: + tags: [Team Members] + operationId: listTeamMembers + summary: List team members + description: People on the Fathom account. + parameters: + - in: query + name: cursor + schema: { type: string } + description: Pagination cursor from a previous response's `next_cursor`. + - in: query + name: team + schema: { type: string } + description: Filter by team name. + responses: + "200": { $ref: "#/components/responses/TeamMemberListResponse" } + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /webhooks: + post: + tags: [Webhooks] + operationId: createWebhook + summary: Create a webhook + description: >- + Register a webhook that fires when new meeting content is ready. At least + one of `include_transcript`, `include_summary`, `include_action_items`, + or `include_crm_matches` must be true. The response `secret` (whsec_...) + verifies delivery signatures (webhook-id / webhook-timestamp / + webhook-signature headers, HMAC-SHA256). + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WebhookCreate" } + responses: + "201": { $ref: "#/components/responses/WebhookResponse" } + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-Api-Key + parameters: + RecordingId: + in: path + name: recording_id + required: true + schema: { type: integer } + description: The numeric id of the meeting recording. + responses: + MeetingListResponse: + description: A page of meetings. + content: + application/json: + schema: + type: object + properties: + limit: { type: integer, nullable: true } + next_cursor: { type: string, nullable: true } + items: + type: array + items: { $ref: "#/components/schemas/Meeting" } + TranscriptResponse: + description: The transcript, or an async-delivery confirmation. + content: + application/json: + schema: + oneOf: + - type: object + properties: + transcript: + type: array + items: { $ref: "#/components/schemas/TranscriptSegment" } + - $ref: "#/components/schemas/AsyncDelivery" + SummaryResponse: + description: The summary, or an async-delivery confirmation. + content: + application/json: + schema: + oneOf: + - type: object + properties: + summary: { $ref: "#/components/schemas/Summary" } + - $ref: "#/components/schemas/AsyncDelivery" + TeamMemberListResponse: + description: A page of team members. + content: + application/json: + schema: + type: object + properties: + limit: { type: integer, nullable: true } + next_cursor: { type: string, nullable: true } + items: + type: array + items: { $ref: "#/components/schemas/TeamMember" } + WebhookResponse: + description: The created webhook. + content: + application/json: + schema: { $ref: "#/components/schemas/Webhook" } + BadRequest: + description: Invalid query parameters or request body. + Unauthorized: + description: Missing or invalid API key. + RateLimited: + description: Rate limit exceeded (60 requests/minute per account). + schemas: + AsyncDelivery: + type: object + description: Returned when a destination_url was supplied. + properties: + destination_url: { type: string, format: uri } + Invitee: + type: object + properties: + name: { type: string } + email: { type: string, format: email } + email_domain: { type: string } + is_external: { type: boolean } + matched_speaker_display_name: { type: string, nullable: true } + Recorder: + type: object + properties: + name: { type: string } + email: { type: string, format: email } + email_domain: { type: string } + team: { type: string, nullable: true } + Meeting: + type: object + properties: + recording_id: { type: integer } + title: { type: string } + meeting_title: { type: string } + meeting_type: { type: string } + url: { type: string, format: uri } + share_url: { type: string, format: uri } + created_at: { type: string, format: date-time } + scheduled_start_time: { type: string, format: date-time } + scheduled_end_time: { type: string, format: date-time } + recording_start_time: { type: string, format: date-time } + recording_end_time: { type: string, format: date-time } + calendar_invitees: + type: array + items: { $ref: "#/components/schemas/Invitee" } + recorded_by: { $ref: "#/components/schemas/Recorder" } + TranscriptSegment: + type: object + properties: + speaker: + type: object + properties: + display_name: { type: string } + matched_calendar_invitee_email: + type: string + format: email + nullable: true + text: { type: string } + timestamp: + type: string + description: Time relative to meeting start, HH:MM:SS. + Summary: + type: object + properties: + template_name: { type: string, nullable: true } + markdown_formatted: { type: string, nullable: true } + TeamMember: + type: object + properties: + name: { type: string } + email: { type: string, format: email } + created_at: { type: string, format: date-time } + WebhookCreate: + type: object + required: [destination_url, triggered_for] + description: >- + At least one of include_transcript, include_summary, + include_action_items, or include_crm_matches must be true. + properties: + destination_url: + type: string + format: uri + description: The endpoint that will receive webhook deliveries. + triggered_for: + type: array + minItems: 1 + items: + type: string + enum: + - my_recordings + - shared_external_recordings + - my_shared_with_team_recordings + - shared_team_recordings + include_transcript: { type: boolean, default: false } + include_summary: { type: boolean, default: false } + include_action_items: { type: boolean, default: false } + include_crm_matches: { type: boolean, default: false } + Webhook: + type: object + properties: + id: { type: string } + url: { type: string, format: uri } + secret: + type: string + description: whsec_... value used to verify delivery signatures. + created_at: { type: string, format: date-time } + triggered_for: + type: array + items: { type: string } + include_transcript: { type: boolean } + include_summary: { type: boolean } + include_action_items: { type: boolean } + include_crm_matches: { type: boolean } diff --git a/packages/v1-ready/fathom/index.js b/packages/v1-ready/fathom/index.js new file mode 100644 index 0000000..3c94a63 --- /dev/null +++ b/packages/v1-ready/fathom/index.js @@ -0,0 +1,7 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); + +module.exports = { + Api, + Definition, +}; diff --git a/packages/v1-ready/fathom/package.json b/packages/v1-ready/fathom/package.json new file mode 100644 index 0000000..c1010ba --- /dev/null +++ b/packages/v1-ready/fathom/package.json @@ -0,0 +1,32 @@ +{ + "name": "@friggframework/api-module-fathom", + "version": "1.0.0", + "description": "Fathom (fathom.video) API module that lets the Frigg Framework interact with Fathom meetings, recordings, transcripts, summaries, and webhooks", + "main": "index.js", + "scripts": { + "test": "jest", + "lint:fix": "prettier --write --loglevel error . && eslint . --fix" + }, + "keywords": [ + "frigg", + "fathom", + "fathom.video", + "api-module", + "meetings", + "transcripts" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "jest": "^28.1.3", + "js-yaml": "^4.1.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/v1-ready/fathom/tests/api.test.js b/packages/v1-ready/fathom/tests/api.test.js new file mode 100644 index 0000000..d12f379 --- /dev/null +++ b/packages/v1-ready/fathom/tests/api.test.js @@ -0,0 +1,159 @@ +const { Api } = require('../api'); + +// Fully offline: we stub the low-level HTTP methods (_get/_post) and assert on +// the request options the API methods construct. No network is touched. +function makeApi(overrides = {}) { + const api = new Api({ api_key: 'test-key', ...overrides }); + api._captured = []; + api._get = async (options) => { + api._captured.push({ method: 'GET', ...options }); + return { items: [], next_cursor: null }; + }; + api._post = async (options) => { + api._captured.push({ method: 'POST', ...options }); + return { id: 'wh_1' }; + }; + return api; +} + +describe('Fathom Api', () => { + describe('construction & auth', () => { + it('sets the base URL and X-Api-Key header name', () => { + const api = new Api({ api_key: 'abc' }); + expect(api.baseUrl).toBe('https://api.fathom.ai/external/v1'); + expect(api.api_key_name).toBe('X-Api-Key'); + expect(api.api_key).toBe('abc'); + }); + + it('accepts access_token as an alias for api_key', () => { + const api = new Api({ access_token: 'from-token' }); + expect(api.api_key).toBe('from-token'); + }); + + it('adds the X-Api-Key auth header', async () => { + const api = new Api({ api_key: 'secret' }); + const headers = await api.addAuthHeaders({}); + expect(headers['X-Api-Key']).toBe('secret'); + }); + + it('reports authenticated only with a non-empty key', () => { + expect(new Api({ api_key: 'x' }).isAuthenticated()).toBe(true); + expect(new Api({}).isAuthenticated()).toBe(false); + }); + }); + + describe('listMeetings', () => { + it('hits GET /meetings', async () => { + const api = makeApi(); + await api.listMeetings(); + expect(api._captured[0].url).toBe( + 'https://api.fathom.ai/external/v1/meetings' + ); + }); + + it('serializes scalar filters as query params', async () => { + const api = makeApi(); + await api.listMeetings({ + cursor: 'c1', + include_summary: true, + created_after: '2026-01-01T00:00:00Z', + }); + const { url } = api._captured[0]; + expect(url).toContain('cursor=c1'); + expect(url).toContain('include_summary=true'); + expect(url).toContain( + 'created_after=2026-01-01T00%3A00%3A00Z' + ); + }); + + it('serializes array filters with the key[] convention', async () => { + const api = makeApi(); + await api.listMeetings({ + recorded_by: ['a@x.com', 'b@x.com'], + teams: ['Sales'], + }); + const { url } = api._captured[0]; + expect(url).toContain('recorded_by%5B%5D=a%40x.com'); + expect(url).toContain('recorded_by%5B%5D=b%40x.com'); + expect(url).toContain('teams%5B%5D=Sales'); + }); + + it('omits null/undefined params', async () => { + const api = makeApi(); + await api.listMeetings({ cursor: undefined, meeting_type: null }); + expect(api._captured[0].url).toBe( + 'https://api.fathom.ai/external/v1/meetings' + ); + }); + }); + + describe('listAllMeetings', () => { + it('follows next_cursor and flattens items', async () => { + const api = new Api({ api_key: 'k' }); + const pages = [ + { items: [{ recording_id: 1 }], next_cursor: 'p2' }, + { items: [{ recording_id: 2 }], next_cursor: null }, + ]; + let call = 0; + api._get = async () => pages[call++]; + const all = await api.listAllMeetings(); + expect(all.map((m) => m.recording_id)).toEqual([1, 2]); + }); + }); + + describe('recording content', () => { + it('getTranscript hits the transcript path', async () => { + const api = makeApi(); + await api.getTranscript(12345); + expect(api._captured[0].url).toBe( + 'https://api.fathom.ai/external/v1/recordings/12345/transcript' + ); + }); + + it('getTranscript forwards destination_url', async () => { + const api = makeApi(); + await api.getTranscript(1, { + destination_url: 'https://hook.example.com/t', + }); + expect(api._captured[0].url).toContain( + 'destination_url=https%3A%2F%2Fhook.example.com%2Ft' + ); + }); + + it('getSummary hits the summary path', async () => { + const api = makeApi(); + await api.getSummary(999); + expect(api._captured[0].url).toBe( + 'https://api.fathom.ai/external/v1/recordings/999/summary' + ); + }); + }); + + describe('listTeamMembers', () => { + it('hits GET /team_members', async () => { + const api = makeApi(); + await api.listTeamMembers(); + expect(api._captured[0].url).toBe( + 'https://api.fathom.ai/external/v1/team_members' + ); + }); + }); + + describe('createWebhook', () => { + it('POSTs to /webhooks with the body', async () => { + const api = makeApi(); + const body = { + destination_url: 'https://hook.example.com', + triggered_for: ['my_recordings'], + include_summary: true, + }; + await api.createWebhook(body); + const captured = api._captured[0]; + expect(captured.method).toBe('POST'); + expect(captured.url).toBe( + 'https://api.fathom.ai/external/v1/webhooks' + ); + expect(captured.body).toEqual(body); + }); + }); +}); diff --git a/packages/v1-ready/fathom/tests/definition.test.js b/packages/v1-ready/fathom/tests/definition.test.js new file mode 100644 index 0000000..e6b3b98 --- /dev/null +++ b/packages/v1-ready/fathom/tests/definition.test.js @@ -0,0 +1,98 @@ +const { Definition } = require('../definition'); +const { Api } = require('../api'); + +// Offline: exercises the Definition auth methods with a fake api whose network +// calls are stubbed. No real HTTP. +describe('Fathom Definition', () => { + it('is an api-key module named fathom wired to the Api class', () => { + expect(Definition.moduleName).toBe('fathom'); + expect(Definition.getName()).toBe('fathom'); + expect(Definition.API).toBe(Api); + expect(Definition.modelName).toBe('Fathom'); + }); + + it('exposes an apiKey authorization form for the api_key field', () => { + const reqs = Definition.requiredAuthMethods.getAuthorizationRequirements(); + expect(reqs.type).toBe('apiKey'); + expect(reqs.data.jsonSchema.required).toContain('api_key'); + expect(reqs.data.jsonSchema.properties.api_key.type).toBe('string'); + expect(reqs.data.uiSchema.api_key['ui:widget']).toBe('password'); + }); + + it('persists only the api_key credential', () => { + expect(Definition.requiredAuthMethods.apiPropertiesToPersist).toEqual({ + credential: ['api_key'], + entity: [], + }); + }); + + it('setAuthParams sets the api key from form params', async () => { + const api = new Api({}); + await Definition.requiredAuthMethods.setAuthParams(api, { + api_key: 'form-key', + }); + expect(api.api_key).toBe('form-key'); + }); + + it('getEntityDetails derives externalId from recorded_by email', async () => { + const api = new Api({ api_key: 'k' }); + api.listMeetings = async () => ({ + items: [ + { + recording_id: 1, + recorded_by: { + name: 'Sean', + email: 'sean@lefthook.co', + team: 'Left Hook', + }, + }, + ], + }); + const details = await Definition.requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-1' + ); + expect(details.identifiers.externalId).toBe('sean@lefthook.co'); + expect(details.identifiers.userId).toBe('user-1'); + expect(details.details.name).toBe('Left Hook'); + }); + + it('getEntityDetails falls back to a stable identity with no meetings', async () => { + const api = new Api({ api_key: 'k' }); + api.listMeetings = async () => ({ items: [] }); + const details = await Definition.requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-2' + ); + expect(details.identifiers.externalId).toBe('fathom-account'); + }); + + it('getEntityDetails stays stable if the API throws', async () => { + const api = new Api({ api_key: 'k' }); + api.listMeetings = async () => { + throw new Error('boom'); + }; + const details = await Definition.requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-3' + ); + expect(details.identifiers.externalId).toBe('fathom-account'); + }); + + it('testAuthRequest performs an authenticated list call', async () => { + const api = new Api({ api_key: 'k' }); + let called = false; + api.listMeetings = async () => { + called = true; + return { items: [] }; + }; + await Definition.requiredAuthMethods.testAuthRequest(api); + expect(called).toBe(true); + }); +}); diff --git a/packages/v1-ready/fathom/tests/spec-sync.test.js b/packages/v1-ready/fathom/tests/spec-sync.test.js new file mode 100644 index 0000000..fc069dc --- /dev/null +++ b/packages/v1-ready/fathom/tests/spec-sync.test.js @@ -0,0 +1,42 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { Api } = require('../api'); + +const spec = yaml.load( + fs.readFileSync(path.join(__dirname, '..', 'fathom.openapi.yaml'), 'utf8') +); + +const specOperationIds = Object.values(spec.paths).flatMap((item) => + Object.entries(item) + .filter(([m]) => ['get', 'post', 'patch', 'put', 'delete'].includes(m)) + .map(([, op]) => op.operationId) +); + +const clientMethods = Object.getOwnPropertyNames(Api.prototype).filter( + (m) => typeof Api.prototype[m] === 'function' && m !== 'constructor' +); + +describe('OpenAPI spec ↔ client sync', () => { + it('every operationId has a matching client method', () => { + const missing = specOperationIds.filter( + (op) => !clientMethods.includes(op) + ); + expect(missing).toEqual([]); + }); + + it('the base server URL matches the client baseUrl', () => { + const api = new Api({ api_key: 'x' }); + expect(spec.servers[0].url).toBe(api.baseUrl); + }); + + it('declares X-Api-Key apiKey security matching the client', () => { + const scheme = spec.components.securitySchemes.ApiKeyAuth; + const api = new Api({ api_key: 'x' }); + expect(scheme.type).toBe('apiKey'); + expect(scheme.in).toBe('header'); + expect(scheme.name).toBe('X-Api-Key'); + // The security scheme header must be exactly what the client sends. + expect(scheme.name).toBe(api.api_key_name); + }); +}); diff --git a/packages/v1-ready/fireflies/.env.example b/packages/v1-ready/fireflies/.env.example new file mode 100644 index 0000000..0b2bceb --- /dev/null +++ b/packages/v1-ready/fireflies/.env.example @@ -0,0 +1,2 @@ +# Fireflies.ai API key — from fireflies.ai > Integrations > Fireflies API +FIREFLIES_API_KEY=your_fireflies_api_key_here diff --git a/packages/v1-ready/fireflies/LICENSE.md b/packages/v1-ready/fireflies/LICENSE.md new file mode 100644 index 0000000..43423f7 --- /dev/null +++ b/packages/v1-ready/fireflies/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Left Hook / Frigg Framework contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/v1-ready/fireflies/README.md b/packages/v1-ready/fireflies/README.md new file mode 100644 index 0000000..5e41466 --- /dev/null +++ b/packages/v1-ready/fireflies/README.md @@ -0,0 +1,91 @@ +# @friggframework/api-module-fireflies + +Frigg API module for [Fireflies.ai](https://fireflies.ai) — the AI meeting notetaker. +Provides access to meeting transcripts, summaries, and attendee data for use in +Frigg integrations (e.g. matching a call's attendees to a CRM contact by email). + +## API shape + +Fireflies exposes a **single GraphQL endpoint**: + +``` +POST https://api.fireflies.ai/graphql +Authorization: Bearer +Content-Type: application/json + +{ "query": "...", "variables": { ... } } +``` + +Docs: + +## Authentication + +This is an **API-key** module built on `ApiKeyRequester`. The key is a bearer +token. The module sets `api_key_name = 'Authorization'` and overrides +`addAuthHeaders()` to emit `Authorization: Bearer ` — so the stored +credential is the bare key (no `"Bearer "` prefix persisted to the database). + +Get your key from **fireflies.ai → Integrations → Fireflies API**. + +```env +FIREFLIES_API_KEY=your_fireflies_api_key_here +``` + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-fireflies'); + +const api = new Api({ api_key: process.env.FIREFLIES_API_KEY }); + +// Confirm the key / identify the account +const user = await api.getUser(); + +// List recent transcripts (all args optional) +const transcripts = await api.listTranscripts({ + limit: 25, + skip: 0, + fromDate: '2026-01-01T00:00:00.000Z', +}); + +// Full transcript: attendees (with emails), summary, sentences +const transcript = await api.getTranscript(transcripts[0].id); + +// Just the AI summary +const summary = await api.getTranscriptSummary(transcript.id); + +// Keyword search +const hits = await api.searchTranscripts('pricing', { limit: 10 }); +``` + +## Methods + +| Method | GraphQL | Purpose | +|---|---|---| +| `getUser()` | `query { user { ... } }` | Authenticated account; used by the auth test | +| `listTranscripts(params)` | `transcripts(limit, skip, fromDate, toDate, organizer_email, participant_email, keyword, mine)` | Page through meetings, newest first | +| `getTranscript(id)` | `transcript(id)` | One meeting with `meeting_attendees` (email), `summary`, `sentences` | +| `getTranscriptSummary(id)` | `transcript(id) { summary }` | AI summary only | +| `searchTranscripts(keyword, params)` | `transcripts(keyword)` | Keyword search wrapper | +| `graphql(query, variables)` | — | Low-level transport; unwraps `data`, throws on `errors[]` | + +### Attendee email fields + +`meeting_attendees` carries `{ displayName, email, name, phoneNumber, location }` +per attendee; the top-level transcript also carries `organizer_email`, +`host_email`, and a `participants` (email) array. These are what an integration +uses to match a call to a CRM contact. + +## Testing + +```bash +npm install +npx jest +``` + +Tests are fully offline — a fake `fetch` is injected and the suite asserts the +GraphQL request body and the `Authorization: Bearer` header. + +## License + +MIT diff --git a/packages/v1-ready/fireflies/api.js b/packages/v1-ready/fireflies/api.js new file mode 100644 index 0000000..01b7ec2 --- /dev/null +++ b/packages/v1-ready/fireflies/api.js @@ -0,0 +1,276 @@ +const { ApiKeyRequester, ModuleConstants, get } = require('@friggframework/core'); + +/** + * Fireflies.ai API module. + * + * Fireflies exposes a single GraphQL endpoint at https://api.fireflies.ai/graphql. + * Every call is an HTTP POST whose JSON body is `{ query, variables }`. + * Auth is a bearer token: `Authorization: Bearer `. + * + * Auth design note: + * ApiKeyRequester.addAuthHeaders() sets `headers[this.api_key_name] = this.api_key`. + * Fireflies needs the value prefixed with the word "Bearer", so we override + * addAuthHeaders() to emit `Authorization: Bearer ` from the raw key. This + * keeps the stored credential clean (just the key, no "Bearer " prefix) while + * still producing the exact header Fireflies requires. (The alternative — + * api_key_name='Authorization' with a 'Bearer '-prefixed api_key — also works; + * the override is used so the persisted secret is the bare key.) + * + * Docs: + * https://docs.fireflies.ai/graphql-api/authorization + * https://docs.fireflies.ai/graphql-api/query/transcripts + * https://docs.fireflies.ai/graphql-api/query/transcript + */ +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + this.baseUrl = 'https://api.fireflies.ai/graphql'; + + // Accept the key under any of the common param names. + const apiKey = + get(params, 'api_key', null) || + get(params, 'access_token', null) || + get(params, 'apiKey', null); + this.api_key_name = 'Authorization'; + if (apiKey) { + this.setApiKey(apiKey); + } + } + + /** + * Emit `Authorization: Bearer `. The stored credential is the bare + * key; the "Bearer " prefix is added here so it never lives in the DB. + */ + async addAuthHeaders(headers) { + const h = headers || {}; + if (this.api_key) { + h[this.api_key_name] = `Bearer ${this.api_key}`; + } + return h; + } + + getAuthorizationRequirements() { + return { + url: null, + type: ModuleConstants.authType.apiKey, + data: { + jsonSchema: { + title: 'Fireflies.ai Authentication', + type: 'object', + required: ['api_key'], + properties: { + api_key: { + type: 'string', + title: 'API Key', + }, + }, + }, + uiSchema: { + api_key: { + 'ui:widget': 'password', + 'ui:help': + 'From fireflies.ai: Integrations > Fireflies API > copy your API key.', + 'ui:placeholder': 'Fireflies API key', + }, + }, + }, + }; + } + + /** + * Core GraphQL transport. Posts `{ query, variables }` to the single + * endpoint and unwraps `data`, throwing on a GraphQL `errors` array. + */ + async graphql(query, variables = {}) { + const options = { + url: this.baseUrl, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { query, variables }, + }; + const response = await this._post(options); + if (response && Array.isArray(response.errors) && response.errors.length) { + const message = response.errors + .map((e) => e.message) + .filter(Boolean) + .join('; '); + throw new Error(`Fireflies GraphQL error: ${message}`); + } + return response ? response.data : undefined; + } + + // ---- Auth / identity ------------------------------------------------ + + /** + * The authenticated user. Used by the module's auth-test / entity flow. + * `query { user { user_id name email } }` + */ + async getUser() { + const query = `query { + user { + user_id + name + email + is_admin + } + }`; + const data = await this.graphql(query); + return data ? data.user : undefined; + } + + // ---- Transcripts ---------------------------------------------------- + + /** + * List meeting transcripts, newest first. All args optional. + * `transcripts(limit, skip, fromDate, toDate, organizerEmail, + * participantEmail, keyword, mine)` + */ + async listTranscripts(params = {}) { + const query = `query ListTranscripts( + $limit: Int + $skip: Int + $fromDate: DateTime + $toDate: DateTime + $organizerEmail: String + $participantEmail: String + $keyword: String + $mine: Boolean + ) { + transcripts( + limit: $limit + skip: $skip + fromDate: $fromDate + toDate: $toDate + organizer_email: $organizerEmail + participant_email: $participantEmail + keyword: $keyword + mine: $mine + ) { + id + title + date + dateString + duration + host_email + organizer_email + participants + meeting_link + transcript_url + meeting_attendees { + displayName + email + name + phoneNumber + location + } + } + }`; + + const variables = {}; + if (params.limit !== undefined) variables.limit = params.limit; + if (params.skip !== undefined) variables.skip = params.skip; + if (params.fromDate !== undefined) variables.fromDate = params.fromDate; + if (params.toDate !== undefined) variables.toDate = params.toDate; + if (params.organizerEmail !== undefined) + variables.organizerEmail = params.organizerEmail; + if (params.participantEmail !== undefined) + variables.participantEmail = params.participantEmail; + if (params.keyword !== undefined) variables.keyword = params.keyword; + if (params.mine !== undefined) variables.mine = params.mine; + + const data = await this.graphql(query, variables); + return data ? data.transcripts : []; + } + + /** + * A single transcript with full detail: attendees (with emails), + * summary, and every sentence. + * `transcript(id: ID!)` + */ + async getTranscript(id) { + const query = `query GetTranscript($id: String!) { + transcript(id: $id) { + id + title + date + dateString + duration + host_email + organizer_email + participants + meeting_link + transcript_url + audio_url + video_url + meeting_attendees { + displayName + email + name + phoneNumber + location + } + speakers { + id + name + } + summary { + overview + short_summary + keywords + action_items + bullet_gist + gist + outline + shorthand_bullet + topics_discussed + } + sentences { + index + speaker_name + speaker_id + text + start_time + end_time + } + } + }`; + const data = await this.graphql(query, { id }); + return data ? data.transcript : undefined; + } + + /** + * Just the AI summary block for a transcript. + */ + async getTranscriptSummary(id) { + const query = `query GetTranscriptSummary($id: String!) { + transcript(id: $id) { + id + title + summary { + overview + short_summary + keywords + action_items + bullet_gist + gist + outline + shorthand_bullet + topics_discussed + } + } + }`; + const data = await this.graphql(query, { id }); + return data ? data.transcript : undefined; + } + + /** + * Keyword search across transcripts. Thin wrapper over listTranscripts. + */ + async searchTranscripts(keyword, params = {}) { + return this.listTranscripts({ ...params, keyword }); + } +} + +module.exports = { Api }; diff --git a/packages/v1-ready/fireflies/defaultConfig.json b/packages/v1-ready/fireflies/defaultConfig.json new file mode 100644 index 0000000..00fa2ae --- /dev/null +++ b/packages/v1-ready/fireflies/defaultConfig.json @@ -0,0 +1,10 @@ +{ + "name": "fireflies", + "config": { + "apiKey": true, + "batch": { + "concurrency": 3, + "delay": 1000 + } + } +} diff --git a/packages/v1-ready/fireflies/definition.js b/packages/v1-ready/fireflies/definition.js new file mode 100644 index 0000000..f18bff1 --- /dev/null +++ b/packages/v1-ready/fireflies/definition.js @@ -0,0 +1,76 @@ +require('dotenv').config(); +const { Api } = require('./api'); +const { get } = require('@friggframework/core'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Fireflies', + requiredAuthMethods: { + // API-key module: renders the interactive CLI / hosted auth form. + getAuthorizationRequirements: (api) => + api.getAuthorizationRequirements(), + + // API-key exchange is a no-op — the key IS the credential. Persist it. + getToken: async (api, params) => { + const apiKey = + get(params, 'api_key', null) || + get(params, 'access_token', null) || + get(params.data || {}, 'api_key', null); + if (apiKey) { + api.setApiKey(apiKey); + } + return { access_token: api.api_key, api_key: api.api_key }; + }, + + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const user = await api.getUser(); + if (!user || !user.user_id) { + throw new Error( + 'Fireflies user query failed to return valid user info. ' + + 'Response: ' + + JSON.stringify(user) + ); + } + return { + identifiers: { externalId: user.user_id, userId }, + details: { name: user.name || user.email }, + }; + }, + + getCredentialDetails: async (api, userId) => { + const user = await api.getUser(); + if (!user || !user.user_id) { + throw new Error( + 'Fireflies user query failed to return valid user info. ' + + 'Response: ' + + JSON.stringify(user) + ); + } + return { + identifiers: { externalId: user.user_id, userId }, + details: {}, + }; + }, + + testAuthRequest: async (api) => { + const user = await api.getUser(); + if (!user || !user.user_id) { + throw new Error('Fireflies token is not valid'); + } + return user; + }, + + apiPropertiesToPersist: { + credential: ['access_token', 'api_key'], + entity: [], + }, + }, + env: { + api_key: process.env.FIREFLIES_API_KEY, + }, +}; + +module.exports = { Definition }; diff --git a/packages/v1-ready/fireflies/fireflies.operations.json b/packages/v1-ready/fireflies/fireflies.operations.json new file mode 100644 index 0000000..491f6c4 --- /dev/null +++ b/packages/v1-ready/fireflies/fireflies.operations.json @@ -0,0 +1,183 @@ +{ + "info": { + "title": "Fireflies.ai GraphQL API", + "version": "1.0.0", + "description": "Canonical machine-readable manifest of the GraphQL operations the hand-written client in api.js sends. Fireflies exposes a SINGLE GraphQL endpoint, so there is no REST path surface to describe with OpenAPI; this manifest is the GraphQL-native equivalent of the reevo.openapi.yaml used elsewhere in this library. Every operation, its variables, and its selected fields were verified 1:1 against the public Fireflies GraphQL docs (see `docs` on each operation and `sources` below) and against what api.js actually transmits. It is the source of truth the client mirrors; tests/spec-sync.test.js asserts client and manifest agree.", + "contact": { "name": "Left Hook", "url": "https://lefthook.com" } + }, + "endpoint": "https://api.fireflies.ai/graphql", + "transport": { + "method": "POST", + "contentType": "application/json", + "accept": "application/json", + "bodyShape": "{ query, variables }", + "note": "GraphQL: one endpoint, one HTTP method (POST). The operation is selected by the query text in the request body, not by URL path or HTTP verb." + }, + "auth": { + "type": "bearer", + "header": "Authorization", + "valueFormat": "Bearer ", + "credential": "Fireflies API key (Settings > Developer Settings / Integrations > Fireflies API)", + "note": "The stored credential is the bare key; api.js's addAuthHeaders() adds the 'Bearer ' prefix at request time so it never lives in the DB.", + "docs": "https://docs.fireflies.ai/fundamentals/authorization" + }, + "sources": [ + "https://docs.fireflies.ai/fundamentals/authorization", + "https://docs.fireflies.ai/graphql-api/query/user", + "https://docs.fireflies.ai/graphql-api/query/transcripts", + "https://docs.fireflies.ai/graphql-api/query/transcript" + ], + "operations": [ + { + "operationId": "getUser", + "clientMethod": "getUser", + "type": "query", + "graphqlOperation": "user", + "summary": "Fetch the authenticated user (owner of the API key). Used by the module's auth-test / entity / credential flow.", + "docs": "https://docs.fireflies.ai/graphql-api/query/user", + "variables": [], + "argsSentToServer": {}, + "selectedFields": ["user_id", "name", "email", "is_admin"], + "query": "query { user { user_id name email is_admin } }", + "returns": "data.user", + "grounding": "Docs confirm the User type exposes user_id, name, email, is_admin (schema listing: user_id, recent_transcript, recent_meeting, num_transcripts, name, minutes_consumed, is_admin, integrations, email, user_groups). `user` with no id argument returns the API-key owner." + }, + { + "operationId": "listTranscripts", + "clientMethod": "listTranscripts", + "type": "query", + "graphqlOperation": "transcripts", + "summary": "List meeting transcripts, newest first. All arguments optional.", + "docs": "https://docs.fireflies.ai/graphql-api/query/transcripts", + "variables": [ + { "name": "limit", "type": "Int" }, + { "name": "skip", "type": "Int" }, + { "name": "fromDate", "type": "DateTime" }, + { "name": "toDate", "type": "DateTime" }, + { "name": "organizerEmail", "type": "String" }, + { "name": "participantEmail", "type": "String" }, + { "name": "keyword", "type": "String" }, + { "name": "mine", "type": "Boolean" } + ], + "argsSentToServer": { + "limit": "$limit", + "skip": "$skip", + "fromDate": "$fromDate", + "toDate": "$toDate", + "organizer_email": "$organizerEmail", + "participant_email": "$participantEmail", + "keyword": "$keyword", + "mine": "$mine" + }, + "selectedFields": [ + "id", + "title", + "date", + "dateString", + "duration", + "host_email", + "organizer_email", + "participants", + "meeting_link", + "transcript_url", + "meeting_attendees { displayName email name phoneNumber location }" + ], + "query": "query ListTranscripts( $limit: Int $skip: Int $fromDate: DateTime $toDate: DateTime $organizerEmail: String $participantEmail: String $keyword: String $mine: Boolean ) { transcripts( limit: $limit skip: $skip fromDate: $fromDate toDate: $toDate organizer_email: $organizerEmail participant_email: $participantEmail keyword: $keyword mine: $mine ) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url meeting_attendees { displayName email name phoneNumber location } } }", + "returns": "data.transcripts", + "grounding": "Docs confirm transcripts(...) accepts limit (Int), skip (Int), fromDate (DateTime), toDate (DateTime), organizer_email (String), participant_email (String), keyword (String), mine (Boolean). GraphQL variable aliases ($organizerEmail -> organizer_email, $participantEmail -> participant_email) are client-side names; the wire argument names are the snake_case ones the docs list. meeting_attendees fields (displayName, email, name, phoneNumber, location) confirmed on the Transcript type." + }, + { + "operationId": "getTranscript", + "clientMethod": "getTranscript", + "type": "query", + "graphqlOperation": "transcript", + "summary": "A single transcript in full detail: attendees (with emails), speakers, AI summary, and every sentence.", + "docs": "https://docs.fireflies.ai/graphql-api/query/transcript", + "variables": [{ "name": "id", "type": "String!" }], + "argsSentToServer": { "id": "$id" }, + "selectedFields": [ + "id", + "title", + "date", + "dateString", + "duration", + "host_email", + "organizer_email", + "participants", + "meeting_link", + "transcript_url", + "audio_url", + "video_url", + "meeting_attendees { displayName email name phoneNumber location }", + "speakers { id name }", + "summary { overview short_summary keywords action_items bullet_gist gist outline shorthand_bullet topics_discussed }", + "sentences { index speaker_name speaker_id text start_time end_time }" + ], + "query": "query GetTranscript($id: String!) { transcript(id: $id) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url audio_url video_url meeting_attendees { displayName email name phoneNumber location } speakers { id name } summary { overview short_summary keywords action_items bullet_gist gist outline shorthand_bullet topics_discussed } sentences { index speaker_name speaker_id text start_time end_time } } }", + "returns": "data.transcript", + "grounding": "Docs confirm the query signature is transcript(id: String!). All selected fields verified on the Transcript type: top-level (id, title, date, dateString, duration, host_email, organizer_email, participants, meeting_link, transcript_url, audio_url, video_url); speakers { id name }; meeting_attendees { displayName email name phoneNumber location }; summary { overview short_summary keywords action_items bullet_gist gist outline shorthand_bullet topics_discussed }; sentences { index speaker_name speaker_id text start_time end_time }." + }, + { + "operationId": "getTranscriptSummary", + "clientMethod": "getTranscriptSummary", + "type": "query", + "graphqlOperation": "transcript", + "summary": "Only the AI summary block for a transcript (subset of the getTranscript selection).", + "docs": "https://docs.fireflies.ai/graphql-api/query/transcript", + "variables": [{ "name": "id", "type": "String!" }], + "argsSentToServer": { "id": "$id" }, + "selectedFields": [ + "id", + "title", + "summary { overview short_summary keywords action_items bullet_gist gist outline shorthand_bullet topics_discussed }" + ], + "query": "query GetTranscriptSummary($id: String!) { transcript(id: $id) { id title summary { overview short_summary keywords action_items bullet_gist gist outline shorthand_bullet topics_discussed } } }", + "returns": "data.transcript", + "grounding": "Same transcript(id: String!) query as getTranscript; selects only id, title, and the summary block. All summary fields verified on the Transcript.summary type." + }, + { + "operationId": "searchTranscripts", + "clientMethod": "searchTranscripts", + "type": "query", + "graphqlOperation": "transcripts", + "summary": "Keyword search across transcripts. Convenience wrapper that presets the `keyword` argument and delegates to the transcripts query (identical wire query to listTranscripts).", + "docs": "https://docs.fireflies.ai/graphql-api/query/transcripts", + "variables": [ + { "name": "limit", "type": "Int" }, + { "name": "skip", "type": "Int" }, + { "name": "fromDate", "type": "DateTime" }, + { "name": "toDate", "type": "DateTime" }, + { "name": "organizerEmail", "type": "String" }, + { "name": "participantEmail", "type": "String" }, + { "name": "keyword", "type": "String" }, + { "name": "mine", "type": "Boolean" } + ], + "argsSentToServer": { + "limit": "$limit", + "skip": "$skip", + "fromDate": "$fromDate", + "toDate": "$toDate", + "organizer_email": "$organizerEmail", + "participant_email": "$participantEmail", + "keyword": "$keyword", + "mine": "$mine" + }, + "selectedFields": [ + "id", + "title", + "date", + "dateString", + "duration", + "host_email", + "organizer_email", + "participants", + "meeting_link", + "transcript_url", + "meeting_attendees { displayName email name phoneNumber location }" + ], + "query": "query ListTranscripts( $limit: Int $skip: Int $fromDate: DateTime $toDate: DateTime $organizerEmail: String $participantEmail: String $keyword: String $mine: Boolean ) { transcripts( limit: $limit skip: $skip fromDate: $fromDate toDate: $toDate organizer_email: $organizerEmail participant_email: $participantEmail keyword: $keyword mine: $mine ) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url meeting_attendees { displayName email name phoneNumber location } } }", + "returns": "data.transcripts", + "grounding": "Delegates to listTranscripts with keyword preset, so it emits the exact same `transcripts` query. `keyword` is the documented (non-deprecated) search argument." + } + ] +} diff --git a/packages/v1-ready/fireflies/index.js b/packages/v1-ready/fireflies/index.js new file mode 100644 index 0000000..3c94a63 --- /dev/null +++ b/packages/v1-ready/fireflies/index.js @@ -0,0 +1,7 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); + +module.exports = { + Api, + Definition, +}; diff --git a/packages/v1-ready/fireflies/package.json b/packages/v1-ready/fireflies/package.json new file mode 100644 index 0000000..50b4d89 --- /dev/null +++ b/packages/v1-ready/fireflies/package.json @@ -0,0 +1,23 @@ +{ + "name": "@friggframework/api-module-fireflies", + "version": "1.0.0", + "description": "Fireflies.ai API module that lets the Frigg Framework interact with Fireflies.ai (GraphQL)", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "jest": "^28.1.3" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/v1-ready/fireflies/tests/api.test.js b/packages/v1-ready/fireflies/tests/api.test.js new file mode 100644 index 0000000..d773a4e --- /dev/null +++ b/packages/v1-ready/fireflies/tests/api.test.js @@ -0,0 +1,163 @@ +const { Api } = require('../api'); + +/** + * Offline tests. We inject a fake `fetch` (supported by the Requester base via + * the `fetch` constructor param) and assert on the exact request the module + * builds: the GraphQL endpoint, the `Authorization: Bearer ` header, and + * the JSON `{ query, variables }` body. + */ +function makeApi(captured, responseData) { + const fakeFetch = async (url, options) => { + captured.url = url; + captured.options = options; + return { + status: 200, + headers: { + get: (name) => + name.toLowerCase() === 'content-type' + ? 'application/json' + : null, + }, + json: async () => ({ data: responseData }), + text: async () => JSON.stringify({ data: responseData }), + }; + }; + return new Api({ api_key: 'ff_test_key_123', fetch: fakeFetch }); +} + +describe('Fireflies Api', () => { + describe('auth header', () => { + it('sends Authorization: Bearer to the GraphQL endpoint', async () => { + const captured = {}; + const api = makeApi(captured, { user: { user_id: 'u1' } }); + + await api.getUser(); + + expect(captured.url).toBe('https://api.fireflies.ai/graphql'); + expect(captured.options.method).toBe('POST'); + expect(captured.options.headers.Authorization).toBe( + 'Bearer ff_test_key_123' + ); + expect(captured.options.headers['Content-Type']).toBe( + 'application/json' + ); + }); + + it('does not persist the "Bearer " prefix on the stored key', () => { + const api = makeApi({}, {}); + expect(api.api_key).toBe('ff_test_key_123'); + }); + }); + + describe('listTranscripts()', () => { + it('POSTs a GraphQL body with the transcripts query and variables', async () => { + const captured = {}; + const api = makeApi(captured, { + transcripts: [{ id: 't1', title: 'Call' }], + }); + + const result = await api.listTranscripts({ + limit: 5, + skip: 0, + fromDate: '2026-01-01T00:00:00.000Z', + }); + + const body = JSON.parse(captured.options.body); + expect(body.query).toContain('transcripts('); + expect(body.query).toContain('meeting_attendees'); + expect(body.query).toContain('email'); + expect(body.variables).toEqual({ + limit: 5, + skip: 0, + fromDate: '2026-01-01T00:00:00.000Z', + }); + expect(result).toEqual([{ id: 't1', title: 'Call' }]); + }); + + it('omits variables that were not supplied', async () => { + const captured = {}; + const api = makeApi(captured, { transcripts: [] }); + + await api.listTranscripts({ limit: 10 }); + + const body = JSON.parse(captured.options.body); + expect(body.variables).toEqual({ limit: 10 }); + }); + }); + + describe('getTranscript()', () => { + it('POSTs the single-transcript query with an id variable and requests summary + sentences', async () => { + const captured = {}; + const api = makeApi(captured, { + transcript: { id: 't42', title: 'Deep Dive' }, + }); + + const result = await api.getTranscript('t42'); + + const body = JSON.parse(captured.options.body); + expect(body.query).toContain('transcript(id: $id)'); + expect(body.query).toContain('summary {'); + expect(body.query).toContain('sentences {'); + expect(body.query).toContain('meeting_attendees {'); + expect(body.variables).toEqual({ id: 't42' }); + expect(result).toEqual({ id: 't42', title: 'Deep Dive' }); + }); + }); + + describe('getTranscriptSummary()', () => { + it('requests only the summary block', async () => { + const captured = {}; + const api = makeApi(captured, { + transcript: { id: 't7', summary: { overview: 'x' } }, + }); + + await api.getTranscriptSummary('t7'); + + const body = JSON.parse(captured.options.body); + expect(body.query).toContain('summary {'); + expect(body.query).not.toContain('sentences {'); + expect(body.variables).toEqual({ id: 't7' }); + }); + }); + + describe('searchTranscripts()', () => { + it('passes the keyword through as a transcripts variable', async () => { + const captured = {}; + const api = makeApi(captured, { transcripts: [] }); + + await api.searchTranscripts('pricing', { limit: 3 }); + + const body = JSON.parse(captured.options.body); + expect(body.variables).toEqual({ limit: 3, keyword: 'pricing' }); + }); + }); + + describe('graphql() error handling', () => { + it('throws when the response carries a GraphQL errors array', async () => { + const fakeFetch = async () => ({ + status: 200, + headers: { + get: () => 'application/json', + }, + json: async () => ({ + errors: [{ message: 'Not authorized' }], + }), + text: async () => + JSON.stringify({ errors: [{ message: 'Not authorized' }] }), + }); + const api = new Api({ api_key: 'k', fetch: fakeFetch }); + + await expect(api.getUser()).rejects.toThrow(/Not authorized/); + }); + }); + + describe('getAuthorizationRequirements()', () => { + it('returns an apiKey JSON Schema form with a password api_key field', () => { + const api = new Api({ api_key: 'k' }); + const reqs = api.getAuthorizationRequirements(); + expect(reqs.type).toBe('apiKey'); + expect(reqs.data.jsonSchema.required).toContain('api_key'); + expect(reqs.data.uiSchema.api_key['ui:widget']).toBe('password'); + }); + }); +}); diff --git a/packages/v1-ready/fireflies/tests/definition.test.js b/packages/v1-ready/fireflies/tests/definition.test.js new file mode 100644 index 0000000..ba5b8e5 --- /dev/null +++ b/packages/v1-ready/fireflies/tests/definition.test.js @@ -0,0 +1,104 @@ +const { Definition } = require('../definition'); + +const { requiredAuthMethods } = Definition; + +const validUser = { + user_id: 'ff-user-9', + name: 'Test User', + email: 'test@example.com', +}; + +function makeStubApi(user, keyRef = {}) { + return { + api_key: keyRef.api_key, + getUser: async () => user, + setApiKey(k) { + this.api_key = k; + keyRef.api_key = k; + }, + }; +} + +describe('Fireflies Definition', () => { + it('is an api-key module named "fireflies"', () => { + expect(Definition.moduleName).toBe('fireflies'); + expect(Definition.getName()).toBe('fireflies'); + }); + + describe('getToken()', () => { + it('stores the supplied api_key and returns it as the credential', async () => { + const api = makeStubApi(validUser); + const token = await requiredAuthMethods.getToken(api, { + api_key: 'sk_from_form', + }); + expect(api.api_key).toBe('sk_from_form'); + expect(token).toEqual({ + access_token: 'sk_from_form', + api_key: 'sk_from_form', + }); + }); + }); + + describe('testAuthRequest()', () => { + it('resolves with the user payload for a valid key', async () => { + const api = makeStubApi(validUser); + await expect( + requiredAuthMethods.testAuthRequest(api) + ).resolves.toEqual(validUser); + }); + + it('rejects when the user query returns nothing', async () => { + const api = makeStubApi(undefined); + await expect( + requiredAuthMethods.testAuthRequest(api) + ).rejects.toThrow(/not valid/i); + }); + }); + + describe('getEntityDetails()', () => { + it('returns identifiers keyed on the Fireflies user_id', async () => { + const api = makeStubApi(validUser); + const result = await requiredAuthMethods.getEntityDetails( + api, + null, + null, + 'frigg-user-1' + ); + expect(result.identifiers).toEqual({ + externalId: 'ff-user-9', + userId: 'frigg-user-1', + }); + expect(result.details.name).toBe('Test User'); + }); + + it('rejects when the user payload lacks user_id', async () => { + const api = makeStubApi({ email: 'x@y.com' }); + await expect( + requiredAuthMethods.getEntityDetails(api, null, null, 'u') + ).rejects.toThrow(/valid user info/i); + }); + }); + + describe('getCredentialDetails()', () => { + it('returns identifiers with empty details', async () => { + const api = makeStubApi(validUser); + const result = await requiredAuthMethods.getCredentialDetails( + api, + 'frigg-user-1' + ); + expect(result.identifiers).toEqual({ + externalId: 'ff-user-9', + userId: 'frigg-user-1', + }); + expect(result.details).toEqual({}); + }); + }); + + describe('apiPropertiesToPersist', () => { + it('persists the credential key', () => { + expect( + requiredAuthMethods.apiPropertiesToPersist.credential + ).toEqual(expect.arrayContaining(['api_key'])); + }); + }); +}); diff --git a/packages/v1-ready/fireflies/tests/spec-sync.test.js b/packages/v1-ready/fireflies/tests/spec-sync.test.js new file mode 100644 index 0000000..f282a6d --- /dev/null +++ b/packages/v1-ready/fireflies/tests/spec-sync.test.js @@ -0,0 +1,136 @@ +const fs = require('fs'); +const path = require('path'); +const { Api } = require('../api'); + +/** + * GraphQL manifest <-> client sync. + * + * Fireflies is a single-endpoint GraphQL API, so there is no REST path surface + * for an OpenAPI document to describe. This is the GraphQL-native equivalent of + * the reevo.openapi.yaml + spec-sync.test.js pair used elsewhere in the library: + * fireflies.operations.json is the canonical manifest of the operations the + * client sends, and this test asserts the hand-written client in api.js and the + * manifest cannot drift apart. + * + * It is stronger than a name-only check: it drives every client method through a + * fake `fetch`, captures the exact request, and asserts the endpoint, the + * `Authorization: Bearer ` header, and the whitespace-normalized GraphQL + * query string all match what the manifest declares. + */ +const manifest = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'fireflies.operations.json'), 'utf8') +); + +// GraphQL ignores insignificant whitespace; compare on a normalized form so the +// manifest is coupled to the operation/fields/variables, not to indentation. +const normalize = (s) => String(s).replace(/\s+/g, ' ').trim(); + +// Api.prototype members that are transport/config, not GraphQL operations. +const NON_OPERATION_METHODS = new Set([ + 'constructor', + 'graphql', // the shared transport + 'addAuthHeaders', // auth plumbing + 'getAuthorizationRequirements', // static form descriptor, no network call +]); + +// How to invoke each client method so it emits its request. Keyed by clientMethod. +const INVOKE = { + getUser: [], + listTranscripts: [{ limit: 1 }], + getTranscript: ['transcript-id-1'], + getTranscriptSummary: ['transcript-id-1'], + searchTranscripts: ['pricing'], +}; + +function makeApi(captured) { + const fakeFetch = async (url, options) => { + captured.url = url; + captured.options = options; + return { + status: 200, + headers: { get: () => 'application/json' }, + json: async () => ({ data: {} }), + text: async () => JSON.stringify({ data: {} }), + }; + }; + return new Api({ api_key: 'spec_sync_key', fetch: fakeFetch }); +} + +const clientMethods = Object.getOwnPropertyNames(Api.prototype).filter( + (m) => typeof Api.prototype[m] === 'function' && m !== 'constructor' +); +const graphqlIssuingMethods = clientMethods.filter( + (m) => !NON_OPERATION_METHODS.has(m) +); +const manifestMethods = manifest.operations.map((op) => op.clientMethod); + +describe('GraphQL manifest <-> client sync', () => { + it('every manifest operation has a matching client method', () => { + const missing = manifestMethods.filter( + (m) => !clientMethods.includes(m) + ); + expect(missing).toEqual([]); + }); + + it('every GraphQL-issuing client method is declared in the manifest (1:1, no orphans)', () => { + const undeclared = graphqlIssuingMethods.filter( + (m) => !manifestMethods.includes(m) + ); + expect(undeclared).toEqual([]); + }); + + it('operationId equals clientMethod for every operation', () => { + for (const op of manifest.operations) { + expect(op.operationId).toBe(op.clientMethod); + } + }); + + it('the manifest endpoint matches the client baseUrl', () => { + const api = new Api({ api_key: 'x' }); + expect(manifest.endpoint).toBe(api.baseUrl); + }); + + it('declares Bearer auth via the Authorization header', () => { + expect(manifest.auth.type).toBe('bearer'); + expect(manifest.auth.header).toBe('Authorization'); + expect(manifest.auth.valueFormat).toBe('Bearer '); + }); + + it('every operation, when invoked, POSTs the exact query the manifest declares', async () => { + for (const op of manifest.operations) { + const captured = {}; + const api = makeApi(captured); + const args = INVOKE[op.clientMethod]; + expect(args).toBeDefined(); // guard: a new method needs an INVOKE entry + await api[op.clientMethod](...args); + + // endpoint + transport + expect(captured.url).toBe(manifest.endpoint); + expect(captured.options.method).toBe(manifest.transport.method); + expect(captured.options.headers['Content-Type']).toBe( + manifest.transport.contentType + ); + + // auth: Authorization: Bearer + expect(captured.options.headers[manifest.auth.header]).toBe( + 'Bearer spec_sync_key' + ); + + // the actual GraphQL query matches the manifest 1:1 (normalized) + const body = JSON.parse(captured.options.body); + expect(normalize(body.query)).toBe(normalize(op.query)); + } + }); + + it('each operation selects the fields the manifest lists', async () => { + for (const op of manifest.operations) { + const captured = {}; + const api = makeApi(captured); + await api[op.clientMethod](...INVOKE[op.clientMethod]); + const query = normalize(JSON.parse(captured.options.body).query); + for (const field of op.selectedFields) { + expect(query).toContain(normalize(field)); + } + } + }); +}); diff --git a/packages/v1-ready/gong/.env.example b/packages/v1-ready/gong/.env.example new file mode 100644 index 0000000..5fdf134 --- /dev/null +++ b/packages/v1-ready/gong/.env.example @@ -0,0 +1,10 @@ +# Gong Basic auth credentials. Combined as Base64(accessKey:accessKeySecret) +# and sent in the `Authorization: Basic ` header. +# Create them under Company Settings → API in Gong (technical administrators only). +GONG_ACCESS_KEY=your_gong_access_key_here +GONG_ACCESS_KEY_SECRET=your_gong_access_key_secret_here + +# Optional: your company-specific API base URL (e.g. https://us-55616.api.gong.io). +# Find it at https://app.gong.io/company/api-authentication. Defaults to +# https://api.gong.io/v2 when unset. +GONG_BASE_URL= diff --git a/packages/v1-ready/gong/LICENSE.md b/packages/v1-ready/gong/LICENSE.md new file mode 100644 index 0000000..77f5cc2 --- /dev/null +++ b/packages/v1-ready/gong/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2022 Left Hook Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/v1-ready/gong/README.md b/packages/v1-ready/gong/README.md new file mode 100644 index 0000000..b26edba --- /dev/null +++ b/packages/v1-ready/gong/README.md @@ -0,0 +1,107 @@ +# @friggframework/api-module-gong + +A Frigg API module for [Gong](https://www.gong.io) — the Revenue Intelligence +platform that records, transcribes, and analyzes customer-facing conversations. +This module lets a Frigg integration list and retrieve calls, pull detailed call +data (including attendee emails), fetch transcripts, and list users. + +## Features + +- HTTP Basic authentication (Access Key + Access Key Secret) +- List calls by date range and retrieve a single call +- Detailed call data via `POST /v2/calls/extensive`, exposing `parties[].emailAddress` + (the field used to match a conversation attendee to a CRM contact) +- Call transcripts via `POST /v2/calls/transcript` +- List/retrieve users +- Company-specific base URL support + +## Installation + +```bash +npm install @friggframework/api-module-gong +``` + +## Authentication + +Gong uses HTTP Basic auth. Credentials are combined as +`Base64(accessKey:accessKeySecret)` and sent in the +`Authorization: Basic ` header. Create an Access Key + Secret under +**Company Settings → API** in Gong (you must be a technical administrator). + +The module accepts the Gong-native names `access_key` and `access_key_secret` +and maps them onto the framework's Basic auth `username`/`password`. + +### Environment variables + +```env +GONG_ACCESS_KEY=your_gong_access_key +GONG_ACCESS_KEY_SECRET=your_gong_access_key_secret +# Optional; defaults to https://api.gong.io/v2 +GONG_BASE_URL=https://us-55616.api.gong.io +``` + +Your company-specific base URL is shown at +`https://app.gong.io/company/api-authentication`. + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-gong'); + +const api = new Api({ + access_key: process.env.GONG_ACCESS_KEY, + access_key_secret: process.env.GONG_ACCESS_KEY_SECRET, + base_url: process.env.GONG_BASE_URL, // optional +}); + +// List calls in a date range +const calls = await api.listCalls({ + fromDateTime: '2026-08-01T00:00:00Z', + toDateTime: '2026-08-19T00:00:00Z', +}); + +// Detailed call data with attendee emails +const detailed = await api.listCallsExtensive({ + filter: { callIds: ['7782342274025502988'] }, + contentSelector: { exposedFields: { parties: true } }, +}); +// → detailed.calls[0].parties[].emailAddress + +// Transcripts +const transcripts = await api.getTranscripts({ + filter: { callIds: ['7782342274025502988'] }, +}); +``` + +## API reference (endpoints) + +| Method | Path | Client method | +|--------|------|---------------| +| GET | `/v2/calls` | `listCalls(params)` | +| GET | `/v2/calls/{id}` | `getCall(callId)` | +| POST | `/v2/calls/extensive` | `listCallsExtensive(body)` | +| POST | `/v2/calls/transcript` | `getTranscripts(body)` | +| GET | `/v2/users` | `listUsers(params)` | +| GET | `/v2/users/{id}` | `getUser(userId)` | + +The machine-readable contract is in [`gong.openapi.yaml`](./gong.openapi.yaml), +kept in sync with `api.js` by `tests/spec-sync.test.js`. + +## Notes + +- **Rate limits:** 3 requests/second and 10,000 requests/day by default. On + `429`, respect the `Retry-After` header. +- **API access tier:** Gong's public API requires an API-enabled Gong package; + Access Keys are created by a technical administrator. If your workspace does + not have API access, contact Gong to enable it. This module is built to the + public documentation regardless. +- **Attendee emails:** only `POST /v2/calls/extensive` returns `parties[]` with + `emailAddress`. The plain `GET /v2/calls` list does not include party emails. + Transcripts reference speakers by `speakerId`, which you resolve to an email + via the `parties` returned from the extensive endpoint. + +Docs: https://help.gong.io/apidocs/introduction-2 + +## License + +MIT diff --git a/packages/v1-ready/gong/api.js b/packages/v1-ready/gong/api.js new file mode 100644 index 0000000..3bd7079 --- /dev/null +++ b/packages/v1-ready/gong/api.js @@ -0,0 +1,188 @@ +const { BasicAuthRequester, ModuleConstants, get } = require('@friggframework/core'); + +/** + * Gong public API client. + * + * Gong (gong.io) is a Revenue Intelligence platform that records, transcribes, + * and analyzes customer-facing conversations (calls, meetings, emails). Its + * public API is a REST surface authenticated with HTTP Basic auth: an Access + * Key and an Access Key Secret are combined as `Base64(accessKey:accessKeySecret)` + * and sent in the `Authorization: Basic ` header. + * + * - Create credentials: https://app.gong.io/company/api (technical admin only) + * - Base URL: company-specific, discoverable at + * https://app.gong.io/company/api-authentication (e.g. https://us-55616.api.gong.io). + * The generic host https://api.gong.io/v2 is the documented default and is + * used here unless a `base_url` param overrides it. + * - Rate limits: 3 calls/sec, 10,000 calls/day; 429 + Retry-After when exceeded. + * + * Docs: https://help.gong.io/apidocs/introduction-2 + * + * The canonical machine-readable contract lives in ./gong.openapi.yaml — this + * client mirrors it 1:1 (one method per operationId). Keep them in sync. + */ +class Api extends BasicAuthRequester { + constructor(params) { + super(params); + + // Gong Basic auth = Base64(accessKey:accessKeySecret). We accept the + // Gong-native names (access_key / access_key_secret) and map them onto + // BasicAuthRequester's username/password, which build the header. + this.access_key = get(params, 'access_key', null); + this.access_key_secret = get(params, 'access_key_secret', null); + if (this.access_key) this.username = this.access_key; + if (this.access_key_secret) this.password = this.access_key_secret; + + // Company-specific base URL is supported; default to the documented host. + this.baseUrl = get(params, 'base_url', null) || 'https://api.gong.io/v2'; + + this.URLs = { + calls: '/calls', + callById: (callId) => `/calls/${callId}`, + callsExtensive: '/calls/extensive', + callsTranscript: '/calls/transcript', + users: '/users', + userById: (userId) => `/users/${userId}`, + }; + } + + getAuthorizationRequirements() { + return { + url: null, + type: ModuleConstants.authType.basic, + data: { + jsonSchema: { + type: 'object', + required: ['access_key', 'access_key_secret'], + properties: { + access_key: { + type: 'string', + title: 'Access Key', + }, + access_key_secret: { + type: 'string', + title: 'Access Key Secret', + }, + }, + }, + uiSchema: { + access_key: { + 'ui:help': + 'Create an Access Key under Company Settings → API in Gong (technical administrators only).', + 'ui:placeholder': 'Your Gong Access Key', + }, + access_key_secret: { + 'ui:widget': 'password', + 'ui:help': + 'The Access Key Secret shown alongside your Access Key. Sent as Base64(accessKey:accessKeySecret) in the Basic Authorization header.', + 'ui:placeholder': 'Your Gong Access Key Secret', + }, + }, + }, + }; + } + + // ---- Calls -------------------------------------------------------------- + + /** + * List calls that took place in a date range. + * Query params: fromDateTime, toDateTime (ISO-8601), cursor, workspaceId. + * Returns a page of call metadata plus a `records.cursor` for pagination. + */ + async listCalls(params = {}) { + const query = {}; + if (params.fromDateTime) query.fromDateTime = params.fromDateTime; + if (params.toDateTime) query.toDateTime = params.toDateTime; + if (params.cursor) query.cursor = params.cursor; + if (params.workspaceId) query.workspaceId = params.workspaceId; + + const options = { + url: this.baseUrl + this.URLs.calls, + query, + }; + return this._get(options); + } + + /** Retrieve a single call's metadata by id. */ + async getCall(callId) { + const options = { + url: this.baseUrl + this.URLs.callById(callId), + }; + return this._get(options); + } + + /** + * Retrieve detailed call data by filter. This is the endpoint that carries + * attendee emails: request `contentSelector.exposedFields.parties: true` and + * each returned `calls[].parties[]` includes `emailAddress`, `name`, + * `affiliation` (Internal/External), `speakerId`, `userId`, and `phoneNumber`. + * + * body = { + * filter: { fromDateTime, toDateTime, callIds, primaryUserIds, workspaceId }, + * contentSelector: { exposedFields: { parties: true, ... }, context, contextTiming }, + * cursor + * } + */ + async listCallsExtensive(body) { + const options = { + url: this.baseUrl + this.URLs.callsExtensive, + headers: { 'Content-Type': 'application/json' }, + body, + }; + return this._post(options); + } + + /** + * Retrieve transcripts for calls. Requires `filter` (by callIds and/or a + * date range). Returns `callTranscripts[]`, each with a `transcript[]` of + * monologues keyed by `speakerId` (resolve speakers to emails via the + * `parties` from listCallsExtensive). + * + * body = { filter: { callIds, fromDateTime, toDateTime, workspaceId }, cursor } + */ + async getTranscripts(body) { + const options = { + url: this.baseUrl + this.URLs.callsTranscript, + headers: { 'Content-Type': 'application/json' }, + body, + }; + return this._post(options); + } + + // ---- Users -------------------------------------------------------------- + + /** List all users. Query params: cursor, includeAvatars. */ + async listUsers(params = {}) { + const query = {}; + if (params.cursor) query.cursor = params.cursor; + if (params.includeAvatars !== undefined) { + query.includeAvatars = params.includeAvatars; + } + const options = { + url: this.baseUrl + this.URLs.users, + query, + }; + return this._get(options); + } + + /** Retrieve a single user by id. */ + async getUser(userId) { + const options = { + url: this.baseUrl + this.URLs.userById(userId), + }; + return this._get(options); + } + + // ---- Auth check --------------------------------------------------------- + + /** + * Lightweight authenticated request used to validate credentials. Listing a + * single user page returns 200 for valid keys; invalid keys return 401, + * which the requester surfaces as an error. + */ + async testAuth() { + return this.listUsers({ includeAvatars: false }); + } +} + +module.exports = { Api }; diff --git a/packages/v1-ready/gong/defaultConfig.json b/packages/v1-ready/gong/defaultConfig.json new file mode 100644 index 0000000..029fbcf --- /dev/null +++ b/packages/v1-ready/gong/defaultConfig.json @@ -0,0 +1,10 @@ +{ + "name": "gong", + "label": "Gong", + "authType": "basic", + "productUrl": "https://www.gong.io", + "apiDocs": "https://help.gong.io/apidocs/introduction-2", + "logoUrl": "https://www.gong.io/favicon.ico", + "categories": ["Revenue Intelligence", "Sales", "Conversation Intelligence"], + "description": "Gong Revenue Intelligence API module — list/retrieve calls, detailed call data with attendee emails, transcripts, and users." +} diff --git a/packages/v1-ready/gong/definition.js b/packages/v1-ready/gong/definition.js new file mode 100644 index 0000000..c7c4602 --- /dev/null +++ b/packages/v1-ready/gong/definition.js @@ -0,0 +1,60 @@ +require('dotenv').config(); +const crypto = require('crypto'); +const { Api } = require('./api'); +const config = require('./defaultConfig.json'); + +// Gong issues a static Access Key + Secret (Basic auth, no OAuth), so there is +// no external account id returned at auth time. We derive a stable, +// non-reversible identifier from the access key so the same credentials always +// map to the same entity/credential. +const keyFingerprint = (accessKey) => + crypto.createHash('sha256').update(String(accessKey)).digest('hex'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Gong', + requiredAuthMethods: { + setAuthParams: async function (api, params) {}, + getEntityDetails: async function ( + api, + callbackParams, + tokenResponse, + userId + ) { + return { + identifiers: { + externalId: keyFingerprint(api.access_key || api.username), + userId, + }, + details: {}, + }; + }, + apiPropertiesToPersist: { + credential: ['access_key', 'access_key_secret'], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + return { + identifiers: { + externalId: keyFingerprint(api.access_key || api.username), + userId, + }, + details: {}, + }; + }, + testAuthRequest: async function (api) { + return api.testAuth(); + }, + }, + env: { + access_key: process.env.GONG_ACCESS_KEY, + access_key_secret: process.env.GONG_ACCESS_KEY_SECRET, + base_url: process.env.GONG_BASE_URL, + }, +}; + +module.exports = { Definition }; diff --git a/packages/v1-ready/gong/gong.openapi.yaml b/packages/v1-ready/gong/gong.openapi.yaml new file mode 100644 index 0000000..f92ee3e --- /dev/null +++ b/packages/v1-ready/gong/gong.openapi.yaml @@ -0,0 +1,304 @@ +openapi: 3.0.3 +info: + title: Gong Public API (subset) + version: "1.0.0" + description: >- + Gong (gong.io) is a Revenue Intelligence platform. This is the subset of + Gong's public REST API used by the @friggframework/api-module-gong module, + authored by Left Hook from Gong's published API reference + (https://help.gong.io/apidocs/introduction-2). It is the source of truth the + hand-written client in api.js mirrors (one method per operationId). + contact: + name: Left Hook + url: https://lefthook.com +servers: + - url: https://api.gong.io/v2 + description: >- + Gong public API default host. Companies also have a dedicated base URL + (e.g. https://us-55616.api.gong.io) shown at + https://app.gong.io/company/api-authentication. +security: + - BasicAuth: [] +tags: + - name: Calls + - name: Users +paths: + /calls: + get: + tags: [Calls] + operationId: listCalls + summary: List calls in a date range + parameters: + - in: query + name: fromDateTime + schema: { type: string, format: date-time } + description: ISO-8601 start of the date range. + - in: query + name: toDateTime + schema: { type: string, format: date-time } + description: ISO-8601 end of the date range. + - in: query + name: cursor + schema: { type: string } + description: Pagination cursor from a prior response's records.cursor. + - in: query + name: workspaceId + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/CallsList" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /calls/{id}: + get: + tags: [Calls] + operationId: getCall + summary: Retrieve a single call's metadata + parameters: + - in: path + name: id + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /calls/extensive: + post: + tags: [Calls] + operationId: listCallsExtensive + summary: Retrieve detailed call data by filter (includes attendee emails) + description: >- + Returns detailed call data. Set contentSelector.exposedFields.parties to + true to receive calls[].parties[], each carrying emailAddress, name, + affiliation (Internal/External), speakerId, userId, and phoneNumber. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CallsExtensiveRequest" } + responses: + "200": { $ref: "#/components/responses/CallsExtensiveResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /calls/transcript: + post: + tags: [Calls] + operationId: getTranscripts + summary: Retrieve transcripts of calls + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/TranscriptRequest" } + responses: + "200": { $ref: "#/components/responses/TranscriptResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + "429": { $ref: "#/components/responses/RateLimited" } + /users: + get: + tags: [Users] + operationId: listUsers + summary: List all users + parameters: + - in: query + name: cursor + schema: { type: string } + - in: query + name: includeAvatars + schema: { type: boolean } + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /users/{id}: + get: + tags: [Users] + operationId: getUser + summary: Retrieve a single user + parameters: + - in: path + name: id + required: true + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } +components: + securitySchemes: + BasicAuth: + type: http + scheme: basic + description: Base64(accessKey:accessKeySecret) in the Authorization header. + schemas: + CallsExtensiveRequest: + type: object + required: [filter] + properties: + cursor: + type: string + filter: + type: object + properties: + fromDateTime: { type: string, format: date-time } + toDateTime: { type: string, format: date-time } + callIds: + type: array + items: { type: string } + primaryUserIds: + type: array + items: { type: string } + workspaceId: { type: string } + contentSelector: + type: object + properties: + context: + type: string + enum: [None, Extended] + contextTiming: + type: array + items: { type: string, enum: [Now, TimeOfCall] } + exposedFields: + type: object + properties: + parties: { type: boolean } + content: + type: object + properties: + structure: { type: boolean } + topics: { type: boolean } + trackers: { type: boolean } + brief: { type: boolean } + outline: { type: boolean } + highlights: { type: boolean } + callOutcome: { type: boolean } + keyPoints: { type: boolean } + interaction: + type: object + properties: + speakers: { type: boolean } + video: { type: boolean } + personInteractionStats: { type: boolean } + questions: { type: boolean } + collaboration: + type: object + properties: + publicComments: { type: boolean } + media: { type: boolean } + Party: + type: object + properties: + id: { type: string } + emailAddress: + type: string + description: The attendee's email address (used to match a CRM contact). + name: { type: string } + speakerId: { type: string } + userId: { type: string } + phoneNumber: { type: string } + affiliation: + type: string + enum: [Internal, External, Unknown] + methods: + type: array + items: { type: string } + TranscriptRequest: + type: object + required: [filter] + properties: + cursor: + type: string + filter: + type: object + properties: + callIds: + type: array + items: { type: string } + fromDateTime: { type: string, format: date-time } + toDateTime: { type: string, format: date-time } + workspaceId: { type: string } + responses: + ObjectResponse: + description: A JSON object response. + content: + application/json: + schema: { type: object } + CallsList: + description: A paged list of call metadata. + content: + application/json: + schema: + type: object + properties: + requestId: { type: string } + records: + type: object + properties: + totalRecords: { type: integer } + currentPageSize: { type: integer } + currentPageNumber: { type: integer } + cursor: { type: string } + calls: + type: array + items: { type: object } + CallsExtensiveResponse: + description: Detailed call data, including parties with attendee emails. + content: + application/json: + schema: + type: object + properties: + requestId: { type: string } + records: + type: object + properties: + cursor: { type: string } + calls: + type: array + items: + type: object + properties: + metaData: { type: object } + parties: + type: array + items: { $ref: "#/components/schemas/Party" } + TranscriptResponse: + description: Transcripts for the requested calls. + content: + application/json: + schema: + type: object + properties: + requestId: { type: string } + records: + type: object + properties: + cursor: { type: string } + callTranscripts: + type: array + items: + type: object + properties: + callId: { type: string } + transcript: + type: array + items: + type: object + properties: + speakerId: { type: string } + topic: { type: string } + sentences: + type: array + items: + type: object + properties: + start: { type: integer } + end: { type: integer } + text: { type: string } + Unauthorized: + description: Missing or invalid credentials. + content: + application/json: + schema: { type: object } + RateLimited: + description: Rate limit exceeded; see the Retry-After header. + content: + application/json: + schema: { type: object } diff --git a/packages/v1-ready/gong/index.js b/packages/v1-ready/gong/index.js new file mode 100644 index 0000000..3c94a63 --- /dev/null +++ b/packages/v1-ready/gong/index.js @@ -0,0 +1,7 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); + +module.exports = { + Api, + Definition, +}; diff --git a/packages/v1-ready/gong/package.json b/packages/v1-ready/gong/package.json new file mode 100644 index 0000000..8717b8c --- /dev/null +++ b/packages/v1-ready/gong/package.json @@ -0,0 +1,28 @@ +{ + "name": "@friggframework/api-module-gong", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Gong API module that lets the Frigg Framework interact with Gong (Revenue Intelligence / conversation intelligence)", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "js-yaml": "^4.1.0", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/v1-ready/gong/tests/api.test.js b/packages/v1-ready/gong/tests/api.test.js new file mode 100644 index 0000000..9d453a7 --- /dev/null +++ b/packages/v1-ready/gong/tests/api.test.js @@ -0,0 +1,145 @@ +const { Api } = require('../api'); + +function makeApi(params = { access_key: 'ak-123', access_key_secret: 'sk-123' }) { + const api = new Api(params); + // Capture requests instead of hitting the network. + api.sent = []; + const record = (method) => async (options) => { + api.sent.push({ method, ...options }); + return { ok: true }; + }; + api._get = record('GET'); + api._post = record('POST'); + api._patch = record('PATCH'); + api._delete = record('DELETE'); + return api; +} + +describe('Gong Api', () => { + describe('auth', () => { + it('maps access_key/access_key_secret onto Basic auth username/password', () => { + const api = new Api({ + access_key: 'ak', + access_key_secret: 'sk', + }); + expect(api.access_key).toBe('ak'); + expect(api.access_key_secret).toBe('sk'); + expect(api.username).toBe('ak'); + expect(api.password).toBe('sk'); + }); + + it('builds the Base64(accessKey:accessKeySecret) Basic header', async () => { + const api = new Api({ + access_key: 'ak', + access_key_secret: 'sk', + }); + const headers = await api.addAuthHeaders({}); + const expected = + 'Basic ' + Buffer.from('ak:sk').toString('base64'); + expect(headers['Authorization']).toBe(expected); + }); + + it('defaults to the documented Gong base URL', () => { + const api = new Api({ access_key: 'ak', access_key_secret: 'sk' }); + expect(api.baseUrl).toBe('https://api.gong.io/v2'); + }); + + it('honors a company-specific base_url override', () => { + const api = new Api({ + access_key: 'ak', + access_key_secret: 'sk', + base_url: 'https://us-55616.api.gong.io', + }); + expect(api.baseUrl).toBe('https://us-55616.api.gong.io'); + }); + }); + + describe('calls endpoints', () => { + it('listCalls GETs /calls with date-range query params', async () => { + const api = makeApi(); + await api.listCalls({ + fromDateTime: '2026-08-01T00:00:00Z', + toDateTime: '2026-08-19T00:00:00Z', + cursor: 'abc', + }); + const req = api.sent[0]; + expect(req.method).toBe('GET'); + expect(req.url).toBe('https://api.gong.io/v2/calls'); + expect(req.query).toEqual({ + fromDateTime: '2026-08-01T00:00:00Z', + toDateTime: '2026-08-19T00:00:00Z', + cursor: 'abc', + }); + }); + + it('getCall GETs /calls/{id}', async () => { + const api = makeApi(); + await api.getCall('call-9'); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe('https://api.gong.io/v2/calls/call-9'); + }); + + it('listCallsExtensive POSTs the filter/contentSelector body to /calls/extensive', async () => { + const api = makeApi(); + const body = { + filter: { callIds: ['call-9'] }, + contentSelector: { exposedFields: { parties: true } }, + }; + await api.listCallsExtensive(body); + const req = api.sent[0]; + expect(req.method).toBe('POST'); + expect(req.url).toBe('https://api.gong.io/v2/calls/extensive'); + expect(req.body).toEqual(body); + expect(req.headers['Content-Type']).toBe('application/json'); + }); + + it('getTranscripts POSTs to /calls/transcript', async () => { + const api = makeApi(); + await api.getTranscripts({ filter: { callIds: ['call-9'] } }); + const req = api.sent[0]; + expect(req.method).toBe('POST'); + expect(req.url).toBe('https://api.gong.io/v2/calls/transcript'); + expect(req.body).toEqual({ filter: { callIds: ['call-9'] } }); + }); + }); + + describe('users endpoints', () => { + it('listUsers GETs /users with the includeAvatars flag', async () => { + const api = makeApi(); + await api.listUsers({ includeAvatars: false }); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe('https://api.gong.io/v2/users'); + expect(api.sent[0].query).toEqual({ includeAvatars: false }); + }); + + it('getUser GETs /users/{id}', async () => { + const api = makeApi(); + await api.getUser('user-1'); + expect(api.sent[0].url).toBe('https://api.gong.io/v2/users/user-1'); + }); + }); + + describe('testAuth', () => { + it('performs a lightweight authenticated users listing', async () => { + const api = makeApi(); + await api.testAuth(); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe('https://api.gong.io/v2/users'); + }); + }); + + describe('getAuthorizationRequirements', () => { + it('declares a basic requirement for access_key and access_key_secret', () => { + const api = new Api({ access_key: 'ak', access_key_secret: 'sk' }); + const reqs = api.getAuthorizationRequirements(); + expect(reqs.type).toBe('basic'); + expect(reqs.data.jsonSchema.required).toEqual([ + 'access_key', + 'access_key_secret', + ]); + expect(reqs.data.uiSchema.access_key_secret['ui:widget']).toBe( + 'password' + ); + }); + }); +}); diff --git a/packages/v1-ready/gong/tests/definition.test.js b/packages/v1-ready/gong/tests/definition.test.js new file mode 100644 index 0000000..c9aad81 --- /dev/null +++ b/packages/v1-ready/gong/tests/definition.test.js @@ -0,0 +1,105 @@ +const { Definition } = require('../definition'); + +const { requiredAuthMethods } = Definition; + +describe('Gong Definition', () => { + it('is named gong and models the Gong entity', () => { + expect(Definition.getName()).toBe('gong'); + expect(Definition.moduleName).toBe('gong'); + expect(Definition.modelName).toBe('Gong'); + }); + + it('persists the access key and secret on the credential', () => { + expect(requiredAuthMethods.apiPropertiesToPersist.credential).toEqual([ + 'access_key', + 'access_key_secret', + ]); + }); + + describe('key fingerprinting', () => { + it('derives a stable, non-reversible externalId from the access key', async () => { + const api = { access_key: 'secret-key' }; + const entity = await requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-1' + ); + const credential = await requiredAuthMethods.getCredentialDetails( + api, + 'user-1' + ); + + // Same key → same id (idempotent linkage). + expect(entity.identifiers.externalId).toBe( + credential.identifiers.externalId + ); + // Never the raw key. + expect(entity.identifiers.externalId).not.toBe('secret-key'); + // sha256 hex. + expect(entity.identifiers.externalId).toMatch(/^[a-f0-9]{64}$/); + expect(entity.identifiers.userId).toBe('user-1'); + }); + + it('falls back to the Basic-auth username when access_key is absent', async () => { + const fromKey = await requiredAuthMethods.getEntityDetails( + { access_key: 'ak' }, + {}, + {}, + 'u' + ); + const fromUsername = await requiredAuthMethods.getEntityDetails( + { username: 'ak' }, + {}, + {}, + 'u' + ); + expect(fromKey.identifiers.externalId).toBe( + fromUsername.identifiers.externalId + ); + }); + + it('produces different ids for different keys', async () => { + const a = await requiredAuthMethods.getEntityDetails( + { access_key: 'key-a' }, + {}, + {}, + 'u' + ); + const b = await requiredAuthMethods.getEntityDetails( + { access_key: 'key-b' }, + {}, + {}, + 'u' + ); + expect(a.identifiers.externalId).not.toBe(b.identifiers.externalId); + }); + }); + + describe('testAuthRequest', () => { + it('delegates to the api testAuth check', async () => { + let called = false; + const api = { + testAuth: async () => { + called = true; + return { ok: true }; + }, + }; + await expect( + requiredAuthMethods.testAuthRequest(api) + ).resolves.toEqual({ ok: true }); + expect(called).toBe(true); + }); + + it('propagates auth failures from the api', async () => { + const api = { + testAuth: async () => { + throw new Error('401 Unauthorized'); + }, + }; + await expect( + requiredAuthMethods.testAuthRequest(api) + ).rejects.toThrow(/401/); + }); + }); +}); diff --git a/packages/v1-ready/gong/tests/spec-sync.test.js b/packages/v1-ready/gong/tests/spec-sync.test.js new file mode 100644 index 0000000..25920f0 --- /dev/null +++ b/packages/v1-ready/gong/tests/spec-sync.test.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { Api } = require('../api'); + +const spec = yaml.load( + fs.readFileSync(path.join(__dirname, '..', 'gong.openapi.yaml'), 'utf8') +); + +const specOperationIds = Object.values(spec.paths).flatMap((item) => + Object.entries(item) + .filter(([m]) => ['get', 'post', 'patch', 'put', 'delete'].includes(m)) + .map(([, op]) => op.operationId) +); + +const clientMethods = Object.getOwnPropertyNames(Api.prototype).filter( + (m) => typeof Api.prototype[m] === 'function' && m !== 'constructor' +); + +describe('OpenAPI spec ↔ client sync', () => { + it('every operationId has a matching client method', () => { + const missing = specOperationIds.filter( + (op) => !clientMethods.includes(op) + ); + expect(missing).toEqual([]); + }); + + it('the base server URL matches the client default baseUrl', () => { + const api = new Api({ access_key: 'x', access_key_secret: 'y' }); + expect(spec.servers[0].url).toBe(api.baseUrl); + }); + + it('declares HTTP Basic auth security', () => { + const scheme = spec.components.securitySchemes.BasicAuth; + expect(scheme.type).toBe('http'); + expect(scheme.scheme).toBe('basic'); + }); +}); diff --git a/packages/v1-ready/otter/.env.example b/packages/v1-ready/otter/.env.example new file mode 100644 index 0000000..4c19a1c --- /dev/null +++ b/packages/v1-ready/otter/.env.example @@ -0,0 +1,4 @@ +# Otter.ai Public API key (Enterprise workspaces only). +# Create it in Otter under Integrations → Developer → Create key. +# Sent on every request as the `Authorization: Bearer ` header. +OTTER_API_KEY=your_otter_api_key_here diff --git a/packages/v1-ready/otter/LICENSE.md b/packages/v1-ready/otter/LICENSE.md new file mode 100644 index 0000000..c307ce6 --- /dev/null +++ b/packages/v1-ready/otter/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Left Hook + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/v1-ready/otter/README.md b/packages/v1-ready/otter/README.md new file mode 100644 index 0000000..affa7cd --- /dev/null +++ b/packages/v1-ready/otter/README.md @@ -0,0 +1,84 @@ +# @friggframework/api-module-otter + +A [Frigg](https://friggframework.org) API module for the **Otter.ai Public API**. + +## Is there really an Otter.ai API? + +Yes — now. Otter.ai spent years with **no** official public developer API; the +only option was an unofficial, reverse-engineered community client +([omerdn1/otter.ai-api](https://github.com/omerdn1/otter.ai-api)), and Otter's own +help center answered "Does Otter offer an open API?" with a no. + +That changed. Otter shipped an official **Public API** (help-center article last +updated **April 23, 2026**), available for **Enterprise** workspaces. This module +targets that official API. + +- **Base URL:** `https://api.otter.ai/v1` +- **Auth:** Bearer token — `Authorization: Bearer `. Create a key in + Otter under **Integrations → Developer → Create key**. +- **Availability:** Enterprise workspaces only. If you do not see the Developer + tab, contact your Otter account manager. +- **Scope:** read channels, conversations, transcripts, audio, action items, + insights, outlines, and workspace details; plus workspace webhooks + (`conversation.completed`, `conversation.shared`). + +Sources: [Otter.ai Public API (Help Center)](https://help.otter.ai/hc/en-us/articles/36130822688279-Otter-ai-Public-API), +[Workspace Webhooks](https://help.otter.ai/hc/en-us/articles/35634832371735-Workspace-Webhooks). + +## Install + +```bash +npm install @friggframework/api-module-otter +``` + +## Usage + +```javascript +const { Api, Definition } = require('@friggframework/api-module-otter'); + +const otter = new Api({ api_token: process.env.OTTER_API_KEY }); + +await otter.getWorkspace(); +await otter.listChannels(); +const { conversations, next_cursor } = await otter.listConversations({ page_size: 25 }); +const convo = await otter.getConversation('conv-123', { include: ['transcript', 'action_items'] }); +``` + +In an integration, consume it through the standard Frigg pattern: + +```javascript +const convo = await this.otter.api.getConversation(id, { include: 'all' }); +``` + +## Authentication + +This is an API-key (Bearer) module. The interactive `frigg auth` CLI and the +hosted auth UI render the form declared by `getAuthorizationRequirements()` +(a single masked `api_token` field). The raw token is persisted on the +credential as `api_token`; the module adds the `Authorization: Bearer` prefix at +request time, so re-hydration never double-prefixes. + +## API surface + +| Method | HTTP | Path | +|---|---|---| +| `getWorkspace()` | GET | `/workspace` | +| `listChannels(query)` | GET | `/channels` | +| `listConversations(query)` | GET | `/conversations` | +| `getConversation(id, {include})` | GET | `/conversations/{id}` | +| `getConversationTranscript(id)` | GET | `/conversations/{id}/transcript` | +| `getConversationAudio(id)` | GET | `/conversations/{id}/audio` | +| `testAuth()` | GET | `/workspace` | + +`listConversations` returns results in reverse chronological order with +cursor-based pagination. `getConversation`'s `include` accepts any of +`transcript`, `action_items`, `insights`, `outline`, or `all`. + +Conversation records expose `abstract_summary`, `action_items`, `insights`, +`outline`, `transcript`, `conf_join_url`, and `calendar_guests` — the emails of +users invited to the calendar event, which is how a consuming integration links +a conversation to a CRM contact. + +## License + +MIT diff --git a/packages/v1-ready/otter/api.js b/packages/v1-ready/otter/api.js new file mode 100644 index 0000000..c6f5041 --- /dev/null +++ b/packages/v1-ready/otter/api.js @@ -0,0 +1,147 @@ +const { ApiKeyRequester, ModuleConstants, get } = require('@friggframework/core'); + +/** + * Otter.ai Public API client. + * + * Otter.ai historically had NO public developer API — only an unofficial, + * reverse-engineered community client existed. That changed: Otter shipped an + * official **Public API** (help-center article last updated April 23, 2026), + * available for Enterprise workspaces. It is a Bearer-token authenticated REST + * surface rooted at `https://api.otter.ai/v1` that exposes channels, + * conversations, transcripts, audio, action items, insights, outlines, workspace + * details, and workspace webhooks (conversation.completed / conversation.shared). + * + * Auth: an API key minted under Integrations → Developer → Create key, sent as + * `Authorization: Bearer `. We model that on top of ApiKeyRequester by + * setting the header NAME to `Authorization` and the header VALUE to + * `Bearer `; the raw token is kept separately (`api_token`) so it can be + * persisted and re-hydrated without double-prefixing. + * + * Docs: https://help.otter.ai/hc/en-us/articles/36130822688279-Otter-ai-Public-API + * https://help.otter.ai/hc/en-us/articles/35634832371735-Workspace-Webhooks + */ +class Api extends ApiKeyRequester { + constructor(params) { + super(params); + + // Accept the token under any of the friendly names, tolerate a caller + // that already prefixed "Bearer ", and store the raw token once. + const raw = + get(params, 'api_token', null) || + get(params, 'access_token', null) || + get(params, 'api_key', null); + const token = raw ? String(raw).replace(/^Bearer\s+/i, '').trim() : null; + + this.api_token = token; + // ApiKeyRequester.addAuthHeaders() sets headers[api_key_name] = api_key. + this.api_key_name = 'Authorization'; + this.api_key = token ? `Bearer ${token}` : null; + + this.baseUrl = 'https://api.otter.ai/v1'; + + this.URLs = { + workspace: '/workspace', + channels: '/channels', + conversations: '/conversations', + conversationById: (id) => `/conversations/${id}`, + conversationTranscript: (id) => `/conversations/${id}/transcript`, + conversationAudio: (id) => `/conversations/${id}/audio`, + }; + } + + getAuthorizationRequirements() { + return { + url: null, + type: ModuleConstants.authType.apiKey, + data: { + jsonSchema: { + title: 'Otter.ai Authentication', + type: 'object', + required: ['api_token'], + properties: { + api_token: { + type: 'string', + title: 'API Key', + }, + }, + }, + uiSchema: { + api_token: { + 'ui:widget': 'password', + 'ui:help': + 'Create a key in Otter under Integrations → Developer → Create key (Enterprise workspaces only). Sent as the Authorization: Bearer header.', + 'ui:placeholder': 'Your Otter.ai API key', + }, + }, + }, + }; + } + + // ---- Workspace ---------------------------------------------------------- + + /** Get the workspace for the authenticated user (id, name, owner, type…). */ + async getWorkspace() { + return this._get({ url: this.baseUrl + this.URLs.workspace }); + } + + // ---- Channels ----------------------------------------------------------- + + /** List channels for the authenticated user (alphabetical by name). */ + async listChannels(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.channels, query }); + } + + // ---- Conversations ------------------------------------------------------ + + /** + * List conversations for the authenticated user. Reverse chronological + * (most recent first), cursor-based pagination. Supports `cursor` and + * `page_size`, plus optional filters (e.g. `channel_id`). + */ + async listConversations(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.conversations, query }); + } + + /** + * Get one conversation's summary and details. Pass `include` to embed + * related data: any of `transcript`, `action_items`, `insights`, `outline`, + * or `all`. Accepts a string or an array (joined with commas). + */ + async getConversation(conversationId, { include } = {}) { + const query = {}; + if (include) { + query.include = Array.isArray(include) ? include.join(',') : include; + } + return this._get({ + url: this.baseUrl + this.URLs.conversationById(conversationId), + query, + }); + } + + /** Get the full transcript for a conversation. */ + async getConversationTranscript(conversationId) { + return this._get({ + url: this.baseUrl + this.URLs.conversationTranscript(conversationId), + }); + } + + /** Get the audio (download URL / stream reference) for a conversation. */ + async getConversationAudio(conversationId) { + return this._get({ + url: this.baseUrl + this.URLs.conversationAudio(conversationId), + }); + } + + // ---- Auth check --------------------------------------------------------- + + /** + * Lightweight authenticated request used to validate the API key. + * A valid key returns 200 with the workspace; an invalid key returns 401, + * which the requester surfaces as an error. + */ + async testAuth() { + return this.getWorkspace(); + } +} + +module.exports = { Api }; diff --git a/packages/v1-ready/otter/defaultConfig.json b/packages/v1-ready/otter/defaultConfig.json new file mode 100644 index 0000000..e861003 --- /dev/null +++ b/packages/v1-ready/otter/defaultConfig.json @@ -0,0 +1,10 @@ +{ + "name": "otter", + "label": "Otter.ai", + "authType": "apiKey", + "productUrl": "https://otter.ai", + "apiDocs": "https://help.otter.ai/hc/en-us/articles/36130822688279-Otter-ai-Public-API", + "logoUrl": "https://otter.ai/favicon.ico", + "categories": ["Conversation Intelligence", "Meetings", "Transcription"], + "description": "Otter.ai Public API module — read workspace, channels, conversations, transcripts, action items, insights, and outlines from an Enterprise Otter workspace." +} diff --git a/packages/v1-ready/otter/definition.js b/packages/v1-ready/otter/definition.js new file mode 100644 index 0000000..55e1cd3 --- /dev/null +++ b/packages/v1-ready/otter/definition.js @@ -0,0 +1,58 @@ +require('dotenv').config(); +const crypto = require('crypto'); +const { Api } = require('./api'); +const config = require('./defaultConfig.json'); + +// Otter issues a static API key (Bearer token, no OAuth), so there is no +// external account id returned at auth time. We derive a stable, non-reversible +// identifier from the raw token so the same key always maps to the same +// entity/credential. +const keyFingerprint = (token) => + crypto.createHash('sha256').update(String(token)).digest('hex'); + +const Definition = { + API: Api, + getName: function () { + return config.name; + }, + moduleName: config.name, + modelName: 'Otter', + requiredAuthMethods: { + setAuthParams: async function (api, params) {}, + getEntityDetails: async function ( + api, + callbackParams, + tokenResponse, + userId + ) { + return { + identifiers: { + externalId: keyFingerprint(api.api_token), + userId, + }, + details: {}, + }; + }, + apiPropertiesToPersist: { + credential: ['api_token'], + entity: [], + }, + getCredentialDetails: async function (api, userId) { + return { + identifiers: { + externalId: keyFingerprint(api.api_token), + userId, + }, + details: {}, + }; + }, + testAuthRequest: async function (api) { + return api.testAuth(); + }, + }, + env: { + api_token: process.env.OTTER_API_KEY, + }, +}; + +module.exports = { Definition }; diff --git a/packages/v1-ready/otter/index.js b/packages/v1-ready/otter/index.js new file mode 100644 index 0000000..3c94a63 --- /dev/null +++ b/packages/v1-ready/otter/index.js @@ -0,0 +1,7 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); + +module.exports = { + Api, + Definition, +}; diff --git a/packages/v1-ready/otter/otter.openapi.yaml b/packages/v1-ready/otter/otter.openapi.yaml new file mode 100644 index 0000000..8a8ddfd --- /dev/null +++ b/packages/v1-ready/otter/otter.openapi.yaml @@ -0,0 +1,160 @@ +openapi: 3.0.3 +info: + title: Otter.ai Public API + version: "1.0.0" + description: >- + Otter.ai is an AI meeting assistant (transcription, summaries, action items, + insights). This is the official Public REST API, available for Enterprise + workspaces, documented at + https://help.otter.ai/hc/en-us/articles/36130822688279-Otter-ai-Public-API + and https://help.otter.ai/hc/en-us/articles/35634832371735-Workspace-Webhooks. + Otter does not publish its own OpenAPI document; this spec was authored by + Left Hook from Otter's help-center reference as the canonical machine-readable + contract for the @friggframework/api-module-otter module. It is the source of + truth the hand-written client in api.js mirrors — every operationId here is a + method on that client, and only the endpoints the client implements appear. + contact: + name: Left Hook + url: https://lefthook.com +servers: + - url: https://api.otter.ai/v1 + description: Otter.ai public API +security: + - BearerAuth: [] +tags: + - name: Workspace + - name: Channels + - name: Conversations +paths: + /workspace: + get: + tags: [Workspace] + operationId: getWorkspace + summary: Get the workspace for the authenticated user + description: >- + Returns the workspace (id, name, owner, type…) for the authenticated + user. Also used as the module's lightweight auth check. + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /channels: + get: + tags: [Channels] + operationId: listChannels + summary: List channels for the authenticated user + description: >- + Lists channels for the authenticated user, returned in alphabetical + order by channel name. + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /conversations: + get: + tags: [Conversations] + operationId: listConversations + summary: List conversations for the authenticated user + description: >- + Lists conversations in reverse chronological order (most recent first) + with cursor-based pagination. Optional filters may be supplied as query + parameters (e.g. channel_id). + parameters: + - in: query + name: cursor + required: false + schema: { type: string } + description: Opaque cursor from a prior page's next_cursor. + - in: query + name: page_size + required: false + schema: { type: integer } + description: Number of conversations to return per page. + - in: query + name: channel_id + required: false + schema: { type: string } + description: Restrict results to a single channel. + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /conversations/{conversation_id}: + parameters: + - $ref: "#/components/parameters/ConversationId" + get: + tags: [Conversations] + operationId: getConversation + summary: Get one conversation's summary and details + description: >- + Returns a single conversation's summary and details. Use `include` to + embed related data in the response. + parameters: + - in: query + name: include + required: false + schema: + type: string + enum: [transcript, action_items, insights, outline, all] + description: >- + Comma-separated list of related data to embed — any of transcript, + action_items, insights, outline — or `all`. + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + "404": { $ref: "#/components/responses/NotFound" } + /conversations/{conversation_id}/transcript: + parameters: + - $ref: "#/components/parameters/ConversationId" + get: + tags: [Conversations] + operationId: getConversationTranscript + summary: Get the full transcript for a conversation + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + "404": { $ref: "#/components/responses/NotFound" } + /conversations/{conversation_id}/audio: + parameters: + - $ref: "#/components/parameters/ConversationId" + get: + tags: [Conversations] + operationId: getConversationAudio + summary: Get the audio (download URL / stream reference) for a conversation + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + "404": { $ref: "#/components/responses/NotFound" } +components: + securitySchemes: + BearerAuth: + type: apiKey + in: header + name: Authorization + description: >- + An API key minted under Integrations → Developer → Create key + (Enterprise workspaces only), sent as the value `Bearer ` in the + Authorization header. The module builds this on ApiKeyRequester by + setting the header name to `Authorization` and the header value to + `Bearer `. + parameters: + ConversationId: + in: path + name: conversation_id + required: true + schema: { type: string } + description: The Otter conversation id. + responses: + ObjectResponse: + description: A single object. + content: + application/json: + schema: { type: object, additionalProperties: true } + ListResponse: + description: A list of matching objects (cursor-paginated where applicable). + content: + application/json: + schema: + type: object + additionalProperties: true + Unauthorized: + description: Missing or invalid API key. + NotFound: + description: Conversation not found. diff --git a/packages/v1-ready/otter/package.json b/packages/v1-ready/otter/package.json new file mode 100644 index 0000000..29588bb --- /dev/null +++ b/packages/v1-ready/otter/package.json @@ -0,0 +1,25 @@ +{ + "name": "@friggframework/api-module-otter", + "version": "1.0.0", + "prettier": "@friggframework/prettier-config", + "description": "Otter.ai Public API module that lets the Frigg Framework read Otter conversations, transcripts, insights, and action items", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "jest": "^28.1.3", + "js-yaml": "^4.1.0" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/v1-ready/otter/tests/api.test.js b/packages/v1-ready/otter/tests/api.test.js new file mode 100644 index 0000000..513073c --- /dev/null +++ b/packages/v1-ready/otter/tests/api.test.js @@ -0,0 +1,139 @@ +const { Api } = require('../api'); + +function makeApi(params = { api_token: 'test-key-123' }) { + const api = new Api(params); + // Capture requests instead of hitting the network. + api.sent = []; + const record = (method) => async (options) => { + api.sent.push({ method, ...options }); + return { ok: true }; + }; + api._get = record('GET'); + api._post = record('POST'); + api._patch = record('PATCH'); + api._delete = record('DELETE'); + return api; +} + +describe('Otter Api', () => { + describe('auth', () => { + it('uses the Authorization header with a Bearer-prefixed value', () => { + const api = new Api({ api_token: 'abc' }); + expect(api.api_key_name).toBe('Authorization'); + expect(api.api_key).toBe('Bearer abc'); + expect(api.api_token).toBe('abc'); + }); + + it('accepts the token under api_key or access_token too', () => { + expect(new Api({ api_key: 'abc' }).api_token).toBe('abc'); + expect(new Api({ access_token: 'abc' }).api_token).toBe('abc'); + }); + + it('strips a pre-existing Bearer prefix so it is never doubled', () => { + const api = new Api({ api_token: 'Bearer abc' }); + expect(api.api_token).toBe('abc'); + expect(api.api_key).toBe('Bearer abc'); + }); + + it('injects the Bearer token into request headers via addAuthHeaders', async () => { + const api = new Api({ api_token: 'abc' }); + const headers = await api.addAuthHeaders({}); + expect(headers['Authorization']).toBe('Bearer abc'); + }); + + it('points at the Otter public API base URL', () => { + const api = new Api({ api_token: 'abc' }); + expect(api.baseUrl).toBe('https://api.otter.ai/v1'); + }); + + it('reports authenticated only with a non-empty key', () => { + expect(new Api({ api_token: 'abc' }).isAuthenticated()).toBe(true); + expect(new Api({ api_token: '' }).isAuthenticated()).toBe(false); + expect(new Api({}).isAuthenticated()).toBe(false); + }); + }); + + describe('endpoints', () => { + it('getWorkspace gets /workspace', async () => { + const api = makeApi(); + await api.getWorkspace(); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe('https://api.otter.ai/v1/workspace'); + }); + + it('listChannels gets /channels', async () => { + const api = makeApi(); + await api.listChannels(); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe('https://api.otter.ai/v1/channels'); + }); + + it('listConversations gets /conversations and forwards pagination query', async () => { + const api = makeApi(); + await api.listConversations({ page_size: 25, cursor: 'abc' }); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe( + 'https://api.otter.ai/v1/conversations' + ); + expect(api.sent[0].query).toEqual({ page_size: 25, cursor: 'abc' }); + }); + + it('getConversation gets /conversations/{id}', async () => { + const api = makeApi(); + await api.getConversation('conv-1'); + expect(api.sent[0].url).toBe( + 'https://api.otter.ai/v1/conversations/conv-1' + ); + expect(api.sent[0].query).toEqual({}); + }); + + it('getConversation joins an include array into a comma string', async () => { + const api = makeApi(); + await api.getConversation('conv-1', { + include: ['transcript', 'action_items'], + }); + expect(api.sent[0].query).toEqual({ + include: 'transcript,action_items', + }); + }); + + it('getConversation passes an include string through unchanged', async () => { + const api = makeApi(); + await api.getConversation('conv-1', { include: 'all' }); + expect(api.sent[0].query).toEqual({ include: 'all' }); + }); + + it('getConversationTranscript gets the nested transcript path', async () => { + const api = makeApi(); + await api.getConversationTranscript('conv-1'); + expect(api.sent[0].url).toBe( + 'https://api.otter.ai/v1/conversations/conv-1/transcript' + ); + }); + + it('getConversationAudio gets the nested audio path', async () => { + const api = makeApi(); + await api.getConversationAudio('conv-1'); + expect(api.sent[0].url).toBe( + 'https://api.otter.ai/v1/conversations/conv-1/audio' + ); + }); + + it('testAuth performs a lightweight workspace fetch', async () => { + const api = makeApi(); + await api.testAuth(); + expect(api.sent[0].method).toBe('GET'); + expect(api.sent[0].url).toBe('https://api.otter.ai/v1/workspace'); + }); + }); + + describe('getAuthorizationRequirements', () => { + it('declares an apiKey requirement for api_token', () => { + const api = new Api({ api_token: 'abc' }); + const reqs = api.getAuthorizationRequirements(); + expect(reqs.type).toBe('apiKey'); + expect(reqs.data.jsonSchema.required).toContain('api_token'); + expect(reqs.data.uiSchema.api_token['ui:widget']).toBe('password'); + }); + }); +}); diff --git a/packages/v1-ready/otter/tests/definition.test.js b/packages/v1-ready/otter/tests/definition.test.js new file mode 100644 index 0000000..58705c6 --- /dev/null +++ b/packages/v1-ready/otter/tests/definition.test.js @@ -0,0 +1,86 @@ +const { Definition } = require('../definition'); + +const { requiredAuthMethods } = Definition; + +describe('Otter Definition', () => { + it('is named otter and models the Otter entity', () => { + expect(Definition.getName()).toBe('otter'); + expect(Definition.moduleName).toBe('otter'); + expect(Definition.modelName).toBe('Otter'); + }); + + it('persists the api_token on the credential', () => { + expect(requiredAuthMethods.apiPropertiesToPersist.credential).toContain( + 'api_token' + ); + }); + + describe('key fingerprinting', () => { + it('derives a stable, non-reversible externalId from the api token', async () => { + const api = { api_token: 'secret-key' }; + const entity = await requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-1' + ); + const credential = await requiredAuthMethods.getCredentialDetails( + api, + 'user-1' + ); + + // Same key → same id (idempotent linkage). + expect(entity.identifiers.externalId).toBe( + credential.identifiers.externalId + ); + // Never the raw key. + expect(entity.identifiers.externalId).not.toBe('secret-key'); + // sha256 hex. + expect(entity.identifiers.externalId).toMatch(/^[a-f0-9]{64}$/); + expect(entity.identifiers.userId).toBe('user-1'); + }); + + it('produces different ids for different keys', async () => { + const a = await requiredAuthMethods.getEntityDetails( + { api_token: 'key-a' }, + {}, + {}, + 'u' + ); + const b = await requiredAuthMethods.getEntityDetails( + { api_token: 'key-b' }, + {}, + {}, + 'u' + ); + expect(a.identifiers.externalId).not.toBe(b.identifiers.externalId); + }); + }); + + describe('testAuthRequest', () => { + it('delegates to the api testAuth check', async () => { + let called = false; + const api = { + testAuth: async () => { + called = true; + return { ok: true }; + }, + }; + await expect( + requiredAuthMethods.testAuthRequest(api) + ).resolves.toEqual({ ok: true }); + expect(called).toBe(true); + }); + + it('propagates auth failures from the api', async () => { + const api = { + testAuth: async () => { + throw new Error('401 Unauthorized'); + }, + }; + await expect( + requiredAuthMethods.testAuthRequest(api) + ).rejects.toThrow(/401/); + }); + }); +}); diff --git a/packages/v1-ready/otter/tests/spec-sync.test.js b/packages/v1-ready/otter/tests/spec-sync.test.js new file mode 100644 index 0000000..4b1dac6 --- /dev/null +++ b/packages/v1-ready/otter/tests/spec-sync.test.js @@ -0,0 +1,39 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { Api } = require('../api'); + +const spec = yaml.load( + fs.readFileSync(path.join(__dirname, '..', 'otter.openapi.yaml'), 'utf8') +); + +const specOperationIds = Object.values(spec.paths).flatMap((item) => + Object.entries(item) + .filter(([m]) => ['get', 'post', 'patch', 'put', 'delete'].includes(m)) + .map(([, op]) => op.operationId) +); + +const clientMethods = Object.getOwnPropertyNames(Api.prototype).filter( + (m) => typeof Api.prototype[m] === 'function' && m !== 'constructor' +); + +describe('OpenAPI spec ↔ client sync', () => { + it('every operationId has a matching client method', () => { + const missing = specOperationIds.filter( + (op) => !clientMethods.includes(op) + ); + expect(missing).toEqual([]); + }); + + it('the base server URL matches the client baseUrl', () => { + const api = new Api({ api_token: 'x' }); + expect(spec.servers[0].url).toBe(api.baseUrl); + }); + + it('declares Bearer/apiKey Authorization security', () => { + const scheme = spec.components.securitySchemes.BearerAuth; + expect(scheme.type).toBe('apiKey'); + expect(scheme.in).toBe('header'); + expect(scheme.name).toBe('Authorization'); + }); +}); diff --git a/packages/v1-ready/quo/.env.example b/packages/v1-ready/quo/.env.example new file mode 100644 index 0000000..02fabe0 --- /dev/null +++ b/packages/v1-ready/quo/.env.example @@ -0,0 +1,8 @@ +# Quo (formerly OpenPhone) API module + +# Your Quo / OpenPhone API key (Settings -> API). +# Sent RAW in the Authorization header (no "Bearer " prefix). +QUO_API_KEY= + +# Optional base URL override. Defaults to https://api.openphone.com/v1 +QUO_BASE_URL=https://api.openphone.com/v1 diff --git a/packages/v1-ready/quo/LICENSE.md b/packages/v1-ready/quo/LICENSE.md new file mode 100644 index 0000000..c307ce6 --- /dev/null +++ b/packages/v1-ready/quo/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Left Hook + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/v1-ready/quo/README.md b/packages/v1-ready/quo/README.md new file mode 100644 index 0000000..2f03a8d --- /dev/null +++ b/packages/v1-ready/quo/README.md @@ -0,0 +1,100 @@ +# @friggframework/api-module-quo + +Frigg API module for **Quo** — the business phone / calling product formerly known as **OpenPhone**. The REST API still lives at `api.openphone.com`; the docs have moved to [quo.com/docs](https://www.quo.com/docs). + +## Features + +- **API-key authentication** (`ApiKeyRequester`) — the key is sent **raw** in the `Authorization` header (no `Bearer` prefix). +- Calls: list, get by ID, recordings, transcripts, summaries. +- Messages: list, get, send. +- Contacts, phone numbers, users. +- Webhooks: list/get/create (calls, messages, call-summaries, call-transcripts)/delete. + +## Installation + +```bash +npm install @friggframework/api-module-quo +``` + +## Configuration + +```env +# Your Quo / OpenPhone API key (Settings -> API) +QUO_API_KEY=op_xxx + +# Optional base URL override (defaults to https://api.openphone.com/v1) +QUO_BASE_URL=https://api.openphone.com/v1 +``` + +## Authentication + +The Quo API **does not use a Bearer token**. The key goes directly in the header: + +``` +Authorization: +``` + +This module sets `api_key_name = 'Authorization'` and stores the raw key, so `ApiKeyRequester.addAuthHeaders` produces exactly that. + +## Usage + +```javascript +const { Api } = require('@friggframework/api-module-quo'); + +const api = new Api({ api_key: process.env.QUO_API_KEY }); + +// List calls for a given Quo number + external participant +const calls = await api.listCalls({ + phoneNumberId: 'PN123abc', + participants: ['+15555550123'], + maxResults: 50, +}); + +// Enrich a single call +const call = await api.getCall('AC...'); +const recordings = await api.getCallRecordings('AC...'); // GET /call-recordings/{id} +const transcript = await api.getCallTranscript('AC...'); // GET /call-transcripts/{id} +const summary = await api.getCallSummary('AC...'); // GET /call-summaries/{id} + +// Messages +const messages = await api.listMessages({ + phoneNumberId: 'PN123abc', + participants: ['+15555550123'], +}); +``` + +## Endpoint reference + +Base URL: `https://api.openphone.com/v1` + +| Method | Path | Client method | +|---|---|---| +| GET | `/calls` | `listCalls(query)` | +| GET | `/calls/{id}` | `getCall(id)` | +| GET | `/call-recordings/{callId}` | `getCallRecordings(callId)` | +| GET | `/call-transcripts/{id}` | `getCallTranscript(callId)` | +| GET | `/call-summaries/{callId}` | `getCallSummary(callId)` | +| GET | `/messages` | `listMessages(query)` | +| GET | `/messages/{id}` | `getMessage(id)` | +| POST | `/messages` | `sendMessage(body)` | +| GET | `/contacts` | `listContacts(query)` | +| GET | `/phone-numbers` | `listPhoneNumbers(query)` | +| GET | `/users` | `listUsers(query)` | +| GET/POST/DELETE | `/webhooks*` | `listWebhooks` / `create*Webhook` / `deleteWebhook` | + +**List parameters** (`listCalls` / `listMessages`): `phoneNumberId` (required, `^PN...`), `participants[]` (E.164; max 1 for calls, 10 for messages), `userId` (`^US...`), `maxResults` (1–100), `pageToken`, `createdAfter`, `createdBefore`. + +### Phone-number matching (for CRM sync) + +Quo/phone data keys on **phone number**, not email. Call objects carry `participants` (E.164) and `direction`; the external party's number is the participant that is not the workspace's own Quo number. Downstream integrations that resolve a CRM record should match on that phone number (e.g. Reevo's `retrieveAccountAndContact({ contact_phone_number })`, URL-encoding `+` as `%2B`). + +## Doc sources + +- Authentication: https://www.quo.com/docs/mdx/api-reference/authentication.md +- List calls: https://www.quo.com/docs/mdx/api-reference/calls/list-calls.md +- Call recordings / transcripts / summaries: `.../calls/get-recordings-for-a-call.md`, `.../calls/get-a-transcription-for-a-call.md`, `.../calls/get-a-summary-for-a-call.md` +- List messages: https://www.quo.com/docs/mdx/api-reference/messages/list-messages.md + +## License + +MIT diff --git a/packages/v1-ready/quo/api.js b/packages/v1-ready/quo/api.js new file mode 100644 index 0000000..c90c1e2 --- /dev/null +++ b/packages/v1-ready/quo/api.js @@ -0,0 +1,198 @@ +const { get, ApiKeyRequester } = require('@friggframework/core'); + +/** + * Quo (formerly OpenPhone) API client. + * + * Auth: API key sent RAW in the `Authorization` header (no `Bearer` prefix). + * Authorization: + * + * Base URL: https://api.openphone.com/v1 + * Docs: https://www.quo.com/docs/api-reference/authentication + */ +class Api extends ApiKeyRequester { + constructor(params = {}) { + super(params); + + // Quo/OpenPhone uses the Authorization header with the raw key. + this.api_key_name = 'Authorization'; + + // Accept either `api_key` or `access_token` (Frigg credential persistence + // uses access_token for API-key modules by convention). + const apiKey = + get(params, 'api_key', null) || + get(params, 'access_token', null) || + process.env.QUO_API_KEY || + null; + if (apiKey) { + this.setApiKey(apiKey); + } + + this.baseUrl = get(params, 'baseUrl', null) || + process.env.QUO_BASE_URL || + 'https://api.openphone.com/v1'; + + this.URLs = { + // Calls + calls: '/calls', + callById: (id) => `/calls/${id}`, + callRecordings: (callId) => `/call-recordings/${callId}`, + callTranscript: (callId) => `/call-transcripts/${callId}`, + callSummary: (callId) => `/call-summaries/${callId}`, + // Messages + messages: '/messages', + messageById: (id) => `/messages/${id}`, + // Contacts + contacts: '/contacts', + contactById: (id) => `/contacts/${id}`, + // Phone numbers + phoneNumbers: '/phone-numbers', + phoneNumberById: (id) => `/phone-numbers/${id}`, + // Users + users: '/users', + userById: (id) => `/users/${id}`, + // Webhooks + webhooks: '/webhooks', + webhookById: (id) => `/webhooks/${id}`, + webhookCalls: '/webhooks/calls', + webhookMessages: '/webhooks/messages', + webhookCallSummaries: '/webhooks/call-summaries', + webhookCallTranscripts: '/webhooks/call-transcripts', + }; + } + + // ---- Phone numbers / users (used for auth test + resolving IDs) ---- + + async listPhoneNumbers(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.phoneNumbers, query }); + } + + async getPhoneNumber(id) { + return this._get({ url: this.baseUrl + this.URLs.phoneNumberById(id) }); + } + + async listUsers(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.users, query }); + } + + async getUser(id) { + return this._get({ url: this.baseUrl + this.URLs.userById(id) }); + } + + // ---- Calls ---- + + /** + * List calls. + * @param {object} query - phoneNumberId (required, ^PN...), participants[] + * (E.164, max 1), userId (^US...), maxResults (1-100), pageToken, + * createdAfter, createdBefore. + */ + async listCalls(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.calls, query }); + } + + async getCall(id) { + return this._get({ url: this.baseUrl + this.URLs.callById(id) }); + } + + async getCallRecordings(callId) { + return this._get({ url: this.baseUrl + this.URLs.callRecordings(callId) }); + } + + async getCallTranscript(callId) { + return this._get({ url: this.baseUrl + this.URLs.callTranscript(callId) }); + } + + async getCallSummary(callId) { + return this._get({ url: this.baseUrl + this.URLs.callSummary(callId) }); + } + + // ---- Messages ---- + + /** + * List messages. + * @param {object} query - phoneNumberId (required, ^PN...), participants[] + * (E.164, max 10), userId, maxResults (1-100), pageToken, + * createdAfter, createdBefore. + */ + async listMessages(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.messages, query }); + } + + async getMessage(id) { + return this._get({ url: this.baseUrl + this.URLs.messageById(id) }); + } + + async sendMessage(body) { + return this._post({ + url: this.baseUrl + this.URLs.messages, + headers: { 'Content-Type': 'application/json' }, + body, + }); + } + + // ---- Contacts ---- + + async listContacts(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.contacts, query }); + } + + async getContact(id) { + return this._get({ url: this.baseUrl + this.URLs.contactById(id) }); + } + + async createContact(body) { + return this._post({ + url: this.baseUrl + this.URLs.contacts, + headers: { 'Content-Type': 'application/json' }, + body, + }); + } + + // ---- Webhooks ---- + + async listWebhooks(query = {}) { + return this._get({ url: this.baseUrl + this.URLs.webhooks, query }); + } + + async getWebhook(id) { + return this._get({ url: this.baseUrl + this.URLs.webhookById(id) }); + } + + async createCallWebhook(body) { + return this._post({ + url: this.baseUrl + this.URLs.webhookCalls, + headers: { 'Content-Type': 'application/json' }, + body, + }); + } + + async createMessageWebhook(body) { + return this._post({ + url: this.baseUrl + this.URLs.webhookMessages, + headers: { 'Content-Type': 'application/json' }, + body, + }); + } + + async createCallSummaryWebhook(body) { + return this._post({ + url: this.baseUrl + this.URLs.webhookCallSummaries, + headers: { 'Content-Type': 'application/json' }, + body, + }); + } + + async createCallTranscriptWebhook(body) { + return this._post({ + url: this.baseUrl + this.URLs.webhookCallTranscripts, + headers: { 'Content-Type': 'application/json' }, + body, + }); + } + + async deleteWebhook(id) { + return this._delete({ url: this.baseUrl + this.URLs.webhookById(id) }); + } +} + +module.exports = { Api }; diff --git a/packages/v1-ready/quo/defaultConfig.json b/packages/v1-ready/quo/defaultConfig.json new file mode 100644 index 0000000..7b3230b --- /dev/null +++ b/packages/v1-ready/quo/defaultConfig.json @@ -0,0 +1,10 @@ +{ + "name": "quo", + "config": { + "apiKey": true, + "batch": { + "concurrency": 3, + "delay": 1000 + } + } +} diff --git a/packages/v1-ready/quo/definition.js b/packages/v1-ready/quo/definition.js new file mode 100644 index 0000000..59b5c78 --- /dev/null +++ b/packages/v1-ready/quo/definition.js @@ -0,0 +1,86 @@ +require('dotenv').config(); +const { get } = require('@friggframework/core'); +const { Api } = require('./api'); +const config = require('./defaultConfig.json'); + +const Definition = { + API: Api, + getName: () => config.name, + moduleName: config.name, + modelName: 'Quo', + requiredAuthMethods: { + // API-key module: render an interactive form (CLI + hosted UI) + getAuthorizationRequirements: () => ({ + type: 'apiKey', + data: { + jsonSchema: { + title: 'Quo API Authorization', + type: 'object', + required: ['api_key'], + properties: { + api_key: { + type: 'string', + title: 'API Key', + }, + }, + }, + uiSchema: { + api_key: { + 'ui:widget': 'password', + 'ui:help': + 'Your Quo (OpenPhone) API key from Settings → API. Sent raw in the Authorization header (no Bearer prefix).', + 'ui:placeholder': 'API Key', + }, + }, + }, + }), + + setAuthParams: async (api, params) => { + const apiKey = + get(params, 'api_key', null) || + get(params, 'access_token', null); + if (apiKey) { + api.setApiKey(apiKey); + } + }, + + // Any authenticated call verifies the key. listPhoneNumbers is cheap + // and always available on a valid workspace token. + testAuthRequest: async (api) => api.listPhoneNumbers(), + + getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { + const phoneNumbers = await api.listPhoneNumbers(); + const first = get(phoneNumbers, 'data', [])[0] || {}; + const externalId = first.id || 'quo-workspace'; + return { + identifiers: { externalId, user: userId }, + details: { + name: first.name || first.number || 'Quo Workspace', + }, + }; + }, + + getCredentialDetails: async (api, userId) => { + const phoneNumbers = await api.listPhoneNumbers(); + const first = get(phoneNumbers, 'data', [])[0] || {}; + const externalId = first.id || 'quo-workspace'; + return { + identifiers: { externalId, user: userId }, + details: {}, + }; + }, + + apiPropertiesToPersist: { + // ApiKeyRequester stores the key; persist it as access_token so the + // Api constructor rehydrates it on the next instantiation. + credential: ['access_token', 'api_key'], + entity: [], + }, + }, + env: { + api_key: process.env.QUO_API_KEY, + base_url: process.env.QUO_BASE_URL, + }, +}; + +module.exports = { Definition }; diff --git a/packages/v1-ready/quo/index.js b/packages/v1-ready/quo/index.js new file mode 100644 index 0000000..3c94a63 --- /dev/null +++ b/packages/v1-ready/quo/index.js @@ -0,0 +1,7 @@ +const { Api } = require('./api'); +const { Definition } = require('./definition'); + +module.exports = { + Api, + Definition, +}; diff --git a/packages/v1-ready/quo/jest.config.js b/packages/v1-ready/quo/jest.config.js new file mode 100644 index 0000000..7654ac3 --- /dev/null +++ b/packages/v1-ready/quo/jest.config.js @@ -0,0 +1,7 @@ +/* + * Offline unit tests only — no live API calls, no database. + */ +module.exports = { + testEnvironment: 'node', + testMatch: ['**/tests/**/*.test.js'], +}; diff --git a/packages/v1-ready/quo/package.json b/packages/v1-ready/quo/package.json new file mode 100644 index 0000000..2ec3008 --- /dev/null +++ b/packages/v1-ready/quo/package.json @@ -0,0 +1,36 @@ +{ + "name": "@friggframework/api-module-quo", + "version": "1.0.0", + "description": "Quo (formerly OpenPhone) API module for the Frigg Framework — calls, recordings, transcripts, summaries, and messages.", + "main": "index.js", + "scripts": { + "lint:fix": "prettier --write --loglevel error . && eslint . --fix", + "test": "jest" + }, + "author": "Left Hook", + "license": "MIT", + "keywords": [ + "frigg", + "quo", + "openphone", + "voip", + "calls", + "sms", + "api-module" + ], + "devDependencies": { + "@aws-sdk/client-scheduler": "^3.1113.0", + "dotenv": "^16.0.3", + "eslint": "^8.22.0", + "jest": "^28.1.3", + "jest-environment-jsdom": "^28.1.3", + "js-yaml": "^4.1.0", + "prettier": "^2.7.1" + }, + "dependencies": { + "@friggframework/core": "^2.0.0-next.107" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/v1-ready/quo/quo.openapi.yaml b/packages/v1-ready/quo/quo.openapi.yaml new file mode 100644 index 0000000..b56edbe --- /dev/null +++ b/packages/v1-ready/quo/quo.openapi.yaml @@ -0,0 +1,507 @@ +openapi: 3.0.3 +info: + title: Quo (OpenPhone) Public API + version: "1.0.0" + description: >- + Quo (formerly OpenPhone) is a business phone system. This is the public REST + API surface documented at https://www.quo.com/docs/api-reference (the + OpenPhone docs at https://www.openphone.com/docs/api-reference 301-redirect + here). This spec covers exactly the endpoints the + @friggframework/api-module-quo client (api.js) implements — calls, + recordings, transcripts, summaries, messages, contacts, phone numbers, + users, and webhook CRUD — and is the source of truth that hand-written + client mirrors. + + + AUTH QUIRK — NO `Bearer` PREFIX. Quo/OpenPhone sends the API key RAW in the + `Authorization` header: + + Authorization: YOUR_API_KEY + + The docs state explicitly: "The Quo API does not use a Bearer token for + authentication." + (https://www.quo.com/docs/api-reference/authentication). This is why the + security scheme below is modeled as an `apiKey` scheme in the `Authorization` + header — NOT as `http`/`bearer`. The client sets `api_key_name = + 'Authorization'` and writes the key verbatim, with no `Bearer ` prefix. + contact: + name: Left Hook + url: https://lefthook.com +servers: + - url: https://api.openphone.com/v1 + description: Quo/OpenPhone production API +security: + - ApiKeyAuth: [] +tags: + - name: Calls + - name: Messages + - name: Contacts + - name: Phone Numbers + - name: Users + - name: Webhooks +paths: + /calls: + get: + tags: [Calls] + operationId: listCalls + summary: List calls + description: >- + Fetch a paginated list of calls associated with a specific Quo number + and another number. + parameters: + - in: query + name: phoneNumberId + required: true + schema: { type: string, pattern: "^PN(.*)$" } + description: The Quo phone number id to scope the calls to. + - in: query + name: participants + schema: + type: array + maxItems: 1 + items: { type: string } + description: E.164 participant filter (max 1). + - in: query + name: userId + schema: { type: string, pattern: "^US(.*)$" } + - in: query + name: maxResults + schema: { type: integer, minimum: 1, maximum: 100 } + - in: query + name: pageToken + schema: { type: string } + - in: query + name: createdAfter + schema: { type: string, format: date-time } + - in: query + name: createdBefore + schema: { type: string, format: date-time } + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /calls/{id}: + parameters: + - $ref: "#/components/parameters/CallId" + get: + tags: [Calls] + operationId: getCall + summary: Get a call by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /call-recordings/{callId}: + parameters: + - $ref: "#/components/parameters/CallIdPath" + get: + tags: [Calls] + operationId: getCallRecordings + summary: Get recordings for a call + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /call-transcripts/{id}: + parameters: + - $ref: "#/components/parameters/CallId" + get: + tags: [Calls] + operationId: getCallTranscript + summary: Get a transcription for a call + description: Available on Business/Scale plans. + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /call-summaries/{callId}: + parameters: + - $ref: "#/components/parameters/CallIdPath" + get: + tags: [Calls] + operationId: getCallSummary + summary: Get a summary for a call + description: Available on Business/Scale plans. + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /messages: + get: + tags: [Messages] + operationId: listMessages + summary: List messages + parameters: + - in: query + name: phoneNumberId + required: true + schema: { type: string, pattern: "^PN(.*)$" } + - in: query + name: participants + schema: + type: array + maxItems: 10 + items: { type: string } + description: E.164 participant filter (max 10). + - in: query + name: userId + schema: { type: string, pattern: "^US(.*)$" } + - in: query + name: maxResults + schema: { type: integer, minimum: 1, maximum: 100 } + - in: query + name: pageToken + schema: { type: string } + - in: query + name: createdAfter + schema: { type: string, format: date-time } + - in: query + name: createdBefore + schema: { type: string, format: date-time } + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + post: + tags: [Messages] + operationId: sendMessage + summary: Send a text message + description: Send a text message from your Quo number to a recipient. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/MessageSend" } + responses: + "202": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /messages/{id}: + parameters: + - $ref: "#/components/parameters/MessageId" + get: + tags: [Messages] + operationId: getMessage + summary: Get a message by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /contacts: + get: + tags: [Contacts] + operationId: listContacts + summary: List contacts + parameters: + - in: query + name: externalIds + schema: + type: array + items: { type: string } + - in: query + name: sources + schema: + type: array + items: { type: string } + - in: query + name: maxResults + schema: { type: integer, minimum: 1, maximum: 100 } + - in: query + name: pageToken + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + post: + tags: [Contacts] + operationId: createContact + summary: Create a contact + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ContactCreate" } + responses: + "201": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /contacts/{id}: + parameters: + - $ref: "#/components/parameters/ContactId" + get: + tags: [Contacts] + operationId: getContact + summary: Get a contact by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /phone-numbers: + get: + tags: [Phone Numbers] + operationId: listPhoneNumbers + summary: List phone numbers + description: >- + View the Quo phone numbers in the workspace and their associated users. + Used by the module's auth test. + parameters: + - in: query + name: userId + schema: { type: string, pattern: "^US(.*)$" } + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /phone-numbers/{id}: + parameters: + - $ref: "#/components/parameters/PhoneNumberId" + get: + tags: [Phone Numbers] + operationId: getPhoneNumber + summary: Get a phone number by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /users: + get: + tags: [Users] + operationId: listUsers + summary: List users + parameters: + - in: query + name: maxResults + schema: { type: integer, minimum: 1, maximum: 100 } + - in: query + name: pageToken + schema: { type: string } + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /users/{id}: + parameters: + - $ref: "#/components/parameters/UserId" + get: + tags: [Users] + operationId: getUser + summary: Get a user by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /webhooks: + get: + tags: [Webhooks] + operationId: listWebhooks + summary: Lists all webhooks + description: Display all webhooks for a user. + parameters: + - in: query + name: userId + schema: { type: string, pattern: "^US(.*)$" } + responses: + "200": { $ref: "#/components/responses/ListResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /webhooks/{id}: + parameters: + - $ref: "#/components/parameters/WebhookId" + get: + tags: [Webhooks] + operationId: getWebhook + summary: Get a webhook by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + delete: + tags: [Webhooks] + operationId: deleteWebhook + summary: Delete a webhook by ID + responses: + "200": { $ref: "#/components/responses/ObjectResponse" } + "404": { $ref: "#/components/responses/NotFound" } + /webhooks/calls: + post: + tags: [Webhooks] + operationId: createCallWebhook + summary: Create a new webhook for calls + description: Creates a new webhook that triggers on events from calls. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WebhookCreate" } + responses: + "201": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /webhooks/messages: + post: + tags: [Webhooks] + operationId: createMessageWebhook + summary: Create a new webhook for messages + description: Creates a new webhook that triggers on events from messages. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WebhookCreate" } + responses: + "201": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /webhooks/call-summaries: + post: + tags: [Webhooks] + operationId: createCallSummaryWebhook + summary: Create a new webhook for call summaries + description: Creates a new webhook that triggers on call summary events. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WebhookCreate" } + responses: + "201": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } + /webhooks/call-transcripts: + post: + tags: [Webhooks] + operationId: createCallTranscriptWebhook + summary: Create a new webhook for call transcripts + description: Creates a new webhook that triggers on call transcript events. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WebhookCreate" } + responses: + "201": { $ref: "#/components/responses/ObjectResponse" } + "401": { $ref: "#/components/responses/Unauthorized" } +components: + securitySchemes: + # NO Bearer prefix — the raw API key is the entire Authorization header value. + # https://www.quo.com/docs/api-reference/authentication + ApiKeyAuth: + type: apiKey + in: header + name: Authorization + description: >- + Raw API key sent as the whole `Authorization` header value, with NO + `Bearer ` prefix. e.g. `Authorization: op_live_xxx`. + parameters: + CallId: + in: path + name: id + required: true + schema: { type: string, pattern: "^AC(.*)$" } + description: The unique identifier of the call. + CallIdPath: + in: path + name: callId + required: true + schema: { type: string, pattern: "^AC(.*)$" } + description: The unique identifier of the call. + MessageId: + in: path + name: id + required: true + schema: { type: string } + ContactId: + in: path + name: id + required: true + schema: { type: string } + PhoneNumberId: + in: path + name: id + required: true + schema: { type: string, pattern: "^PN(.*)$" } + UserId: + in: path + name: id + required: true + schema: { type: string, pattern: "^US(.*)$" } + WebhookId: + in: path + name: id + required: true + schema: { type: string } + responses: + ObjectResponse: + description: A single object, wrapped in a `data` envelope. + content: + application/json: + schema: + type: object + additionalProperties: true + properties: + data: { type: object, additionalProperties: true } + ListResponse: + description: A paginated list, wrapped in a `data` envelope with pagination cursors. + content: + application/json: + schema: + type: object + additionalProperties: true + properties: + data: + type: array + items: { type: object, additionalProperties: true } + totalItems: { type: integer } + nextPageToken: { type: string, nullable: true } + Unauthorized: + description: Missing or invalid API key. + NotFound: + description: Object not found. + schemas: + MessageSend: + type: object + required: [content, from, to] + properties: + content: { type: string, description: The text body of the message. } + from: + type: string + description: The sending Quo phone number id (^PN...) or E.164 number. + to: + type: array + items: { type: string } + description: Recipient phone numbers in E.164 format. + userId: { type: string, pattern: "^US(.*)$" } + setInboxStatus: + type: string + enum: [done] + ContactCreate: + type: object + required: [defaultFields] + properties: + defaultFields: + type: object + properties: + firstName: { type: string } + lastName: { type: string } + company: { type: string } + role: { type: string } + emails: + type: array + items: + type: object + properties: + name: { type: string } + value: { type: string, format: email } + phoneNumbers: + type: array + items: + type: object + properties: + name: { type: string } + value: { type: string } + customFields: + type: array + items: + type: object + additionalProperties: true + source: { type: string } + externalId: { type: string } + WebhookCreate: + type: object + required: [url, events] + properties: + url: { type: string, format: uri, description: The destination URL for webhook deliveries. } + events: + type: array + items: { type: string } + description: The event types to subscribe to. + label: { type: string } + userId: { type: string, pattern: "^US(.*)$" } + resourceIds: + type: array + items: { type: string } + description: Phone number ids (^PN...) or "*" for all. + status: + type: string + enum: [enabled, disabled] diff --git a/packages/v1-ready/quo/tests/api.test.js b/packages/v1-ready/quo/tests/api.test.js new file mode 100644 index 0000000..9a59320 --- /dev/null +++ b/packages/v1-ready/quo/tests/api.test.js @@ -0,0 +1,90 @@ +const { Api } = require('../api'); + +describe('Quo Api (offline)', () => { + describe('construction / auth', () => { + it('sets the Authorization header name and raw key (no Bearer prefix)', async () => { + const api = new Api({ api_key: 'op_test_123' }); + expect(api.api_key_name).toBe('Authorization'); + expect(api.api_key).toBe('op_test_123'); + expect(api.isAuthenticated()).toBe(true); + + const headers = await api.addAuthHeaders({}); + expect(headers.Authorization).toBe('op_test_123'); + expect(headers.Authorization).not.toMatch(/^Bearer /); + }); + + it('accepts access_token as an alias for api_key (credential rehydration)', () => { + const api = new Api({ access_token: 'op_from_credential' }); + expect(api.api_key).toBe('op_from_credential'); + expect(api.isAuthenticated()).toBe(true); + }); + + it('is unauthenticated with no key', () => { + const api = new Api({}); + expect(api.isAuthenticated()).toBe(false); + }); + + it('defaults the base URL to the production Quo/OpenPhone host', () => { + const api = new Api({ api_key: 'x' }); + expect(api.baseUrl).toBe('https://api.openphone.com/v1'); + }); + + it('honors a baseUrl override', () => { + const api = new Api({ api_key: 'x', baseUrl: 'https://example.test/v1' }); + expect(api.baseUrl).toBe('https://example.test/v1'); + }); + }); + + describe('URL builders', () => { + const api = new Api({ api_key: 'x' }); + + it('builds call endpoints', () => { + expect(api.URLs.calls).toBe('/calls'); + expect(api.URLs.callById('AC1')).toBe('/calls/AC1'); + expect(api.URLs.callRecordings('AC1')).toBe('/call-recordings/AC1'); + expect(api.URLs.callTranscript('AC1')).toBe('/call-transcripts/AC1'); + expect(api.URLs.callSummary('AC1')).toBe('/call-summaries/AC1'); + }); + + it('builds message and contact endpoints', () => { + expect(api.URLs.messages).toBe('/messages'); + expect(api.URLs.messageById('AC9')).toBe('/messages/AC9'); + expect(api.URLs.contacts).toBe('/contacts'); + }); + + it('exposes the read methods used by the Reevo integration', () => { + for (const m of ['listCalls', 'getCall', 'getCallRecordings', + 'getCallTranscript', 'getCallSummary', 'listMessages', + 'listPhoneNumbers']) { + expect(typeof api[m]).toBe('function'); + } + }); + }); + + describe('request wiring (mocked transport)', () => { + it('listCalls issues a GET to /calls with the query', async () => { + const api = new Api({ api_key: 'x' }); + const spy = jest + .spyOn(api, '_get') + .mockResolvedValue({ data: [] }); + + await api.listCalls({ phoneNumberId: 'PN1', maxResults: 50 }); + + expect(spy).toHaveBeenCalledWith({ + url: 'https://api.openphone.com/v1/calls', + query: { phoneNumberId: 'PN1', maxResults: 50 }, + }); + }); + + it('getCallTranscript issues a GET to /call-transcripts/{id}', async () => { + const api = new Api({ api_key: 'x' }); + const spy = jest.spyOn(api, '_get').mockResolvedValue({ dialogue: [] }); + + await api.getCallTranscript('AC42'); + + expect(spy).toHaveBeenCalledWith({ + url: 'https://api.openphone.com/v1/call-transcripts/AC42', + }); + }); + }); +}); diff --git a/packages/v1-ready/quo/tests/definition.test.js b/packages/v1-ready/quo/tests/definition.test.js new file mode 100644 index 0000000..8be10cd --- /dev/null +++ b/packages/v1-ready/quo/tests/definition.test.js @@ -0,0 +1,51 @@ +const { Definition } = require('../definition'); +const { Api } = require('../api'); + +describe('Quo Definition (offline)', () => { + it('is an API-key module named "quo"', () => { + expect(Definition.moduleName).toBe('quo'); + expect(Definition.getName()).toBe('quo'); + expect(Definition.API).toBe(Api); + expect(Definition.modelName).toBe('Quo'); + }); + + it('exposes an apiKey authorization form requiring api_key (masked)', () => { + const reqs = Definition.requiredAuthMethods.getAuthorizationRequirements(); + expect(reqs.type).toBe('apiKey'); + expect(reqs.data.jsonSchema.required).toContain('api_key'); + expect(reqs.data.uiSchema.api_key['ui:widget']).toBe('password'); + }); + + it('setAuthParams sets the key on the api instance', async () => { + const api = new Api({}); + expect(api.isAuthenticated()).toBe(false); + await Definition.requiredAuthMethods.setAuthParams(api, { api_key: 'k1' }); + expect(api.api_key).toBe('k1'); + expect(api.isAuthenticated()).toBe(true); + }); + + it('persists the key as access_token for rehydration', () => { + expect(Definition.requiredAuthMethods.apiPropertiesToPersist.credential) + .toEqual(expect.arrayContaining(['access_token'])); + }); + + it('testAuthRequest calls an authenticated endpoint', async () => { + const api = new Api({ api_key: 'k' }); + const spy = jest.spyOn(api, 'listPhoneNumbers').mockResolvedValue({ data: [] }); + await Definition.requiredAuthMethods.testAuthRequest(api); + expect(spy).toHaveBeenCalled(); + }); + + it('getEntityDetails derives identifiers from the first phone number', async () => { + const api = new Api({ api_key: 'k' }); + jest.spyOn(api, 'listPhoneNumbers').mockResolvedValue({ + data: [{ id: 'PN123', name: 'Sales Line', number: '+15555550100' }], + }); + const details = await api.constructor === Api // noop guard + ? await Definition.requiredAuthMethods.getEntityDetails(api, {}, {}, 'user-1') + : null; + expect(details.identifiers.externalId).toBe('PN123'); + expect(details.identifiers.user).toBe('user-1'); + expect(details.details.name).toBe('Sales Line'); + }); +}); diff --git a/packages/v1-ready/quo/tests/spec-sync.test.js b/packages/v1-ready/quo/tests/spec-sync.test.js new file mode 100644 index 0000000..5b80c75 --- /dev/null +++ b/packages/v1-ready/quo/tests/spec-sync.test.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { Api } = require('../api'); + +const spec = yaml.load( + fs.readFileSync(path.join(__dirname, '..', 'quo.openapi.yaml'), 'utf8') +); + +const specOperationIds = Object.values(spec.paths).flatMap((item) => + Object.entries(item) + .filter(([m]) => ['get', 'post', 'patch', 'put', 'delete'].includes(m)) + .map(([, op]) => op.operationId) +); + +const clientMethods = Object.getOwnPropertyNames(Api.prototype).filter( + (m) => typeof Api.prototype[m] === 'function' && m !== 'constructor' +); + +describe('OpenAPI spec ↔ client sync', () => { + it('every operationId has a matching client method', () => { + const missing = specOperationIds.filter( + (op) => !clientMethods.includes(op) + ); + expect(missing).toEqual([]); + }); + + it('the base server URL matches the client default baseUrl', () => { + const api = new Api({ api_key: 'x' }); + expect(spec.servers[0].url).toBe(api.baseUrl); + }); + + it('declares Authorization apiKey security with NO Bearer prefix', () => { + const scheme = spec.components.securitySchemes.ApiKeyAuth; + expect(scheme.type).toBe('apiKey'); + expect(scheme.in).toBe('header'); + // Quo/OpenPhone sends the raw key in `Authorization` — not `x-api-key`, + // and NOT as an http/bearer scheme. The client mirrors this via + // api_key_name = 'Authorization'. + expect(scheme.name).toBe('Authorization'); + expect(scheme.type).not.toBe('http'); + const api = new Api({ api_key: 'x' }); + expect(api.api_key_name).toBe(scheme.name); + }); +}); From 84014ecf6546def47a3265987a1204b7998fe616 Mon Sep 17 00:00:00 2001 From: "Sean Matthews (via Claude Code)" Date: Wed, 19 Aug 2026 06:36:46 +0000 Subject: [PATCH 2/2] Address adversarial review: auth callbacks, endpoint accuracy, stable identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes from three independent adversarial reviewers (issues masked by green tests): - fireflies: add setAuthParams (apiKey module had only getToken → core's callback threw TypeError in prod); migrate deprecated organizer_email/participant_email GraphQL args → organizers/participants arrays. - gong: implement setAuthParams (was a no-op that dropped form-entered Access Key/Secret); expose getAuthorizationRequirements in requiredAuthMethods (CLI form). - otter: implement setAuthParams (dropped form token); getConversationTranscript now delegates to getConversation(include:'transcript') and the fabricated standalone /transcript + /audio paths are removed; expose getAuthorizationRequirements. - fathom: replace constant externalId fallback ('fathom-account') with a sha256 api-key fingerprint (was colliding across accounts). - quo: replace unstable phone-id / 'quo-workspace' externalId with a sha256 key fingerprint; add real request-wiring test coverage for all 22 endpoint methods (was 2). All suites green after fixes (gong 27, fireflies 29, fathom 30, otter 29, quo 43); no dependency changes (lockfile unaffected). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JDh45c1vm91ySYtvVv9Z65 --- packages/v1-ready/fathom/definition.js | 27 ++++- .../v1-ready/fathom/tests/definition.test.js | 94 ++++++++++++++- packages/v1-ready/fireflies/api.js | 26 +++-- packages/v1-ready/fireflies/definition.js | 12 ++ .../fireflies/fireflies.operations.json | 22 ++-- packages/v1-ready/fireflies/tests/api.test.js | 38 ++++++ .../fireflies/tests/definition.test.js | 28 +++++ packages/v1-ready/gong/definition.js | 31 ++++- .../v1-ready/gong/tests/definition.test.js | 73 ++++++++++++ packages/v1-ready/otter/api.js | 23 ++-- packages/v1-ready/otter/definition.js | 20 +++- packages/v1-ready/otter/otter.openapi.yaml | 22 ---- packages/v1-ready/otter/tests/api.test.js | 14 +-- .../v1-ready/otter/tests/definition.test.js | 46 ++++++++ packages/v1-ready/quo/definition.js | 41 +++++-- packages/v1-ready/quo/tests/api.test.js | 109 ++++++++++++++++++ .../v1-ready/quo/tests/definition.test.js | 64 +++++++++- 17 files changed, 599 insertions(+), 91 deletions(-) diff --git a/packages/v1-ready/fathom/definition.js b/packages/v1-ready/fathom/definition.js index 6aa891f..643c141 100644 --- a/packages/v1-ready/fathom/definition.js +++ b/packages/v1-ready/fathom/definition.js @@ -1,4 +1,5 @@ require('dotenv').config(); +const crypto = require('crypto'); const { Api } = require('./api'); const { get } = require('@friggframework/core'); const config = require('./defaultConfig.json'); @@ -6,10 +7,19 @@ const config = require('./defaultConfig.json'); /** * Fathom is API-key authenticated (X-Api-Key header). There is no OAuth flow * and no dedicated "/me" identity endpoint on the public REST API, so identity - * is derived from the first meeting's `recorded_by` where available, falling - * back to a stable label. testAuthRequest simply performs an authenticated - * list call. + * is derived from the first meeting's `recorded_by` where available. + * + * When no meeting is available (empty list, missing `recorded_by.email`, or an + * API error) we fall back to a sha256 fingerprint of the API key itself — a + * stable, non-reversible, per-credential identifier that is always available + * and never collides across accounts. This mirrors the gong/otter modules. + * + * There is deliberately NO shared constant fallback: two different customers + * must never map to the same entity/credential. */ +const keyFingerprint = (apiKey) => + crypto.createHash('sha256').update(String(apiKey)).digest('hex'); + async function resolveAccountIdentity(api) { try { const result = await api.listMeetings({}); @@ -22,9 +32,16 @@ async function resolveAccountIdentity(api) { }; } } catch (e) { - // fall through to a stable default identity + // fall through to the per-credential key fingerprint + } + + const apiKey = api?.api_key; + if (!apiKey) { + throw new Error( + 'Fathom: cannot derive a stable account identity — no meeting identity and no API key to fingerprint.' + ); } - return { externalId: 'fathom-account', name: 'Fathom' }; + return { externalId: keyFingerprint(apiKey), name: 'Fathom' }; } const Definition = { diff --git a/packages/v1-ready/fathom/tests/definition.test.js b/packages/v1-ready/fathom/tests/definition.test.js index e6b3b98..83ddc37 100644 --- a/packages/v1-ready/fathom/tests/definition.test.js +++ b/packages/v1-ready/fathom/tests/definition.test.js @@ -1,6 +1,12 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); const { Definition } = require('../definition'); const { Api } = require('../api'); +const sha256 = (v) => + crypto.createHash('sha256').update(String(v)).digest('hex'); + // Offline: exercises the Definition auth methods with a fake api whose network // calls are stubbed. No real HTTP. describe('Fathom Definition', () => { @@ -59,8 +65,8 @@ describe('Fathom Definition', () => { expect(details.details.name).toBe('Left Hook'); }); - it('getEntityDetails falls back to a stable identity with no meetings', async () => { - const api = new Api({ api_key: 'k' }); + it('getEntityDetails falls back to the api-key fingerprint with no meetings', async () => { + const api = new Api({ api_key: 'key-abc' }); api.listMeetings = async () => ({ items: [] }); const details = await Definition.requiredAuthMethods.getEntityDetails( api, @@ -68,11 +74,27 @@ describe('Fathom Definition', () => { {}, 'user-2' ); - expect(details.identifiers.externalId).toBe('fathom-account'); + // Genuinely per-account: a sha256 of the key, NOT a shared constant. + expect(details.identifiers.externalId).toBe(sha256('key-abc')); + expect(details.identifiers.externalId).not.toBe('fathom-account'); }); - it('getEntityDetails stays stable if the API throws', async () => { - const api = new Api({ api_key: 'k' }); + it('getEntityDetails falls back to the fingerprint when recorded_by has no email', async () => { + const api = new Api({ api_key: 'key-xyz' }); + api.listMeetings = async () => ({ + items: [{ recording_id: 1, recorded_by: { name: 'Anon' } }], + }); + const details = await Definition.requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-2b' + ); + expect(details.identifiers.externalId).toBe(sha256('key-xyz')); + }); + + it('getEntityDetails uses the fingerprint if the API throws', async () => { + const api = new Api({ api_key: 'key-boom' }); api.listMeetings = async () => { throw new Error('boom'); }; @@ -82,7 +104,67 @@ describe('Fathom Definition', () => { {}, 'user-3' ); - expect(details.identifiers.externalId).toBe('fathom-account'); + expect(details.identifiers.externalId).toBe(sha256('key-boom')); + }); + + it('fingerprint fallback is STABLE for the same key and UNIQUE across keys', async () => { + const makeApi = (key) => { + const api = new Api({ api_key: key }); + api.listMeetings = async () => ({ items: [] }); + return api; + }; + const idFor = async (key) => + ( + await Definition.requiredAuthMethods.getEntityDetails( + makeApi(key), + {}, + {}, + 'u' + ) + ).identifiers.externalId; + const credIdFor = async (key) => + ( + await Definition.requiredAuthMethods.getCredentialDetails( + makeApi(key), + 'u' + ) + ).identifiers.externalId; + + // Same key -> same id (stable). + expect(await idFor('cust-A-key')).toBe(await idFor('cust-A-key')); + // Two different customers -> different ids (no collision). + expect(await idFor('cust-A-key')).not.toBe(await idFor('cust-B-key')); + // Entity and credential agree for one account. + expect(await idFor('cust-A-key')).toBe(await credIdFor('cust-A-key')); + }); + + it('getCredentialDetails falls back to the api-key fingerprint', async () => { + const api = new Api({ api_key: 'cred-key' }); + api.listMeetings = async () => ({ items: [] }); + const details = + await Definition.requiredAuthMethods.getCredentialDetails( + api, + 'user-c' + ); + expect(details.identifiers.externalId).toBe(sha256('cred-key')); + }); + + it('throws rather than returning a shared constant when nothing can be derived', async () => { + const api = new Api({}); + api.api_key = null; + api.listMeetings = async () => ({ items: [] }); + await expect( + Definition.requiredAuthMethods.getEntityDetails(api, {}, {}, 'u') + ).rejects.toThrow(/stable account identity/i); + }); + + it('retains NO hardcoded constant fallback in the source', () => { + const src = fs.readFileSync( + path.join(__dirname, '..', 'definition.js'), + 'utf8' + ); + expect(src).not.toMatch(/externalId:\s*['"]fathom-account['"]/); + expect(src).not.toMatch(/return\s*\{\s*externalId:\s*['"]fathom-account['"]/); }); it('testAuthRequest performs an authenticated list call', async () => { diff --git a/packages/v1-ready/fireflies/api.js b/packages/v1-ready/fireflies/api.js index 01b7ec2..f25e0f0 100644 --- a/packages/v1-ready/fireflies/api.js +++ b/packages/v1-ready/fireflies/api.js @@ -124,8 +124,14 @@ class Api extends ApiKeyRequester { /** * List meeting transcripts, newest first. All args optional. - * `transcripts(limit, skip, fromDate, toDate, organizerEmail, - * participantEmail, keyword, mine)` + * `transcripts(limit, skip, fromDate, toDate, organizers, participants, + * keyword, mine)` + * + * Note: the public signature still accepts a single `organizerEmail` / + * `participantEmail` string; Fireflies deprecated the scalar + * `organizer_email` / `participant_email` args in favor of the array + * `organizers: [String]` / `participants: [String]`, so a single email is + * wrapped in an array internally. */ async listTranscripts(params = {}) { const query = `query ListTranscripts( @@ -133,8 +139,8 @@ class Api extends ApiKeyRequester { $skip: Int $fromDate: DateTime $toDate: DateTime - $organizerEmail: String - $participantEmail: String + $organizers: [String] + $participants: [String] $keyword: String $mine: Boolean ) { @@ -143,8 +149,8 @@ class Api extends ApiKeyRequester { skip: $skip fromDate: $fromDate toDate: $toDate - organizer_email: $organizerEmail - participant_email: $participantEmail + organizers: $organizers + participants: $participants keyword: $keyword mine: $mine ) { @@ -174,9 +180,13 @@ class Api extends ApiKeyRequester { if (params.fromDate !== undefined) variables.fromDate = params.fromDate; if (params.toDate !== undefined) variables.toDate = params.toDate; if (params.organizerEmail !== undefined) - variables.organizerEmail = params.organizerEmail; + variables.organizers = Array.isArray(params.organizerEmail) + ? params.organizerEmail + : [params.organizerEmail]; if (params.participantEmail !== undefined) - variables.participantEmail = params.participantEmail; + variables.participants = Array.isArray(params.participantEmail) + ? params.participantEmail + : [params.participantEmail]; if (params.keyword !== undefined) variables.keyword = params.keyword; if (params.mine !== undefined) variables.mine = params.mine; diff --git a/packages/v1-ready/fireflies/definition.js b/packages/v1-ready/fireflies/definition.js index f18bff1..654c71c 100644 --- a/packages/v1-ready/fireflies/definition.js +++ b/packages/v1-ready/fireflies/definition.js @@ -13,6 +13,18 @@ const Definition = { getAuthorizationRequirements: (api) => api.getAuthorizationRequirements(), + // Core's process-authorization-callback calls setAuthParams(api, params) + // for every non-oauth2 module. Without it, the real callback throws + // `TypeError: setAuthParams is not a function`. Set the key onto the api. + setAuthParams: async (api, params) => { + const key = + get(params, 'api_key', null) || + get(params, 'access_token', null) || + get(params?.data || {}, 'api_key', null) || + get(params?.data || {}, 'access_token', null); + if (key) api.setApiKey(key); + }, + // API-key exchange is a no-op — the key IS the credential. Persist it. getToken: async (api, params) => { const apiKey = diff --git a/packages/v1-ready/fireflies/fireflies.operations.json b/packages/v1-ready/fireflies/fireflies.operations.json index 491f6c4..0396c13 100644 --- a/packages/v1-ready/fireflies/fireflies.operations.json +++ b/packages/v1-ready/fireflies/fireflies.operations.json @@ -54,8 +54,8 @@ { "name": "skip", "type": "Int" }, { "name": "fromDate", "type": "DateTime" }, { "name": "toDate", "type": "DateTime" }, - { "name": "organizerEmail", "type": "String" }, - { "name": "participantEmail", "type": "String" }, + { "name": "organizers", "type": "[String]" }, + { "name": "participants", "type": "[String]" }, { "name": "keyword", "type": "String" }, { "name": "mine", "type": "Boolean" } ], @@ -64,8 +64,8 @@ "skip": "$skip", "fromDate": "$fromDate", "toDate": "$toDate", - "organizer_email": "$organizerEmail", - "participant_email": "$participantEmail", + "organizers": "$organizers", + "participants": "$participants", "keyword": "$keyword", "mine": "$mine" }, @@ -82,9 +82,9 @@ "transcript_url", "meeting_attendees { displayName email name phoneNumber location }" ], - "query": "query ListTranscripts( $limit: Int $skip: Int $fromDate: DateTime $toDate: DateTime $organizerEmail: String $participantEmail: String $keyword: String $mine: Boolean ) { transcripts( limit: $limit skip: $skip fromDate: $fromDate toDate: $toDate organizer_email: $organizerEmail participant_email: $participantEmail keyword: $keyword mine: $mine ) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url meeting_attendees { displayName email name phoneNumber location } } }", + "query": "query ListTranscripts( $limit: Int $skip: Int $fromDate: DateTime $toDate: DateTime $organizers: [String] $participants: [String] $keyword: String $mine: Boolean ) { transcripts( limit: $limit skip: $skip fromDate: $fromDate toDate: $toDate organizers: $organizers participants: $participants keyword: $keyword mine: $mine ) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url meeting_attendees { displayName email name phoneNumber location } } }", "returns": "data.transcripts", - "grounding": "Docs confirm transcripts(...) accepts limit (Int), skip (Int), fromDate (DateTime), toDate (DateTime), organizer_email (String), participant_email (String), keyword (String), mine (Boolean). GraphQL variable aliases ($organizerEmail -> organizer_email, $participantEmail -> participant_email) are client-side names; the wire argument names are the snake_case ones the docs list. meeting_attendees fields (displayName, email, name, phoneNumber, location) confirmed on the Transcript type." + "grounding": "Docs confirm transcripts(...) accepts limit (Int), skip (Int), fromDate (DateTime), toDate (DateTime), organizers ([String]), participants ([String]), keyword (String), mine (Boolean). Fireflies deprecated the scalar organizer_email / participant_email arguments in favor of the array organizers / participants arguments; the client's public methods still accept a single email and wrap it in an array internally. meeting_attendees fields (displayName, email, name, phoneNumber, location) confirmed on the Transcript type." }, { "operationId": "getTranscript", @@ -147,8 +147,8 @@ { "name": "skip", "type": "Int" }, { "name": "fromDate", "type": "DateTime" }, { "name": "toDate", "type": "DateTime" }, - { "name": "organizerEmail", "type": "String" }, - { "name": "participantEmail", "type": "String" }, + { "name": "organizers", "type": "[String]" }, + { "name": "participants", "type": "[String]" }, { "name": "keyword", "type": "String" }, { "name": "mine", "type": "Boolean" } ], @@ -157,8 +157,8 @@ "skip": "$skip", "fromDate": "$fromDate", "toDate": "$toDate", - "organizer_email": "$organizerEmail", - "participant_email": "$participantEmail", + "organizers": "$organizers", + "participants": "$participants", "keyword": "$keyword", "mine": "$mine" }, @@ -175,7 +175,7 @@ "transcript_url", "meeting_attendees { displayName email name phoneNumber location }" ], - "query": "query ListTranscripts( $limit: Int $skip: Int $fromDate: DateTime $toDate: DateTime $organizerEmail: String $participantEmail: String $keyword: String $mine: Boolean ) { transcripts( limit: $limit skip: $skip fromDate: $fromDate toDate: $toDate organizer_email: $organizerEmail participant_email: $participantEmail keyword: $keyword mine: $mine ) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url meeting_attendees { displayName email name phoneNumber location } } }", + "query": "query ListTranscripts( $limit: Int $skip: Int $fromDate: DateTime $toDate: DateTime $organizers: [String] $participants: [String] $keyword: String $mine: Boolean ) { transcripts( limit: $limit skip: $skip fromDate: $fromDate toDate: $toDate organizers: $organizers participants: $participants keyword: $keyword mine: $mine ) { id title date dateString duration host_email organizer_email participants meeting_link transcript_url meeting_attendees { displayName email name phoneNumber location } } }", "returns": "data.transcripts", "grounding": "Delegates to listTranscripts with keyword preset, so it emits the exact same `transcripts` query. `keyword` is the documented (non-deprecated) search argument." } diff --git a/packages/v1-ready/fireflies/tests/api.test.js b/packages/v1-ready/fireflies/tests/api.test.js index d773a4e..77a39cf 100644 --- a/packages/v1-ready/fireflies/tests/api.test.js +++ b/packages/v1-ready/fireflies/tests/api.test.js @@ -83,6 +83,44 @@ describe('Fireflies Api', () => { const body = JSON.parse(captured.options.body); expect(body.variables).toEqual({ limit: 10 }); }); + + it('uses the array organizers/participants args (not the deprecated scalar *_email args) and wraps a single email', async () => { + const captured = {}; + const api = makeApi(captured, { transcripts: [] }); + + await api.listTranscripts({ + organizerEmail: 'host@example.com', + participantEmail: 'guest@example.com', + }); + + const body = JSON.parse(captured.options.body); + // deprecated scalar args must be gone from the query text + expect(body.query).not.toContain('organizer_email:'); + expect(body.query).not.toContain('participant_email:'); + expect(body.query).toContain('organizers: $organizers'); + expect(body.query).toContain('participants: $participants'); + expect(body.query).toContain('$organizers: [String]'); + expect(body.query).toContain('$participants: [String]'); + // a single email is wrapped in an array on the wire + expect(body.variables).toEqual({ + organizers: ['host@example.com'], + participants: ['guest@example.com'], + }); + }); + + it('passes an array of emails through unchanged', async () => { + const captured = {}; + const api = makeApi(captured, { transcripts: [] }); + + await api.listTranscripts({ + organizerEmail: ['a@example.com', 'b@example.com'], + }); + + const body = JSON.parse(captured.options.body); + expect(body.variables).toEqual({ + organizers: ['a@example.com', 'b@example.com'], + }); + }); }); describe('getTranscript()', () => { diff --git a/packages/v1-ready/fireflies/tests/definition.test.js b/packages/v1-ready/fireflies/tests/definition.test.js index ba5b8e5..286ff94 100644 --- a/packages/v1-ready/fireflies/tests/definition.test.js +++ b/packages/v1-ready/fireflies/tests/definition.test.js @@ -39,6 +39,34 @@ describe('Fireflies Definition', () => { }); }); + describe('setAuthParams()', () => { + it('exists (core calls it on the real non-oauth2 callback)', () => { + expect(typeof requiredAuthMethods.setAuthParams).toBe('function'); + }); + + it('sets the api key from api_key', async () => { + const api = makeStubApi(validUser); + await requiredAuthMethods.setAuthParams(api, { + api_key: 'sk_from_callback', + }); + expect(api.api_key).toBe('sk_from_callback'); + }); + + it('falls back to access_token and to nested data', async () => { + const a1 = makeStubApi(validUser); + await requiredAuthMethods.setAuthParams(a1, { + access_token: 'sk_access', + }); + expect(a1.api_key).toBe('sk_access'); + + const a2 = makeStubApi(validUser); + await requiredAuthMethods.setAuthParams(a2, { + data: { api_key: 'sk_nested' }, + }); + expect(a2.api_key).toBe('sk_nested'); + }); + }); + describe('testAuthRequest()', () => { it('resolves with the user payload for a valid key', async () => { const api = makeStubApi(validUser); diff --git a/packages/v1-ready/gong/definition.js b/packages/v1-ready/gong/definition.js index c7c4602..48f9bad 100644 --- a/packages/v1-ready/gong/definition.js +++ b/packages/v1-ready/gong/definition.js @@ -1,6 +1,7 @@ require('dotenv').config(); const crypto = require('crypto'); const { Api } = require('./api'); +const { get } = require('@friggframework/core'); const config = require('./defaultConfig.json'); // Gong issues a static Access Key + Secret (Basic auth, no OAuth), so there is @@ -18,7 +19,35 @@ const Definition = { moduleName: config.name, modelName: 'Gong', requiredAuthMethods: { - setAuthParams: async function (api, params) {}, + // Renders the interactive CLI / hosted auth form for this Basic-auth + // (API-key style) module. Delegates to the Api class definition. + getAuthorizationRequirements: async (api) => + api.getAuthorizationRequirements(), + + // On the auth callback the user-entered Access Key + Secret arrive in + // `params` (flat or nested under `params.data`). Wire them onto the + // BasicAuthRequester so the Base64(accessKey:accessKeySecret) header is + // built for testAuthRequest and every subsequent call. Without this the + // form credentials are silently dropped and requests run unauthenticated. + setAuthParams: async function (api, params) { + const data = (params && params.data) || {}; + const accessKey = + get(params, 'access_key', null) || + get(data, 'access_key', null); + const accessKeySecret = + get(params, 'access_key_secret', null) || + get(data, 'access_key_secret', null); + + if (accessKey) { + api.access_key = accessKey; + api.username = accessKey; + } + if (accessKeySecret) { + api.access_key_secret = accessKeySecret; + api.password = accessKeySecret; + } + return api; + }, getEntityDetails: async function ( api, callbackParams, diff --git a/packages/v1-ready/gong/tests/definition.test.js b/packages/v1-ready/gong/tests/definition.test.js index c9aad81..e8a99a5 100644 --- a/packages/v1-ready/gong/tests/definition.test.js +++ b/packages/v1-ready/gong/tests/definition.test.js @@ -1,4 +1,5 @@ const { Definition } = require('../definition'); +const { Api } = require('../api'); const { requiredAuthMethods } = Definition; @@ -76,6 +77,78 @@ describe('Gong Definition', () => { }); }); + describe('getAuthorizationRequirements', () => { + it('delegates to the api form definition so the CLI renders it', async () => { + let called = false; + const api = { + getAuthorizationRequirements: () => { + called = true; + return { type: 'basic', data: { jsonSchema: {} } }; + }, + }; + const reqs = + await requiredAuthMethods.getAuthorizationRequirements(api); + expect(called).toBe(true); + expect(reqs.type).toBe('basic'); + }); + }); + + describe('setAuthParams', () => { + it('wires the form access key onto the Basic-auth username and password', async () => { + const api = {}; + await requiredAuthMethods.setAuthParams(api, { + access_key: 'ak-123', + access_key_secret: 'secret-xyz', + }); + + // The request-wiring the header is built from. + expect(api.username).toBe('ak-123'); + expect(api.password).toBe('secret-xyz'); + // Gong-native names kept in sync for fingerprinting/persistence. + expect(api.access_key).toBe('ak-123'); + expect(api.access_key_secret).toBe('secret-xyz'); + }); + + it('reads form fields nested under params.data', async () => { + const api = {}; + await requiredAuthMethods.setAuthParams(api, { + data: { + access_key: 'nested-ak', + access_key_secret: 'nested-secret', + }, + }); + expect(api.username).toBe('nested-ak'); + expect(api.password).toBe('nested-secret'); + }); + + it('does not overwrite existing creds when params are empty', async () => { + const api = { username: 'existing', password: 'existing-secret' }; + await requiredAuthMethods.setAuthParams(api, {}); + expect(api.username).toBe('existing'); + expect(api.password).toBe('existing-secret'); + }); + + it('makes the credentials usable for a real Basic-auth request', async () => { + // Mirrors what the framework does on the callback: instantiate the + // real Api, then apply the form params via setAuthParams. + const api = new Api({}); + expect(api.username).toBeNull(); + expect(api.password).toBeNull(); + + await requiredAuthMethods.setAuthParams(api, { + access_key: 'AK', + access_key_secret: 'SK', + }); + + // BasicAuthRequester builds Base64(username:password) from these. + expect(api.username).toBe('AK'); + expect(api.password).toBe('SK'); + expect( + Buffer.from(`${api.username}:${api.password}`).toString('base64') + ).toBe(Buffer.from('AK:SK').toString('base64')); + }); + }); + describe('testAuthRequest', () => { it('delegates to the api testAuth check', async () => { let called = false; diff --git a/packages/v1-ready/otter/api.js b/packages/v1-ready/otter/api.js index c6f5041..702006c 100644 --- a/packages/v1-ready/otter/api.js +++ b/packages/v1-ready/otter/api.js @@ -44,8 +44,6 @@ class Api extends ApiKeyRequester { channels: '/channels', conversations: '/conversations', conversationById: (id) => `/conversations/${id}`, - conversationTranscript: (id) => `/conversations/${id}/transcript`, - conversationAudio: (id) => `/conversations/${id}/audio`, }; } @@ -118,18 +116,17 @@ class Api extends ApiKeyRequester { }); } - /** Get the full transcript for a conversation. */ + /** + * Get the full transcript for a conversation. + * + * Otter exposes the transcript as an `include` option on the + * conversation-detail endpoint, not as a standalone path — so this + * delegates to `getConversation(id, { include: 'transcript' })`. The method + * name is preserved because consumers (e.g. the reevo--frigg app's + * `get_transcript` tool) call it directly. + */ async getConversationTranscript(conversationId) { - return this._get({ - url: this.baseUrl + this.URLs.conversationTranscript(conversationId), - }); - } - - /** Get the audio (download URL / stream reference) for a conversation. */ - async getConversationAudio(conversationId) { - return this._get({ - url: this.baseUrl + this.URLs.conversationAudio(conversationId), - }); + return this.getConversation(conversationId, { include: 'transcript' }); } // ---- Auth check --------------------------------------------------------- diff --git a/packages/v1-ready/otter/definition.js b/packages/v1-ready/otter/definition.js index 55e1cd3..e05fbdd 100644 --- a/packages/v1-ready/otter/definition.js +++ b/packages/v1-ready/otter/definition.js @@ -1,5 +1,6 @@ require('dotenv').config(); const crypto = require('crypto'); +const { get } = require('@friggframework/core'); const { Api } = require('./api'); const config = require('./defaultConfig.json'); @@ -18,7 +19,24 @@ const Definition = { moduleName: config.name, modelName: 'Otter', requiredAuthMethods: { - setAuthParams: async function (api, params) {}, + setAuthParams: async function (api, params) { + // The form submits the user-entered API token; rehydrate the api + // client exactly as its constructor does — store the raw token and + // set the Authorization header value to `Bearer `, stripping + // any pre-existing "Bearer " prefix so it is never doubled. + const raw = ( + get(params, 'api_token', null) || + get(params, 'api_key', null) || + '' + ).replace(/^Bearer\s+/i, ''); + if (raw) { + api.api_token = raw; + api.setApiKey('Bearer ' + raw); + } + }, + getAuthorizationRequirements: async function (api) { + return api.getAuthorizationRequirements(); + }, getEntityDetails: async function ( api, callbackParams, diff --git a/packages/v1-ready/otter/otter.openapi.yaml b/packages/v1-ready/otter/otter.openapi.yaml index 8a8ddfd..6e79409 100644 --- a/packages/v1-ready/otter/otter.openapi.yaml +++ b/packages/v1-ready/otter/otter.openapi.yaml @@ -100,28 +100,6 @@ paths: "200": { $ref: "#/components/responses/ObjectResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } - /conversations/{conversation_id}/transcript: - parameters: - - $ref: "#/components/parameters/ConversationId" - get: - tags: [Conversations] - operationId: getConversationTranscript - summary: Get the full transcript for a conversation - responses: - "200": { $ref: "#/components/responses/ObjectResponse" } - "401": { $ref: "#/components/responses/Unauthorized" } - "404": { $ref: "#/components/responses/NotFound" } - /conversations/{conversation_id}/audio: - parameters: - - $ref: "#/components/parameters/ConversationId" - get: - tags: [Conversations] - operationId: getConversationAudio - summary: Get the audio (download URL / stream reference) for a conversation - responses: - "200": { $ref: "#/components/responses/ObjectResponse" } - "401": { $ref: "#/components/responses/Unauthorized" } - "404": { $ref: "#/components/responses/NotFound" } components: securitySchemes: BearerAuth: diff --git a/packages/v1-ready/otter/tests/api.test.js b/packages/v1-ready/otter/tests/api.test.js index 513073c..90ac302 100644 --- a/packages/v1-ready/otter/tests/api.test.js +++ b/packages/v1-ready/otter/tests/api.test.js @@ -103,20 +103,14 @@ describe('Otter Api', () => { expect(api.sent[0].query).toEqual({ include: 'all' }); }); - it('getConversationTranscript gets the nested transcript path', async () => { + it('getConversationTranscript delegates to getConversation with include=transcript', async () => { const api = makeApi(); await api.getConversationTranscript('conv-1'); + expect(api.sent[0].method).toBe('GET'); expect(api.sent[0].url).toBe( - 'https://api.otter.ai/v1/conversations/conv-1/transcript' - ); - }); - - it('getConversationAudio gets the nested audio path', async () => { - const api = makeApi(); - await api.getConversationAudio('conv-1'); - expect(api.sent[0].url).toBe( - 'https://api.otter.ai/v1/conversations/conv-1/audio' + 'https://api.otter.ai/v1/conversations/conv-1' ); + expect(api.sent[0].query).toEqual({ include: 'transcript' }); }); it('testAuth performs a lightweight workspace fetch', async () => { diff --git a/packages/v1-ready/otter/tests/definition.test.js b/packages/v1-ready/otter/tests/definition.test.js index 58705c6..6875e67 100644 --- a/packages/v1-ready/otter/tests/definition.test.js +++ b/packages/v1-ready/otter/tests/definition.test.js @@ -57,6 +57,52 @@ describe('Otter Definition', () => { }); }); + describe('setAuthParams', () => { + it('rehydrates the api from a form-submitted api_token', async () => { + const calls = []; + const api = { setApiKey: (v) => calls.push(v) }; + await requiredAuthMethods.setAuthParams(api, { + api_token: 'form-key', + }); + expect(api.api_token).toBe('form-key'); + expect(calls).toEqual(['Bearer form-key']); + }); + + it('accepts the token under api_key too', async () => { + const api = { setApiKey: jest.fn() }; + await requiredAuthMethods.setAuthParams(api, { api_key: 'k2' }); + expect(api.api_token).toBe('k2'); + expect(api.setApiKey).toHaveBeenCalledWith('Bearer k2'); + }); + + it('strips a pre-existing Bearer prefix so it is never doubled', async () => { + const api = { setApiKey: jest.fn() }; + await requiredAuthMethods.setAuthParams(api, { + api_token: 'Bearer k3', + }); + expect(api.api_token).toBe('k3'); + expect(api.setApiKey).toHaveBeenCalledWith('Bearer k3'); + }); + + it('does nothing when no token is supplied', async () => { + const api = { setApiKey: jest.fn() }; + await requiredAuthMethods.setAuthParams(api, {}); + expect(api.api_token).toBeUndefined(); + expect(api.setApiKey).not.toHaveBeenCalled(); + }); + }); + + describe('getAuthorizationRequirements', () => { + it('delegates to the api getAuthorizationRequirements', async () => { + const api = { + getAuthorizationRequirements: () => ({ type: 'apiKey' }), + }; + await expect( + requiredAuthMethods.getAuthorizationRequirements(api) + ).resolves.toEqual({ type: 'apiKey' }); + }); + }); + describe('testAuthRequest', () => { it('delegates to the api testAuth check', async () => { let called = false; diff --git a/packages/v1-ready/quo/definition.js b/packages/v1-ready/quo/definition.js index 59b5c78..ff880f9 100644 --- a/packages/v1-ready/quo/definition.js +++ b/packages/v1-ready/quo/definition.js @@ -1,8 +1,25 @@ require('dotenv').config(); +const crypto = require('crypto'); const { get } = require('@friggframework/core'); const { Api } = require('./api'); const config = require('./defaultConfig.json'); +// Quo (OpenPhone) authenticates with a single static API key and returns no +// stable account/workspace id at auth time. We derive a stable, non-reversible +// identifier from the API key itself (mirroring the gong/otter modules) so the +// same credential always maps to the same entity/credential. A phone-number id +// is NOT stable — reordering or deleting a number would change it and fork a new +// entity on re-auth — and a shared constant collides across accounts, so +// neither is acceptable as the externalId. +const keyFingerprint = (apiKey) => { + if (!apiKey || typeof apiKey !== 'string' || apiKey.trim().length === 0) { + throw new Error( + 'Quo: cannot derive a stable externalId — no API key present on the api instance.' + ); + } + return crypto.createHash('sha256').update(apiKey).digest('hex'); +}; + const Definition = { API: Api, getName: () => config.name, @@ -49,21 +66,27 @@ const Definition = { testAuthRequest: async (api) => api.listPhoneNumbers(), getEntityDetails: async (api, callbackParams, tokenResponse, userId) => { - const phoneNumbers = await api.listPhoneNumbers(); - const first = get(phoneNumbers, 'data', [])[0] || {}; - const externalId = first.id || 'quo-workspace'; + // externalId is a stable sha256 fingerprint of the API key — unique + // per credential and unchanged across phone-number churn. The + // workspace's first phone number is still used only for a friendly + // display name (best-effort), never for identity. + const externalId = keyFingerprint(api.api_key); + let name = 'Quo Workspace'; + try { + const phoneNumbers = await api.listPhoneNumbers(); + const first = get(phoneNumbers, 'data', [])[0] || {}; + name = first.name || first.number || name; + } catch (e) { + // Display name is non-critical; identity does not depend on it. + } return { identifiers: { externalId, user: userId }, - details: { - name: first.name || first.number || 'Quo Workspace', - }, + details: { name }, }; }, getCredentialDetails: async (api, userId) => { - const phoneNumbers = await api.listPhoneNumbers(); - const first = get(phoneNumbers, 'data', [])[0] || {}; - const externalId = first.id || 'quo-workspace'; + const externalId = keyFingerprint(api.api_key); return { identifiers: { externalId, user: userId }, details: {}, diff --git a/packages/v1-ready/quo/tests/api.test.js b/packages/v1-ready/quo/tests/api.test.js index 9a59320..97fc59a 100644 --- a/packages/v1-ready/quo/tests/api.test.js +++ b/packages/v1-ready/quo/tests/api.test.js @@ -87,4 +87,113 @@ describe('Quo Api (offline)', () => { }); }); }); + + // Full request-wiring coverage: every endpoint method asserts the ACTUAL + // transport verb (_get / _post / _delete), URL, and (for writes) the body, + // so a wrong path/verb/body would fail — not just a missing method. + describe('request wiring — every endpoint (mocked transport)', () => { + const BASE = 'https://api.openphone.com/v1'; + const JSON_HEADERS = { 'Content-Type': 'application/json' }; + + let api; + let getSpy; + let postSpy; + let deleteSpy; + + beforeEach(() => { + api = new Api({ api_key: 'x' }); + getSpy = jest.spyOn(api, '_get').mockResolvedValue({ data: [] }); + postSpy = jest.spyOn(api, '_post').mockResolvedValue({ id: 'new' }); + deleteSpy = jest.spyOn(api, '_delete').mockResolvedValue({}); + }); + + // ---- GET endpoints: [method, args, expected {url, query?}] ---- + const GET_CASES = [ + ['getCall', ['AC1'], { url: `${BASE}/calls/AC1` }], + ['getCallRecordings', ['AC1'], { url: `${BASE}/call-recordings/AC1` }], + ['getCallSummary', ['AC1'], { url: `${BASE}/call-summaries/AC1` }], + [ + 'listMessages', + [{ phoneNumberId: 'PN1', maxResults: 25 }], + { url: `${BASE}/messages`, query: { phoneNumberId: 'PN1', maxResults: 25 } }, + ], + ['getMessage', ['AC9'], { url: `${BASE}/messages/AC9` }], + [ + 'listContacts', + [{ maxResults: 10 }], + { url: `${BASE}/contacts`, query: { maxResults: 10 } }, + ], + ['getContact', ['CT7'], { url: `${BASE}/contacts/CT7` }], + [ + 'listPhoneNumbers', + [{ userId: 'US1' }], + { url: `${BASE}/phone-numbers`, query: { userId: 'US1' } }, + ], + ['getPhoneNumber', ['PN5'], { url: `${BASE}/phone-numbers/PN5` }], + [ + 'listUsers', + [{ maxResults: 5 }], + { url: `${BASE}/users`, query: { maxResults: 5 } }, + ], + ['getUser', ['US3'], { url: `${BASE}/users/US3` }], + [ + 'listWebhooks', + [{ userId: 'US2' }], + { url: `${BASE}/webhooks`, query: { userId: 'US2' } }, + ], + ['getWebhook', ['WH1'], { url: `${BASE}/webhooks/WH1` }], + ]; + + it.each(GET_CASES)( + '%s issues a GET to the correct URL (and query)', + async (method, args, expected) => { + await api[method](...args); + expect(getSpy).toHaveBeenCalledTimes(1); + expect(getSpy).toHaveBeenCalledWith(expected); + expect(postSpy).not.toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + } + ); + + // ---- POST endpoints: [method, bodyArg, expectedUrl] ---- + const POST_CASES = [ + ['sendMessage', { content: 'hi', to: ['+15555550100'] }, `${BASE}/messages`], + ['createContact', { firstName: 'Ada' }, `${BASE}/contacts`], + ['createCallWebhook', { url: 'https://cb/1' }, `${BASE}/webhooks/calls`], + ['createMessageWebhook', { url: 'https://cb/2' }, `${BASE}/webhooks/messages`], + [ + 'createCallSummaryWebhook', + { url: 'https://cb/3' }, + `${BASE}/webhooks/call-summaries`, + ], + [ + 'createCallTranscriptWebhook', + { url: 'https://cb/4' }, + `${BASE}/webhooks/call-transcripts`, + ], + ]; + + it.each(POST_CASES)( + '%s issues a POST to the correct URL with the JSON body', + async (method, body, url) => { + await api[method](body); + expect(postSpy).toHaveBeenCalledTimes(1); + expect(postSpy).toHaveBeenCalledWith({ + url, + headers: JSON_HEADERS, + body, + }); + expect(getSpy).not.toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + } + ); + + it('deleteWebhook issues a DELETE to /webhooks/{id}', async () => { + await api.deleteWebhook('WH9'); + expect(deleteSpy).toHaveBeenCalledTimes(1); + expect(deleteSpy).toHaveBeenCalledWith({ url: `${BASE}/webhooks/WH9` }); + expect(getSpy).not.toHaveBeenCalled(); + expect(postSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/v1-ready/quo/tests/definition.test.js b/packages/v1-ready/quo/tests/definition.test.js index 8be10cd..aa7640d 100644 --- a/packages/v1-ready/quo/tests/definition.test.js +++ b/packages/v1-ready/quo/tests/definition.test.js @@ -36,16 +36,70 @@ describe('Quo Definition (offline)', () => { expect(spy).toHaveBeenCalled(); }); - it('getEntityDetails derives identifiers from the first phone number', async () => { + it('getEntityDetails uses the first phone number only for the display name, not identity', async () => { const api = new Api({ api_key: 'k' }); jest.spyOn(api, 'listPhoneNumbers').mockResolvedValue({ data: [{ id: 'PN123', name: 'Sales Line', number: '+15555550100' }], }); - const details = await api.constructor === Api // noop guard - ? await Definition.requiredAuthMethods.getEntityDetails(api, {}, {}, 'user-1') - : null; - expect(details.identifiers.externalId).toBe('PN123'); + const details = await Definition.requiredAuthMethods.getEntityDetails( + api, + {}, + {}, + 'user-1' + ); + // Identity is a sha256 fingerprint of the key — NOT the phone-number id. + expect(details.identifiers.externalId).not.toBe('PN123'); + expect(details.identifiers.externalId).toMatch(/^[a-f0-9]{64}$/); expect(details.identifiers.user).toBe('user-1'); expect(details.details.name).toBe('Sales Line'); }); + + describe('externalId identity (key fingerprint)', () => { + const { requiredAuthMethods } = Definition; + + const entityIdFor = async (key) => { + const api = new Api({ api_key: key }); + // A phone-number listing must not influence identity. + jest.spyOn(api, 'listPhoneNumbers').mockResolvedValue({ + data: [{ id: `PN-${Math.random()}` }], + }); + const d = await requiredAuthMethods.getEntityDetails(api, {}, {}, 'u'); + return d.identifiers.externalId; + }; + + it('is a stable sha256 hex derived from the key, never a constant', async () => { + const first = await entityIdFor('op_key_alpha'); + const second = await entityIdFor('op_key_alpha'); + expect(first).toMatch(/^[a-f0-9]{64}$/); + // Stable: same key → same id, regardless of phone-number churn. + expect(first).toBe(second); + // Never the removed shared constant. + expect(first).not.toBe('quo-workspace'); + // Never the raw key. + expect(first).not.toBe('op_key_alpha'); + }); + + it('is unique per key (different keys → different ids)', async () => { + const a = await entityIdFor('op_key_alpha'); + const b = await entityIdFor('op_key_beta'); + expect(a).not.toBe(b); + }); + + it('getEntityDetails and getCredentialDetails agree for the same key', async () => { + const api = new Api({ api_key: 'op_key_shared' }); + jest.spyOn(api, 'listPhoneNumbers').mockResolvedValue({ data: [] }); + const entity = await requiredAuthMethods.getEntityDetails(api, {}, {}, 'u'); + const credential = await requiredAuthMethods.getCredentialDetails(api, 'u'); + expect(entity.identifiers.externalId).toBe( + credential.identifiers.externalId + ); + }); + + it('throws rather than emitting a shared constant when no key is present', async () => { + const api = new Api({}); + await expect( + requiredAuthMethods.getCredentialDetails(api, 'u') + ).rejects.toThrow(); + }); + }); });