@geekapps/post-fastify 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin.d.ts +0 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/service-account.d.ts +5 -14
- package/dist/service-account.d.ts.map +1 -1
- package/dist/service-account.js +35 -24
- package/dist/service-account.js.map +1 -1
- package/package.json +2 -1
- package/src/plugin.ts +1 -3
- package/src/service-account.ts +41 -26
- package/tsconfig.json +1 -0
package/dist/plugin.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ import { type ServiceAccountOptions } from "./service-account.js";
|
|
|
4
4
|
export interface PostPluginOptions {
|
|
5
5
|
/** Geekapps Post API base URL */
|
|
6
6
|
baseUrl: string;
|
|
7
|
-
/** Organization ID to use on all requests */
|
|
8
|
-
organizationId: string;
|
|
9
7
|
/** Service account credentials — required */
|
|
10
8
|
serviceAccount: ServiceAccountOptions;
|
|
11
9
|
}
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEnE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAA+B,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE/F,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEnE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAA+B,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE/F,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,cAAc,EAAE,qBAAqB,CAAC;CACvC;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,UAAU,EAAE,UAAU,CAAC;KACxB;CACF;;AAiBD,wBAGG"}
|
package/dist/plugin.js
CHANGED
|
@@ -6,7 +6,7 @@ const postPlugin = async (fastify, options) => {
|
|
|
6
6
|
const client = new PostClient({
|
|
7
7
|
baseUrl: options.baseUrl,
|
|
8
8
|
token: () => tokenProvider.getToken(),
|
|
9
|
-
organizationId:
|
|
9
|
+
organizationId: () => tokenProvider.getOrganizationId(),
|
|
10
10
|
});
|
|
11
11
|
fastify.decorate("postClient", client);
|
|
12
12
|
};
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,2BAA2B,EAA8B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,2BAA2B,EAA8B,MAAM,sBAAsB,CAAC;AAe/F,MAAM,UAAU,GAA0C,KAAK,EAC7D,OAAwB,EACxB,OAA0B,EAC1B,EAAE;IACF,MAAM,aAAa,GAAG,IAAI,2BAA2B,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAE9E,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE;QACrC,cAAc,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,iBAAiB,EAAqB;KAC3E,CAAC,CAAC;IAEH,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC,CAAC;AAEF,eAAe,EAAE,CAAC,UAAU,EAAE;IAC5B,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,wBAAwB;CAC/B,CAAC,CAAC"}
|
|
@@ -12,6 +12,7 @@ export interface ServiceAccountOptions {
|
|
|
12
12
|
* client_credentials grant and caches it until near expiry.
|
|
13
13
|
*
|
|
14
14
|
* Tokens are valid for ~7 min. A new one is fetched 30 s before that.
|
|
15
|
+
* The organizationId claim is extracted automatically from the JWT payload.
|
|
15
16
|
*/
|
|
16
17
|
export declare class ServiceAccountTokenProvider {
|
|
17
18
|
private readonly issuerUrl;
|
|
@@ -21,27 +22,17 @@ export declare class ServiceAccountTokenProvider {
|
|
|
21
22
|
private cached;
|
|
22
23
|
private inflight;
|
|
23
24
|
constructor(options: ServiceAccountOptions);
|
|
24
|
-
|
|
25
|
+
private ensureFresh;
|
|
26
|
+
/** Returns a valid access token. */
|
|
25
27
|
getToken(): Promise<string>;
|
|
28
|
+
/** Returns the organizationId embedded in the service account JWT. */
|
|
29
|
+
getOrganizationId(): Promise<string | undefined>;
|
|
26
30
|
private fetchToken;
|
|
27
31
|
/** Returns this provider as a TokenResolver to pass directly to PostClient. */
|
|
28
32
|
asTokenResolver(): TokenResolver;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* Convenience factory. Returns a TokenResolver ready for use in PostClient.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* const post = new PostClient({
|
|
36
|
-
* baseUrl: process.env.POST_API_URL!,
|
|
37
|
-
* token: createServiceAccountToken({
|
|
38
|
-
* issuerUrl: process.env.GEEKAPPS_AUTH_ISSUER!,
|
|
39
|
-
* clientId: process.env.GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID!,
|
|
40
|
-
* clientSecret: process.env.GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET!,
|
|
41
|
-
* }),
|
|
42
|
-
* organizationId: process.env.POST_ORGANIZATION_ID!,
|
|
43
|
-
* });
|
|
44
|
-
* ```
|
|
45
36
|
*/
|
|
46
37
|
export declare function createServiceAccountToken(options: ServiceAccountOptions): TokenResolver;
|
|
47
38
|
//# sourceMappingURL=service-account.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-account.d.ts","sourceRoot":"","sources":["../src/service-account.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;
|
|
1
|
+
{"version":3,"file":"service-account.d.ts","sourceRoot":"","sources":["../src/service-account.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAmBD;;;;;;GAMG;AACH,qBAAa,2BAA2B;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAqC;gBAEzC,OAAO,EAAE,qBAAqB;YAO5B,WAAW;IAgBzB,oCAAoC;IACpC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAI3B,sEAAsE;IACtE,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;YAIlC,UAAU;IAqCxB,+EAA+E;IAC/E,eAAe,IAAI,aAAa;CAGjC;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,qBAAqB,GAC7B,aAAa,CAEf"}
|
package/dist/service-account.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
function decodeJwtPayload(token) {
|
|
2
|
+
try {
|
|
3
|
+
const [, payload] = token.split(".");
|
|
4
|
+
if (!payload)
|
|
5
|
+
return {};
|
|
6
|
+
const json = Buffer.from(payload, "base64url").toString("utf-8");
|
|
7
|
+
return JSON.parse(json);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
1
13
|
/**
|
|
2
14
|
* Fetches a short-lived token from the Geekapps Auth service using
|
|
3
15
|
* client_credentials grant and caches it until near expiry.
|
|
4
16
|
*
|
|
5
17
|
* Tokens are valid for ~7 min. A new one is fetched 30 s before that.
|
|
18
|
+
* The organizationId claim is extracted automatically from the JWT payload.
|
|
6
19
|
*/
|
|
7
20
|
export class ServiceAccountTokenProvider {
|
|
8
21
|
constructor(options) {
|
|
@@ -13,13 +26,11 @@ export class ServiceAccountTokenProvider {
|
|
|
13
26
|
this.clientSecret = options.clientSecret;
|
|
14
27
|
this.refreshBeforeMs = (options.refreshBeforeExpirySeconds ?? 30) * 1000;
|
|
15
28
|
}
|
|
16
|
-
|
|
17
|
-
getToken() {
|
|
29
|
+
async ensureFresh() {
|
|
18
30
|
const now = Date.now();
|
|
19
31
|
if (this.cached && this.cached.expiresAt - now > this.refreshBeforeMs) {
|
|
20
|
-
return
|
|
32
|
+
return this.cached;
|
|
21
33
|
}
|
|
22
|
-
// Deduplicate concurrent calls — only one fetch in flight at a time
|
|
23
34
|
if (this.inflight)
|
|
24
35
|
return this.inflight;
|
|
25
36
|
this.inflight = this.fetchToken().finally(() => {
|
|
@@ -27,27 +38,40 @@ export class ServiceAccountTokenProvider {
|
|
|
27
38
|
});
|
|
28
39
|
return this.inflight;
|
|
29
40
|
}
|
|
41
|
+
/** Returns a valid access token. */
|
|
42
|
+
getToken() {
|
|
43
|
+
return this.ensureFresh().then((c) => c.value);
|
|
44
|
+
}
|
|
45
|
+
/** Returns the organizationId embedded in the service account JWT. */
|
|
46
|
+
getOrganizationId() {
|
|
47
|
+
return this.ensureFresh().then((c) => c.organizationId);
|
|
48
|
+
}
|
|
30
49
|
async fetchToken() {
|
|
31
|
-
const
|
|
32
|
-
body.set("grant_type", "client_credentials");
|
|
33
|
-
body.set("client_id", this.clientId);
|
|
34
|
-
body.set("client_secret", this.clientSecret);
|
|
50
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
|
|
35
51
|
const res = await fetch(`${this.issuerUrl}/auth/token`, {
|
|
36
52
|
method: "POST",
|
|
37
|
-
headers: {
|
|
38
|
-
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
Authorization: `Basic ${credentials}`,
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ grant_type: "service_account" }),
|
|
39
58
|
});
|
|
40
59
|
if (!res.ok) {
|
|
41
60
|
const err = await res.json().catch(() => ({}));
|
|
42
61
|
throw new Error(`ServiceAccount token fetch failed (${res.status}): ${err.error ?? "unknown error"}`);
|
|
43
62
|
}
|
|
44
63
|
const data = await res.json();
|
|
64
|
+
const claims = decodeJwtPayload(data.access_token);
|
|
65
|
+
const organizationId = typeof claims.organizationId === "string" ? claims.organizationId :
|
|
66
|
+
typeof claims.organization_id === "string" ? claims.organization_id :
|
|
67
|
+
undefined;
|
|
45
68
|
const expiresInMs = (data.expires_in ?? 420) * 1000;
|
|
46
69
|
this.cached = {
|
|
47
70
|
value: data.access_token,
|
|
48
71
|
expiresAt: Date.now() + expiresInMs,
|
|
72
|
+
organizationId,
|
|
49
73
|
};
|
|
50
|
-
return this.cached
|
|
74
|
+
return this.cached;
|
|
51
75
|
}
|
|
52
76
|
/** Returns this provider as a TokenResolver to pass directly to PostClient. */
|
|
53
77
|
asTokenResolver() {
|
|
@@ -56,19 +80,6 @@ export class ServiceAccountTokenProvider {
|
|
|
56
80
|
}
|
|
57
81
|
/**
|
|
58
82
|
* Convenience factory. Returns a TokenResolver ready for use in PostClient.
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```ts
|
|
62
|
-
* const post = new PostClient({
|
|
63
|
-
* baseUrl: process.env.POST_API_URL!,
|
|
64
|
-
* token: createServiceAccountToken({
|
|
65
|
-
* issuerUrl: process.env.GEEKAPPS_AUTH_ISSUER!,
|
|
66
|
-
* clientId: process.env.GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID!,
|
|
67
|
-
* clientSecret: process.env.GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET!,
|
|
68
|
-
* }),
|
|
69
|
-
* organizationId: process.env.POST_ORGANIZATION_ID!,
|
|
70
|
-
* });
|
|
71
|
-
* ```
|
|
72
83
|
*/
|
|
73
84
|
export function createServiceAccountToken(options) {
|
|
74
85
|
return new ServiceAccountTokenProvider(options).asTokenResolver();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-account.js","sourceRoot":"","sources":["../src/service-account.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service-account.js","sourceRoot":"","sources":["../src/service-account.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekapps/post-fastify",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Geekapps Post SDK for Fastify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"dev": "tsc --watch"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
+
"@types/node": "^25.9.1",
|
|
20
21
|
"fastify": "^5.8.5",
|
|
21
22
|
"fastify-plugin": "^5.1.0",
|
|
22
23
|
"typescript": "^5.9.3"
|
package/src/plugin.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { ServiceAccountTokenProvider, type ServiceAccountOptions } from "./servi
|
|
|
6
6
|
export interface PostPluginOptions {
|
|
7
7
|
/** Geekapps Post API base URL */
|
|
8
8
|
baseUrl: string;
|
|
9
|
-
/** Organization ID to use on all requests */
|
|
10
|
-
organizationId: string;
|
|
11
9
|
/** Service account credentials — required */
|
|
12
10
|
serviceAccount: ServiceAccountOptions;
|
|
13
11
|
}
|
|
@@ -27,7 +25,7 @@ const postPlugin: FastifyPluginAsync<PostPluginOptions> = async (
|
|
|
27
25
|
const client = new PostClient({
|
|
28
26
|
baseUrl: options.baseUrl,
|
|
29
27
|
token: () => tokenProvider.getToken(),
|
|
30
|
-
organizationId:
|
|
28
|
+
organizationId: () => tokenProvider.getOrganizationId() as Promise<string>,
|
|
31
29
|
});
|
|
32
30
|
|
|
33
31
|
fastify.decorate("postClient", client);
|
package/src/service-account.ts
CHANGED
|
@@ -12,6 +12,18 @@ export interface ServiceAccountOptions {
|
|
|
12
12
|
interface CachedToken {
|
|
13
13
|
value: string;
|
|
14
14
|
expiresAt: number; // ms epoch
|
|
15
|
+
organizationId: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function decodeJwtPayload(token: string): Record<string, unknown> {
|
|
19
|
+
try {
|
|
20
|
+
const [, payload] = token.split(".");
|
|
21
|
+
if (!payload) return {};
|
|
22
|
+
const json = Buffer.from(payload, "base64url").toString("utf-8");
|
|
23
|
+
return JSON.parse(json) as Record<string, unknown>;
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
/**
|
|
@@ -19,6 +31,7 @@ interface CachedToken {
|
|
|
19
31
|
* client_credentials grant and caches it until near expiry.
|
|
20
32
|
*
|
|
21
33
|
* Tokens are valid for ~7 min. A new one is fetched 30 s before that.
|
|
34
|
+
* The organizationId claim is extracted automatically from the JWT payload.
|
|
22
35
|
*/
|
|
23
36
|
export class ServiceAccountTokenProvider {
|
|
24
37
|
private readonly issuerUrl: string;
|
|
@@ -26,7 +39,7 @@ export class ServiceAccountTokenProvider {
|
|
|
26
39
|
private readonly clientSecret: string;
|
|
27
40
|
private readonly refreshBeforeMs: number;
|
|
28
41
|
private cached: CachedToken | null = null;
|
|
29
|
-
private inflight: Promise<
|
|
42
|
+
private inflight: Promise<CachedToken> | null = null;
|
|
30
43
|
|
|
31
44
|
constructor(options: ServiceAccountOptions) {
|
|
32
45
|
this.issuerUrl = options.issuerUrl.replace(/\/$/, "");
|
|
@@ -35,15 +48,13 @@ export class ServiceAccountTokenProvider {
|
|
|
35
48
|
this.refreshBeforeMs = (options.refreshBeforeExpirySeconds ?? 30) * 1000;
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
getToken(): Promise<string> {
|
|
51
|
+
private async ensureFresh(): Promise<CachedToken> {
|
|
40
52
|
const now = Date.now();
|
|
41
53
|
|
|
42
54
|
if (this.cached && this.cached.expiresAt - now > this.refreshBeforeMs) {
|
|
43
|
-
return
|
|
55
|
+
return this.cached;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
// Deduplicate concurrent calls — only one fetch in flight at a time
|
|
47
58
|
if (this.inflight) return this.inflight;
|
|
48
59
|
|
|
49
60
|
this.inflight = this.fetchToken().finally(() => {
|
|
@@ -53,16 +64,26 @@ export class ServiceAccountTokenProvider {
|
|
|
53
64
|
return this.inflight;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
/** Returns a valid access token. */
|
|
68
|
+
getToken(): Promise<string> {
|
|
69
|
+
return this.ensureFresh().then((c) => c.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Returns the organizationId embedded in the service account JWT. */
|
|
73
|
+
getOrganizationId(): Promise<string | undefined> {
|
|
74
|
+
return this.ensureFresh().then((c) => c.organizationId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async fetchToken(): Promise<CachedToken> {
|
|
78
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
|
|
61
79
|
|
|
62
80
|
const res = await fetch(`${this.issuerUrl}/auth/token`, {
|
|
63
81
|
method: "POST",
|
|
64
|
-
headers: {
|
|
65
|
-
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
Authorization: `Basic ${credentials}`,
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({ grant_type: "service_account" }),
|
|
66
87
|
});
|
|
67
88
|
|
|
68
89
|
if (!res.ok) {
|
|
@@ -74,13 +95,20 @@ export class ServiceAccountTokenProvider {
|
|
|
74
95
|
|
|
75
96
|
const data = await res.json() as { access_token: string; expires_in?: number };
|
|
76
97
|
|
|
98
|
+
const claims = decodeJwtPayload(data.access_token);
|
|
99
|
+
const organizationId =
|
|
100
|
+
typeof claims.organizationId === "string" ? claims.organizationId :
|
|
101
|
+
typeof claims.organization_id === "string" ? claims.organization_id :
|
|
102
|
+
undefined;
|
|
103
|
+
|
|
77
104
|
const expiresInMs = (data.expires_in ?? 420) * 1000;
|
|
78
105
|
this.cached = {
|
|
79
106
|
value: data.access_token,
|
|
80
107
|
expiresAt: Date.now() + expiresInMs,
|
|
108
|
+
organizationId,
|
|
81
109
|
};
|
|
82
110
|
|
|
83
|
-
return this.cached
|
|
111
|
+
return this.cached;
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
/** Returns this provider as a TokenResolver to pass directly to PostClient. */
|
|
@@ -91,19 +119,6 @@ export class ServiceAccountTokenProvider {
|
|
|
91
119
|
|
|
92
120
|
/**
|
|
93
121
|
* Convenience factory. Returns a TokenResolver ready for use in PostClient.
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```ts
|
|
97
|
-
* const post = new PostClient({
|
|
98
|
-
* baseUrl: process.env.POST_API_URL!,
|
|
99
|
-
* token: createServiceAccountToken({
|
|
100
|
-
* issuerUrl: process.env.GEEKAPPS_AUTH_ISSUER!,
|
|
101
|
-
* clientId: process.env.GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID!,
|
|
102
|
-
* clientSecret: process.env.GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET!,
|
|
103
|
-
* }),
|
|
104
|
-
* organizationId: process.env.POST_ORGANIZATION_ID!,
|
|
105
|
-
* });
|
|
106
|
-
* ```
|
|
107
122
|
*/
|
|
108
123
|
export function createServiceAccountToken(
|
|
109
124
|
options: ServiceAccountOptions,
|