diff --git a/CHANGELOG.md b/CHANGELOG.md index b27d240..bf298fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `getAuthorization` to use the correct API URL. - Rename `getConnection(name: string)` -> `getAuthorization(developerName: string, attachmentNameOrColorUrl = "HEROKU_APPLINK")`, accepting a new attachmentNameOrColorOrUrl to use a specific Applink addon's config. - Remove node-fetch in favor of native fetch, add `HTTPResponseError` + +## Unreleased +- Add `X-App-UUID` header, `heroku-applink-node-sdk` UA. diff --git a/src/add-ons/heroku-applink.ts b/src/add-ons/heroku-applink.ts index 1ac91c3..a8277b9 100644 --- a/src/add-ons/heroku-applink.ts +++ b/src/add-ons/heroku-applink.ts @@ -47,6 +47,7 @@ export async function getAuthorization( method: "GET", headers: { Authorization: `Bearer ${config.token}`, + "X-App-UUID": config.appUuid, "Content-Type": "application/json", }, retry: { diff --git a/src/utils/addon-config.ts b/src/utils/addon-config.ts index d7e60eb..c5c9783 100644 --- a/src/utils/addon-config.ts +++ b/src/utils/addon-config.ts @@ -5,14 +5,23 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +type AppUuid = string; + interface AddonConfig { apiUrl: string; token: string; + appUuid: AppUuid; } export function resolveAddonConfigByAttachmentOrColor( attachmentOrColor: string ): AddonConfig { + const appUuid = process.env.HEROKU_APP_ID; + + if (!appUuid) { + throw Error(`Heroku Applink app UUID not found`); + } + const addon = process.env.HEROKU_APPLINK_ADDON_NAME || "HEROKU_APPLINK"; let apiUrl; @@ -37,10 +46,17 @@ export function resolveAddonConfigByAttachmentOrColor( return { apiUrl, token, + appUuid, }; } export function resolveAddonConfigByUrl(url: string): AddonConfig { + const appUuid = process.env.HEROKU_APP_ID; + + if (!appUuid) { + throw Error(`Heroku Applink app UUID not found`); + } + // Find the environment variable ending with _API_URL that matches the given URL const envVarEntries = Object.entries(process.env); const matchingApiUrlEntry = envVarEntries.find( @@ -65,5 +81,6 @@ export function resolveAddonConfigByUrl(url: string): AddonConfig { return { apiUrl: matchingApiUrlEntry[1], token, + appUuid, }; } diff --git a/src/utils/request.ts b/src/utils/request.ts index 9509923..942a060 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,6 +5,9 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import fs from "fs"; +import path from "path"; + /** Error thrown by the SDK when receiving non-2xx responses on HTTP requests. */ export class HTTPResponseError extends Error { response: any; @@ -19,7 +22,19 @@ export class HTTPResponseError extends Error { */ export class HttpRequestUtil { async request(url: string, opts: any, json = true) { - const response = await fetch(url, opts); + const pjson = fs.readFileSync( + path.join(__dirname, "..", "package.json"), + "utf8" + ); + const pkg = JSON.parse(pjson); + + const defaultOpts = { + headers: { + "User-Agent": `heroku-applink-node-sdk/${pkg.version}`, + }, + }; + + const response = await fetch(url, { ...defaultOpts, ...opts }); if (!response.ok) { throw new HTTPResponseError(response); diff --git a/test/add-ons/heroku-applink.test.ts b/test/add-ons/heroku-applink.test.ts index c2f7cb2..729d8fe 100644 --- a/test/add-ons/heroku-applink.test.ts +++ b/test/add-ons/heroku-applink.test.ts @@ -45,6 +45,7 @@ describe("getAuthorization", () => { process.env.HEROKU_APPLINK_PURPLE_API_URL = "https://applink.staging.herokudev.com/addons/536c15d8-c2c1-47f7-a582-76f5aae385e0"; process.env.HEROKU_APPLINK_PURPLE_TOKEN = "purple-token"; + process.env.HEROKU_APP_ID = "d52a726b-11a4-47a1-a4b6-2e18a771c2ac"; httpRequestStub = sinon.stub(HttpRequestUtil.prototype, "request"); }); diff --git a/test/utils/addon-config.test.ts b/test/utils/addon-config.test.ts index 2cf9dd1..36f26f3 100644 --- a/test/utils/addon-config.test.ts +++ b/test/utils/addon-config.test.ts @@ -18,6 +18,7 @@ describe("resolveAddonConfigByAttachmentOrColor", () => { beforeEach(() => { originalEnv = { ...process.env }; + process.env.HEROKU_APP_ID = "d52a726b-11a4-47a1-a4b6-2e18a771c2ac"; }); afterEach(() => { @@ -81,6 +82,14 @@ describe("resolveAddonConfigByAttachmentOrColor", () => { "Heroku Applink config not found under attachment or color HEROKU_APPLINK" ); }); + + it("throws if HEROKU_APP_ID is not set", () => { + delete process.env.HEROKU_APP_ID; + + expect(() => resolveAddonConfigByAttachmentOrColor(ATTACHMENT)).to.throw( + "Heroku Applink app UUID not found" + ); + }); }); describe("resolveAddonConfigByUrl", () => { @@ -88,6 +97,7 @@ describe("resolveAddonConfigByUrl", () => { beforeEach(() => { originalEnv = { ...process.env }; + process.env.HEROKU_APP_ID = "d52a726b-11a4-47a1-a4b6-2e18a771c2ac"; }); afterEach(() => { @@ -131,4 +141,12 @@ describe("resolveAddonConfigByUrl", () => { `Heroku Applink token not found for API URL: ${testUrl}` ); }); + + it("throws if HEROKU_APP_ID is not set", () => { + delete process.env.HEROKU_APP_ID; + + expect(() => resolveAddonConfigByUrl("https://api.example.com")).to.throw( + "Heroku Applink app UUID not found" + ); + }); });