@foxscheduling/sdk 0.1.0

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.
Files changed (74) hide show
  1. package/README.md +58 -0
  2. package/dist/auth/apiKey.d.ts +10 -0
  3. package/dist/auth/apiKey.d.ts.map +1 -0
  4. package/dist/auth/apiKey.js +17 -0
  5. package/dist/auth/oauth.d.ts +29 -0
  6. package/dist/auth/oauth.d.ts.map +1 -0
  7. package/dist/auth/oauth.js +110 -0
  8. package/dist/auth/pkce.d.ts +4 -0
  9. package/dist/auth/pkce.d.ts.map +1 -0
  10. package/dist/auth/pkce.js +10 -0
  11. package/dist/auth/tokenManager.d.ts +24 -0
  12. package/dist/auth/tokenManager.d.ts.map +1 -0
  13. package/dist/auth/tokenManager.js +50 -0
  14. package/dist/auth/tokenStore.d.ts +21 -0
  15. package/dist/auth/tokenStore.d.ts.map +1 -0
  16. package/dist/auth/tokenStore.js +49 -0
  17. package/dist/cjs/auth/apiKey.cjs +21 -0
  18. package/dist/cjs/auth/apiKey.d.ts +10 -0
  19. package/dist/cjs/auth/apiKey.d.ts.map +1 -0
  20. package/dist/cjs/auth/oauth.cjs +114 -0
  21. package/dist/cjs/auth/oauth.d.ts +29 -0
  22. package/dist/cjs/auth/oauth.d.ts.map +1 -0
  23. package/dist/cjs/auth/pkce.cjs +15 -0
  24. package/dist/cjs/auth/pkce.d.ts +4 -0
  25. package/dist/cjs/auth/pkce.d.ts.map +1 -0
  26. package/dist/cjs/auth/tokenManager.cjs +55 -0
  27. package/dist/cjs/auth/tokenManager.d.ts +24 -0
  28. package/dist/cjs/auth/tokenManager.d.ts.map +1 -0
  29. package/dist/cjs/auth/tokenStore.cjs +87 -0
  30. package/dist/cjs/auth/tokenStore.d.ts +21 -0
  31. package/dist/cjs/auth/tokenStore.d.ts.map +1 -0
  32. package/dist/cjs/client.cjs +48 -0
  33. package/dist/cjs/client.d.ts +16 -0
  34. package/dist/cjs/client.d.ts.map +1 -0
  35. package/dist/cjs/errors.cjs +19 -0
  36. package/dist/cjs/errors.d.ts +9 -0
  37. package/dist/cjs/errors.d.ts.map +1 -0
  38. package/dist/cjs/http/client.cjs +62 -0
  39. package/dist/cjs/http/client.d.ts +13 -0
  40. package/dist/cjs/http/client.d.ts.map +1 -0
  41. package/dist/cjs/index.cjs +19 -0
  42. package/dist/cjs/index.d.ts +7 -0
  43. package/dist/cjs/index.d.ts.map +1 -0
  44. package/dist/cjs/resources/index.cjs +120 -0
  45. package/dist/cjs/resources/index.d.ts +71 -0
  46. package/dist/cjs/resources/index.d.ts.map +1 -0
  47. package/dist/cjs/types.cjs +4 -0
  48. package/dist/cjs/types.d.ts +31 -0
  49. package/dist/cjs/types.d.ts.map +1 -0
  50. package/dist/cjs/webhooks/verify.cjs +16 -0
  51. package/dist/cjs/webhooks/verify.d.ts +7 -0
  52. package/dist/cjs/webhooks/verify.d.ts.map +1 -0
  53. package/dist/client.d.ts +16 -0
  54. package/dist/client.d.ts.map +1 -0
  55. package/dist/client.js +44 -0
  56. package/dist/errors.d.ts +9 -0
  57. package/dist/errors.d.ts.map +1 -0
  58. package/dist/errors.js +14 -0
  59. package/dist/http/client.d.ts +13 -0
  60. package/dist/http/client.d.ts.map +1 -0
  61. package/dist/http/client.js +58 -0
  62. package/dist/index.d.ts +7 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +6 -0
  65. package/dist/resources/index.d.ts +71 -0
  66. package/dist/resources/index.d.ts.map +1 -0
  67. package/dist/resources/index.js +110 -0
  68. package/dist/types.d.ts +31 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +1 -0
  71. package/dist/webhooks/verify.d.ts +7 -0
  72. package/dist/webhooks/verify.d.ts.map +1 -0
  73. package/dist/webhooks/verify.js +13 -0
  74. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @foxscheduling/sdk
2
+
3
+ Official Node.js SDK for the [Fox Scheduling Partner API](https://foxscheduling.com/developers).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @foxscheduling/sdk @foxscheduling/shared
9
+ ```
10
+
11
+ ## API key (single business)
12
+
13
+ ```typescript
14
+ import { FoxScheduling } from "@foxscheduling/sdk";
15
+
16
+ const fox = new FoxScheduling({
17
+ auth: { type: "apiKey", apiKey: process.env.FOX_API_KEY! },
18
+ });
19
+
20
+ const business = await fox.business.get();
21
+ ```
22
+
23
+ Create keys under **Settings → API keys** in the Fox Scheduling dashboard.
24
+
25
+ ## OAuth (multi-tenant integrations)
26
+
27
+ ```typescript
28
+ import { FoxScheduling, FileTokenStore } from "@foxscheduling/sdk";
29
+
30
+ const fox = new FoxScheduling({
31
+ auth: {
32
+ type: "oauth",
33
+ clientId: process.env.FOX_CLIENT_ID!,
34
+ clientSecret: process.env.FOX_CLIENT_SECRET!,
35
+ redirectUri: "https://your-domain.com/oauth/callback",
36
+ tokenStore: new FileTokenStore("./fox-tokens.json"),
37
+ },
38
+ });
39
+
40
+ const { url, codeVerifier } = fox.oauth!.getAuthorizationUrl({
41
+ scopes: ["business:read", "bookings:read"],
42
+ });
43
+ // Redirect user to url, then on callback:
44
+ await fox.oauth!.exchangeCode(code, codeVerifier);
45
+ ```
46
+
47
+ ## Webhook verification
48
+
49
+ ```typescript
50
+ import { verifyFoxWebhookSignature } from "@foxscheduling/sdk";
51
+
52
+ const ok = verifyFoxWebhookSignature({
53
+ secret: webhookSecret,
54
+ timestamp: req.headers["x-fox-timestamp"],
55
+ rawBody: req.rawBody,
56
+ signatureHex: req.headers["x-fox-signature"],
57
+ });
58
+ ```
@@ -0,0 +1,10 @@
1
+ export interface AuthProvider {
2
+ getAuthorizationHeader(): Promise<string>;
3
+ onUnauthorized?(): Promise<boolean>;
4
+ }
5
+ export declare class ApiKeyAuthProvider implements AuthProvider {
6
+ private readonly apiKey;
7
+ constructor(apiKey: string);
8
+ getAuthorizationHeader(): Promise<string>;
9
+ }
10
+ //# sourceMappingURL=apiKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiKey.d.ts","sourceRoot":"","sources":["../../src/auth/apiKey.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,cAAc,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAUD,qBAAa,kBAAmB,YAAW,YAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAQrC,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;CAGhD"}
@@ -0,0 +1,17 @@
1
+ function isBrowserEnvironment() {
2
+ return (typeof globalThis !== "undefined" &&
3
+ "document" in globalThis &&
4
+ typeof globalThis.document !== "undefined");
5
+ }
6
+ export class ApiKeyAuthProvider {
7
+ apiKey;
8
+ constructor(apiKey) {
9
+ this.apiKey = apiKey;
10
+ if (isBrowserEnvironment()) {
11
+ throw new Error("@foxscheduling/sdk: API keys must not be used in browser environments.");
12
+ }
13
+ }
14
+ async getAuthorizationHeader() {
15
+ return `Bearer ${this.apiKey}`;
16
+ }
17
+ }
@@ -0,0 +1,29 @@
1
+ import type { OAuthScope } from "@foxscheduling/shared";
2
+ import type { AuthorizationUrlResult, TokenSet } from "../types.js";
3
+ import { TokenManager, type OAuthTokenClient } from "./tokenManager.js";
4
+ import type { TokenStore } from "./tokenStore.js";
5
+ import type { AuthProvider } from "./apiKey.js";
6
+ export declare class OAuthClient implements OAuthTokenClient, AuthProvider {
7
+ private readonly config;
8
+ readonly tokenManager: TokenManager;
9
+ constructor(config: {
10
+ baseUrl: string;
11
+ clientId: string;
12
+ clientSecret?: string;
13
+ redirectUri: string;
14
+ tokenStore: TokenStore;
15
+ fetchImpl: typeof fetch;
16
+ });
17
+ getAuthorizationUrl(params: {
18
+ scopes: OAuthScope[];
19
+ state?: string;
20
+ }): AuthorizationUrlResult;
21
+ exchangeCode(code: string, codeVerifier: string): Promise<TokenSet>;
22
+ revoke(): Promise<void>;
23
+ getAuthorizationHeader(): Promise<string>;
24
+ onUnauthorized(): Promise<boolean>;
25
+ refresh(refreshToken: string): Promise<TokenSet>;
26
+ private exchangeAuthorizationCode;
27
+ private postToken;
28
+ }
29
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMpE,OAAO,EACL,YAAY,EAEZ,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUhD,qBAAa,WAAY,YAAW,gBAAgB,EAAE,YAAY;IAI9D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHzB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;gBAGjB,MAAM,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,UAAU,CAAC;QACvB,SAAS,EAAE,OAAO,KAAK,CAAC;KACzB;IAKH,mBAAmB,CAAC,MAAM,EAAE;QAC1B,MAAM,EAAE,UAAU,EAAE,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,sBAAsB;IAmBpB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAUnE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBvB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAKzC,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAWlC,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAYxC,yBAAyB;YAkBzB,SAAS;CAkBxB"}
@@ -0,0 +1,110 @@
1
+ import { generateCodeChallengeS256, generateCodeVerifier, generateState, } from "./pkce.js";
2
+ import { TokenManager, tokenSetFromOAuthResponse, } from "./tokenManager.js";
3
+ export class OAuthClient {
4
+ config;
5
+ tokenManager;
6
+ constructor(config) {
7
+ this.config = config;
8
+ this.tokenManager = new TokenManager(config.tokenStore, this);
9
+ }
10
+ getAuthorizationUrl(params) {
11
+ const codeVerifier = generateCodeVerifier();
12
+ const state = params.state ?? generateState();
13
+ const query = new URLSearchParams({
14
+ client_id: this.config.clientId,
15
+ redirect_uri: this.config.redirectUri,
16
+ response_type: "code",
17
+ scope: params.scopes.join(" "),
18
+ state,
19
+ code_challenge: generateCodeChallengeS256(codeVerifier),
20
+ code_challenge_method: "S256",
21
+ });
22
+ return {
23
+ url: `${this.config.baseUrl}/oauth/authorize?${query.toString()}`,
24
+ codeVerifier,
25
+ state,
26
+ };
27
+ }
28
+ async exchangeCode(code, codeVerifier) {
29
+ const tokens = await this.exchangeAuthorizationCode({
30
+ code,
31
+ codeVerifier,
32
+ redirectUri: this.config.redirectUri,
33
+ });
34
+ await this.tokenManager.setTokens(tokens);
35
+ return tokens;
36
+ }
37
+ async revoke() {
38
+ const tokens = await this.config.tokenStore.load();
39
+ if (!tokens)
40
+ return;
41
+ const res = await this.config.fetchImpl(`${this.config.baseUrl}/oauth/revoke`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({
45
+ token: tokens.refreshToken,
46
+ client_id: this.config.clientId,
47
+ ...(this.config.clientSecret
48
+ ? { client_secret: this.config.clientSecret }
49
+ : {}),
50
+ }),
51
+ });
52
+ if (!res.ok) {
53
+ const json = (await res.json());
54
+ throw new Error(json.errorCode ?? "REVOKE_FAILED");
55
+ }
56
+ await this.tokenManager.clear();
57
+ }
58
+ async getAuthorizationHeader() {
59
+ const token = await this.tokenManager.getValidAccessToken();
60
+ return `Bearer ${token}`;
61
+ }
62
+ async onUnauthorized() {
63
+ const tokens = await this.config.tokenStore.load();
64
+ if (!tokens?.refreshToken)
65
+ return false;
66
+ try {
67
+ await this.tokenManager.refresh(tokens.refreshToken);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ async refresh(refreshToken) {
75
+ const data = await this.postToken({
76
+ grant_type: "refresh_token",
77
+ refresh_token: refreshToken,
78
+ client_id: this.config.clientId,
79
+ ...(this.config.clientSecret
80
+ ? { client_secret: this.config.clientSecret }
81
+ : {}),
82
+ });
83
+ return tokenSetFromOAuthResponse(data);
84
+ }
85
+ async exchangeAuthorizationCode(params) {
86
+ const data = await this.postToken({
87
+ grant_type: "authorization_code",
88
+ code: params.code,
89
+ redirect_uri: params.redirectUri,
90
+ client_id: this.config.clientId,
91
+ code_verifier: params.codeVerifier,
92
+ ...(this.config.clientSecret
93
+ ? { client_secret: this.config.clientSecret }
94
+ : {}),
95
+ });
96
+ return tokenSetFromOAuthResponse(data);
97
+ }
98
+ async postToken(body) {
99
+ const res = await this.config.fetchImpl(`${this.config.baseUrl}/oauth/token`, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify(body),
103
+ });
104
+ const json = (await res.json());
105
+ if (json.statusMessage !== "SUCCESS" || !json.data) {
106
+ throw new Error(json.errorCode ?? "TOKEN_EXCHANGE_FAILED");
107
+ }
108
+ return json.data;
109
+ }
110
+ }
@@ -0,0 +1,4 @@
1
+ export declare function generateCodeVerifier(): string;
2
+ export declare function generateCodeChallengeS256(codeVerifier: string): string;
3
+ export declare function generateState(): string;
4
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
@@ -0,0 +1,10 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ export function generateCodeVerifier() {
3
+ return randomBytes(32).toString("base64url");
4
+ }
5
+ export function generateCodeChallengeS256(codeVerifier) {
6
+ return createHash("sha256").update(codeVerifier).digest("base64url");
7
+ }
8
+ export function generateState() {
9
+ return randomBytes(16).toString("base64url");
10
+ }
@@ -0,0 +1,24 @@
1
+ import type { TokenSet } from "../types.js";
2
+ import type { TokenStore } from "./tokenStore.js";
3
+ export interface OAuthTokenClient {
4
+ exchangeCode(code: string, codeVerifier: string): Promise<TokenSet>;
5
+ refresh(refreshToken: string): Promise<TokenSet>;
6
+ }
7
+ export declare class TokenManager {
8
+ private readonly store;
9
+ private readonly client;
10
+ private refreshPromise;
11
+ constructor(store: TokenStore, client: OAuthTokenClient);
12
+ getValidAccessToken(): Promise<string>;
13
+ setTokens(tokens: TokenSet): Promise<void>;
14
+ refresh(refreshToken: string): Promise<TokenSet>;
15
+ clear(): Promise<void>;
16
+ }
17
+ export declare function tokenSetFromOAuthResponse(data: {
18
+ access_token: string;
19
+ refresh_token: string;
20
+ expires_in: number;
21
+ scope: string;
22
+ token_type: string;
23
+ }): TokenSet;
24
+ //# sourceMappingURL=tokenManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenManager.d.ts","sourceRoot":"","sources":["../../src/auth/tokenManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpE,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClD;AAED,qBAAa,YAAY;IAIrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,OAAO,CAAC,cAAc,CAAkC;gBAGrC,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,gBAAgB;IAGrC,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC;IAYtC,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAehD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,QAAQ,CAQX"}
@@ -0,0 +1,50 @@
1
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
2
+ export class TokenManager {
3
+ store;
4
+ client;
5
+ refreshPromise = null;
6
+ constructor(store, client) {
7
+ this.store = store;
8
+ this.client = client;
9
+ }
10
+ async getValidAccessToken() {
11
+ const tokens = await this.store.load();
12
+ if (!tokens) {
13
+ throw new Error("No OAuth tokens stored. Complete authorization first.");
14
+ }
15
+ if (Date.now() >= tokens.expiresAt - REFRESH_BUFFER_MS) {
16
+ const refreshed = await this.refresh(tokens.refreshToken);
17
+ return refreshed.accessToken;
18
+ }
19
+ return tokens.accessToken;
20
+ }
21
+ async setTokens(tokens) {
22
+ await this.store.save(tokens);
23
+ }
24
+ async refresh(refreshToken) {
25
+ if (!this.refreshPromise) {
26
+ this.refreshPromise = this.client
27
+ .refresh(refreshToken)
28
+ .then(async (tokens) => {
29
+ await this.store.save(tokens);
30
+ return tokens;
31
+ })
32
+ .finally(() => {
33
+ this.refreshPromise = null;
34
+ });
35
+ }
36
+ return this.refreshPromise;
37
+ }
38
+ async clear() {
39
+ await this.store.clear();
40
+ }
41
+ }
42
+ export function tokenSetFromOAuthResponse(data) {
43
+ return {
44
+ accessToken: data.access_token,
45
+ refreshToken: data.refresh_token,
46
+ expiresAt: Date.now() + data.expires_in * 1000,
47
+ scope: data.scope,
48
+ tokenType: data.token_type,
49
+ };
50
+ }
@@ -0,0 +1,21 @@
1
+ import type { TokenSet } from "../types.js";
2
+ export interface TokenStore {
3
+ load(): Promise<TokenSet | null>;
4
+ save(tokens: TokenSet): Promise<void>;
5
+ clear(): Promise<void>;
6
+ }
7
+ export declare class MemoryTokenStore implements TokenStore {
8
+ private tokens;
9
+ load(): Promise<TokenSet | null>;
10
+ save(tokens: TokenSet): Promise<void>;
11
+ clear(): Promise<void>;
12
+ }
13
+ export declare class FileTokenStore implements TokenStore {
14
+ private readonly filePath;
15
+ constructor(filePath: string);
16
+ private readFile;
17
+ load(): Promise<TokenSet | null>;
18
+ save(tokens: TokenSet): Promise<void>;
19
+ clear(): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=tokenStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenStore.d.ts","sourceRoot":"","sources":["../../src/auth/tokenStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,qBAAa,gBAAiB,YAAW,UAAU;IACjD,OAAO,CAAC,MAAM,CAAyB;IAEjC,IAAI,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAIhC,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED,qBAAa,cAAe,YAAW,UAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;YAE/B,QAAQ;IAUhB,IAAI,IAAI,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAMhC,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ7B"}
@@ -0,0 +1,49 @@
1
+ export class MemoryTokenStore {
2
+ tokens = null;
3
+ async load() {
4
+ return this.tokens;
5
+ }
6
+ async save(tokens) {
7
+ this.tokens = tokens;
8
+ }
9
+ async clear() {
10
+ this.tokens = null;
11
+ }
12
+ }
13
+ export class FileTokenStore {
14
+ filePath;
15
+ constructor(filePath) {
16
+ this.filePath = filePath;
17
+ }
18
+ async readFile() {
19
+ const fs = await import("node:fs/promises");
20
+ try {
21
+ return await fs.readFile(this.filePath, "utf8");
22
+ }
23
+ catch (err) {
24
+ if (err.code === "ENOENT")
25
+ return null;
26
+ throw err;
27
+ }
28
+ }
29
+ async load() {
30
+ const raw = await this.readFile();
31
+ if (!raw)
32
+ return null;
33
+ return JSON.parse(raw);
34
+ }
35
+ async save(tokens) {
36
+ const fs = await import("node:fs/promises");
37
+ await fs.writeFile(this.filePath, JSON.stringify(tokens, null, 2), "utf8");
38
+ }
39
+ async clear() {
40
+ const fs = await import("node:fs/promises");
41
+ try {
42
+ await fs.unlink(this.filePath);
43
+ }
44
+ catch (err) {
45
+ if (err.code !== "ENOENT")
46
+ throw err;
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiKeyAuthProvider = void 0;
4
+ function isBrowserEnvironment() {
5
+ return (typeof globalThis !== "undefined" &&
6
+ "document" in globalThis &&
7
+ typeof globalThis.document !== "undefined");
8
+ }
9
+ class ApiKeyAuthProvider {
10
+ apiKey;
11
+ constructor(apiKey) {
12
+ this.apiKey = apiKey;
13
+ if (isBrowserEnvironment()) {
14
+ throw new Error("@foxscheduling/sdk: API keys must not be used in browser environments.");
15
+ }
16
+ }
17
+ async getAuthorizationHeader() {
18
+ return `Bearer ${this.apiKey}`;
19
+ }
20
+ }
21
+ exports.ApiKeyAuthProvider = ApiKeyAuthProvider;
@@ -0,0 +1,10 @@
1
+ export interface AuthProvider {
2
+ getAuthorizationHeader(): Promise<string>;
3
+ onUnauthorized?(): Promise<boolean>;
4
+ }
5
+ export declare class ApiKeyAuthProvider implements AuthProvider {
6
+ private readonly apiKey;
7
+ constructor(apiKey: string);
8
+ getAuthorizationHeader(): Promise<string>;
9
+ }
10
+ //# sourceMappingURL=apiKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiKey.d.ts","sourceRoot":"","sources":["../../../src/auth/apiKey.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,cAAc,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAUD,qBAAa,kBAAmB,YAAW,YAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAQrC,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;CAGhD"}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthClient = void 0;
4
+ const pkce_js_1 = require("./pkce.cjs");
5
+ const tokenManager_js_1 = require("./tokenManager.cjs");
6
+ class OAuthClient {
7
+ config;
8
+ tokenManager;
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.tokenManager = new tokenManager_js_1.TokenManager(config.tokenStore, this);
12
+ }
13
+ getAuthorizationUrl(params) {
14
+ const codeVerifier = (0, pkce_js_1.generateCodeVerifier)();
15
+ const state = params.state ?? (0, pkce_js_1.generateState)();
16
+ const query = new URLSearchParams({
17
+ client_id: this.config.clientId,
18
+ redirect_uri: this.config.redirectUri,
19
+ response_type: "code",
20
+ scope: params.scopes.join(" "),
21
+ state,
22
+ code_challenge: (0, pkce_js_1.generateCodeChallengeS256)(codeVerifier),
23
+ code_challenge_method: "S256",
24
+ });
25
+ return {
26
+ url: `${this.config.baseUrl}/oauth/authorize?${query.toString()}`,
27
+ codeVerifier,
28
+ state,
29
+ };
30
+ }
31
+ async exchangeCode(code, codeVerifier) {
32
+ const tokens = await this.exchangeAuthorizationCode({
33
+ code,
34
+ codeVerifier,
35
+ redirectUri: this.config.redirectUri,
36
+ });
37
+ await this.tokenManager.setTokens(tokens);
38
+ return tokens;
39
+ }
40
+ async revoke() {
41
+ const tokens = await this.config.tokenStore.load();
42
+ if (!tokens)
43
+ return;
44
+ const res = await this.config.fetchImpl(`${this.config.baseUrl}/oauth/revoke`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({
48
+ token: tokens.refreshToken,
49
+ client_id: this.config.clientId,
50
+ ...(this.config.clientSecret
51
+ ? { client_secret: this.config.clientSecret }
52
+ : {}),
53
+ }),
54
+ });
55
+ if (!res.ok) {
56
+ const json = (await res.json());
57
+ throw new Error(json.errorCode ?? "REVOKE_FAILED");
58
+ }
59
+ await this.tokenManager.clear();
60
+ }
61
+ async getAuthorizationHeader() {
62
+ const token = await this.tokenManager.getValidAccessToken();
63
+ return `Bearer ${token}`;
64
+ }
65
+ async onUnauthorized() {
66
+ const tokens = await this.config.tokenStore.load();
67
+ if (!tokens?.refreshToken)
68
+ return false;
69
+ try {
70
+ await this.tokenManager.refresh(tokens.refreshToken);
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ async refresh(refreshToken) {
78
+ const data = await this.postToken({
79
+ grant_type: "refresh_token",
80
+ refresh_token: refreshToken,
81
+ client_id: this.config.clientId,
82
+ ...(this.config.clientSecret
83
+ ? { client_secret: this.config.clientSecret }
84
+ : {}),
85
+ });
86
+ return (0, tokenManager_js_1.tokenSetFromOAuthResponse)(data);
87
+ }
88
+ async exchangeAuthorizationCode(params) {
89
+ const data = await this.postToken({
90
+ grant_type: "authorization_code",
91
+ code: params.code,
92
+ redirect_uri: params.redirectUri,
93
+ client_id: this.config.clientId,
94
+ code_verifier: params.codeVerifier,
95
+ ...(this.config.clientSecret
96
+ ? { client_secret: this.config.clientSecret }
97
+ : {}),
98
+ });
99
+ return (0, tokenManager_js_1.tokenSetFromOAuthResponse)(data);
100
+ }
101
+ async postToken(body) {
102
+ const res = await this.config.fetchImpl(`${this.config.baseUrl}/oauth/token`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify(body),
106
+ });
107
+ const json = (await res.json());
108
+ if (json.statusMessage !== "SUCCESS" || !json.data) {
109
+ throw new Error(json.errorCode ?? "TOKEN_EXCHANGE_FAILED");
110
+ }
111
+ return json.data;
112
+ }
113
+ }
114
+ exports.OAuthClient = OAuthClient;
@@ -0,0 +1,29 @@
1
+ import type { OAuthScope } from "@foxscheduling/shared";
2
+ import type { AuthorizationUrlResult, TokenSet } from "../types.js";
3
+ import { TokenManager, type OAuthTokenClient } from "./tokenManager.js";
4
+ import type { TokenStore } from "./tokenStore.js";
5
+ import type { AuthProvider } from "./apiKey.js";
6
+ export declare class OAuthClient implements OAuthTokenClient, AuthProvider {
7
+ private readonly config;
8
+ readonly tokenManager: TokenManager;
9
+ constructor(config: {
10
+ baseUrl: string;
11
+ clientId: string;
12
+ clientSecret?: string;
13
+ redirectUri: string;
14
+ tokenStore: TokenStore;
15
+ fetchImpl: typeof fetch;
16
+ });
17
+ getAuthorizationUrl(params: {
18
+ scopes: OAuthScope[];
19
+ state?: string;
20
+ }): AuthorizationUrlResult;
21
+ exchangeCode(code: string, codeVerifier: string): Promise<TokenSet>;
22
+ revoke(): Promise<void>;
23
+ getAuthorizationHeader(): Promise<string>;
24
+ onUnauthorized(): Promise<boolean>;
25
+ refresh(refreshToken: string): Promise<TokenSet>;
26
+ private exchangeAuthorizationCode;
27
+ private postToken;
28
+ }
29
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../src/auth/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMpE,OAAO,EACL,YAAY,EAEZ,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUhD,qBAAa,WAAY,YAAW,gBAAgB,EAAE,YAAY;IAI9D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHzB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;gBAGjB,MAAM,EAAE;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,UAAU,CAAC;QACvB,SAAS,EAAE,OAAO,KAAK,CAAC;KACzB;IAKH,mBAAmB,CAAC,MAAM,EAAE;QAC1B,MAAM,EAAE,UAAU,EAAE,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,sBAAsB;IAmBpB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAUnE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBvB,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAKzC,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAWlC,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAYxC,yBAAyB;YAkBzB,SAAS;CAkBxB"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateCodeVerifier = generateCodeVerifier;
4
+ exports.generateCodeChallengeS256 = generateCodeChallengeS256;
5
+ exports.generateState = generateState;
6
+ const node_crypto_1 = require("node:crypto");
7
+ function generateCodeVerifier() {
8
+ return (0, node_crypto_1.randomBytes)(32).toString("base64url");
9
+ }
10
+ function generateCodeChallengeS256(codeVerifier) {
11
+ return (0, node_crypto_1.createHash)("sha256").update(codeVerifier).digest("base64url");
12
+ }
13
+ function generateState() {
14
+ return (0, node_crypto_1.randomBytes)(16).toString("base64url");
15
+ }
@@ -0,0 +1,4 @@
1
+ export declare function generateCodeVerifier(): string;
2
+ export declare function generateCodeChallengeS256(codeVerifier: string): string;
3
+ export declare function generateState(): string;
4
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../../src/auth/pkce.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}