diff --git a/.github/workflows/mock-e2e-pr.yml b/.github/workflows/mock-e2e-pr.yml index 490dc84..ad9c424 100644 --- a/.github/workflows/mock-e2e-pr.yml +++ b/.github/workflows/mock-e2e-pr.yml @@ -58,7 +58,7 @@ jobs: with: verbose: true env: - CONFIG_STORE_NAME: "E2ETest" + STORE_NAME_PREFIX: "E2ETest" - name: Deploy id: deploy env: @@ -67,7 +67,7 @@ jobs: FASTLY_API_TOKEN: ${{secrets.FASTLY_API_TOKEN}} FPJS_BACKEND_URL: ${{secrets.MOCK_FPCDN}} FPCDN_URL: ${{secrets.MOCK_FPCDN}} - CONFIG_STORE_NAME: "E2ETest" + STORE_NAME_PREFIX: "E2ETest" run: pnpm run ci - name: Wait for 60s shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dfb952..ab28085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [0.2.0-rc.2](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/compare/v0.2.0-rc.1...v0.2.0-rc.2) (2024-11-08) + + +### Features + +* move proxy secret to secret store ([b943878](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/commit/b94387882bd4d485733faa6cc712ee6e298d6e58)) +* show all configurations on status page ([e996354](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/commit/e9963545ae6be1fa44e2fa41ef74306067e6a75e)) + +## [0.2.0-rc.1](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/compare/v0.1.0...v0.2.0-rc.1) (2024-11-05) + + +### Features + +* add prefix to config store name ([3838318](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/commit/38383186439c5b1f7362b7462ea1a578287a59e3)) + ## [0.1.0](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/compare/v0.0.0...v0.1.0) (2024-10-29) diff --git a/README.md b/README.md index 0bffe9e..014f8cf 100644 --- a/README.md +++ b/README.md @@ -30,22 +30,29 @@ The Fastly Compute Proxy Integration is responsible for proxying identification ## Getting started -This is a quick overview of the installation setup. For detailed step-by-step instructions, see the [Fastly Compute proxy integration guide in our documentation](https://dev.fingerprint.com/docs/fastly-compute-edge-proxy-integration). +This is a quick overview of the installation setup. For detailed step-by-step instructions, see the [Fastly Compute proxy integration guide in our documentation](https://dev.fingerprint.com/docs/fastly-compute-proxy-integration). 1. Go to the Fingerprint Dashboard > [**API Keys**](https://dashboard.fingerprint.com/api-keys) and click **Create Proxy Key** to create a proxy secret. You will use it later to authenticate your requests to Fingerprint APIs. -2. [Create a Config store](https://docs.fastly.com/en/guides/working-with-config-stores#creating-a-config-store) in your Fastly account named exactly `Fingerprint` and add the following values: +2. [Create an empty Compute Service](https://docs.fastly.com/en/guides/working-with-compute-services#creating-a-new-compute-service) in your Fastly account. - | Key | Example Value | Description | - |------------------------------|----------------------|---------------------------------------------------------------------------------------------| - | PROXY_SECRET | 6XI9CLf3C9oHSB12TTaI | Fingerprint proxy secret generated in Step 1. | - | OPEN_CLIENT_RESPONSE_ENABLED | false | Set to `true` if you have [Open client response](https://dev.fingerprint.com/docs/open-client-response) enabled for your Fingerprint application. Defaults to `false`. | - | AGENT_SCRIPT_DOWNLOAD_PATH | z5kms2 | Random path segment for downloading the JavaScript agent. | - | GET_RESULT_PATH | nocmjw | Random path segment for Fingerprint identification requests. | +3. [Create a Config store](https://docs.fastly.com/en/guides/working-with-config-stores#creating-a-config-store) named `Fingerprint_Compute_Config_Store_`, where the suffix is your proxy integration's [Compute Service ID](https://docs.fastly.com/en/guides/about-services). Add the following values: -3. Go to [Releases](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/releases) to download the latest `fingerprint-proxy-integration.tar.gz` package file. -4. Upload package to your Fastly Compute Service's **Package**. -5. Configure the Fingerprint [JavaScript Agent](https://dev.fingerprint.com/docs/install-the-javascript-agent#configuring-the-agent) on your website using the paths defined in Step 2. + | Key | Example Value | Description | + | ---------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | AGENT_SCRIPT_DOWNLOAD_PATH | z5kms2 | Random path segment for downloading the JavaScript agent. | + | GET_RESULT_PATH | nocmjw | Random path segment for Fingerprint identification requests. | + | OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED | false | Set to `true` if you have [Open client response](https://dev.fingerprint.com/docs/open-client-response) enabled for your Fingerprint application. Defaults to `false`. | + +4. [Create a Secret store](https://docs.fastly.com/en/guides/working-with-secret-stores#creating-a-secret-store) named `Fingerprint_Compute_Secret_Store_`, where the suffix is your proxy integration's [Compute Service ID](https://docs.fastly.com/en/guides/about-services). Add your proxy secret: + + | Key | Example Value | Description | + | ------------ | -------------------- | --------------------------------------------- | + | PROXY_SECRET | 6XI9CLf3C9oHSB12TTaI | Fingerprint proxy secret generated in Step 1. | + +5. Go to [Releases](https://github.com/fingerprintjs/fingerprint-pro-fastly-compute-proxy-integration/releases) to download the latest `fingerprint-proxy-integration.tar.gz` package file. +6. Upload package to your Fastly Compute Service's **Package**. +7. Configure the Fingerprint [JavaScript Agent](https://dev.fingerprint.com/docs/install-the-javascript-agent#configuring-the-agent) on your website using the paths defined in Step 3. ```javascript import * as FingerprintJS from '@fingerprintjs/fingerprintjs-pro' @@ -62,16 +69,27 @@ This is a quick overview of the installation setup. For detailed step-by-step in }); ``` -See the [Fastly Compute proxy integration guide](https://dev.fingerprint.com/docs/fastly-compute-edge-proxy-integration#step-9-configure-the-fingerprint-client-agent-on-to-use-your-service) in our documentation for more details. +See the [Fastly Compute proxy integration guide](https://dev.fingerprint.com/docs/fastly-compute-proxy-integration#step-4-configure-the-fingerprint-client-agent-to-use-your-service) in our documentation for more details. + +### Using custom store names -### Using a custom config store name +By default, the service package provided in releases assumes the following names for the Config store and Secret Store: -The worker package provided in Releases assumes the config store used by the integration is named exactly `Fingerprint`. If you need to use a different config store name, you can pass the name to the `CONFIG_STORE_NAME` environment variable and build a custom worker package: +* `Fingerprint_Compute_Config_Store_` +* `Fingerprint_Compute_Secret_Store_` -```shell -CONFIG_STORE_NAME=MyCustomStoreName pnpm run build +To use a custom name prefix for both stores, use the `STORE_NAME_PREFIX` environment variable to build a custom service package: + +```shell= +STORE_NAME_PREFIX=CustomName pnpm run build ``` +Your custom built package in `pkg/package.tar.gz` will use your custom prefix in store names like: + +* `CustomName_Config_Store_` +* `CustomName_Secret_Store_` + + ## Feedback and support Please reach out to our [Customer Success team](https://fingerprint.com/support/) if run into any issues with the integration. diff --git a/__mocks__/fastly:env.js b/__mocks__/fastly:env.js new file mode 100644 index 0000000..2a5a0c4 --- /dev/null +++ b/__mocks__/fastly:env.js @@ -0,0 +1,3 @@ +export function env(key) { + return `TEST_${key}` +} diff --git a/__mocks__/fastly:kv-store.js b/__mocks__/fastly:kv-store.js new file mode 100644 index 0000000..833fe89 --- /dev/null +++ b/__mocks__/fastly:kv-store.js @@ -0,0 +1,5 @@ +export class KVStore { + async put(name, value) { + return { name, value } + } +} diff --git a/__mocks__/fastly:secret-store.js b/__mocks__/fastly:secret-store.js index a71acad..437766f 100644 --- a/__mocks__/fastly:secret-store.js +++ b/__mocks__/fastly:secret-store.js @@ -1,11 +1,15 @@ const store = new Map() -export class ConfigStore { +export class SecretStore { constructor(storeName) { this.storeName = storeName } - get(key) { - return store.get(key) || null + async get(key) { + return { + plaintext: () => { + return store.get(key) || null + }, + } } set(key, value) { diff --git a/build.ts b/build.ts index deb9357..5f9f8e9 100644 --- a/build.ts +++ b/build.ts @@ -1,7 +1,7 @@ import { build } from 'esbuild' // Load environment variables from process.env -const configStoreName = process.env.CONFIG_STORE_NAME || 'Fingerprint' +const configStoreNamePrefix = process.env.STORE_NAME_PREFIX || 'Fingerprint_Compute' build({ entryPoints: ['./src/index.ts'], @@ -9,5 +9,5 @@ build({ bundle: true, format: 'cjs', external: ['fastly:*'], - define: { 'process.env.CONFIG_STORE_NAME': `"${configStoreName}"` }, + define: { 'process.env.STORE_NAME_PREFIX': `"${configStoreNamePrefix}"` }, }).catch(() => process.exit(1)) diff --git a/package.json b/package.json index 915a344..2683452 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fingerprint-pro-fastly-compute-proxy-integration", - "version": "0.1.0", + "version": "0.2.0-rc.2", "engines": { "node": ">=20" }, @@ -12,7 +12,6 @@ "@fingerprintjs/commit-lint-dx-team": "^0.1.0", "@fingerprintjs/conventional-changelog-dx-team": "^0.1.0", "@fingerprintjs/eslint-config-dx-team": "^0.1.0", - "@fingerprintjs/fingerprintjs-pro-server-api": "^5.0.0", "@fingerprintjs/prettier-config-dx-team": "^0.2.0", "@fingerprintjs/tsconfig-dx-team": "^0.0.2", "@jest/globals": "^29.7.0", @@ -23,7 +22,7 @@ "@types/pako": "^2.0.3", "babel-jest": "^29.7.0", "esbuild": "^0.24.0", - "fastly": "7.3.0", + "fastly": "^7.10.0", "fs": "0.0.1-security", "husky": "^9.1.5", "jest": "^29.7.0", @@ -62,5 +61,8 @@ "commitizen": { "path": "./node_modules/cz-conventional-changelog" } + }, + "peerDependencies": { + "@fingerprintjs/fingerprintjs-pro-server-api": "^5.2.0" } } diff --git a/plugins/index.ts b/plugins/index.ts index 2c7c5ce..7209875 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -1,10 +1,10 @@ -// import { saveFingerprintResultToKVStore } from './saveToKVStore' +import { saveFingerprintResultToKVStore } from './saveToKVStore' import { Plugin } from '../src/utils/registerPlugin' export default [ - // { - // name: 'Save Fingerprint Result to KV Store', - // callback: saveFingerprintResultToKVStore, - // type: 'processOpenClientResponse', - // }, + { + name: 'Save Fingerprint Result to KV Store', + callback: saveFingerprintResultToKVStore, + type: 'processOpenClientResponse', + }, ] satisfies Plugin[] diff --git a/plugins/saveToKVStore.ts b/plugins/saveToKVStore.ts index e0c4d03..44c1294 100644 --- a/plugins/saveToKVStore.ts +++ b/plugins/saveToKVStore.ts @@ -2,11 +2,23 @@ import { KVStore } from 'fastly:kv-store' import { ProcessOpenClientResponseContext } from '../src/utils/registerPlugin' +import { getConfigStore } from '../src/utils/getStore' +import { env } from 'fastly:env' export async function saveFingerprintResultToKVStore(context: ProcessOpenClientResponseContext) { + const configStore = getConfigStore() + const isPluginEnabled = configStore?.get('SAVE_TO_KV_STORE_PLUGIN_ENABLED') === 'true' + + if (!isPluginEnabled) { + console.log("Plugin 'saveFingerprintResultToKVStore' is not enabled") + return + } + const requestId = context.event?.products.identification?.data?.requestId if (!requestId) { + console.log('[saveFingerprintResultToKVStore] Plugin Error: request ID is undefined in the event response.') return } - const store = new KVStore('FingerprintResults') + const serviceId = env('FASTLY_SERVICE_ID') + const store = new KVStore(`Fingerprint_Results_${serviceId}`) await store.put(requestId, JSON.stringify(context.event)) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f49f22..b9ce502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fastly/js-compute': specifier: ^3.21.4 version: 3.25.0 + '@fingerprintjs/fingerprintjs-pro-server-api': + specifier: ^5.2.0 + version: 5.2.0 cookie: specifier: 0.7.0 version: 0.7.0 @@ -36,9 +39,6 @@ importers: '@fingerprintjs/eslint-config-dx-team': specifier: ^0.1.0 version: 0.1.0(prettier@3.2.4)(typescript@5.3.3) - '@fingerprintjs/fingerprintjs-pro-server-api': - specifier: ^5.0.0 - version: 5.1.0 '@fingerprintjs/prettier-config-dx-team': specifier: ^0.2.0 version: 0.2.0 @@ -70,8 +70,8 @@ importers: specifier: ^0.24.0 version: 0.24.0 fastly: - specifier: 7.3.0 - version: 7.3.0 + specifier: ^7.10.0 + version: 7.10.0 fs: specifier: 0.0.1-security version: 0.0.1-security @@ -1002,8 +1002,8 @@ packages: peerDependencies: prettier: '>=3' - '@fingerprintjs/fingerprintjs-pro-server-api@5.1.0': - resolution: {integrity: sha512-lB9Iz+v8xccQlyQa8fsLb2F/fQ4ti1iegs+BvTMND6XBsawFcG2+HzdZwmb87Dg7EHf6Ez+vDvNElzjVV+Qmwg==} + '@fingerprintjs/fingerprintjs-pro-server-api@5.2.0': + resolution: {integrity: sha512-o53t3fVO2sC3h3WFxniu9Pw3QZYCXnkSz4vOZLzh9YtiyWf3JGV8ejN7YEnwHWM+k6uMjVhqHOpDpOXIsCEHfA==} engines: {node: '>=18.17.0'} '@fingerprintjs/prettier-config-dx-team@0.2.0': @@ -1314,6 +1314,9 @@ packages: '@types/node@22.7.9': resolution: {integrity: sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==} + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + '@types/pako@2.0.3': resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==} @@ -1460,6 +1463,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -1986,8 +1994,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fastly@7.3.0: - resolution: {integrity: sha512-drYeTPEruVWHMTKn6e2s4pSf09MLYKYkKDqZHyOpbzIMadt8y93Kvi3u/fdLS8EuQA5j81ChxY0C03ozWLK29g==} + fastly@7.10.0: + resolution: {integrity: sha512-aLN6iVCe3AUwURrvGtv88UJt5HMnApHq5Wan77w5OD91SF84U4/q8oJ8rVk3I5y/zyhRdkW+hGyBjwFFgJtyFA==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -4213,7 +4221,7 @@ snapshots: - supports-color - typescript - '@fingerprintjs/fingerprintjs-pro-server-api@5.1.0': {} + '@fingerprintjs/fingerprintjs-pro-server-api@5.2.0': {} '@fingerprintjs/prettier-config-dx-team@0.2.0': dependencies: @@ -4601,6 +4609,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.9.0': + dependencies: + undici-types: 6.19.8 + '@types/pako@2.0.3': {} '@types/semver@7.5.8': {} @@ -4781,9 +4793,9 @@ snapshots: '@xtuc/long@4.2.2': {} - acorn-import-attributes@1.9.5(acorn@8.13.0): + acorn-import-attributes@1.9.5(acorn@8.14.0): dependencies: - acorn: 8.13.0 + acorn: 8.14.0 acorn-jsx@5.3.2(acorn@8.13.0): dependencies: @@ -4795,6 +4807,8 @@ snapshots: acorn@8.13.0: {} + acorn@8.14.0: {} + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -5384,7 +5398,7 @@ snapshots: fast-safe-stringify@2.1.1: {} - fastly@7.3.0: + fastly@7.10.0: dependencies: superagent: 6.1.0 transitivePeerDependencies: @@ -5947,7 +5961,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.7.9 + '@types/node': 22.9.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -6649,8 +6663,8 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@webassemblyjs/wasm-edit': 1.12.1 '@webassemblyjs/wasm-parser': 1.12.1 - acorn: 8.13.0 - acorn-import-attributes: 1.9.5(acorn@8.13.0) + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) browserslist: 4.24.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.17.1 diff --git a/scripts/api/createService.ts b/scripts/api/createService.ts index df67fbe..c10b3f4 100644 --- a/scripts/api/createService.ts +++ b/scripts/api/createService.ts @@ -2,7 +2,7 @@ import { createClient } from '../utils/createClient' import { activateVersion } from './activateVersion' import { deployPackage } from './deployPackage' -const CONFIG_STORE_NAME = process.env.CONFIG_STORE_NAME ?? 'Fingerprint' +const STORE_NAME_PREFIX = process.env.STORE_NAME_PREFIX ?? 'E2ETest' export async function createService(domain: string) { const client = createClient('service') @@ -21,11 +21,19 @@ export async function createService(domain: string) { }) await createDomain(domain, createResponse.id) await createOrigin(createResponse.id, domain) - const configStore = await createConfigStore() + const configStore = await createConfigStore(createResponse.id) console.log('config store created') + const secretStore = await createSecretStore(createResponse.id) + console.log('secret store created') + console.log('linking config store') - await linkConfigStore(createResponse.id, 1, configStore.id) + await linkStoreResource(createResponse.id, 1, configStore.id) console.log('config store linked') + + console.log('linking secret store') + await linkStoreResource(createResponse.id, 1, secretStore.id, 'secret') + console.log('secret store linked') + console.log('deploying package') await deployPackage(createResponse.id, 1) console.log('package deployed') @@ -68,27 +76,64 @@ async function createDomain(domain: string, serviceId: string) { }) } -async function linkConfigStore(service_id: string, version_id: number, resource_id: string) { +async function linkStoreResource( + service_id: string, + version_id: number, + resource_id: string, + type: 'secret' | 'config' = 'config' +) { + const storeNameWithPrefix = `${STORE_NAME_PREFIX}_${type === 'config' ? 'Config_Store' : 'Secret_Store'}_${service_id}` return createClient('resource').createResource({ service_id, version_id, resource_id, - name: CONFIG_STORE_NAME, + name: storeNameWithPrefix, + }) +} + +async function createSecretStore(service_id: string) { + console.log('Creating secret store') + const secretStoreNameWithPrefix = `${STORE_NAME_PREFIX}_Secret_Store_${service_id}` + const secretStoreClient = createClient('secretStore') + const secretStoreItemClient = createClient('secretStoreItem') + let secretStore + try { + secretStore = await secretStoreClient.createSecretStore({ + secret_store: { + name: secretStoreNameWithPrefix, + }, + }) + } catch (e) { + console.error('Could not create secret store', e) + const stores = await secretStoreClient.getSecretStores() + return stores.find((t: any) => t.name === secretStoreNameWithPrefix) + } + + await secretStoreItemClient.createSecret({ + secret: { + name: 'PROXY_SECRET', + secret: btoa(process.env.PROXY_SECRET ?? 'secret'), + }, + store_id: secretStore.id, }) + + return secretStore } -async function createConfigStore() { +async function createConfigStore(service_id: string) { console.log('Creating config store') + const configStoreNameWithPrefix = `${STORE_NAME_PREFIX}_Config_Store_${service_id}` const configStoreClient = createClient('configStore') const configStoreItemClient = createClient('configStoreItem') let configStore try { configStore = await configStoreClient.createConfigStore({ - name: CONFIG_STORE_NAME, + name: configStoreNameWithPrefix, }) - } catch (_) { + } catch (e) { + console.error('Could not create config store', e) const stores = await configStoreClient.listConfigStores() - return stores.find((t: any) => t.name === CONFIG_STORE_NAME) + return stores.find((t: any) => t.name === configStoreNameWithPrefix) } await configStoreItemClient.createConfigStoreItem({ config_store_id: configStore.id, @@ -102,12 +147,7 @@ async function createConfigStore() { }) await configStoreItemClient.createConfigStoreItem({ config_store_id: configStore.id, - item_key: 'PROXY_SECRET', - item_value: 'secret', - }) - await configStoreItemClient.createConfigStoreItem({ - config_store_id: configStore.id, - item_key: 'OPEN_CLIENT_RESPONSE_ENABLED', + item_key: 'OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED', item_value: 'false', }) diff --git a/scripts/utils/createClient.ts b/scripts/utils/createClient.ts index dd24a02..0ca8fbe 100644 --- a/scripts/utils/createClient.ts +++ b/scripts/utils/createClient.ts @@ -9,6 +9,8 @@ export type FastlyClientTypes = | 'configStoreItem' | 'backend' | 'resource' + | 'secretStore' + | 'secretStoreItem' export function createClient(api: FastlyClientTypes) { let client switch (api) { @@ -36,6 +38,12 @@ export function createClient(api: FastlyClientTypes) { case 'resource': client = new Fastly.ResourceApi() break + case 'secretStore': + client = new Fastly.SecretStoreApi() + break + case 'secretStoreItem': + client = new Fastly.SecretStoreItemApi() + break } Fastly.ApiClient.instance.authenticate(process.env.FASTLY_API_TOKEN) return client diff --git a/src/env.ts b/src/env.ts index 2547d1e..a1d184c 100644 --- a/src/env.ts +++ b/src/env.ts @@ -2,19 +2,18 @@ export type IntegrationEnv = { AGENT_SCRIPT_DOWNLOAD_PATH: string | null GET_RESULT_PATH: string | null PROXY_SECRET: string | null - OPEN_CLIENT_RESPONSE_ENABLED: string | null + OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED: string | null + DECRYPTION_KEY: string | null } const Defaults: IntegrationEnv = { AGENT_SCRIPT_DOWNLOAD_PATH: 'agent', GET_RESULT_PATH: 'result', PROXY_SECRET: null, - OPEN_CLIENT_RESPONSE_ENABLED: 'false', + OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED: 'false', + DECRYPTION_KEY: null, } -export const FingerprintSecretStoreName = 'FingerprintSecrets' -export const FingerprintDecryptionKeyName = 'decryptionKey' - function getVarOrDefault( variable: keyof IntegrationEnv, defaults: IntegrationEnv @@ -52,8 +51,13 @@ export const proxySecretVarName = 'PROXY_SECRET' const getProxySecretVar = getVarOrDefault(proxySecretVarName, Defaults) export const isProxySecretSet = isVarSet(proxySecretVarName) -export const openClientResponseVarName = 'OPEN_CLIENT_RESPONSE_ENABLED' -export const isOpenClientResponseSet = isVarSet(openClientResponseVarName) +export const decryptionKeyVarName = 'DECRYPTION_KEY' +const getDecryptionKeyVar = getVarOrDefault(decryptionKeyVarName, Defaults) +export const isDecryptionKeySet = isVarSet(decryptionKeyVarName) + +export const openClientResponseVarName = 'OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED' +export const isOpenClientResponseSet = (env: IntegrationEnv) => + env.OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED === 'true' || env.OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED === 'false' export const isOpenClientResponseEnabled = (env: IntegrationEnv) => env[openClientResponseVarName]?.toLowerCase() === 'true' @@ -62,6 +66,10 @@ export function getProxySecret(env: IntegrationEnv): string | null { return getProxySecretVar(env) } +export function getDecryptionKey(env: IntegrationEnv): string | null { + return getDecryptionKeyVar(env) +} + export function getStatusPagePath(): string { return `/status` } diff --git a/src/handlers/handleIngressAPI.ts b/src/handlers/handleIngressAPI.ts index 1f47cad..757fec9 100644 --- a/src/handlers/handleIngressAPI.ts +++ b/src/handlers/handleIngressAPI.ts @@ -29,16 +29,22 @@ async function makeIngressRequest(receivedRequest: Request, env: IntegrationEnv) const response = await fetch(request, { backend: getIngressBackendByRegion(url) }) if (!isOpenClientResponseEnabled(env)) { + console.log( + "Open client response plugings are disabled. Set OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED to `true` in your proxy integration's Config store to enable them." + ) return response } - const responseBody = await response.text() - - processOpenClientResponse(responseBody, response).catch((e) => - console.error('failed when processing open client response', e) - ) + console.log('Plugin system for Open Client Response is enabled') + if (response.status >= 200 && response.status < 300) { + const responseBody = await response.text() + processOpenClientResponse(responseBody, response, env).catch((e) => + console.error('Processing open client response failed: ', e) + ) + return cloneFastlyResponse(responseBody, response) + } - return cloneFastlyResponse(responseBody, response) + return response } function makeCacheEndpointRequest(receivedRequest: Request, routeMatches: RegExpMatchArray | undefined) { diff --git a/src/handlers/handleStatusPage.ts b/src/handlers/handleStatusPage.ts index d45626b..cdbee0c 100644 --- a/src/handlers/handleStatusPage.ts +++ b/src/handlers/handleStatusPage.ts @@ -8,6 +8,9 @@ import { proxySecretVarName, isOpenClientResponseSet, openClientResponseVarName, + decryptionKeyVarName, + isOpenClientResponseEnabled, + isDecryptionKeySet, } from '../env' import packageJson from '../../package.json' @@ -47,54 +50,67 @@ function createContactInformationElement(): string { ` } +type ConfigurationStatus = { + label: string + isSet: boolean + required: boolean + message?: string + value?: string | null +} function createEnvVarsInformationElement(env: IntegrationEnv): string { - const isScriptDownloadPathAvailable = isScriptDownloadPathSet(env) - const isGetResultPathAvailable = isGetResultPathSet(env) - const isProxySecretAvailable = isProxySecretSet(env) - const isOpenClientResponseVarSet = isOpenClientResponseSet(env) - const isAllVarsAvailable = isScriptDownloadPathAvailable && isGetResultPathAvailable && isProxySecretAvailable + const incorrectConfigurationMessage = 'Your integration is not working correctly.' + const configurations: ConfigurationStatus[] = [ + { + label: agentScriptDownloadPathVarName, + isSet: isScriptDownloadPathSet(env), + required: true, + message: incorrectConfigurationMessage, + }, + { + label: getResultPathVarName, + isSet: isGetResultPathSet(env), + required: true, + message: incorrectConfigurationMessage, + }, + { + label: proxySecretVarName, + isSet: isProxySecretSet(env), + required: true, + message: incorrectConfigurationMessage, + }, + { + label: decryptionKeyVarName, + isSet: isDecryptionKeySet(env), + required: isOpenClientResponseEnabled(env), + message: incorrectConfigurationMessage, + }, + { + label: openClientResponseVarName, + isSet: isOpenClientResponseSet(env), + required: false, + message: + "Your integration will work without the 'Open Client Response' feature. If you didn't set it intentionally, you can ignore this warning.", + }, + ] + + const isAllVarsAvailable = configurations.filter((t) => t.required && !t.isSet).length === 0 let result = '' - if (!isAllVarsAvailable) { - result += ` - - The following environment variables are not defined. Please reach out our support team. - - ` - if (!isScriptDownloadPathAvailable) { - result += ` - - ⚠️ ${agentScriptDownloadPathVarName} is not set - - ` - } - if (!isGetResultPathAvailable) { - result += ` - - ⚠️ ${getResultPathVarName} is not set - - ` - } - if (!isProxySecretAvailable) { - result += ` - - ⚠️ ${proxySecretVarName} is not set - - ` - } - } else { + if (isAllVarsAvailable) { result += ` - ✅ All required environment variables are set + ✅ All required configuration values are set ` } - if (!isOpenClientResponseVarSet) { + result += `Your integration’s configuration values:` + + for (const configuration of configurations) { result += ` - ⚠️ ${openClientResponseVarName} optional environment variable is not set
- This environment variable is optional; your integration will work without the 'Open Client Response' feature. If you didn't set it intentionally, you can ignore this warning. + ${configuration.isSet ? '✅' : '⚠️'} ${configuration.label} (${configuration.required ? 'REQUIRED' : 'OPTIONAL'}) is${!configuration.isSet ? ' not ' : ' '}set + ${!configuration.isSet && configuration.message ? `${configuration.message}` : ''}
` } diff --git a/src/index.ts b/src/index.ts index 0be6d65..86daebb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,17 @@ /// import { handleReq } from './handler' -import { IntegrationEnv } from './env' -import { ConfigStore } from 'fastly:config-store' +import { + agentScriptDownloadPathVarName, + decryptionKeyVarName, + getResultPathVarName, + IntegrationEnv, + openClientResponseVarName, + proxySecretVarName, +} from './env' import { returnHttpResponse } from './utils/returnHttpResponse' import { createFallbackErrorResponse } from './utils' import { setClientIp } from './utils/clientIp' +import { getConfigStore, getSecretStore } from './utils/getStore' addEventListener('fetch', (event) => event.respondWith(handleRequest(event))) @@ -12,7 +19,7 @@ export async function handleRequest(event: FetchEvent): Promise { setClientIp(event.client.address) try { const request = event.request - const envObj = getEnvObject() + const envObj = await getEnvObject() return handleReq(request, envObj).then(returnHttpResponse) } catch (e) { console.error(e) @@ -20,27 +27,21 @@ export async function handleRequest(event: FetchEvent): Promise { } } -function getEnvObject(): IntegrationEnv { - let config +async function getEnvObject(): Promise { + let configStore + let secretStore try { - config = new ConfigStore(process.env.CONFIG_STORE_NAME ?? 'Fingerprint') + configStore = getConfigStore() + secretStore = getSecretStore() } catch (e) { console.error(e) } - if (config == null) { - return { - AGENT_SCRIPT_DOWNLOAD_PATH: null, - GET_RESULT_PATH: null, - PROXY_SECRET: null, - OPEN_CLIENT_RESPONSE_ENABLED: 'false', - } - } - return { - AGENT_SCRIPT_DOWNLOAD_PATH: config.get('AGENT_SCRIPT_DOWNLOAD_PATH'), - GET_RESULT_PATH: config.get('GET_RESULT_PATH'), - PROXY_SECRET: config.get('PROXY_SECRET'), - OPEN_CLIENT_RESPONSE_ENABLED: config.get('OPEN_CLIENT_RESPONSE_ENABLED'), + AGENT_SCRIPT_DOWNLOAD_PATH: configStore?.get(agentScriptDownloadPathVarName) ?? null, + GET_RESULT_PATH: configStore?.get(getResultPathVarName) ?? null, + OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED: configStore?.get(openClientResponseVarName) ?? null, + PROXY_SECRET: (await secretStore?.get(proxySecretVarName))?.plaintext() ?? null, + DECRYPTION_KEY: (await secretStore?.get(decryptionKeyVarName))?.plaintext() ?? null, } } diff --git a/src/utils/getStore.ts b/src/utils/getStore.ts new file mode 100644 index 0000000..4bf3200 --- /dev/null +++ b/src/utils/getStore.ts @@ -0,0 +1,24 @@ +import { env } from 'fastly:env' +import { ConfigStore } from 'fastly:config-store' +import { SecretStore } from 'fastly:secret-store' + +export function getNamesForStores() { + const serviceId = env('FASTLY_SERVICE_ID') + const storeNamePrefix = process.env.STORE_NAME_PREFIX + const configStoreName = `${storeNamePrefix}_Config_Store_${serviceId}` + const secretStoreName = `${storeNamePrefix}_Secret_Store_${serviceId}` + + return { + configStoreName, + secretStoreName, + } +} +export function getConfigStore() { + const { configStoreName } = getNamesForStores() + return new ConfigStore(configStoreName) +} + +export function getSecretStore() { + const { secretStoreName } = getNamesForStores() + return new SecretStore(secretStoreName) +} diff --git a/src/utils/processOpenClientResponse.ts b/src/utils/processOpenClientResponse.ts index 810bd4f..191ab16 100644 --- a/src/utils/processOpenClientResponse.ts +++ b/src/utils/processOpenClientResponse.ts @@ -1,20 +1,18 @@ import { plugins } from './registerPlugin' import { unsealData } from './unsealData' -import { SecretStore } from 'fastly:secret-store' -import { FingerprintDecryptionKeyName, FingerprintSecretStoreName } from '../env' import { cloneFastlyResponse } from './cloneFastlyResponse' +import { getDecryptionKey, IntegrationEnv } from '../env' type FingerprintSealedIngressResponseBody = { sealedResult: string } -async function getDecryptionKey() { - const secretStore = new SecretStore(FingerprintSecretStoreName) - return secretStore.get(FingerprintDecryptionKeyName).then((v) => v?.plaintext()) -} - -export async function processOpenClientResponse(body: string | undefined, response: Response): Promise { - const decryptionKey = await getDecryptionKey() +export async function processOpenClientResponse( + body: string | undefined, + response: Response, + env: IntegrationEnv +): Promise { + const decryptionKey = getDecryptionKey(env) if (!decryptionKey) { throw new Error('Decryption key not found in secret store') } diff --git a/test/handlers/ingressAPI.test.ts b/test/handlers/ingressAPI.test.ts index d199cfd..0ab9913 100644 --- a/test/handlers/ingressAPI.test.ts +++ b/test/handlers/ingressAPI.test.ts @@ -3,9 +3,12 @@ import { ConfigStore } from 'fastly:config-store' import { makeRequest } from '../utils/makeRequest' import { handleRequest } from '../../src' import cookie from 'cookie' +import { SecretStore } from 'fastly:secret-store' +import { env } from 'fastly:env' describe('Browser Cache', () => { let requestHeaders: Headers + let storeName: string beforeAll(() => { jest.spyOn(globalThis, 'fetch').mockImplementation((request, init) => { @@ -14,9 +17,13 @@ describe('Browser Cache', () => { } return globalThis.fetch(request, init) }) + + const serviceId = env('FASTLY_SERVICE_ID') + const storeNamePrefix = process.env.STORE_NAME_PREFIX + storeName = `${storeNamePrefix}_${serviceId}` }) beforeEach(() => { - const config = new ConfigStore('Fingerprint') + const config = new ConfigStore(storeName) // @ts-ignore config.set('GET_RESULT_PATH', 'result') // Reset fetch spy calls between tests if needed @@ -44,6 +51,7 @@ describe('Browser Cache', () => { describe('Ingress', () => { let requestHeaders: Headers + let storeName: string beforeAll(() => { jest.spyOn(globalThis, 'fetch').mockImplementation((request, init) => { @@ -52,9 +60,12 @@ describe('Ingress', () => { } return globalThis.fetch(request, init) }) + const serviceId = env('FASTLY_SERVICE_ID') + const storeNamePrefix = process.env.STORE_NAME_PREFIX + storeName = `${storeNamePrefix}_${serviceId}` }) beforeEach(() => { - const config = new ConfigStore('Fingerprint') + const config = new ConfigStore(storeName) // @ts-ignore config.set('GET_RESULT_PATH', 'result') // Reset fetch spy calls between tests if needed @@ -104,9 +115,9 @@ describe('Ingress', () => { }) it('should add proxy integration headers if PROXY_SECRET is present', async () => { - const config = new ConfigStore('Fingerprint') + const secretStore = new SecretStore('Fingerprint') // @ts-ignore - config.set('PROXY_SECRET', 'secret') + secretStore.set('PROXY_SECRET', 'secret') const request = makeRequest(new URL('https://test/result'), { method: 'POST' }) await handleRequest(request) @@ -117,9 +128,9 @@ describe('Ingress', () => { }) it('should set client ip if request has header Fastly-Client-IP', async () => { - const config = new ConfigStore('Fingerprint') + const secretStore = new SecretStore('Fingerprint') // @ts-ignore - config.set('PROXY_SECRET', 'secret') + secretStore.set('PROXY_SECRET', 'secret') const request = makeRequest(new URL('https://test/result'), { method: 'POST', diff --git a/test/handlers/statusPage.test.ts b/test/handlers/statusPage.test.ts index 2ac1923..d13107e 100644 --- a/test/handlers/statusPage.test.ts +++ b/test/handlers/statusPage.test.ts @@ -3,6 +3,14 @@ import { handleRequest } from '../../src' import { expect } from '@jest/globals' import { ConfigStore } from 'fastly:config-store' import packageJson from '../../package.json' +import { SecretStore } from 'fastly:secret-store' +import { + agentScriptDownloadPathVarName, + decryptionKeyVarName, + getResultPathVarName, + openClientResponseVarName, + proxySecretVarName, +} from '../../src/env' describe('Status Page', () => { it('should return text/html with status 200', async () => { @@ -13,24 +21,37 @@ describe('Status Page', () => { expect(response.headers.get('Content-Type')).toBe('text/html') }) - it('should show warning for undefined variables', async () => { + it('should show error for undefined required configurations', async () => { const config = new ConfigStore('Fingerprint') + const secret = new SecretStore('Fingerprint') // @ts-ignore - config.set('GET_RESULT_PATH', 'result') + config.set(getResultPathVarName, null) + // @ts-ignore + config.set(agentScriptDownloadPathVarName, null) + // @ts-ignore + config.set(openClientResponseVarName, 'true') // @ts-ignore - config.set('PROXY_SECRET', 'secret') + secret.set(proxySecretVarName, null) + // @ts-ignore + secret.set(decryptionKeyVarName, null) const request = makeRequest(new URL('https://test/status')) const response = await handleRequest(request) const responseText = await response.text() - const isAllSet = responseText.includes('All environment variables are set') - const agentDownloadScriptPathWarning = responseText.includes( - 'AGENT_SCRIPT_DOWNLOAD_PATH is not set' + const isAllSet = responseText.includes('All required configurations are set') + const agentDownloadScriptPathError = responseText.includes( + 'AGENT_SCRIPT_DOWNLOAD_PATH (REQUIRED) is not set' ) + const resultPathError = responseText.includes('GET_RESULT_PATH (REQUIRED) is not set') + const proxySecretError = responseText.includes('PROXY_SECRET (REQUIRED) is not set') + const decryptionKeyError = responseText.includes('DECRYPTION_KEY (REQUIRED) is not set') expect(isAllSet).toBe(false) - expect(agentDownloadScriptPathWarning).toBe(true) + expect(agentDownloadScriptPathError).toBe(true) + expect(resultPathError).toBe(true) + expect(proxySecretError).toBe(true) + expect(decryptionKeyError).toBe(true) }) it('should show correctly setup env', async () => { @@ -40,15 +61,19 @@ describe('Status Page', () => { // @ts-ignore config.set('GET_RESULT_PATH', 'result') // @ts-ignore - config.set('PROXY_SECRET', 'secret') + config.set('OPEN_CLIENT_RESPONSE_PLUGINS_ENABLED', 'true') + + const secretStore = new SecretStore('Fingerprint') + // @ts-ignore + secretStore.set('PROXY_SECRET', 'secret') // @ts-ignore - config.set('OPEN_CLIENT_RESPONSE_ENABLED', 'true') + secretStore.set('DECRYPTION_KEY', 'secret') const request = makeRequest(new URL('https://test/status')) const response = await handleRequest(request) const responseText = await response.text() - const isAllSet = responseText.includes('All required environment variables are set') + const isAllSet = responseText.includes('All required configuration values are set') expect(isAllSet).toBe(true) })