@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 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
  }
@@ -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,MAAM,CAAC;IACvB,6CAA6C;IAC7C,cAAc,EAAE,qBAAqB,CAAC;CACvC;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,UAAU,EAAE,UAAU,CAAC;KACxB;CACF;;AAiBD,wBAGG"}
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: options.organizationId,
9
+ organizationId: () => tokenProvider.getOrganizationId(),
10
10
  });
11
11
  fastify.decorate("postClient", client);
12
12
  };
@@ -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;AAiB/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,OAAO,CAAC,cAAc;KACvC,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"}
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
- /** Returns a valid token, fetching a new one if necessary. */
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;AAOD;;;;;GAKG;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,CAAgC;gBAEpC,OAAO,EAAE,qBAAqB;IAO1C,8DAA8D;IAC9D,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;YAiBb,UAAU;IA8BxB,+EAA+E;IAC/E,eAAe,IAAI,aAAa;CAGjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,qBAAqB,GAC7B,aAAa,CAEf"}
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"}
@@ -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
- /** Returns a valid token, fetching a new one if necessary. */
17
- getToken() {
29
+ async ensureFresh() {
18
30
  const now = Date.now();
19
31
  if (this.cached && this.cached.expiresAt - now > this.refreshBeforeMs) {
20
- return Promise.resolve(this.cached.value);
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 body = new URLSearchParams();
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: { "Content-Type": "application/x-www-form-urlencoded" },
38
- body,
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.value;
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":"AAgBA;;;;;GAKG;AACH,MAAM,OAAO,2BAA2B;IAQtC,YAAY,OAA8B;QAHlC,WAAM,GAAuB,IAAI,CAAC;QAClC,aAAQ,GAA2B,IAAI,CAAC;QAG9C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,eAAe,GAAG,CAAC,OAAO,CAAC,0BAA0B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAC3E,CAAC;IAED,8DAA8D;IAC9D,QAAQ;QACN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEvB,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;YACtE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC;QAED,oEAAoE;QACpE,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC;QAExC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAC7C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAEO,KAAK,CAAC,UAAU;QACtB,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAC;QACnC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC;QAC7C,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAE7C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,aAAa,EAAE;YACtD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;YAChE,IAAI;SACL,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAuB,CAAC;YACrE,MAAM,IAAI,KAAK,CACb,sCAAsC,GAAG,CAAC,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,eAAe,EAAE,CACrF,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAmD,CAAC;QAE/E,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC;QACpD,IAAI,CAAC,MAAM,GAAG;YACZ,KAAK,EAAE,IAAI,CAAC,YAAY;YACxB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW;SACpC,CAAC;QAEF,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IAC3B,CAAC;IAED,+EAA+E;IAC/E,eAAe;QACb,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC/B,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAA8B;IAE9B,OAAO,IAAI,2BAA2B,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,CAAC;AACpE,CAAC"}
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.0",
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: options.organizationId,
28
+ organizationId: () => tokenProvider.getOrganizationId() as Promise<string>,
31
29
  });
32
30
 
33
31
  fastify.decorate("postClient", client);
@@ -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<string> | null = null;
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
- /** Returns a valid token, fetching a new one if necessary. */
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 Promise.resolve(this.cached.value);
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
- private async fetchToken(): Promise<string> {
57
- const body = new URLSearchParams();
58
- body.set("grant_type", "client_credentials");
59
- body.set("client_id", this.clientId);
60
- body.set("client_secret", this.clientSecret);
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: { "Content-Type": "application/x-www-form-urlencoded" },
65
- body,
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.value;
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,
package/tsconfig.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
6
  "lib": ["ES2020", "DOM"],
7
+ "types": ["node"],
7
8
  "outDir": "./dist",
8
9
  "rootDir": "./src",
9
10
  "declaration": true,