From abd95581ec0f2759d6348055875ad2cb25d57733 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 16:28:28 -0500 Subject: [PATCH 1/8] feat: adding in getExecutionContext --- CLAUDE.md | 1 + src/execution-context.ts | 125 ++++++++++++++++++++ src/index.ts | 10 ++ test/execution-context.test.ts | 203 +++++++++++++++++++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 CLAUDE.md create mode 100644 src/execution-context.ts create mode 100644 test/execution-context.test.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eef4bd2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file diff --git a/src/execution-context.ts b/src/execution-context.ts new file mode 100644 index 0000000..92e1850 --- /dev/null +++ b/src/execution-context.ts @@ -0,0 +1,125 @@ +import { CacheableNet } from "@cacheable/net"; +import { Cacheable } from "cacheable"; + +export type ExecutionContextOptions = { + /** + * The API key for the Hyphen API. + */ + apiKey: string; + /** + * The organization ID for the Hyphen API. + */ + organizationId?: string; + /** + * The base URI for the Hyphen API. + * @default "https://api.hyphen.ai" + */ + baseUri?: string; + /** + * Whether to enable caching for the request. + * @default false + */ + cache?: boolean; +}; + +export type ExecutionContextRequest = { + id?: string; + causationId?: string; + correlationId?: string; +}; + +export type ExecutionContextUser = { + id?: string; + name?: string; + rules?: Record[]; + type?: string; +}; + +export type ExecutionContextMember = { + id?: string; + name?: string; + organization?: { + id?: string; + name?: string; + }; + rules?: Record[]; +}; + +export type ExecutionContextOrganization = { + id?: string; + name?: string; +}; + +export type ExecutionContextLocation = { + country?: string; + region?: string; + city?: string; + lat?: number; + lng?: number; + postalCode?: string; + timezone?: string; +}; + +export type ExecutionContext = { + request?: ExecutionContextRequest; + user?: ExecutionContextUser; + member?: ExecutionContextMember; + organization?: ExecutionContextOrganization; + ipAddress?: string; + location?: ExecutionContextLocation; +}; + +/** + * Get the execution context for the provided API key. + * This validates the API key and returns information about the organization, user, and request context. + * + * @param options - The options for getting the execution context. + * @returns The execution context. + * @throws Error if the API key is not provided or if the request fails. + * + * @example + * ```typescript + * import { getExecutionContext } from '@hyphen/sdk'; + * + * const context = await getExecutionContext({ + * apiKey: 'your-api-key', + * organizationId: 'optional-org-id', + * }); + * + * console.log(context.organization?.name); + * ``` + */ +export async function getExecutionContext( + options: ExecutionContextOptions, +): Promise { + if (!options.apiKey) { + throw new Error("API key is required"); + } + + const baseUri = options.baseUri ?? "https://api.hyphen.ai"; + let url = `${baseUri}/api/execution-context`; + + if (options.organizationId) { + url += `?organizationId=${encodeURIComponent(options.organizationId)}`; + } + + const net = new CacheableNet({ + cache: options.cache ? new Cacheable() : undefined, + }); + + const response = await net.get(url, { + headers: { + "x-api-key": options.apiKey, + "content-type": "application/json", + accept: "application/json", + }, + }); + + if (response.response.status !== 200) { + throw new Error( + `Failed to get execution context: ${response.response.statusText}`, + ); + } + + return response.data; +} diff --git a/src/index.ts b/src/index.ts index 9ae8e4a..78eb641 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,14 @@ export { type EnvOptions, env, type LoadEnvOptions, loadEnv } from "./env.js"; +export { + type ExecutionContext, + type ExecutionContextLocation, + type ExecutionContextMember, + type ExecutionContextOptions, + type ExecutionContextOrganization, + type ExecutionContextRequest, + type ExecutionContextUser, + getExecutionContext, +} from "./execution-context.js"; export { Hyphen, type HyphenOptions } from "./hyphen.js"; export { Toggle, diff --git a/test/execution-context.test.ts b/test/execution-context.test.ts new file mode 100644 index 0000000..4105f12 --- /dev/null +++ b/test/execution-context.test.ts @@ -0,0 +1,203 @@ +import process from "node:process"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { getExecutionContext } from "../src/execution-context.js"; + +const testTimeout = 10_000; + +// Shared mock state that can be configured per test +const mockState: { + getResponse: { + response: { status: number; statusText?: string }; + data: Record | null; + }; + getCalls: Array<[string, unknown]>; +} = { + getResponse: { + response: { status: 200 }, + data: { organization: { id: "default-org" } }, + }, + getCalls: [], +}; + +// Mock for CacheableNet +vi.mock("@cacheable/net", () => { + return { + CacheableNet: class MockCacheableNet { + async get(url: string, config: unknown) { + mockState.getCalls.push([url, config]); + return mockState.getResponse; + } + }, + }; +}); + +describe("getExecutionContext", () => { + beforeEach(() => { + // Reset mock state before each test + mockState.getResponse = { + response: { status: 200 }, + data: { organization: { id: "default-org" } }, + }; + mockState.getCalls = []; + }); + + test("should throw an error if apiKey is not provided", async () => { + await expect(getExecutionContext({ apiKey: "" })).rejects.toThrow( + "API key is required", + ); + }); + + test("should fetch execution context successfully with mocked response", async () => { + mockState.getResponse = { + response: { status: 200 }, + data: { + organization: { id: "org-123", name: "Test Org" }, + user: { id: "user-123", name: "Test User" }, + }, + }; + + const context = await getExecutionContext({ apiKey: "test-api-key" }); + + expect(context).toBeDefined(); + expect(context.organization).toEqual({ id: "org-123", name: "Test Org" }); + expect(context.user).toEqual({ id: "user-123", name: "Test User" }); + expect(mockState.getCalls.length).toBe(1); + expect(mockState.getCalls[0][0]).toBe( + "https://api.hyphen.ai/api/execution-context", + ); + const callConfig = mockState.getCalls[0][1] as { + headers: Record; + }; + expect(callConfig.headers["x-api-key"]).toBe("test-api-key"); + expect(callConfig.headers["content-type"]).toBe("application/json"); + expect(callConfig.headers.accept).toBe("application/json"); + }); + + test("should include organizationId in URL when provided", async () => { + mockState.getResponse = { + response: { status: 200 }, + data: { organization: { id: "org-456" } }, + }; + + await getExecutionContext({ + apiKey: "test-api-key", + organizationId: "org-456", + }); + + expect(mockState.getCalls[0][0]).toBe( + "https://api.hyphen.ai/api/execution-context?organizationId=org-456", + ); + }); + + test("should use custom baseUri when provided", async () => { + mockState.getResponse = { + response: { status: 200 }, + data: { organization: { id: "org-789" } }, + }; + + await getExecutionContext({ + apiKey: "test-api-key", + baseUri: "https://custom.api.com", + }); + + expect(mockState.getCalls[0][0]).toBe( + "https://custom.api.com/api/execution-context", + ); + }); + + test("should throw an error when response status is not 200", async () => { + mockState.getResponse = { + response: { status: 401, statusText: "Unauthorized" }, + data: null, + }; + + await expect( + getExecutionContext({ apiKey: "invalid-api-key" }), + ).rejects.toThrow("Failed to get execution context: Unauthorized"); + }); + + test( + "should fetch execution context successfully (integration)", + async () => { + const apiKey = process.env.HYPHEN_API_KEY; + if (!apiKey) { + console.log("Skipping test: HYPHEN_API_KEY not set"); + return; + } + + mockState.getResponse = { + response: { status: 200 }, + data: { organization: { id: "integration-org" } }, + }; + + const context = await getExecutionContext({ apiKey }); + expect(context).toBeDefined(); + expect(context).toHaveProperty("organization"); + }, + testTimeout, + ); + + test( + "should fetch execution context with organizationId (integration)", + async () => { + const apiKey = process.env.HYPHEN_API_KEY; + const organizationId = process.env.HYPHEN_ORGANIZATION_ID; + if (!apiKey) { + console.log("Skipping test: HYPHEN_API_KEY not set"); + return; + } + + mockState.getResponse = { + response: { status: 200 }, + data: { organization: { id: organizationId ?? "default-org" } }, + }; + + const context = await getExecutionContext({ + apiKey, + organizationId, + }); + expect(context).toBeDefined(); + expect(context).toHaveProperty("organization"); + }, + testTimeout, + ); + + test( + "should allow custom baseUri (integration)", + async () => { + const apiKey = process.env.HYPHEN_API_KEY; + if (!apiKey) { + console.log("Skipping test: HYPHEN_API_KEY not set"); + return; + } + + mockState.getResponse = { + response: { status: 200 }, + data: { organization: { id: "custom-uri-org" } }, + }; + + const context = await getExecutionContext({ + apiKey, + baseUri: "https://api.hyphen.ai", + }); + expect(context).toBeDefined(); + expect(context).toHaveProperty("organization"); + }, + testTimeout, + ); + + test( + "should throw an error for invalid API key (integration)", + async () => { + mockState.getResponse = { + response: { status: 401, statusText: "Unauthorized" }, + data: null, + }; + + await expect( + getExecutionContext({ apiKey: "invalid_api_key" }), + ).rejects.toThrow(); + }, + testTimeout, + ); +}); From e0d1df0b173831d253e3bd675f37377e4cefa13a Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 16:32:33 -0500 Subject: [PATCH 2/8] Update execution-context.test.ts --- test/execution-context.test.ts | 88 ---------------------------------- 1 file changed, 88 deletions(-) diff --git a/test/execution-context.test.ts b/test/execution-context.test.ts index 4105f12..4378280 100644 --- a/test/execution-context.test.ts +++ b/test/execution-context.test.ts @@ -1,9 +1,6 @@ -import process from "node:process"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { getExecutionContext } from "../src/execution-context.js"; -const testTimeout = 10_000; - // Shared mock state that can be configured per test const mockState: { getResponse: { @@ -115,89 +112,4 @@ describe("getExecutionContext", () => { getExecutionContext({ apiKey: "invalid-api-key" }), ).rejects.toThrow("Failed to get execution context: Unauthorized"); }); - - test( - "should fetch execution context successfully (integration)", - async () => { - const apiKey = process.env.HYPHEN_API_KEY; - if (!apiKey) { - console.log("Skipping test: HYPHEN_API_KEY not set"); - return; - } - - mockState.getResponse = { - response: { status: 200 }, - data: { organization: { id: "integration-org" } }, - }; - - const context = await getExecutionContext({ apiKey }); - expect(context).toBeDefined(); - expect(context).toHaveProperty("organization"); - }, - testTimeout, - ); - - test( - "should fetch execution context with organizationId (integration)", - async () => { - const apiKey = process.env.HYPHEN_API_KEY; - const organizationId = process.env.HYPHEN_ORGANIZATION_ID; - if (!apiKey) { - console.log("Skipping test: HYPHEN_API_KEY not set"); - return; - } - - mockState.getResponse = { - response: { status: 200 }, - data: { organization: { id: organizationId ?? "default-org" } }, - }; - - const context = await getExecutionContext({ - apiKey, - organizationId, - }); - expect(context).toBeDefined(); - expect(context).toHaveProperty("organization"); - }, - testTimeout, - ); - - test( - "should allow custom baseUri (integration)", - async () => { - const apiKey = process.env.HYPHEN_API_KEY; - if (!apiKey) { - console.log("Skipping test: HYPHEN_API_KEY not set"); - return; - } - - mockState.getResponse = { - response: { status: 200 }, - data: { organization: { id: "custom-uri-org" } }, - }; - - const context = await getExecutionContext({ - apiKey, - baseUri: "https://api.hyphen.ai", - }); - expect(context).toBeDefined(); - expect(context).toHaveProperty("organization"); - }, - testTimeout, - ); - - test( - "should throw an error for invalid API key (integration)", - async () => { - mockState.getResponse = { - response: { status: 401, statusText: "Unauthorized" }, - data: null, - }; - - await expect( - getExecutionContext({ apiKey: "invalid_api_key" }), - ).rejects.toThrow(); - }, - testTimeout, - ); }); From 1f731d8098ea49ba5454ed39cbdc7470d5eb0212 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 16:48:50 -0500 Subject: [PATCH 3/8] moving apiKey to param --- src/execution-context.ts | 23 ++++++++++------------- test/execution-context.test.ts | 18 +++++++----------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/execution-context.ts b/src/execution-context.ts index 92e1850..4ed282a 100644 --- a/src/execution-context.ts +++ b/src/execution-context.ts @@ -2,10 +2,6 @@ import { CacheableNet } from "@cacheable/net"; import { Cacheable } from "cacheable"; export type ExecutionContextOptions = { - /** - * The API key for the Hyphen API. - */ - apiKey: string; /** * The organization ID for the Hyphen API. */ @@ -73,7 +69,8 @@ export type ExecutionContext = { * Get the execution context for the provided API key. * This validates the API key and returns information about the organization, user, and request context. * - * @param options - The options for getting the execution context. + * @param apiKey - The API key for the Hyphen API. + * @param options - Additional options for the request. * @returns The execution context. * @throws Error if the API key is not provided or if the request fails. * @@ -81,8 +78,7 @@ export type ExecutionContext = { * ```typescript * import { getExecutionContext } from '@hyphen/sdk'; * - * const context = await getExecutionContext({ - * apiKey: 'your-api-key', + * const context = await getExecutionContext('your-api-key', { * organizationId: 'optional-org-id', * }); * @@ -90,26 +86,27 @@ export type ExecutionContext = { * ``` */ export async function getExecutionContext( - options: ExecutionContextOptions, + apiKey: string, + options?: ExecutionContextOptions, ): Promise { - if (!options.apiKey) { + if (!apiKey) { throw new Error("API key is required"); } - const baseUri = options.baseUri ?? "https://api.hyphen.ai"; + const baseUri = options?.baseUri ?? "https://api.hyphen.ai"; let url = `${baseUri}/api/execution-context`; - if (options.organizationId) { + if (options?.organizationId) { url += `?organizationId=${encodeURIComponent(options.organizationId)}`; } const net = new CacheableNet({ - cache: options.cache ? new Cacheable() : undefined, + cache: options?.cache ? new Cacheable() : undefined, }); const response = await net.get(url, { headers: { - "x-api-key": options.apiKey, + "x-api-key": apiKey, "content-type": "application/json", accept: "application/json", }, diff --git a/test/execution-context.test.ts b/test/execution-context.test.ts index 4378280..a6de7c6 100644 --- a/test/execution-context.test.ts +++ b/test/execution-context.test.ts @@ -39,7 +39,7 @@ describe("getExecutionContext", () => { }); test("should throw an error if apiKey is not provided", async () => { - await expect(getExecutionContext({ apiKey: "" })).rejects.toThrow( + await expect(getExecutionContext("")).rejects.toThrow( "API key is required", ); }); @@ -53,7 +53,7 @@ describe("getExecutionContext", () => { }, }; - const context = await getExecutionContext({ apiKey: "test-api-key" }); + const context = await getExecutionContext("test-api-key"); expect(context).toBeDefined(); expect(context.organization).toEqual({ id: "org-123", name: "Test Org" }); @@ -76,10 +76,7 @@ describe("getExecutionContext", () => { data: { organization: { id: "org-456" } }, }; - await getExecutionContext({ - apiKey: "test-api-key", - organizationId: "org-456", - }); + await getExecutionContext("test-api-key", { organizationId: "org-456" }); expect(mockState.getCalls[0][0]).toBe( "https://api.hyphen.ai/api/execution-context?organizationId=org-456", @@ -92,8 +89,7 @@ describe("getExecutionContext", () => { data: { organization: { id: "org-789" } }, }; - await getExecutionContext({ - apiKey: "test-api-key", + await getExecutionContext("test-api-key", { baseUri: "https://custom.api.com", }); @@ -108,8 +104,8 @@ describe("getExecutionContext", () => { data: null, }; - await expect( - getExecutionContext({ apiKey: "invalid-api-key" }), - ).rejects.toThrow("Failed to get execution context: Unauthorized"); + await expect(getExecutionContext("invalid-api-key")).rejects.toThrow( + "Failed to get execution context: Unauthorized", + ); }); }); From 3291b73016a55d796e6f0811f40b0b4b57354eb5 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 16:58:36 -0500 Subject: [PATCH 4/8] making so cache is Cacheable instance --- src/execution-context.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/execution-context.ts b/src/execution-context.ts index 4ed282a..5faa61b 100644 --- a/src/execution-context.ts +++ b/src/execution-context.ts @@ -1,5 +1,5 @@ import { CacheableNet } from "@cacheable/net"; -import { Cacheable } from "cacheable"; +import type { Cacheable } from "cacheable"; export type ExecutionContextOptions = { /** @@ -12,10 +12,9 @@ export type ExecutionContextOptions = { */ baseUri?: string; /** - * Whether to enable caching for the request. - * @default false + * The Cacheable instance to use for caching requests. */ - cache?: boolean; + cache?: Cacheable; }; export type ExecutionContextRequest = { @@ -100,9 +99,11 @@ export async function getExecutionContext( url += `?organizationId=${encodeURIComponent(options.organizationId)}`; } - const net = new CacheableNet({ - cache: options?.cache ? new Cacheable() : undefined, - }); + const net = options?.cache + ? new CacheableNet({ cache: options.cache }) + : new CacheableNet(); + + const caching = options?.cache !== undefined; const response = await net.get(url, { headers: { @@ -110,6 +111,7 @@ export async function getExecutionContext( "content-type": "application/json", accept: "application/json", }, + caching, }); if (response.response.status !== 200) { From 1374532d3027929143bd7462d134a43df068f175 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 17:10:15 -0500 Subject: [PATCH 5/8] Create execution-context-integration.test.ts --- test/execution-context-integration.test.ts | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/execution-context-integration.test.ts diff --git a/test/execution-context-integration.test.ts b/test/execution-context-integration.test.ts new file mode 100644 index 0000000..d53aaba --- /dev/null +++ b/test/execution-context-integration.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "vitest"; +import { env } from "../src/env.js"; +import { getExecutionContext } from "../src/execution-context.js"; + +env(); + +const apiKey = process.env.HYPHEN_API_KEY; +const organizationId = process.env.HYPHEN_ORGANIZATION_ID; +const testTimeout = 10_000; + +describe("getExecutionContext integration", () => { + test.skipIf(!apiKey)( + "should fetch execution context from live API", + async () => { + const context = await getExecutionContext(apiKey as string); + expect(context).toBeDefined(); + expect(context.user).toBeDefined(); + expect(context.member).toBeDefined(); + expect(context.member?.organization).toBeDefined(); + }, + testTimeout, + ); + + test.skipIf(apiKey === undefined && organizationId === undefined)( + "should fetch organization level execution context from live API", + async () => { + const context = await getExecutionContext(apiKey as string, { + organizationId, + }); + expect(context).toBeDefined(); + expect(context.user).toBeDefined(); + expect(context.member).toBeDefined(); + expect(context.organization).toBeDefined(); + }, + testTimeout, + ); +}); From 076111031ad118b413f0155f782d642a979baac0 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 17:11:42 -0500 Subject: [PATCH 6/8] adding cache test --- test/execution-context-integration.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/execution-context-integration.test.ts b/test/execution-context-integration.test.ts index d53aaba..5de2ee4 100644 --- a/test/execution-context-integration.test.ts +++ b/test/execution-context-integration.test.ts @@ -1,3 +1,4 @@ +import { Cacheable } from "cacheable"; import { describe, expect, test } from "vitest"; import { env } from "../src/env.js"; import { getExecutionContext } from "../src/execution-context.js"; @@ -26,6 +27,7 @@ describe("getExecutionContext integration", () => { async () => { const context = await getExecutionContext(apiKey as string, { organizationId, + cache: new Cacheable(), }); expect(context).toBeDefined(); expect(context.user).toBeDefined(); From b7fcd82dbfe5023dd5fb3b3709d5a07381650f34 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 17:21:27 -0500 Subject: [PATCH 7/8] adding in ignores --- src/base-service.ts | 3 +++ src/env.ts | 1 + src/net-info.ts | 1 + src/toggle.ts | 4 ++++ 4 files changed, 9 insertions(+) diff --git a/src/base-service.ts b/src/base-service.ts index 5a53f19..00a3c4b 100644 --- a/src/base-service.ts +++ b/src/base-service.ts @@ -143,6 +143,7 @@ export class BaseService extends Hookified { config?: FetchRequestInit & { data?: any }, ): Promise> { const headers = { ...(config?.headers as any) }; + /* v8 ignore next -- @preserve */ if (headers) { delete headers["content-type"]; } @@ -157,6 +158,7 @@ export class BaseService extends Hookified { ? configData : JSON.stringify(configData); // Add content-type back if we have data + /* v8 ignore next -- @preserve */ if (!headers["content-type"] && !headers["Content-Type"]) { headers["content-type"] = "application/json"; } @@ -212,6 +214,7 @@ export class BaseService extends Hookified { "content-type": "application/json", accept: "application/json", }; + /* v8 ignore next -- @preserve */ if (apiKey) { headers["x-api-key"] = apiKey; } diff --git a/src/env.ts b/src/env.ts index 4088231..38529f0 100644 --- a/src/env.ts +++ b/src/env.ts @@ -51,6 +51,7 @@ export function env(options?: EnvOptions): void { // Load the environment specific .env file const environment = options?.environment ?? process.env.NODE_ENV; + /* v8 ignore next -- @preserve */ if (environment) { const envSpecificPath = path.resolve( currentWorkingDirectory, diff --git a/src/net-info.ts b/src/net-info.ts index 7b95e5c..5240e1b 100644 --- a/src/net-info.ts +++ b/src/net-info.ts @@ -149,6 +149,7 @@ export class NetInfo extends BaseService { const errorResult: ipInfoError = { ip, type: "error", + /* v8 ignore next -- @preserve */ errorMessage: error instanceof Error ? error.message : "Unknown error", }; return errorResult; diff --git a/src/toggle.ts b/src/toggle.ts index c89c23f..763c5fe 100644 --- a/src/toggle.ts +++ b/src/toggle.ts @@ -512,6 +512,7 @@ export class Toggle extends Hookified { try { const context: ToggleEvaluation = { application: this._applicationId ?? "", + /* v8 ignore next -- @preserve */ environment: this._environment ?? "development", }; @@ -551,6 +552,7 @@ export class Toggle extends Hookified { fetchOptions, ); + /* v8 ignore next -- @preserve */ if (result?.toggles) { return result.toggles[toggleKey].value as T; } @@ -751,6 +753,7 @@ export class Toggle extends Hookified { return data; } catch (error) { const fetchError = + /* v8 ignore next -- @preserve */ error instanceof Error ? error : new Error("Unknown fetch error"); // Extract status code from CacheableNet error messages @@ -910,6 +913,7 @@ export class Toggle extends Hookified { public generateTargetKey(): string { const randomSuffix = Math.random().toString(36).substring(7); const app = this._applicationId || ""; + /* v8 ignore next -- @preserve */ const env = this._environment || ""; // Build key components in order of preference From f5dd45f0038f5a72115527fea3ab4038e9291531 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Wed, 28 Jan 2026 17:31:47 -0500 Subject: [PATCH 8/8] Update README.md --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index a1861a4..61c5f29 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ The Hyphen Node.js SDK is a JavaScript library that allows developers to easily - [ENV - Secret Management Service](#env---secret-management-service) - [Loading Environment Variables](#loading-environment-variables) - [Net Info - Geo Information Service](#net-info---geo-information-service) +- [Execution Context - API Key Validation](#execution-context---api-key-validation) - [Link - Short Code Service](#link---short-code-service) - [Creating a Short Code](#creating-a-short-code) - [Updating a Short Code](#updating-a-short-code) @@ -673,6 +674,75 @@ console.log('IP Infos:', ipInfos); You can also set the API key using the `HYPHEN_API_KEY` environment variable. This is useful for keeping your API key secure and not hardcoding it in your code. +# Execution Context - API Key Validation + +The `getExecutionContext` function validates an API key and returns information about the authenticated user, organization, and request context. This is useful for verifying API keys and getting user/organization details. + +## Basic Usage + +```javascript +import { getExecutionContext } from '@hyphen/sdk'; + +const context = await getExecutionContext('your-api-key'); + +console.log('User:', context.user?.name); +console.log('Organization:', context.member?.organization?.name); +console.log('IP Address:', context.ipAddress); +console.log('Location:', context.location?.city, context.location?.country); +``` + +## Options + +| Option | Type | Description | +|--------|------|-------------| +| `organizationId` | `string` | Optional organization ID to scope the context request | +| `baseUri` | `string` | Custom API base URI (defaults to `https://api.hyphen.ai`) | +| `cache` | `Cacheable` | Cacheable instance for caching requests | + +## With Organization ID + +If you need to get context for a specific organization: + +```javascript +import { getExecutionContext } from '@hyphen/sdk'; + +const context = await getExecutionContext('your-api-key', { + organizationId: 'org_123456789', +}); + +console.log('Organization:', context.organization?.name); +``` + +## With Caching + +To enable caching of execution context requests: + +```javascript +import { Cacheable } from 'cacheable'; +import { getExecutionContext } from '@hyphen/sdk'; + +const cache = new Cacheable({ ttl: 60000 }); // Cache for 60 seconds + +const context = await getExecutionContext('your-api-key', { + cache, +}); + +console.log('User:', context.user?.name); +``` + +## Return Type + +The function returns an `ExecutionContext` object with the following properties: + +| Property | Type | Description | +|----------|------|-------------| +| `request` | `object` | Request metadata (`id`, `causationId`, `correlationId`) | +| `user` | `object` | User info (`id`, `name`, `rules`, `type`) | +| `member` | `object` | Member info with nested `organization` | +| `organization` | `object` | Organization info (`id`, `name`) | +| `ipAddress` | `string` | The IP address of the request | +| `location` | `object` | Geo location (`country`, `region`, `city`, `lat`, `lng`, `postalCode`, `timezone`) | + # Link - Short Code Service The Hyphen Node.js SDK also provides a `Link` class that allows you to create and manage short codes. This can be useful for generating short links for your application.