Skip to content

Commit 51c23f6

Browse files
feat: add getEmbedAccessToken functionality (#6)
1 parent 580ff10 commit 51c23f6

File tree

9 files changed

+173
-15
lines changed

9 files changed

+173
-15
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,50 @@ app.use((req: Request, res: Response, next: NextFunction) => {
336336
ctrl.setMaskingOpts(Masking.withRequestHeaderMask("authorization"))
337337
next();
338338
});
339+
```
340+
341+
## Embedded Request Viewer Access Tokens
342+
343+
The Speakeasy SDK can generate access tokens for the [Embedded Request Viewer](https://docs.speakeasyapi.dev/speakeasy-user-guide/request-viewer/embedded-request-viewer) that can be used to view requests captured by the SDK.
344+
345+
For documentation on how to configure filters, find that [HERE](https://docs.speakeasyapi.dev/speakeasy-user-guide/request-viewer/embedded-request-viewer).
346+
347+
Below are some examples on how to generate access tokens:
348+
349+
```typescript
350+
import { EmbedAccessTokenRequest } from "@speakeasy-api/speakeasy-schemas/registry/embedaccesstoken/embedaccesstoken_pb";
351+
352+
// If the SDK is configured as a global instance, an access token can be generated using the `generateAccessToken` function on the speakeasy package.
353+
const req = new EmbedAccessTokenRequest();
354+
const filter = new EmbedAccessTokenRequest.Filter();
355+
filter.setKey("customer_id");
356+
filter.setOperator("=");
357+
filter.setValue("a-customer-id");
358+
359+
req.setFiltersList([filter]);
360+
const accessToken = await speakeasy.getEmbedAccessToken(req);
361+
362+
// If you have followed the `Advanced Configuration` section above you can also generate an access token using the `GenerateAccessToken` function on the sdk instance.
363+
const req = new EmbedAccessTokenRequest();
364+
const filter = new EmbedAccessTokenRequest.Filter();
365+
filter.setKey("customer_id");
366+
filter.setOperator("=");
367+
filter.setValue("a-customer-id");
368+
369+
req.setFiltersList([filter]);
370+
const accessToken = await storeSDK.getEmbedAccessToken(req);
371+
372+
// Or finally if you have a handler that you would like to generate an access token from, you can get the SDK instance for that handler from the middleware controller and use the `GetEmbedAccessToken` function it.
373+
app.all("/", (req, res) => {
374+
const req = new EmbedAccessTokenRequest();
375+
const filter = new EmbedAccessTokenRequest.Filter();
376+
filter.setKey("customer_id");
377+
filter.setOperator("=");
378+
filter.setValue("a-customer-id");
379+
380+
req.setFiltersList([filter]);
381+
const accessToken = await req.controller.getSDKInstance().getEmbedAccessToken(req);
382+
383+
// the rest of your handlers code
384+
});
339385
```

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@speakeasy-api/speakeasy-typescript-sdk",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/speakeasy-api/speakeasy-typescript-sdk"
@@ -45,7 +45,7 @@
4545
},
4646
"dependencies": {
4747
"@grpc/grpc-js": "^1.6.9",
48-
"@speakeasy-api/speakeasy-schemas": "^1.1.2",
48+
"@speakeasy-api/speakeasy-schemas": "^1.3.0",
4949
"content-type": "^1.0.4",
5050
"cookie": "^0.5.0",
5151
"raw-body": "^2.5.1",

src/controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import { SpeakeasySDK } from "./speakeasy";
2+
13
type MaskingOption = (m: Masking) => void;
24

35
export class MiddlewareController {
6+
private sdkInstance: SpeakeasySDK = null;
47
private pathHint: string = "";
58
private customerID: string = "";
69
private masking: Masking = new Masking();
710

11+
public constructor(sdkInstance: SpeakeasySDK | null) {
12+
this.sdkInstance = sdkInstance;
13+
}
14+
815
public getPathHint(): string {
916
return this.pathHint;
1017
}
@@ -28,6 +35,10 @@ export class MiddlewareController {
2835
public getMasking(): Masking {
2936
return this.masking;
3037
}
38+
39+
public getSDKInstance(): SpeakeasySDK {
40+
return this.sdkInstance;
41+
}
3142
}
3243

3344
export const DefaultStringMask: string = "__masked__";

src/middleware/common/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function expressCompatibleMiddleware(
2424

2525
const reqResWriter = new RequestResponseWriter(req, res);
2626

27-
const controller = new MiddlewareController();
27+
const controller = new MiddlewareController(speakeasy);
2828

2929
res.on("finish", () => {
3030
reqResWriter.end();

src/speakeasy.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EmbedAccessTokenRequest } from "@speakeasy-api/speakeasy-schemas/registry/embedaccesstoken/embedaccesstoken_pb";
12
import { GRPCClient } from "./transport";
23
import type { RequestHandler } from "express";
34
import { expressMiddleware as eMiddleware } from "./middleware/express/middleware";
@@ -58,6 +59,10 @@ export class SpeakeasySDK {
5859
public send(har: string, pathHint: string, customerID: string) {
5960
this.grpcClient.send(har, pathHint, customerID);
6061
}
62+
63+
public getEmbedAccessToken(req: EmbedAccessTokenRequest): Promise<string> {
64+
return this.grpcClient.getEmbedAccessToken(req);
65+
}
6166
}
6267

6368
export function configure(config: Config): void {
@@ -80,6 +85,16 @@ export function nestJSMiddleware(): RequestHandler {
8085
return speakeasyInstance.nestJSMiddleware();
8186
}
8287

88+
export function getEmbedAccessToken(
89+
req: EmbedAccessTokenRequest
90+
): Promise<string> {
91+
if (!speakeasyInstance) {
92+
throw new Error("speakeasy is not configured");
93+
}
94+
95+
return speakeasyInstance.getEmbedAccessToken(req);
96+
}
97+
8398
const maxIDSize = 128;
8499
const validCharsRegexStr = "[^a-zA-Z0-9.\\-_~]";
85100

src/transport.ts

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import * as grpc from "@grpc/grpc-js";
22

3+
import {
4+
CallOptions,
5+
Client,
6+
ClientUnaryCall,
7+
Metadata,
8+
ServiceError,
9+
} from "@grpc/grpc-js";
10+
11+
import { EmbedAccessTokenRequest } from "@speakeasy-api/speakeasy-schemas/registry/embedaccesstoken/embedaccesstoken_pb";
12+
import { EmbedAccessTokenServiceClient } from "@speakeasy-api/speakeasy-schemas/registry/embedaccesstoken/embedaccesstoken_grpc_pb";
313
import { IngestRequest } from "@speakeasy-api/speakeasy-schemas/registry/ingest/ingest_pb";
414
import { IngestServiceClient } from "@speakeasy-api/speakeasy-schemas/registry/ingest/ingest_grpc_pb";
15+
import { Message } from "google-protobuf";
516

617
export class GRPCClient {
7-
private client: IngestServiceClient;
18+
private ingestClient: Promisified<IngestServiceClient>;
19+
private embedClient: Promisified<EmbedAccessTokenServiceClient>;
820
private apiKey: string;
921
private apiID: string;
1022
private versionID: string;
@@ -21,7 +33,12 @@ export class GRPCClient {
2133
credentials = grpc.credentials.createInsecure();
2234
}
2335

24-
this.client = new IngestServiceClient(address, credentials);
36+
this.ingestClient = promisify(
37+
new IngestServiceClient(address, credentials)
38+
);
39+
this.embedClient = promisify(
40+
new EmbedAccessTokenServiceClient(address, credentials)
41+
);
2542

2643
this.apiKey = apiKey;
2744
this.apiID = apiID;
@@ -39,10 +56,79 @@ export class GRPCClient {
3956
request.setPathHint(pathHint);
4057
request.setCustomerId(customerID);
4158

42-
this.client.ingest(request, metadata, (err, response) => {
59+
this.ingestClient.ingest(request, metadata).catch((err) => {
4360
if (err) {
44-
console.error(err); // TODO log error?
61+
console.error(err); // TODO log error with a provided logger?
4562
}
4663
});
4764
}
65+
66+
public async getEmbedAccessToken(
67+
req: EmbedAccessTokenRequest
68+
): Promise<string> {
69+
const metadata = new grpc.Metadata();
70+
metadata.set("x-api-key", this.apiKey);
71+
72+
const res = await this.embedClient.get(req, metadata);
73+
return res.getAccesstoken();
74+
}
75+
}
76+
77+
type OriginalCall<T, U> = (
78+
request: T,
79+
metadata: Metadata,
80+
options: Partial<CallOptions>,
81+
callback: (error: ServiceError, res: U) => void
82+
) => ClientUnaryCall;
83+
84+
type PromisifiedCall<T, U> = (
85+
request: T,
86+
metadata?: Metadata,
87+
options?: Partial<CallOptions>
88+
) => Promise<U>;
89+
90+
export type Promisified<C> = { $: C } & {
91+
[prop in Exclude<keyof C, keyof Client>]: C[prop] extends OriginalCall<
92+
infer T,
93+
infer U
94+
>
95+
? PromisifiedCall<T, U>
96+
: never;
97+
};
98+
99+
export function promisify<C extends Client>(client: C): Promisified<C> {
100+
return new Proxy(client, {
101+
get: (target, descriptor) => {
102+
let stack = "";
103+
104+
// this step is required to get the correct stack trace
105+
// of course, this has some performance impact, but it's not that big in comparison with grpc calls
106+
try {
107+
throw new Error();
108+
} catch (e) {
109+
stack = e.stack;
110+
}
111+
112+
if (descriptor === "$") {
113+
return target;
114+
}
115+
116+
return (...args: any[]) =>
117+
new Promise((resolve, reject) =>
118+
target[descriptor](
119+
...[
120+
...args,
121+
(err: ServiceError, res: Message) => {
122+
if (err) {
123+
err.stack += stack;
124+
reject(err);
125+
} else {
126+
resolve(res);
127+
}
128+
},
129+
]
130+
)
131+
);
132+
},
133+
}) as unknown as Promisified<C>;
48134
}

test/masking/masking.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe("Masking Configuration", () => {
8888
];
8989

9090
test.each(tests)("$name", ({ name, args, wantQueryStringMasks }) => {
91-
const controller = new MiddlewareController();
91+
const controller = new MiddlewareController(null);
9292
controller.setMaskingOpts(
9393
Masking.withQueryStringMask(args.keys, ...args.masks)
9494
);
@@ -181,7 +181,7 @@ describe("Masking Configuration", () => {
181181
];
182182

183183
test.each(tests)("$name", ({ name, args, wantRequestCookieMasks }) => {
184-
const controller = new MiddlewareController();
184+
const controller = new MiddlewareController(null);
185185
controller.setMaskingOpts(
186186
Masking.withRequestCookieMask(args.keys, ...args.masks)
187187
);

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -648,10 +648,10 @@
648648
dependencies:
649649
"@sinonjs/commons" "^1.7.0"
650650

651-
"@speakeasy-api/speakeasy-schemas@^1.1.2":
652-
version "1.1.2"
653-
resolved "https://registry.yarnpkg.com/@speakeasy-api/speakeasy-schemas/-/speakeasy-schemas-1.1.2.tgz#fd3485bc0bfbbafe892ce240b61c0bb3147cbd81"
654-
integrity sha512-WpkQqafGg4d1X0Ylo9oVuQJ0EP//iKiro8cQHulTkEPudIOnP3U17XKsD76htdeUSrpLbrioi0WYWtiI68JhrA==
651+
"@speakeasy-api/speakeasy-schemas@^1.3.0":
652+
version "1.3.0"
653+
resolved "https://registry.yarnpkg.com/@speakeasy-api/speakeasy-schemas/-/speakeasy-schemas-1.3.0.tgz#75f24c153a3fec97c84ed6680bbb0fa0cf7dca4d"
654+
integrity sha512-EwbXWmJfJoaPWSqBrUurg2hgXJfpRVNvhgmaggzmILuMLNx4cme8z1KNLl75hCTSQdIV5154g/ZcfA0Iz4ed8g==
655655
dependencies:
656656
"@types/google-protobuf" "^3.15.6"
657657
google-protobuf "^3.21.0"

0 commit comments

Comments
 (0)