Skip to content

Commit a442d4f

Browse files
authored
Merge pull request #114 from Hyphen/feat-adding-in-getExecutionContext
feat: adding in getExecutionContext
2 parents adb2b6a + f5dd45f commit a442d4f

10 files changed

Lines changed: 364 additions & 0 deletions

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The Hyphen Node.js SDK is a JavaScript library that allows developers to easily
2424
- [ENV - Secret Management Service](#env---secret-management-service)
2525
- [Loading Environment Variables](#loading-environment-variables)
2626
- [Net Info - Geo Information Service](#net-info---geo-information-service)
27+
- [Execution Context - API Key Validation](#execution-context---api-key-validation)
2728
- [Link - Short Code Service](#link---short-code-service)
2829
- [Creating a Short Code](#creating-a-short-code)
2930
- [Updating a Short Code](#updating-a-short-code)
@@ -673,6 +674,75 @@ console.log('IP Infos:', ipInfos);
673674
674675
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.
675676
677+
# Execution Context - API Key Validation
678+
679+
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.
680+
681+
## Basic Usage
682+
683+
```javascript
684+
import { getExecutionContext } from '@hyphen/sdk';
685+
686+
const context = await getExecutionContext('your-api-key');
687+
688+
console.log('User:', context.user?.name);
689+
console.log('Organization:', context.member?.organization?.name);
690+
console.log('IP Address:', context.ipAddress);
691+
console.log('Location:', context.location?.city, context.location?.country);
692+
```
693+
694+
## Options
695+
696+
| Option | Type | Description |
697+
|--------|------|-------------|
698+
| `organizationId` | `string` | Optional organization ID to scope the context request |
699+
| `baseUri` | `string` | Custom API base URI (defaults to `https://api.hyphen.ai`) |
700+
| `cache` | `Cacheable` | Cacheable instance for caching requests |
701+
702+
## With Organization ID
703+
704+
If you need to get context for a specific organization:
705+
706+
```javascript
707+
import { getExecutionContext } from '@hyphen/sdk';
708+
709+
const context = await getExecutionContext('your-api-key', {
710+
organizationId: 'org_123456789',
711+
});
712+
713+
console.log('Organization:', context.organization?.name);
714+
```
715+
716+
## With Caching
717+
718+
To enable caching of execution context requests:
719+
720+
```javascript
721+
import { Cacheable } from 'cacheable';
722+
import { getExecutionContext } from '@hyphen/sdk';
723+
724+
const cache = new Cacheable({ ttl: 60000 }); // Cache for 60 seconds
725+
726+
const context = await getExecutionContext('your-api-key', {
727+
cache,
728+
});
729+
730+
console.log('User:', context.user?.name);
731+
```
732+
733+
## Return Type
734+
735+
The function returns an `ExecutionContext` object with the following properties:
736+
737+
| Property | Type | Description |
738+
|----------|------|-------------|
739+
| `request` | `object` | Request metadata (`id`, `causationId`, `correlationId`) |
740+
| `user` | `object` | User info (`id`, `name`, `rules`, `type`) |
741+
| `member` | `object` | Member info with nested `organization` |
742+
| `organization` | `object` | Organization info (`id`, `name`) |
743+
| `ipAddress` | `string` | The IP address of the request |
744+
| `location` | `object` | Geo location (`country`, `region`, `city`, `lat`, `lng`, `postalCode`, `timezone`) |
745+
676746
# Link - Short Code Service
677747
678748
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.

src/base-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export class BaseService extends Hookified {
143143
config?: FetchRequestInit & { data?: any },
144144
): Promise<HttpResponse<T>> {
145145
const headers = { ...(config?.headers as any) };
146+
/* v8 ignore next -- @preserve */
146147
if (headers) {
147148
delete headers["content-type"];
148149
}
@@ -157,6 +158,7 @@ export class BaseService extends Hookified {
157158
? configData
158159
: JSON.stringify(configData);
159160
// Add content-type back if we have data
161+
/* v8 ignore next -- @preserve */
160162
if (!headers["content-type"] && !headers["Content-Type"]) {
161163
headers["content-type"] = "application/json";
162164
}
@@ -212,6 +214,7 @@ export class BaseService extends Hookified {
212214
"content-type": "application/json",
213215
accept: "application/json",
214216
};
217+
/* v8 ignore next -- @preserve */
215218
if (apiKey) {
216219
headers["x-api-key"] = apiKey;
217220
}

src/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function env(options?: EnvOptions): void {
5151
// Load the environment specific .env file
5252
const environment = options?.environment ?? process.env.NODE_ENV;
5353

54+
/* v8 ignore next -- @preserve */
5455
if (environment) {
5556
const envSpecificPath = path.resolve(
5657
currentWorkingDirectory,

src/execution-context.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { CacheableNet } from "@cacheable/net";
2+
import type { Cacheable } from "cacheable";
3+
4+
export type ExecutionContextOptions = {
5+
/**
6+
* The organization ID for the Hyphen API.
7+
*/
8+
organizationId?: string;
9+
/**
10+
* The base URI for the Hyphen API.
11+
* @default "https://api.hyphen.ai"
12+
*/
13+
baseUri?: string;
14+
/**
15+
* The Cacheable instance to use for caching requests.
16+
*/
17+
cache?: Cacheable;
18+
};
19+
20+
export type ExecutionContextRequest = {
21+
id?: string;
22+
causationId?: string;
23+
correlationId?: string;
24+
};
25+
26+
export type ExecutionContextUser = {
27+
id?: string;
28+
name?: string;
29+
rules?: Record<string, unknown>[];
30+
type?: string;
31+
};
32+
33+
export type ExecutionContextMember = {
34+
id?: string;
35+
name?: string;
36+
organization?: {
37+
id?: string;
38+
name?: string;
39+
};
40+
rules?: Record<string, unknown>[];
41+
};
42+
43+
export type ExecutionContextOrganization = {
44+
id?: string;
45+
name?: string;
46+
};
47+
48+
export type ExecutionContextLocation = {
49+
country?: string;
50+
region?: string;
51+
city?: string;
52+
lat?: number;
53+
lng?: number;
54+
postalCode?: string;
55+
timezone?: string;
56+
};
57+
58+
export type ExecutionContext = {
59+
request?: ExecutionContextRequest;
60+
user?: ExecutionContextUser;
61+
member?: ExecutionContextMember;
62+
organization?: ExecutionContextOrganization;
63+
ipAddress?: string;
64+
location?: ExecutionContextLocation;
65+
};
66+
67+
/**
68+
* Get the execution context for the provided API key.
69+
* This validates the API key and returns information about the organization, user, and request context.
70+
*
71+
* @param apiKey - The API key for the Hyphen API.
72+
* @param options - Additional options for the request.
73+
* @returns The execution context.
74+
* @throws Error if the API key is not provided or if the request fails.
75+
*
76+
* @example
77+
* ```typescript
78+
* import { getExecutionContext } from '@hyphen/sdk';
79+
*
80+
* const context = await getExecutionContext('your-api-key', {
81+
* organizationId: 'optional-org-id',
82+
* });
83+
*
84+
* console.log(context.organization?.name);
85+
* ```
86+
*/
87+
export async function getExecutionContext(
88+
apiKey: string,
89+
options?: ExecutionContextOptions,
90+
): Promise<ExecutionContext> {
91+
if (!apiKey) {
92+
throw new Error("API key is required");
93+
}
94+
95+
const baseUri = options?.baseUri ?? "https://api.hyphen.ai";
96+
let url = `${baseUri}/api/execution-context`;
97+
98+
if (options?.organizationId) {
99+
url += `?organizationId=${encodeURIComponent(options.organizationId)}`;
100+
}
101+
102+
const net = options?.cache
103+
? new CacheableNet({ cache: options.cache })
104+
: new CacheableNet();
105+
106+
const caching = options?.cache !== undefined;
107+
108+
const response = await net.get<ExecutionContext>(url, {
109+
headers: {
110+
"x-api-key": apiKey,
111+
"content-type": "application/json",
112+
accept: "application/json",
113+
},
114+
caching,
115+
});
116+
117+
if (response.response.status !== 200) {
118+
throw new Error(
119+
`Failed to get execution context: ${response.response.statusText}`,
120+
);
121+
}
122+
123+
return response.data;
124+
}

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
export { type EnvOptions, env, type LoadEnvOptions, loadEnv } from "./env.js";
2+
export {
3+
type ExecutionContext,
4+
type ExecutionContextLocation,
5+
type ExecutionContextMember,
6+
type ExecutionContextOptions,
7+
type ExecutionContextOrganization,
8+
type ExecutionContextRequest,
9+
type ExecutionContextUser,
10+
getExecutionContext,
11+
} from "./execution-context.js";
212
export { Hyphen, type HyphenOptions } from "./hyphen.js";
313
export {
414
Toggle,

src/net-info.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export class NetInfo extends BaseService {
149149
const errorResult: ipInfoError = {
150150
ip,
151151
type: "error",
152+
/* v8 ignore next -- @preserve */
152153
errorMessage: error instanceof Error ? error.message : "Unknown error",
153154
};
154155
return errorResult;

src/toggle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ export class Toggle extends Hookified {
512512
try {
513513
const context: ToggleEvaluation = {
514514
application: this._applicationId ?? "",
515+
/* v8 ignore next -- @preserve */
515516
environment: this._environment ?? "development",
516517
};
517518

@@ -551,6 +552,7 @@ export class Toggle extends Hookified {
551552
fetchOptions,
552553
);
553554

555+
/* v8 ignore next -- @preserve */
554556
if (result?.toggles) {
555557
return result.toggles[toggleKey].value as T;
556558
}
@@ -751,6 +753,7 @@ export class Toggle extends Hookified {
751753
return data;
752754
} catch (error) {
753755
const fetchError =
756+
/* v8 ignore next -- @preserve */
754757
error instanceof Error ? error : new Error("Unknown fetch error");
755758

756759
// Extract status code from CacheableNet error messages
@@ -910,6 +913,7 @@ export class Toggle extends Hookified {
910913
public generateTargetKey(): string {
911914
const randomSuffix = Math.random().toString(36).substring(7);
912915
const app = this._applicationId || "";
916+
/* v8 ignore next -- @preserve */
913917
const env = this._environment || "";
914918

915919
// Build key components in order of preference
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Cacheable } from "cacheable";
2+
import { describe, expect, test } from "vitest";
3+
import { env } from "../src/env.js";
4+
import { getExecutionContext } from "../src/execution-context.js";
5+
6+
env();
7+
8+
const apiKey = process.env.HYPHEN_API_KEY;
9+
const organizationId = process.env.HYPHEN_ORGANIZATION_ID;
10+
const testTimeout = 10_000;
11+
12+
describe("getExecutionContext integration", () => {
13+
test.skipIf(!apiKey)(
14+
"should fetch execution context from live API",
15+
async () => {
16+
const context = await getExecutionContext(apiKey as string);
17+
expect(context).toBeDefined();
18+
expect(context.user).toBeDefined();
19+
expect(context.member).toBeDefined();
20+
expect(context.member?.organization).toBeDefined();
21+
},
22+
testTimeout,
23+
);
24+
25+
test.skipIf(apiKey === undefined && organizationId === undefined)(
26+
"should fetch organization level execution context from live API",
27+
async () => {
28+
const context = await getExecutionContext(apiKey as string, {
29+
organizationId,
30+
cache: new Cacheable(),
31+
});
32+
expect(context).toBeDefined();
33+
expect(context.user).toBeDefined();
34+
expect(context.member).toBeDefined();
35+
expect(context.organization).toBeDefined();
36+
},
37+
testTimeout,
38+
);
39+
});

0 commit comments

Comments
 (0)