@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.
- package/README.md +58 -0
- package/dist/auth/apiKey.d.ts +10 -0
- package/dist/auth/apiKey.d.ts.map +1 -0
- package/dist/auth/apiKey.js +17 -0
- package/dist/auth/oauth.d.ts +29 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +110 -0
- package/dist/auth/pkce.d.ts +4 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +10 -0
- package/dist/auth/tokenManager.d.ts +24 -0
- package/dist/auth/tokenManager.d.ts.map +1 -0
- package/dist/auth/tokenManager.js +50 -0
- package/dist/auth/tokenStore.d.ts +21 -0
- package/dist/auth/tokenStore.d.ts.map +1 -0
- package/dist/auth/tokenStore.js +49 -0
- package/dist/cjs/auth/apiKey.cjs +21 -0
- package/dist/cjs/auth/apiKey.d.ts +10 -0
- package/dist/cjs/auth/apiKey.d.ts.map +1 -0
- package/dist/cjs/auth/oauth.cjs +114 -0
- package/dist/cjs/auth/oauth.d.ts +29 -0
- package/dist/cjs/auth/oauth.d.ts.map +1 -0
- package/dist/cjs/auth/pkce.cjs +15 -0
- package/dist/cjs/auth/pkce.d.ts +4 -0
- package/dist/cjs/auth/pkce.d.ts.map +1 -0
- package/dist/cjs/auth/tokenManager.cjs +55 -0
- package/dist/cjs/auth/tokenManager.d.ts +24 -0
- package/dist/cjs/auth/tokenManager.d.ts.map +1 -0
- package/dist/cjs/auth/tokenStore.cjs +87 -0
- package/dist/cjs/auth/tokenStore.d.ts +21 -0
- package/dist/cjs/auth/tokenStore.d.ts.map +1 -0
- package/dist/cjs/client.cjs +48 -0
- package/dist/cjs/client.d.ts +16 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/errors.cjs +19 -0
- package/dist/cjs/errors.d.ts +9 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/http/client.cjs +62 -0
- package/dist/cjs/http/client.d.ts +13 -0
- package/dist/cjs/http/client.d.ts.map +1 -0
- package/dist/cjs/index.cjs +19 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/resources/index.cjs +120 -0
- package/dist/cjs/resources/index.d.ts +71 -0
- package/dist/cjs/resources/index.d.ts.map +1 -0
- package/dist/cjs/types.cjs +4 -0
- package/dist/cjs/types.d.ts +31 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/webhooks/verify.cjs +16 -0
- package/dist/cjs/webhooks/verify.d.ts +7 -0
- package/dist/cjs/webhooks/verify.d.ts.map +1 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +44 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/http/client.d.ts +13 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/resources/index.d.ts +71 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +110 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/webhooks/verify.d.ts +7 -0
- package/dist/webhooks/verify.d.ts.map +1 -0
- package/dist/webhooks/verify.js +13 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|