@37signals/basecamp 0.2.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 +600 -0
- package/dist/auth-strategy.d.ts +36 -0
- package/dist/auth-strategy.d.ts.map +1 -0
- package/dist/auth-strategy.js +33 -0
- package/dist/auth-strategy.js.map +1 -0
- package/dist/client.d.ts +224 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +715 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +137 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +269 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/metadata.json +2281 -0
- package/dist/generated/openapi-stripped.json +21268 -0
- package/dist/generated/path-mapping.d.ts +8 -0
- package/dist/generated/path-mapping.d.ts.map +1 -0
- package/dist/generated/path-mapping.js +192 -0
- package/dist/generated/path-mapping.js.map +1 -0
- package/dist/generated/path-mapping.ts +199 -0
- package/dist/generated/schema.d.ts +14883 -0
- package/dist/generated/services/attachments.d.ts +27 -0
- package/dist/generated/services/attachments.d.ts.map +1 -0
- package/dist/generated/services/attachments.js +49 -0
- package/dist/generated/services/attachments.js.map +1 -0
- package/dist/generated/services/attachments.ts +59 -0
- package/dist/generated/services/boosts.d.ts +114 -0
- package/dist/generated/services/boosts.d.ts.map +1 -0
- package/dist/generated/services/boosts.js +179 -0
- package/dist/generated/services/boosts.js.map +1 -0
- package/dist/generated/services/boosts.ts +250 -0
- package/dist/generated/services/campfires.d.ts +202 -0
- package/dist/generated/services/campfires.d.ts.map +1 -0
- package/dist/generated/services/campfires.js +309 -0
- package/dist/generated/services/campfires.js.map +1 -0
- package/dist/generated/services/campfires.ts +433 -0
- package/dist/generated/services/card-columns.d.ts +163 -0
- package/dist/generated/services/card-columns.d.ts.map +1 -0
- package/dist/generated/services/card-columns.js +264 -0
- package/dist/generated/services/card-columns.js.map +1 -0
- package/dist/generated/services/card-columns.ts +360 -0
- package/dist/generated/services/card-steps.d.ts +105 -0
- package/dist/generated/services/card-steps.d.ts.map +1 -0
- package/dist/generated/services/card-steps.js +148 -0
- package/dist/generated/services/card-steps.js.map +1 -0
- package/dist/generated/services/card-steps.ts +221 -0
- package/dist/generated/services/card-tables.d.ts +27 -0
- package/dist/generated/services/card-tables.d.ts.map +1 -0
- package/dist/generated/services/card-tables.js +40 -0
- package/dist/generated/services/card-tables.js.map +1 -0
- package/dist/generated/services/card-tables.ts +55 -0
- package/dist/generated/services/cards.d.ts +118 -0
- package/dist/generated/services/cards.d.ts.map +1 -0
- package/dist/generated/services/cards.js +166 -0
- package/dist/generated/services/cards.js.map +1 -0
- package/dist/generated/services/cards.ts +247 -0
- package/dist/generated/services/checkins.d.ts +278 -0
- package/dist/generated/services/checkins.d.ts.map +1 -0
- package/dist/generated/services/checkins.js +420 -0
- package/dist/generated/services/checkins.js.map +1 -0
- package/dist/generated/services/checkins.ts +600 -0
- package/dist/generated/services/client-approvals.d.ts +45 -0
- package/dist/generated/services/client-approvals.d.ts.map +1 -0
- package/dist/generated/services/client-approvals.js +58 -0
- package/dist/generated/services/client-approvals.js.map +1 -0
- package/dist/generated/services/client-approvals.ts +89 -0
- package/dist/generated/services/client-correspondences.d.ts +45 -0
- package/dist/generated/services/client-correspondences.d.ts.map +1 -0
- package/dist/generated/services/client-correspondences.js +58 -0
- package/dist/generated/services/client-correspondences.js.map +1 -0
- package/dist/generated/services/client-correspondences.ts +89 -0
- package/dist/generated/services/client-replies.d.ts +47 -0
- package/dist/generated/services/client-replies.d.ts.map +1 -0
- package/dist/generated/services/client-replies.js +65 -0
- package/dist/generated/services/client-replies.js.map +1 -0
- package/dist/generated/services/client-replies.ts +95 -0
- package/dist/generated/services/client-visibility.d.ts +35 -0
- package/dist/generated/services/client-visibility.d.ts.map +1 -0
- package/dist/generated/services/client-visibility.js +44 -0
- package/dist/generated/services/client-visibility.js.map +1 -0
- package/dist/generated/services/client-visibility.ts +68 -0
- package/dist/generated/services/comments.d.ts +86 -0
- package/dist/generated/services/comments.d.ts.map +1 -0
- package/dist/generated/services/comments.js +129 -0
- package/dist/generated/services/comments.js.map +1 -0
- package/dist/generated/services/comments.ts +185 -0
- package/dist/generated/services/documents.d.ts +92 -0
- package/dist/generated/services/documents.d.ts.map +1 -0
- package/dist/generated/services/documents.js +129 -0
- package/dist/generated/services/documents.js.map +1 -0
- package/dist/generated/services/documents.ts +191 -0
- package/dist/generated/services/events.d.ts +34 -0
- package/dist/generated/services/events.d.ts.map +1 -0
- package/dist/generated/services/events.js +39 -0
- package/dist/generated/services/events.js.map +1 -0
- package/dist/generated/services/events.ts +64 -0
- package/dist/generated/services/forwards.d.ts +112 -0
- package/dist/generated/services/forwards.d.ts.map +1 -0
- package/dist/generated/services/forwards.js +172 -0
- package/dist/generated/services/forwards.js.map +1 -0
- package/dist/generated/services/forwards.ts +241 -0
- package/dist/generated/services/index.d.ts +39 -0
- package/dist/generated/services/index.d.ts.map +1 -0
- package/dist/generated/services/index.js +39 -0
- package/dist/generated/services/index.js.map +1 -0
- package/dist/generated/services/index.ts +38 -0
- package/dist/generated/services/lineup.d.ts +67 -0
- package/dist/generated/services/lineup.d.ts.map +1 -0
- package/dist/generated/services/lineup.js +99 -0
- package/dist/generated/services/lineup.js.map +1 -0
- package/dist/generated/services/lineup.ts +143 -0
- package/dist/generated/services/message-boards.d.ts +27 -0
- package/dist/generated/services/message-boards.d.ts.map +1 -0
- package/dist/generated/services/message-boards.js +40 -0
- package/dist/generated/services/message-boards.js.map +1 -0
- package/dist/generated/services/message-boards.ts +55 -0
- package/dist/generated/services/message-types.d.ts +100 -0
- package/dist/generated/services/message-types.d.ts.map +1 -0
- package/dist/generated/services/message-types.js +144 -0
- package/dist/generated/services/message-types.js.map +1 -0
- package/dist/generated/services/message-types.ts +210 -0
- package/dist/generated/services/messages.d.ts +122 -0
- package/dist/generated/services/messages.d.ts.map +1 -0
- package/dist/generated/services/messages.js +180 -0
- package/dist/generated/services/messages.js.map +1 -0
- package/dist/generated/services/messages.ts +258 -0
- package/dist/generated/services/people.d.ts +122 -0
- package/dist/generated/services/people.d.ts.map +1 -0
- package/dist/generated/services/people.js +167 -0
- package/dist/generated/services/people.js.map +1 -0
- package/dist/generated/services/people.ts +252 -0
- package/dist/generated/services/projects.d.ts +109 -0
- package/dist/generated/services/projects.d.ts.map +1 -0
- package/dist/generated/services/projects.js +153 -0
- package/dist/generated/services/projects.js.map +1 -0
- package/dist/generated/services/projects.ts +224 -0
- package/dist/generated/services/recordings.d.ts +93 -0
- package/dist/generated/services/recordings.d.ts.map +1 -0
- package/dist/generated/services/recordings.js +138 -0
- package/dist/generated/services/recordings.js.map +1 -0
- package/dist/generated/services/recordings.ts +191 -0
- package/dist/generated/services/reports.d.ts +90 -0
- package/dist/generated/services/reports.d.ts.map +1 -0
- package/dist/generated/services/reports.js +124 -0
- package/dist/generated/services/reports.js.map +1 -0
- package/dist/generated/services/reports.ts +187 -0
- package/dist/generated/services/schedules.d.ts +162 -0
- package/dist/generated/services/schedules.d.ts.map +1 -0
- package/dist/generated/services/schedules.js +228 -0
- package/dist/generated/services/schedules.js.map +1 -0
- package/dist/generated/services/schedules.ts +335 -0
- package/dist/generated/services/search.d.ts +45 -0
- package/dist/generated/services/search.d.ts.map +1 -0
- package/dist/generated/services/search.js +56 -0
- package/dist/generated/services/search.js.map +1 -0
- package/dist/generated/services/search.ts +89 -0
- package/dist/generated/services/subscriptions.d.ts +73 -0
- package/dist/generated/services/subscriptions.d.ts.map +1 -0
- package/dist/generated/services/subscriptions.js +119 -0
- package/dist/generated/services/subscriptions.js.map +1 -0
- package/dist/generated/services/subscriptions.ts +160 -0
- package/dist/generated/services/templates.d.ts +140 -0
- package/dist/generated/services/templates.d.ts.map +1 -0
- package/dist/generated/services/templates.js +207 -0
- package/dist/generated/services/templates.js.map +1 -0
- package/dist/generated/services/templates.ts +294 -0
- package/dist/generated/services/timeline.d.ts +31 -0
- package/dist/generated/services/timeline.d.ts.map +1 -0
- package/dist/generated/services/timeline.js +39 -0
- package/dist/generated/services/timeline.js.map +1 -0
- package/dist/generated/services/timeline.ts +62 -0
- package/dist/generated/services/timesheets.d.ts +145 -0
- package/dist/generated/services/timesheets.d.ts.map +1 -0
- package/dist/generated/services/timesheets.js +186 -0
- package/dist/generated/services/timesheets.js.map +1 -0
- package/dist/generated/services/timesheets.ts +289 -0
- package/dist/generated/services/todolist-groups.d.ts +74 -0
- package/dist/generated/services/todolist-groups.d.ts.map +1 -0
- package/dist/generated/services/todolist-groups.js +100 -0
- package/dist/generated/services/todolist-groups.js.map +1 -0
- package/dist/generated/services/todolist-groups.ts +151 -0
- package/dist/generated/services/todolists.d.ts +95 -0
- package/dist/generated/services/todolists.d.ts.map +1 -0
- package/dist/generated/services/todolists.js +130 -0
- package/dist/generated/services/todolists.js.map +1 -0
- package/dist/generated/services/todolists.ts +192 -0
- package/dist/generated/services/todos.d.ts +175 -0
- package/dist/generated/services/todos.d.ts.map +1 -0
- package/dist/generated/services/todos.js +255 -0
- package/dist/generated/services/todos.js.map +1 -0
- package/dist/generated/services/todos.ts +369 -0
- package/dist/generated/services/todosets.d.ts +27 -0
- package/dist/generated/services/todosets.d.ts.map +1 -0
- package/dist/generated/services/todosets.js +40 -0
- package/dist/generated/services/todosets.js.map +1 -0
- package/dist/generated/services/todosets.ts +55 -0
- package/dist/generated/services/tools.d.ts +122 -0
- package/dist/generated/services/tools.d.ts.map +1 -0
- package/dist/generated/services/tools.js +197 -0
- package/dist/generated/services/tools.js.map +1 -0
- package/dist/generated/services/tools.ts +267 -0
- package/dist/generated/services/uploads.d.ts +109 -0
- package/dist/generated/services/uploads.d.ts.map +1 -0
- package/dist/generated/services/uploads.js +153 -0
- package/dist/generated/services/uploads.js.map +1 -0
- package/dist/generated/services/uploads.ts +227 -0
- package/dist/generated/services/vaults.d.ts +86 -0
- package/dist/generated/services/vaults.d.ts.map +1 -0
- package/dist/generated/services/vaults.js +126 -0
- package/dist/generated/services/vaults.js.map +1 -0
- package/dist/generated/services/vaults.ts +182 -0
- package/dist/generated/services/webhooks.d.ts +106 -0
- package/dist/generated/services/webhooks.d.ts.map +1 -0
- package/dist/generated/services/webhooks.js +157 -0
- package/dist/generated/services/webhooks.js.map +1 -0
- package/dist/generated/services/webhooks.ts +226 -0
- package/dist/hooks/otel.d.ts +136 -0
- package/dist/hooks/otel.d.ts.map +1 -0
- package/dist/hooks/otel.js +240 -0
- package/dist/hooks/otel.js.map +1 -0
- package/dist/hooks.d.ts +171 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +196 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/authorize.d.ts +48 -0
- package/dist/oauth/authorize.d.ts.map +1 -0
- package/dist/oauth/authorize.js +56 -0
- package/dist/oauth/authorize.js.map +1 -0
- package/dist/oauth/callback-server.d.ts +56 -0
- package/dist/oauth/callback-server.d.ts.map +1 -0
- package/dist/oauth/callback-server.js +150 -0
- package/dist/oauth/callback-server.js.map +1 -0
- package/dist/oauth/discovery.d.ts +54 -0
- package/dist/oauth/discovery.d.ts.map +1 -0
- package/dist/oauth/discovery.js +116 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/exchange.d.ts +96 -0
- package/dist/oauth/exchange.d.ts.map +1 -0
- package/dist/oauth/exchange.js +324 -0
- package/dist/oauth/exchange.js.map +1 -0
- package/dist/oauth/identity.d.ts +30 -0
- package/dist/oauth/identity.d.ts.map +1 -0
- package/dist/oauth/identity.js +82 -0
- package/dist/oauth/identity.js.map +1 -0
- package/dist/oauth/index.d.ts +50 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +58 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth/interactive-login.d.ts +62 -0
- package/dist/oauth/interactive-login.d.ts.map +1 -0
- package/dist/oauth/interactive-login.js +106 -0
- package/dist/oauth/interactive-login.js.map +1 -0
- package/dist/oauth/pkce.d.ts +68 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +80 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/token-manager.d.ts +92 -0
- package/dist/oauth/token-manager.d.ts.map +1 -0
- package/dist/oauth/token-manager.js +126 -0
- package/dist/oauth/token-manager.js.map +1 -0
- package/dist/oauth/token-store.d.ts +45 -0
- package/dist/oauth/token-store.d.ts.map +1 -0
- package/dist/oauth/token-store.js +105 -0
- package/dist/oauth/token-store.js.map +1 -0
- package/dist/oauth/types.d.ts +98 -0
- package/dist/oauth/types.d.ts.map +1 -0
- package/dist/oauth/types.js +8 -0
- package/dist/oauth/types.js.map +1 -0
- package/dist/pagination-utils.d.ts +27 -0
- package/dist/pagination-utils.d.ts.map +1 -0
- package/dist/pagination-utils.js +54 -0
- package/dist/pagination-utils.js.map +1 -0
- package/dist/pagination.d.ts +56 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +57 -0
- package/dist/pagination.js.map +1 -0
- package/dist/security.d.ts +68 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +107 -0
- package/dist/security.js.map +1 -0
- package/dist/services/authorization.d.ts +124 -0
- package/dist/services/authorization.d.ts.map +1 -0
- package/dist/services/authorization.js +117 -0
- package/dist/services/authorization.js.map +1 -0
- package/dist/services/base.d.ts +102 -0
- package/dist/services/base.d.ts.map +1 -0
- package/dist/services/base.js +230 -0
- package/dist/services/base.js.map +1 -0
- package/dist/webhooks/adapters/node-http.d.ts +14 -0
- package/dist/webhooks/adapters/node-http.d.ts.map +1 -0
- package/dist/webhooks/adapters/node-http.js +60 -0
- package/dist/webhooks/adapters/node-http.js.map +1 -0
- package/dist/webhooks/events.d.ts +52 -0
- package/dist/webhooks/events.d.ts.map +1 -0
- package/dist/webhooks/events.js +52 -0
- package/dist/webhooks/events.js.map +1 -0
- package/dist/webhooks/handler.d.ts +62 -0
- package/dist/webhooks/handler.d.ts.map +1 -0
- package/dist/webhooks/handler.js +226 -0
- package/dist/webhooks/handler.js.map +1 -0
- package/dist/webhooks/index.d.ts +5 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +5 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/verify.d.ts +11 -0
- package/dist/webhooks/verify.d.ts.map +1 -0
- package/dist/webhooks/verify.js +25 -0
- package/dist/webhooks/verify.js.map +1 -0
- package/package.json +70 -0
- package/src/generated/metadata.json +2281 -0
- package/src/generated/openapi-stripped.json +21268 -0
- package/src/generated/path-mapping.ts +199 -0
- package/src/generated/schema.d.ts +14883 -0
- package/src/generated/services/attachments.ts +59 -0
- package/src/generated/services/boosts.ts +250 -0
- package/src/generated/services/campfires.ts +433 -0
- package/src/generated/services/card-columns.ts +360 -0
- package/src/generated/services/card-steps.ts +221 -0
- package/src/generated/services/card-tables.ts +55 -0
- package/src/generated/services/cards.ts +247 -0
- package/src/generated/services/checkins.ts +600 -0
- package/src/generated/services/client-approvals.ts +89 -0
- package/src/generated/services/client-correspondences.ts +89 -0
- package/src/generated/services/client-replies.ts +95 -0
- package/src/generated/services/client-visibility.ts +68 -0
- package/src/generated/services/comments.ts +185 -0
- package/src/generated/services/documents.ts +191 -0
- package/src/generated/services/events.ts +64 -0
- package/src/generated/services/forwards.ts +241 -0
- package/src/generated/services/index.ts +38 -0
- package/src/generated/services/lineup.ts +143 -0
- package/src/generated/services/message-boards.ts +55 -0
- package/src/generated/services/message-types.ts +210 -0
- package/src/generated/services/messages.ts +258 -0
- package/src/generated/services/people.ts +252 -0
- package/src/generated/services/projects.ts +224 -0
- package/src/generated/services/recordings.ts +191 -0
- package/src/generated/services/reports.ts +187 -0
- package/src/generated/services/schedules.ts +335 -0
- package/src/generated/services/search.ts +89 -0
- package/src/generated/services/subscriptions.ts +160 -0
- package/src/generated/services/templates.ts +294 -0
- package/src/generated/services/timeline.ts +62 -0
- package/src/generated/services/timesheets.ts +289 -0
- package/src/generated/services/todolist-groups.ts +151 -0
- package/src/generated/services/todolists.ts +192 -0
- package/src/generated/services/todos.ts +369 -0
- package/src/generated/services/todosets.ts +55 -0
- package/src/generated/services/tools.ts +267 -0
- package/src/generated/services/uploads.ts +227 -0
- package/src/generated/services/vaults.ts +182 -0
- package/src/generated/services/webhooks.ts +226 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive OAuth login flow for CLI and desktop applications.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the full OAuth 2.0 authorization code flow:
|
|
5
|
+
* discovery, PKCE, local callback server, browser launch, code exchange.
|
|
6
|
+
*/
|
|
7
|
+
import { discover, discoverLaunchpad } from "./discovery.js";
|
|
8
|
+
import { generateState, generatePKCE } from "./pkce.js";
|
|
9
|
+
import { buildAuthorizationUrl } from "./authorize.js";
|
|
10
|
+
import { startCallbackServer } from "./callback-server.js";
|
|
11
|
+
import { exchangeCode } from "./exchange.js";
|
|
12
|
+
import { BasecampError } from "../errors.js";
|
|
13
|
+
/**
|
|
14
|
+
* Performs the full interactive OAuth login flow.
|
|
15
|
+
*
|
|
16
|
+
* Steps:
|
|
17
|
+
* 1. Discover OAuth endpoints
|
|
18
|
+
* 2. Generate PKCE and state parameters
|
|
19
|
+
* 3. Start local callback server
|
|
20
|
+
* 4. Build authorization URL and open browser
|
|
21
|
+
* 5. Wait for callback with authorization code
|
|
22
|
+
* 6. Exchange code for tokens
|
|
23
|
+
* 7. Save token to store
|
|
24
|
+
*
|
|
25
|
+
* @param options - Login flow configuration
|
|
26
|
+
* @returns The resulting OAuth token
|
|
27
|
+
* @throws BasecampError on any flow failure
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import open from "open"; // or use child_process
|
|
32
|
+
*
|
|
33
|
+
* const token = await performInteractiveLogin({
|
|
34
|
+
* clientId: process.env.BASECAMP_CLIENT_ID!,
|
|
35
|
+
* clientSecret: process.env.BASECAMP_CLIENT_SECRET!,
|
|
36
|
+
* store: new FileTokenStore("~/.config/basecamp/tokens.json"),
|
|
37
|
+
* openBrowser: (url) => open(url),
|
|
38
|
+
* onStatus: (msg) => console.log(msg),
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export async function performInteractiveLogin(options) {
|
|
43
|
+
const { clientId, clientSecret, store, baseUrl, useLegacyFormat = true, callbackPort, openBrowser, promptForManualVisit, onStatus, } = options;
|
|
44
|
+
// 1. Discover OAuth endpoints
|
|
45
|
+
onStatus?.("Discovering OAuth endpoints...");
|
|
46
|
+
const config = baseUrl ? await discover(baseUrl) : await discoverLaunchpad();
|
|
47
|
+
// 2. Generate PKCE and state
|
|
48
|
+
const state = generateState();
|
|
49
|
+
const pkce = await generatePKCE();
|
|
50
|
+
// 3. Start callback server
|
|
51
|
+
onStatus?.("Starting callback server...");
|
|
52
|
+
const { url: redirectUri, waitForCallback, close } = await startCallbackServer({
|
|
53
|
+
port: callbackPort,
|
|
54
|
+
expectedState: state,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
// 4. Build authorization URL
|
|
58
|
+
const authUrl = buildAuthorizationUrl({
|
|
59
|
+
authorizationEndpoint: config.authorizationEndpoint,
|
|
60
|
+
clientId,
|
|
61
|
+
redirectUri,
|
|
62
|
+
state,
|
|
63
|
+
pkce,
|
|
64
|
+
});
|
|
65
|
+
// 5. Open browser
|
|
66
|
+
onStatus?.("Opening browser for authorization...");
|
|
67
|
+
let browserOpened = false;
|
|
68
|
+
try {
|
|
69
|
+
await openBrowser(authUrl.toString());
|
|
70
|
+
browserOpened = true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Browser launch failed
|
|
74
|
+
}
|
|
75
|
+
if (!browserOpened) {
|
|
76
|
+
if (promptForManualVisit) {
|
|
77
|
+
await promptForManualVisit(authUrl.toString());
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw new BasecampError("auth", "Failed to open browser and no manual visit prompt configured");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 6. Wait for callback
|
|
84
|
+
onStatus?.("Waiting for authorization...");
|
|
85
|
+
const { code } = await waitForCallback();
|
|
86
|
+
// 7. Exchange code for tokens
|
|
87
|
+
onStatus?.("Exchanging authorization code for tokens...");
|
|
88
|
+
const token = await exchangeCode({
|
|
89
|
+
tokenEndpoint: config.tokenEndpoint,
|
|
90
|
+
code,
|
|
91
|
+
redirectUri,
|
|
92
|
+
clientId,
|
|
93
|
+
clientSecret,
|
|
94
|
+
codeVerifier: pkce.verifier,
|
|
95
|
+
useLegacyFormat,
|
|
96
|
+
});
|
|
97
|
+
// 8. Save token
|
|
98
|
+
await store.save(token);
|
|
99
|
+
onStatus?.("Authorization complete.");
|
|
100
|
+
return token;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=interactive-login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive-login.js","sourceRoot":"","sources":["../../src/oauth/interactive-login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0.
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographically secure code verifier and challenge generation
|
|
5
|
+
* to protect against authorization code interception attacks.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* PKCE parameters for OAuth 2.0 authorization code flow with PKCE.
|
|
9
|
+
*/
|
|
10
|
+
export interface PKCE {
|
|
11
|
+
/** The code_verifier to send during token exchange */
|
|
12
|
+
verifier: string;
|
|
13
|
+
/** The code_challenge (SHA256 hash of verifier) to send during authorization */
|
|
14
|
+
challenge: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generates a cryptographically secure PKCE code verifier and challenge.
|
|
18
|
+
*
|
|
19
|
+
* The verifier is 43 characters (32 random bytes, base64url-encoded).
|
|
20
|
+
* The challenge is the base64url-encoded SHA256 hash of the verifier.
|
|
21
|
+
*
|
|
22
|
+
* Use code_challenge_method=S256 with the challenge in the authorization request.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const pkce = await generatePKCE();
|
|
27
|
+
*
|
|
28
|
+
* // In authorization request:
|
|
29
|
+
* const authUrl = new URL(authEndpoint);
|
|
30
|
+
* authUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
31
|
+
* authUrl.searchParams.set("code_challenge_method", "S256");
|
|
32
|
+
*
|
|
33
|
+
* // Later, in token exchange:
|
|
34
|
+
* const token = await exchangeCode({
|
|
35
|
+
* code,
|
|
36
|
+
* codeVerifier: pkce.verifier,
|
|
37
|
+
* // ...
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function generatePKCE(): Promise<PKCE>;
|
|
42
|
+
/**
|
|
43
|
+
* Generates a cryptographically secure OAuth state parameter.
|
|
44
|
+
*
|
|
45
|
+
* The state is 22 characters (16 random bytes, base64url-encoded).
|
|
46
|
+
* Use this to prevent CSRF attacks on the OAuth flow.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const state = generateState();
|
|
51
|
+
*
|
|
52
|
+
* // Store state before redirect:
|
|
53
|
+
* sessionStorage.setItem("oauth_state", state);
|
|
54
|
+
*
|
|
55
|
+
* // In authorization request:
|
|
56
|
+
* const authUrl = new URL(authEndpoint);
|
|
57
|
+
* authUrl.searchParams.set("state", state);
|
|
58
|
+
*
|
|
59
|
+
* // In callback handler:
|
|
60
|
+
* const returnedState = new URL(window.location.href).searchParams.get("state");
|
|
61
|
+
* const savedState = sessionStorage.getItem("oauth_state");
|
|
62
|
+
* if (returnedState !== savedState) {
|
|
63
|
+
* throw new Error("State mismatch - possible CSRF attack");
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function generateState(): string;
|
|
68
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,IAAI;IACnB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;CACnB;AAYD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAWlD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAGtC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0.
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographically secure code verifier and challenge generation
|
|
5
|
+
* to protect against authorization code interception attacks.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Base64url encodes a Uint8Array without padding.
|
|
9
|
+
*/
|
|
10
|
+
function base64UrlEncode(bytes) {
|
|
11
|
+
// Convert to base64
|
|
12
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
13
|
+
// Convert to base64url (replace + with -, / with _, remove padding)
|
|
14
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generates a cryptographically secure PKCE code verifier and challenge.
|
|
18
|
+
*
|
|
19
|
+
* The verifier is 43 characters (32 random bytes, base64url-encoded).
|
|
20
|
+
* The challenge is the base64url-encoded SHA256 hash of the verifier.
|
|
21
|
+
*
|
|
22
|
+
* Use code_challenge_method=S256 with the challenge in the authorization request.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const pkce = await generatePKCE();
|
|
27
|
+
*
|
|
28
|
+
* // In authorization request:
|
|
29
|
+
* const authUrl = new URL(authEndpoint);
|
|
30
|
+
* authUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
31
|
+
* authUrl.searchParams.set("code_challenge_method", "S256");
|
|
32
|
+
*
|
|
33
|
+
* // Later, in token exchange:
|
|
34
|
+
* const token = await exchangeCode({
|
|
35
|
+
* code,
|
|
36
|
+
* codeVerifier: pkce.verifier,
|
|
37
|
+
* // ...
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export async function generatePKCE() {
|
|
42
|
+
// Generate 32 random bytes
|
|
43
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
44
|
+
const verifier = base64UrlEncode(bytes);
|
|
45
|
+
// Compute SHA256 hash of the verifier
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
|
|
48
|
+
const challenge = base64UrlEncode(new Uint8Array(hashBuffer));
|
|
49
|
+
return { verifier, challenge };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generates a cryptographically secure OAuth state parameter.
|
|
53
|
+
*
|
|
54
|
+
* The state is 22 characters (16 random bytes, base64url-encoded).
|
|
55
|
+
* Use this to prevent CSRF attacks on the OAuth flow.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const state = generateState();
|
|
60
|
+
*
|
|
61
|
+
* // Store state before redirect:
|
|
62
|
+
* sessionStorage.setItem("oauth_state", state);
|
|
63
|
+
*
|
|
64
|
+
* // In authorization request:
|
|
65
|
+
* const authUrl = new URL(authEndpoint);
|
|
66
|
+
* authUrl.searchParams.set("state", state);
|
|
67
|
+
*
|
|
68
|
+
* // In callback handler:
|
|
69
|
+
* const returnedState = new URL(window.location.href).searchParams.get("state");
|
|
70
|
+
* const savedState = sessionStorage.getItem("oauth_state");
|
|
71
|
+
* if (returnedState !== savedState) {
|
|
72
|
+
* throw new Error("State mismatch - possible CSRF attack");
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function generateState() {
|
|
77
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
78
|
+
return base64UrlEncode(bytes);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH;;GAEG;AACH,SAAS,eAAe,CAAC,KAAiB;IACxC,oBAAoB;IACpB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IACnD,oEAAoE;IACpE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,2BAA2B;IAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAExC,sCAAsC;IACtC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnF,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;IAE9D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IACzD,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 token lifecycle management.
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic token refresh, expiry detection, and refresh deduplication.
|
|
5
|
+
*/
|
|
6
|
+
import type { TokenStore } from "./token-store.js";
|
|
7
|
+
import type { RefreshRequest, OAuthToken } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Options for creating a TokenManager.
|
|
10
|
+
*/
|
|
11
|
+
export interface TokenManagerOptions {
|
|
12
|
+
/** Token persistence store */
|
|
13
|
+
store: TokenStore;
|
|
14
|
+
/** Function to perform the token refresh HTTP call */
|
|
15
|
+
refreshToken: (req: RefreshRequest) => Promise<OAuthToken>;
|
|
16
|
+
/** URL of the token endpoint */
|
|
17
|
+
tokenEndpoint: string;
|
|
18
|
+
/** The client identifier (optional) */
|
|
19
|
+
clientId?: string;
|
|
20
|
+
/** The client secret (optional) */
|
|
21
|
+
clientSecret?: string;
|
|
22
|
+
/** Use Launchpad's non-standard token format (optional) */
|
|
23
|
+
useLegacyFormat?: boolean;
|
|
24
|
+
/** Seconds before expiry to trigger refresh (default: 120) */
|
|
25
|
+
bufferSeconds?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Manages OAuth token lifecycle with automatic refresh and persistence.
|
|
29
|
+
*
|
|
30
|
+
* Loads tokens from a store on first access, refreshes when expired,
|
|
31
|
+
* and deduplicates concurrent refresh requests.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const manager = new TokenManager({
|
|
36
|
+
* store: new FileTokenStore("~/.config/basecamp/tokens.json"),
|
|
37
|
+
* refreshToken,
|
|
38
|
+
* tokenEndpoint: config.tokenEndpoint,
|
|
39
|
+
* clientId: "my_client_id",
|
|
40
|
+
* clientSecret: "my_client_secret",
|
|
41
|
+
* useLegacyFormat: true,
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Use as a TokenProvider
|
|
45
|
+
* const client = createBasecampClient({
|
|
46
|
+
* accountId: "12345",
|
|
47
|
+
* accessToken: () => manager.getToken(),
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare class TokenManager {
|
|
52
|
+
private readonly store;
|
|
53
|
+
private readonly doRefresh;
|
|
54
|
+
private readonly tokenEndpoint;
|
|
55
|
+
private readonly clientId?;
|
|
56
|
+
private readonly clientSecret?;
|
|
57
|
+
private readonly useLegacyFormat?;
|
|
58
|
+
private readonly bufferSeconds;
|
|
59
|
+
private token;
|
|
60
|
+
private loaded;
|
|
61
|
+
private refreshPromise;
|
|
62
|
+
constructor(options: TokenManagerOptions);
|
|
63
|
+
/**
|
|
64
|
+
* Returns a valid access token string, refreshing if necessary.
|
|
65
|
+
*
|
|
66
|
+
* On first call, loads from store. If expired, refreshes and saves.
|
|
67
|
+
* Concurrent calls during refresh share the same promise.
|
|
68
|
+
*
|
|
69
|
+
* @throws if no token is available and refresh fails
|
|
70
|
+
*/
|
|
71
|
+
getToken(): Promise<string>;
|
|
72
|
+
/**
|
|
73
|
+
* Forces a token refresh regardless of expiry.
|
|
74
|
+
*
|
|
75
|
+
* Concurrent calls share the same in-flight request.
|
|
76
|
+
*
|
|
77
|
+
* @throws if no refresh token is available or refresh fails
|
|
78
|
+
*/
|
|
79
|
+
forceRefresh(): Promise<OAuthToken>;
|
|
80
|
+
/**
|
|
81
|
+
* Checks if the current token is expired.
|
|
82
|
+
*
|
|
83
|
+
* Returns true if no token is loaded or the token has expired.
|
|
84
|
+
*/
|
|
85
|
+
isExpired(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The currently loaded token, or null if none.
|
|
88
|
+
*/
|
|
89
|
+
get currentToken(): OAuthToken | null;
|
|
90
|
+
private executeRefresh;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=token-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/oauth/token-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,sDAAsD;IACtD,YAAY,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3D,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+C;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,cAAc,CAAoC;gBAE9C,OAAO,EAAE,mBAAmB;IAUxC;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAcjC;;;;;;OAMG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC;IAczC;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAKpB;;OAEG;IACH,IAAI,YAAY,IAAI,UAAU,GAAG,IAAI,CAEpC;YAEa,cAAc;CAuB7B"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 token lifecycle management.
|
|
3
|
+
*
|
|
4
|
+
* Handles automatic token refresh, expiry detection, and refresh deduplication.
|
|
5
|
+
*/
|
|
6
|
+
import { isTokenExpired } from "./exchange.js";
|
|
7
|
+
/**
|
|
8
|
+
* Manages OAuth token lifecycle with automatic refresh and persistence.
|
|
9
|
+
*
|
|
10
|
+
* Loads tokens from a store on first access, refreshes when expired,
|
|
11
|
+
* and deduplicates concurrent refresh requests.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const manager = new TokenManager({
|
|
16
|
+
* store: new FileTokenStore("~/.config/basecamp/tokens.json"),
|
|
17
|
+
* refreshToken,
|
|
18
|
+
* tokenEndpoint: config.tokenEndpoint,
|
|
19
|
+
* clientId: "my_client_id",
|
|
20
|
+
* clientSecret: "my_client_secret",
|
|
21
|
+
* useLegacyFormat: true,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Use as a TokenProvider
|
|
25
|
+
* const client = createBasecampClient({
|
|
26
|
+
* accountId: "12345",
|
|
27
|
+
* accessToken: () => manager.getToken(),
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class TokenManager {
|
|
32
|
+
store;
|
|
33
|
+
doRefresh;
|
|
34
|
+
tokenEndpoint;
|
|
35
|
+
clientId;
|
|
36
|
+
clientSecret;
|
|
37
|
+
useLegacyFormat;
|
|
38
|
+
bufferSeconds;
|
|
39
|
+
token = null;
|
|
40
|
+
loaded = false;
|
|
41
|
+
refreshPromise = null;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.store = options.store;
|
|
44
|
+
this.doRefresh = options.refreshToken;
|
|
45
|
+
this.tokenEndpoint = options.tokenEndpoint;
|
|
46
|
+
this.clientId = options.clientId;
|
|
47
|
+
this.clientSecret = options.clientSecret;
|
|
48
|
+
this.useLegacyFormat = options.useLegacyFormat;
|
|
49
|
+
this.bufferSeconds = options.bufferSeconds ?? 120;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns a valid access token string, refreshing if necessary.
|
|
53
|
+
*
|
|
54
|
+
* On first call, loads from store. If expired, refreshes and saves.
|
|
55
|
+
* Concurrent calls during refresh share the same promise.
|
|
56
|
+
*
|
|
57
|
+
* @throws if no token is available and refresh fails
|
|
58
|
+
*/
|
|
59
|
+
async getToken() {
|
|
60
|
+
if (!this.loaded) {
|
|
61
|
+
this.token = await this.store.load();
|
|
62
|
+
this.loaded = true;
|
|
63
|
+
}
|
|
64
|
+
if (this.token && !isTokenExpired(this.token, this.bufferSeconds)) {
|
|
65
|
+
return this.token.accessToken;
|
|
66
|
+
}
|
|
67
|
+
const refreshed = await this.forceRefresh();
|
|
68
|
+
return refreshed.accessToken;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Forces a token refresh regardless of expiry.
|
|
72
|
+
*
|
|
73
|
+
* Concurrent calls share the same in-flight request.
|
|
74
|
+
*
|
|
75
|
+
* @throws if no refresh token is available or refresh fails
|
|
76
|
+
*/
|
|
77
|
+
async forceRefresh() {
|
|
78
|
+
if (this.refreshPromise) {
|
|
79
|
+
return this.refreshPromise;
|
|
80
|
+
}
|
|
81
|
+
this.refreshPromise = this.executeRefresh();
|
|
82
|
+
try {
|
|
83
|
+
return await this.refreshPromise;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
this.refreshPromise = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks if the current token is expired.
|
|
91
|
+
*
|
|
92
|
+
* Returns true if no token is loaded or the token has expired.
|
|
93
|
+
*/
|
|
94
|
+
isExpired() {
|
|
95
|
+
if (!this.token)
|
|
96
|
+
return true;
|
|
97
|
+
return isTokenExpired(this.token, this.bufferSeconds);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The currently loaded token, or null if none.
|
|
101
|
+
*/
|
|
102
|
+
get currentToken() {
|
|
103
|
+
return this.token;
|
|
104
|
+
}
|
|
105
|
+
async executeRefresh() {
|
|
106
|
+
const refreshTokenValue = this.token?.refreshToken;
|
|
107
|
+
if (!refreshTokenValue) {
|
|
108
|
+
throw new Error("No refresh token available");
|
|
109
|
+
}
|
|
110
|
+
const newToken = await this.doRefresh({
|
|
111
|
+
tokenEndpoint: this.tokenEndpoint,
|
|
112
|
+
refreshToken: refreshTokenValue,
|
|
113
|
+
clientId: this.clientId,
|
|
114
|
+
clientSecret: this.clientSecret,
|
|
115
|
+
useLegacyFormat: this.useLegacyFormat,
|
|
116
|
+
});
|
|
117
|
+
// Preserve the previous refresh token when the server omits one
|
|
118
|
+
const merged = newToken.refreshToken
|
|
119
|
+
? newToken
|
|
120
|
+
: { ...newToken, refreshToken: refreshTokenValue };
|
|
121
|
+
this.token = merged;
|
|
122
|
+
await this.store.save(merged);
|
|
123
|
+
return merged;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=token-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/oauth/token-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token persistence for OAuth 2.0 tokens.
|
|
3
|
+
*
|
|
4
|
+
* Provides a file-based token store with atomic writes and secure permissions.
|
|
5
|
+
*/
|
|
6
|
+
import type { OAuthToken } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Interface for persisting OAuth tokens.
|
|
9
|
+
*/
|
|
10
|
+
export interface TokenStore {
|
|
11
|
+
/** Load a previously saved token, or null if none exists */
|
|
12
|
+
load(): Promise<OAuthToken | null>;
|
|
13
|
+
/** Save a token for later retrieval */
|
|
14
|
+
save(token: OAuthToken): Promise<void>;
|
|
15
|
+
/** Remove the stored token */
|
|
16
|
+
clear(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* File-based token store with atomic writes and secure permissions.
|
|
20
|
+
*
|
|
21
|
+
* Tokens are stored as JSON with 0o600 permissions (owner read/write only).
|
|
22
|
+
* Writes are atomic: data is written to a temporary file then renamed.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const store = new FileTokenStore("~/.config/basecamp/tokens.json");
|
|
27
|
+
*
|
|
28
|
+
* // Save a token
|
|
29
|
+
* await store.save(token);
|
|
30
|
+
*
|
|
31
|
+
* // Load it back
|
|
32
|
+
* const loaded = await store.load();
|
|
33
|
+
*
|
|
34
|
+
* // Clear when done
|
|
35
|
+
* await store.clear();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class FileTokenStore implements TokenStore {
|
|
39
|
+
private readonly filePath;
|
|
40
|
+
constructor(filePath: string);
|
|
41
|
+
load(): Promise<OAuthToken | null>;
|
|
42
|
+
save(token: OAuthToken): Promise<void>;
|
|
43
|
+
clear(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../src/oauth/token-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4DAA4D;IAC5D,IAAI,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnC,uCAAuC;IACvC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,8BAA8B;IAC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,cAAe,YAAW,UAAU;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,QAAQ,EAAE,MAAM;IAItB,IAAI,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAuBlC,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAU7B"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token persistence for OAuth 2.0 tokens.
|
|
3
|
+
*
|
|
4
|
+
* Provides a file-based token store with atomic writes and secure permissions.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
/**
|
|
10
|
+
* Resolves ~ to the user's home directory.
|
|
11
|
+
*/
|
|
12
|
+
function expandHome(filePath) {
|
|
13
|
+
if (filePath.startsWith("~/")) {
|
|
14
|
+
return filePath.replace("~", homedir());
|
|
15
|
+
}
|
|
16
|
+
if (filePath === "~") {
|
|
17
|
+
return homedir();
|
|
18
|
+
}
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* File-based token store with atomic writes and secure permissions.
|
|
23
|
+
*
|
|
24
|
+
* Tokens are stored as JSON with 0o600 permissions (owner read/write only).
|
|
25
|
+
* Writes are atomic: data is written to a temporary file then renamed.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const store = new FileTokenStore("~/.config/basecamp/tokens.json");
|
|
30
|
+
*
|
|
31
|
+
* // Save a token
|
|
32
|
+
* await store.save(token);
|
|
33
|
+
*
|
|
34
|
+
* // Load it back
|
|
35
|
+
* const loaded = await store.load();
|
|
36
|
+
*
|
|
37
|
+
* // Clear when done
|
|
38
|
+
* await store.clear();
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class FileTokenStore {
|
|
42
|
+
filePath;
|
|
43
|
+
constructor(filePath) {
|
|
44
|
+
this.filePath = expandHome(filePath);
|
|
45
|
+
}
|
|
46
|
+
async load() {
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await readFile(this.filePath, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
return {
|
|
59
|
+
accessToken: data.accessToken,
|
|
60
|
+
refreshToken: data.refreshToken,
|
|
61
|
+
tokenType: data.tokenType,
|
|
62
|
+
expiresIn: data.expiresIn,
|
|
63
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : undefined,
|
|
64
|
+
scope: data.scope,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async save(token) {
|
|
68
|
+
const serialized = {
|
|
69
|
+
accessToken: token.accessToken,
|
|
70
|
+
refreshToken: token.refreshToken,
|
|
71
|
+
tokenType: token.tokenType,
|
|
72
|
+
expiresIn: token.expiresIn,
|
|
73
|
+
expiresAt: token.expiresAt?.toISOString(),
|
|
74
|
+
scope: token.scope,
|
|
75
|
+
};
|
|
76
|
+
const json = JSON.stringify(serialized, null, 2) + "\n";
|
|
77
|
+
const tmpPath = this.filePath + ".tmp";
|
|
78
|
+
// Ensure directory exists
|
|
79
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
80
|
+
// Remove stale tmp file so writeFile creates a fresh one with 0o600
|
|
81
|
+
try {
|
|
82
|
+
await unlink(tmpPath);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) {
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Atomic write: write to tmp, then rename
|
|
90
|
+
await writeFile(tmpPath, json, { mode: 0o600 });
|
|
91
|
+
await rename(tmpPath, this.filePath);
|
|
92
|
+
}
|
|
93
|
+
async clear() {
|
|
94
|
+
try {
|
|
95
|
+
await unlink(this.filePath);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
99
|
+
return; // Already gone
|
|
100
|
+
}
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/oauth/token-store.ts"],"names":[],"mappings":"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"}
|