@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
package/dist/client.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basecamp TypeScript SDK Client
|
|
3
|
+
*
|
|
4
|
+
* Creates a type-safe client for the Basecamp 3 API using openapi-fetch.
|
|
5
|
+
* Includes middleware for authentication, retry with exponential backoff,
|
|
6
|
+
* and ETag-based caching.
|
|
7
|
+
*/
|
|
8
|
+
import createClient from "openapi-fetch";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { PATH_TO_OPERATION } from "./generated/path-mapping.js";
|
|
11
|
+
import { BasecampError } from "./errors.js";
|
|
12
|
+
import { isLocalhost } from "./security.js";
|
|
13
|
+
import { parseNextLink, resolveURL, isSameOrigin } from "./pagination-utils.js";
|
|
14
|
+
import { bearerAuth } from "./auth-strategy.js";
|
|
15
|
+
// Use createRequire for JSON import (Node 18+ compatible)
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const metadata = require("./generated/metadata.json");
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Services - Generated from OpenAPI spec (spec-driven, not hand-written)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
import { ProjectsService } from "./generated/services/projects.js";
|
|
22
|
+
import { TodosService } from "./generated/services/todos.js";
|
|
23
|
+
import { TodolistsService } from "./generated/services/todolists.js";
|
|
24
|
+
import { TodosetsService } from "./generated/services/todosets.js";
|
|
25
|
+
import { PeopleService } from "./generated/services/people.js";
|
|
26
|
+
import { MessagesService } from "./generated/services/messages.js";
|
|
27
|
+
import { CommentsService } from "./generated/services/comments.js";
|
|
28
|
+
import { CampfiresService } from "./generated/services/campfires.js";
|
|
29
|
+
import { CardTablesService } from "./generated/services/card-tables.js";
|
|
30
|
+
import { CardsService } from "./generated/services/cards.js";
|
|
31
|
+
import { CardColumnsService } from "./generated/services/card-columns.js";
|
|
32
|
+
import { CardStepsService } from "./generated/services/card-steps.js";
|
|
33
|
+
import { MessageBoardsService } from "./generated/services/message-boards.js";
|
|
34
|
+
import { MessageTypesService } from "./generated/services/message-types.js";
|
|
35
|
+
import { ForwardsService } from "./generated/services/forwards.js";
|
|
36
|
+
import { CheckinsService } from "./generated/services/checkins.js";
|
|
37
|
+
import { ClientApprovalsService } from "./generated/services/client-approvals.js";
|
|
38
|
+
import { ClientCorrespondencesService } from "./generated/services/client-correspondences.js";
|
|
39
|
+
import { ClientRepliesService } from "./generated/services/client-replies.js";
|
|
40
|
+
import { WebhooksService } from "./generated/services/webhooks.js";
|
|
41
|
+
import { SubscriptionsService } from "./generated/services/subscriptions.js";
|
|
42
|
+
import { AttachmentsService } from "./generated/services/attachments.js";
|
|
43
|
+
import { VaultsService } from "./generated/services/vaults.js";
|
|
44
|
+
import { DocumentsService } from "./generated/services/documents.js";
|
|
45
|
+
import { UploadsService } from "./generated/services/uploads.js";
|
|
46
|
+
import { SchedulesService } from "./generated/services/schedules.js";
|
|
47
|
+
import { EventsService } from "./generated/services/events.js";
|
|
48
|
+
import { RecordingsService } from "./generated/services/recordings.js";
|
|
49
|
+
import { SearchService } from "./generated/services/search.js";
|
|
50
|
+
import { ReportsService } from "./generated/services/reports.js";
|
|
51
|
+
import { TemplatesService } from "./generated/services/templates.js";
|
|
52
|
+
import { LineupService } from "./generated/services/lineup.js";
|
|
53
|
+
import { TodolistGroupsService } from "./generated/services/todolist-groups.js";
|
|
54
|
+
import { ToolsService } from "./generated/services/tools.js";
|
|
55
|
+
import { TimesheetsService } from "./generated/services/timesheets.js";
|
|
56
|
+
import { TimelineService } from "./generated/services/timeline.js";
|
|
57
|
+
import { ClientVisibilityService } from "./generated/services/client-visibility.js";
|
|
58
|
+
import { BoostsService } from "./generated/services/boosts.js";
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Services - Hand-written (not spec-driven, e.g., OAuth flows)
|
|
61
|
+
// ============================================================================
|
|
62
|
+
import { AuthorizationService } from "./services/authorization.js";
|
|
63
|
+
const VERSION = "0.2.0";
|
|
64
|
+
const DEFAULT_USER_AGENT = `basecamp-sdk-ts/${VERSION}`;
|
|
65
|
+
/**
|
|
66
|
+
* Creates a type-safe Basecamp API client with built-in middleware for:
|
|
67
|
+
* - Authentication (Bearer token)
|
|
68
|
+
* - Retry with exponential backoff (respects Retry-After header)
|
|
69
|
+
* - ETag-based HTTP caching
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { createBasecampClient } from "@37signals/basecamp";
|
|
74
|
+
*
|
|
75
|
+
* const client = createBasecampClient({
|
|
76
|
+
* accountId: "12345",
|
|
77
|
+
* accessToken: process.env.BASECAMP_TOKEN!,
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* const { data, error } = await client.GET("/projects.json");
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function createBasecampClient(options) {
|
|
84
|
+
const { accountId, accessToken, auth, baseUrl = `https://3.basecampapi.com/${accountId}`, userAgent = DEFAULT_USER_AGENT, enableCache = false, enableRetry = true, hooks, } = options;
|
|
85
|
+
// Validate auth options: exactly one of auth or accessToken must be provided
|
|
86
|
+
if (auth && accessToken) {
|
|
87
|
+
throw new BasecampError("usage", "Provide either 'auth' or 'accessToken', not both");
|
|
88
|
+
}
|
|
89
|
+
if (!auth && !accessToken) {
|
|
90
|
+
throw new BasecampError("usage", "Either 'auth' or 'accessToken' is required");
|
|
91
|
+
}
|
|
92
|
+
const authStrategy = auth ?? bearerAuth(accessToken);
|
|
93
|
+
// Validate configuration (skip HTTPS check for localhost in dev/test)
|
|
94
|
+
if (baseUrl) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = new URL(baseUrl);
|
|
97
|
+
if (parsed.protocol !== "https:" && !isLocalhost(parsed.hostname)) {
|
|
98
|
+
throw new BasecampError("usage", `Base URL must use HTTPS: ${baseUrl}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err instanceof BasecampError)
|
|
103
|
+
throw err;
|
|
104
|
+
throw new BasecampError("usage", `Invalid base URL: ${baseUrl}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const client = createClient({ baseUrl });
|
|
108
|
+
// Apply middleware in order: auth first, then hooks, then cache, then retry
|
|
109
|
+
client.use(createAuthMiddleware(authStrategy, userAgent));
|
|
110
|
+
if (hooks) {
|
|
111
|
+
client.use(createHooksMiddleware(hooks));
|
|
112
|
+
}
|
|
113
|
+
if (enableCache) {
|
|
114
|
+
client.use(createCacheMiddleware());
|
|
115
|
+
}
|
|
116
|
+
if (enableRetry) {
|
|
117
|
+
client.use(createRetryMiddleware(hooks));
|
|
118
|
+
}
|
|
119
|
+
// Create enhanced client with additional properties
|
|
120
|
+
const enhancedClient = client;
|
|
121
|
+
Object.defineProperty(enhancedClient, "raw", {
|
|
122
|
+
value: client,
|
|
123
|
+
writable: false,
|
|
124
|
+
enumerable: false,
|
|
125
|
+
});
|
|
126
|
+
Object.defineProperty(enhancedClient, "hooks", {
|
|
127
|
+
value: hooks,
|
|
128
|
+
writable: false,
|
|
129
|
+
enumerable: false,
|
|
130
|
+
});
|
|
131
|
+
// Create fetchPage closure for pagination — uses same auth & User-Agent as main client
|
|
132
|
+
const fetchPage = async (url) => {
|
|
133
|
+
const headers = new Headers({
|
|
134
|
+
"User-Agent": userAgent,
|
|
135
|
+
Accept: "application/json",
|
|
136
|
+
});
|
|
137
|
+
await authStrategy.authenticate(headers);
|
|
138
|
+
return fetch(url, { headers });
|
|
139
|
+
};
|
|
140
|
+
// Add lazy-initialized service accessors
|
|
141
|
+
// Services are created on first access and cached
|
|
142
|
+
// Uses nullish coalescing assignment for atomic check-and-set in single-threaded JS
|
|
143
|
+
const serviceCache = {};
|
|
144
|
+
const defineService = (name, factory) => {
|
|
145
|
+
Object.defineProperty(enhancedClient, name, {
|
|
146
|
+
get() {
|
|
147
|
+
// Nullish coalescing assignment is atomic in single-threaded JS.
|
|
148
|
+
// This prevents duplicate service creation during async interleaving.
|
|
149
|
+
return (serviceCache[name] ??= factory());
|
|
150
|
+
},
|
|
151
|
+
enumerable: true,
|
|
152
|
+
configurable: false,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
defineService("projects", () => new ProjectsService(client, hooks, fetchPage));
|
|
156
|
+
defineService("todos", () => new TodosService(client, hooks, fetchPage));
|
|
157
|
+
defineService("todolists", () => new TodolistsService(client, hooks, fetchPage));
|
|
158
|
+
defineService("todosets", () => new TodosetsService(client, hooks, fetchPage));
|
|
159
|
+
defineService("people", () => new PeopleService(client, hooks, fetchPage));
|
|
160
|
+
defineService("authorization", () => new AuthorizationService(client, hooks, authStrategy, userAgent));
|
|
161
|
+
defineService("messages", () => new MessagesService(client, hooks, fetchPage));
|
|
162
|
+
defineService("comments", () => new CommentsService(client, hooks, fetchPage));
|
|
163
|
+
defineService("campfires", () => new CampfiresService(client, hooks, fetchPage));
|
|
164
|
+
defineService("cardTables", () => new CardTablesService(client, hooks, fetchPage));
|
|
165
|
+
defineService("cards", () => new CardsService(client, hooks, fetchPage));
|
|
166
|
+
defineService("cardColumns", () => new CardColumnsService(client, hooks, fetchPage));
|
|
167
|
+
defineService("cardSteps", () => new CardStepsService(client, hooks, fetchPage));
|
|
168
|
+
defineService("messageBoards", () => new MessageBoardsService(client, hooks, fetchPage));
|
|
169
|
+
defineService("messageTypes", () => new MessageTypesService(client, hooks, fetchPage));
|
|
170
|
+
defineService("forwards", () => new ForwardsService(client, hooks, fetchPage));
|
|
171
|
+
defineService("checkins", () => new CheckinsService(client, hooks, fetchPage));
|
|
172
|
+
defineService("clientApprovals", () => new ClientApprovalsService(client, hooks, fetchPage));
|
|
173
|
+
defineService("clientCorrespondences", () => new ClientCorrespondencesService(client, hooks, fetchPage));
|
|
174
|
+
defineService("clientReplies", () => new ClientRepliesService(client, hooks, fetchPage));
|
|
175
|
+
defineService("webhooks", () => new WebhooksService(client, hooks, fetchPage));
|
|
176
|
+
defineService("subscriptions", () => new SubscriptionsService(client, hooks, fetchPage));
|
|
177
|
+
defineService("attachments", () => new AttachmentsService(client, hooks, fetchPage));
|
|
178
|
+
defineService("vaults", () => new VaultsService(client, hooks, fetchPage));
|
|
179
|
+
defineService("documents", () => new DocumentsService(client, hooks, fetchPage));
|
|
180
|
+
defineService("uploads", () => new UploadsService(client, hooks, fetchPage));
|
|
181
|
+
defineService("schedules", () => new SchedulesService(client, hooks, fetchPage));
|
|
182
|
+
defineService("events", () => new EventsService(client, hooks, fetchPage));
|
|
183
|
+
defineService("recordings", () => new RecordingsService(client, hooks, fetchPage));
|
|
184
|
+
defineService("search", () => new SearchService(client, hooks, fetchPage));
|
|
185
|
+
defineService("reports", () => new ReportsService(client, hooks, fetchPage));
|
|
186
|
+
defineService("templates", () => new TemplatesService(client, hooks, fetchPage));
|
|
187
|
+
defineService("lineup", () => new LineupService(client, hooks, fetchPage));
|
|
188
|
+
defineService("todolistGroups", () => new TodolistGroupsService(client, hooks, fetchPage));
|
|
189
|
+
defineService("tools", () => new ToolsService(client, hooks, fetchPage));
|
|
190
|
+
defineService("timesheets", () => new TimesheetsService(client, hooks, fetchPage));
|
|
191
|
+
defineService("timeline", () => new TimelineService(client, hooks, fetchPage));
|
|
192
|
+
defineService("clientVisibility", () => new ClientVisibilityService(client, hooks, fetchPage));
|
|
193
|
+
defineService("boosts", () => new BoostsService(client, hooks, fetchPage));
|
|
194
|
+
return enhancedClient;
|
|
195
|
+
}
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// Auth Middleware
|
|
198
|
+
// =============================================================================
|
|
199
|
+
function createAuthMiddleware(authStrategy, userAgent) {
|
|
200
|
+
return {
|
|
201
|
+
async onRequest({ request }) {
|
|
202
|
+
await authStrategy.authenticate(request.headers);
|
|
203
|
+
request.headers.set("User-Agent", userAgent);
|
|
204
|
+
// Only set Content-Type if not already set (preserves binary uploads, etc.)
|
|
205
|
+
if (!request.headers.has("Content-Type")) {
|
|
206
|
+
request.headers.set("Content-Type", "application/json");
|
|
207
|
+
}
|
|
208
|
+
request.headers.set("Accept", "application/json");
|
|
209
|
+
return request;
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** Counter for generating unique request IDs */
|
|
214
|
+
let requestIdCounter = 0;
|
|
215
|
+
function createHooksMiddleware(hooks) {
|
|
216
|
+
// Track request timing by unique request ID
|
|
217
|
+
const timings = new Map();
|
|
218
|
+
return {
|
|
219
|
+
async onRequest({ request }) {
|
|
220
|
+
// Generate unique request ID to handle concurrent identical requests
|
|
221
|
+
const requestId = `${++requestIdCounter}`;
|
|
222
|
+
request.headers.set("X-SDK-Request-Id", requestId);
|
|
223
|
+
const attemptHeader = request.headers.get("X-Retry-Attempt");
|
|
224
|
+
const attempt = attemptHeader ? parseInt(attemptHeader, 10) + 1 : 1;
|
|
225
|
+
timings.set(requestId, { startTime: performance.now(), attempt });
|
|
226
|
+
const info = {
|
|
227
|
+
method: request.method,
|
|
228
|
+
url: request.url,
|
|
229
|
+
attempt,
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
hooks.onRequestStart?.(info);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Hooks should not interrupt the request
|
|
236
|
+
}
|
|
237
|
+
return request;
|
|
238
|
+
},
|
|
239
|
+
async onResponse({ request, response }) {
|
|
240
|
+
const requestId = request.headers.get("X-SDK-Request-Id") ?? "";
|
|
241
|
+
const timing = timings.get(requestId);
|
|
242
|
+
const durationMs = timing ? Math.round(performance.now() - timing.startTime) : 0;
|
|
243
|
+
const attempt = timing?.attempt ?? 1;
|
|
244
|
+
timings.delete(requestId);
|
|
245
|
+
const info = {
|
|
246
|
+
method: request.method,
|
|
247
|
+
url: request.url,
|
|
248
|
+
attempt,
|
|
249
|
+
};
|
|
250
|
+
// Check for cache hit via header set by cache middleware
|
|
251
|
+
const fromCacheHeader = response.headers.get("X-From-Cache");
|
|
252
|
+
const fromCache = fromCacheHeader === "1" ||
|
|
253
|
+
response.status === 304;
|
|
254
|
+
const result = {
|
|
255
|
+
statusCode: response.status,
|
|
256
|
+
durationMs,
|
|
257
|
+
fromCache,
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
hooks.onRequestEnd?.(info, result);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Hooks should not interrupt the response
|
|
264
|
+
}
|
|
265
|
+
return response;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const MAX_CACHE_ENTRIES = 1000;
|
|
270
|
+
function createCacheMiddleware() {
|
|
271
|
+
// Use Map for insertion-order iteration (approximates LRU)
|
|
272
|
+
const cache = new Map();
|
|
273
|
+
// Store cache keys per-request without leaking them onto the wire.
|
|
274
|
+
const cacheKeyStore = new WeakMap();
|
|
275
|
+
// Derive a short token hash from the Authorization header for cache key isolation.
|
|
276
|
+
// Different auth contexts must not share cached responses.
|
|
277
|
+
// Re-computed per request so refreshed tokens produce new cache keys.
|
|
278
|
+
//
|
|
279
|
+
// Security: The map is bounded to MAX_TOKEN_HASH_ENTRIES to prevent unbounded growth.
|
|
280
|
+
// LRU-like eviction removes oldest entries when the limit is reached.
|
|
281
|
+
const MAX_TOKEN_HASH_ENTRIES = 100;
|
|
282
|
+
const hashTokenMap = new Map();
|
|
283
|
+
// Track pending hash computations to coalesce concurrent requests for the same token.
|
|
284
|
+
// This prevents duplicate crypto operations during async interleaving.
|
|
285
|
+
const pendingHashes = new Map();
|
|
286
|
+
const evictOldestHash = () => {
|
|
287
|
+
if (hashTokenMap.size >= MAX_TOKEN_HASH_ENTRIES) {
|
|
288
|
+
// Delete oldest entry (first key in insertion order)
|
|
289
|
+
const firstKey = hashTokenMap.keys().next().value;
|
|
290
|
+
if (firstKey)
|
|
291
|
+
hashTokenMap.delete(firstKey);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const getTokenHash = async (authHeader) => {
|
|
295
|
+
if (!authHeader)
|
|
296
|
+
return "";
|
|
297
|
+
// Check completed cache first
|
|
298
|
+
const cached = hashTokenMap.get(authHeader);
|
|
299
|
+
if (cached)
|
|
300
|
+
return cached;
|
|
301
|
+
// Check if computation already in progress (coalesce concurrent requests)
|
|
302
|
+
const pending = pendingHashes.get(authHeader);
|
|
303
|
+
if (pending)
|
|
304
|
+
return pending;
|
|
305
|
+
// Start new computation with promise coalescing
|
|
306
|
+
const promise = (async () => {
|
|
307
|
+
const data = new TextEncoder().encode(authHeader);
|
|
308
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
309
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
310
|
+
const hash = Array.from(hashArray.slice(0, 8))
|
|
311
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
312
|
+
.join("");
|
|
313
|
+
// Evict oldest before adding new entry
|
|
314
|
+
evictOldestHash();
|
|
315
|
+
hashTokenMap.set(authHeader, hash);
|
|
316
|
+
return hash;
|
|
317
|
+
})();
|
|
318
|
+
pendingHashes.set(authHeader, promise);
|
|
319
|
+
promise.finally(() => pendingHashes.delete(authHeader));
|
|
320
|
+
return promise;
|
|
321
|
+
};
|
|
322
|
+
const evictOldest = () => {
|
|
323
|
+
if (cache.size >= MAX_CACHE_ENTRIES) {
|
|
324
|
+
// Delete oldest entry (first key in insertion order)
|
|
325
|
+
const firstKey = cache.keys().next().value;
|
|
326
|
+
if (firstKey)
|
|
327
|
+
cache.delete(firstKey);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
return {
|
|
331
|
+
async onRequest({ request }) {
|
|
332
|
+
if (request.method !== "GET")
|
|
333
|
+
return request;
|
|
334
|
+
const tokenHash = await getTokenHash(request.headers.get("Authorization"));
|
|
335
|
+
const cacheKey = getCacheKey(request.url, tokenHash);
|
|
336
|
+
const entry = cache.get(cacheKey);
|
|
337
|
+
if (entry?.etag) {
|
|
338
|
+
request.headers.set("If-None-Match", entry.etag);
|
|
339
|
+
}
|
|
340
|
+
// Store cache key internally — not on the wire
|
|
341
|
+
cacheKeyStore.set(request, cacheKey);
|
|
342
|
+
return request;
|
|
343
|
+
},
|
|
344
|
+
async onResponse({ request, response }) {
|
|
345
|
+
if (request.method !== "GET")
|
|
346
|
+
return response;
|
|
347
|
+
// Prefer stored key; fall back to recomputing from the Authorization header
|
|
348
|
+
// (handles cases where middleware clones the Request, breaking WeakMap identity).
|
|
349
|
+
const cacheKey = cacheKeyStore.get(request) ??
|
|
350
|
+
getCacheKey(request.url, await getTokenHash(request.headers.get("Authorization")));
|
|
351
|
+
// Handle 304 Not Modified - return cached body with cache indicator
|
|
352
|
+
if (response.status === 304) {
|
|
353
|
+
const entry = cache.get(cacheKey);
|
|
354
|
+
if (entry) {
|
|
355
|
+
const headers = new Headers(response.headers);
|
|
356
|
+
headers.set("X-From-Cache", "1");
|
|
357
|
+
return new Response(entry.body, {
|
|
358
|
+
status: 200,
|
|
359
|
+
headers,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Cache successful responses with ETag
|
|
364
|
+
if (response.ok) {
|
|
365
|
+
const etag = response.headers.get("ETag");
|
|
366
|
+
if (etag) {
|
|
367
|
+
const body = await response.clone().text();
|
|
368
|
+
evictOldest();
|
|
369
|
+
cache.set(cacheKey, { etag, body });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return response;
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function getCacheKey(url, tokenHash) {
|
|
377
|
+
return `${tokenHash}:${url}`;
|
|
378
|
+
}
|
|
379
|
+
/** Default retry config used when no operation-specific config is available */
|
|
380
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
381
|
+
maxAttempts: 3,
|
|
382
|
+
baseDelayMs: 1000,
|
|
383
|
+
backoff: "exponential",
|
|
384
|
+
retryOn: [429, 503],
|
|
385
|
+
};
|
|
386
|
+
const MAX_JITTER_MS = 100;
|
|
387
|
+
// PATH_TO_OPERATION is imported from generated/path-mapping.js
|
|
388
|
+
/**
|
|
389
|
+
* Normalizes a URL path by replacing numeric IDs with placeholder tokens.
|
|
390
|
+
* For example: /12345/todos/456 → /{accountId}/todos/{todoId}
|
|
391
|
+
*/
|
|
392
|
+
export function normalizeUrlPath(url) {
|
|
393
|
+
// Parse the URL and extract the pathname
|
|
394
|
+
const urlObj = new URL(url);
|
|
395
|
+
let path = urlObj.pathname;
|
|
396
|
+
// Remove .json suffix if present (we'll add it back for matching)
|
|
397
|
+
const hasJsonSuffix = path.endsWith(".json");
|
|
398
|
+
if (hasJsonSuffix) {
|
|
399
|
+
path = path.slice(0, -5);
|
|
400
|
+
}
|
|
401
|
+
// Split path into segments
|
|
402
|
+
const segments = path.split("/").filter(Boolean);
|
|
403
|
+
// Map of resource names to their ID placeholder tokens
|
|
404
|
+
// Note: Some paths have context-dependent placeholders, but we use consistent
|
|
405
|
+
// placeholders that match our PATH_TO_OPERATION entries
|
|
406
|
+
const idMapping = {
|
|
407
|
+
buckets: "{projectId}",
|
|
408
|
+
projects: "{projectId}",
|
|
409
|
+
templates: "{templateId}",
|
|
410
|
+
card_tables: "{cardTableId}",
|
|
411
|
+
cards: "{cardId}",
|
|
412
|
+
columns: "{columnId}",
|
|
413
|
+
lists: "{columnId}",
|
|
414
|
+
steps: "{stepId}",
|
|
415
|
+
categories: "{typeId}",
|
|
416
|
+
chats: "{campfireId}",
|
|
417
|
+
integrations: "{chatbotId}",
|
|
418
|
+
lines: "{lineId}",
|
|
419
|
+
approvals: "{approvalId}",
|
|
420
|
+
correspondences: "{correspondenceId}",
|
|
421
|
+
replies: "{replyId}",
|
|
422
|
+
recordings: "{recordingId}",
|
|
423
|
+
comments: "{commentId}",
|
|
424
|
+
tools: "{toolId}", // dock/tools/{toolId}
|
|
425
|
+
documents: "{documentId}",
|
|
426
|
+
inbox_forwards: "{forwardId}",
|
|
427
|
+
inboxes: "{inboxId}",
|
|
428
|
+
message_boards: "{boardId}",
|
|
429
|
+
messages: "{messageId}",
|
|
430
|
+
question_answers: "{answerId}",
|
|
431
|
+
questionnaires: "{questionnaireId}",
|
|
432
|
+
questions: "{questionId}",
|
|
433
|
+
by: "{personId}", // questions/{questionId}/answers/by/{personId}
|
|
434
|
+
schedule_entries: "{entryId}",
|
|
435
|
+
occurrences: "{date}", // schedule_entries/{entryId}/occurrences/{date}
|
|
436
|
+
schedules: "{scheduleId}",
|
|
437
|
+
todolists: "{todolistId}", // Also handles {id} and {groupId} via context
|
|
438
|
+
groups: "{groupId}", // todolists/{todolistId}/groups
|
|
439
|
+
todos: "{todoId}",
|
|
440
|
+
todosets: "{todosetId}",
|
|
441
|
+
uploads: "{uploadId}",
|
|
442
|
+
vaults: "{vaultId}",
|
|
443
|
+
webhooks: "{webhookId}",
|
|
444
|
+
timesheet_entries: "{entryId}",
|
|
445
|
+
people: "{personId}",
|
|
446
|
+
markers: "{markerId}", // lineup/markers/{markerId}
|
|
447
|
+
project_constructions: "{constructionId}",
|
|
448
|
+
assigned: "{personId}", // reports/todos/assigned/{personId}
|
|
449
|
+
progress: "{personId}", // reports/users/progress/{personId}
|
|
450
|
+
users: "{personId}", // Alternative for users/progress
|
|
451
|
+
};
|
|
452
|
+
// Context-dependent overrides: when the segment following the ID matches a key,
|
|
453
|
+
// override the placeholder from the default idMapping. This handles cases like
|
|
454
|
+
// /buckets/{id}/webhooks → {bucketId} vs /buckets/{id}/timeline → {projectId}.
|
|
455
|
+
const contextOverrides = {
|
|
456
|
+
buckets: { webhooks: "{bucketId}" },
|
|
457
|
+
};
|
|
458
|
+
// Build normalized path by replacing IDs and dates based on context
|
|
459
|
+
const normalized = [];
|
|
460
|
+
let prevSegment = null;
|
|
461
|
+
let isFirstSegment = true;
|
|
462
|
+
// Pattern for ISO-8601 date (YYYY-MM-DD)
|
|
463
|
+
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
464
|
+
for (let i = 0; i < segments.length; i++) {
|
|
465
|
+
const segment = segments[i];
|
|
466
|
+
const nextSegment = i + 1 < segments.length ? segments[i + 1] : undefined;
|
|
467
|
+
// Check if this segment is a numeric ID
|
|
468
|
+
if (/^\d+$/.test(segment)) {
|
|
469
|
+
// First numeric segment is always the accountId
|
|
470
|
+
if (isFirstSegment) {
|
|
471
|
+
normalized.push("{accountId}");
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Check context-dependent overrides first (look ahead to next segment)
|
|
475
|
+
const overrides = prevSegment ? contextOverrides[prevSegment] : undefined;
|
|
476
|
+
const override = overrides && nextSegment ? overrides[nextSegment] : undefined;
|
|
477
|
+
// Fall back to default idMapping
|
|
478
|
+
const placeholder = override ?? (prevSegment ? idMapping[prevSegment] : undefined);
|
|
479
|
+
normalized.push(placeholder ?? "{id}");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (datePattern.test(segment)) {
|
|
483
|
+
// ISO-8601 date - map based on preceding segment (e.g., occurrences → {date})
|
|
484
|
+
const placeholder = prevSegment ? idMapping[prevSegment] : undefined;
|
|
485
|
+
normalized.push(placeholder ?? "{date}");
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
normalized.push(segment);
|
|
489
|
+
}
|
|
490
|
+
prevSegment = segment;
|
|
491
|
+
isFirstSegment = false;
|
|
492
|
+
}
|
|
493
|
+
// Reconstruct path
|
|
494
|
+
let normalizedPath = "/" + normalized.join("/");
|
|
495
|
+
if (hasJsonSuffix) {
|
|
496
|
+
normalizedPath += ".json";
|
|
497
|
+
}
|
|
498
|
+
return normalizedPath;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Gets the retry config for a specific request based on operation metadata.
|
|
502
|
+
*/
|
|
503
|
+
function getRetryConfigForRequest(method, url) {
|
|
504
|
+
const normalizedPath = normalizeUrlPath(url);
|
|
505
|
+
const key = `${method.toUpperCase()}:${normalizedPath}`;
|
|
506
|
+
const operationName = PATH_TO_OPERATION[key];
|
|
507
|
+
if (operationName) {
|
|
508
|
+
const opMeta = metadata.operations[operationName];
|
|
509
|
+
if (opMeta?.retry) {
|
|
510
|
+
return opMeta.retry;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return DEFAULT_RETRY_CONFIG;
|
|
514
|
+
}
|
|
515
|
+
function createRetryMiddleware(hooks) {
|
|
516
|
+
// Store request body clones keyed by a request identifier
|
|
517
|
+
// This is needed because Request.body can only be read once
|
|
518
|
+
const bodyCache = new Map();
|
|
519
|
+
return {
|
|
520
|
+
async onRequest({ request }) {
|
|
521
|
+
// For methods that may have a body, clone it before the initial fetch
|
|
522
|
+
// so we can use it for retries. Request.body can only be consumed once.
|
|
523
|
+
const method = request.method.toUpperCase();
|
|
524
|
+
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
|
525
|
+
const requestId = `${method}:${request.url}:${Date.now()}`;
|
|
526
|
+
request.headers.set("X-Request-Id", requestId);
|
|
527
|
+
if (request.body) {
|
|
528
|
+
// Clone the body before it gets consumed
|
|
529
|
+
const cloned = request.clone();
|
|
530
|
+
bodyCache.set(requestId, await cloned.arrayBuffer());
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
bodyCache.set(requestId, null);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return request;
|
|
537
|
+
},
|
|
538
|
+
async onResponse({ request, response }) {
|
|
539
|
+
// Get operation-specific retry config from metadata
|
|
540
|
+
const retryConfig = getRetryConfigForRequest(request.method, request.url);
|
|
541
|
+
const requestId = request.headers.get("X-Request-Id");
|
|
542
|
+
// Helper to clean up cached body
|
|
543
|
+
const cleanupBody = () => {
|
|
544
|
+
if (requestId)
|
|
545
|
+
bodyCache.delete(requestId);
|
|
546
|
+
};
|
|
547
|
+
// Check if status code should trigger retry
|
|
548
|
+
if (!retryConfig.retryOn.includes(response.status)) {
|
|
549
|
+
cleanupBody();
|
|
550
|
+
return response;
|
|
551
|
+
}
|
|
552
|
+
// Extract current retry attempt from custom header
|
|
553
|
+
const attemptHeader = request.headers.get("X-Retry-Attempt");
|
|
554
|
+
const attempt = attemptHeader ? parseInt(attemptHeader, 10) : 0;
|
|
555
|
+
// Check if we've exhausted retries (maxAttempts is total attempts, not retries)
|
|
556
|
+
// With maxAttempts=3: attempt 0 (initial), 1 (retry 1), 2 (retry 2) = 3 total
|
|
557
|
+
if (attempt >= retryConfig.maxAttempts - 1) {
|
|
558
|
+
cleanupBody();
|
|
559
|
+
return response;
|
|
560
|
+
}
|
|
561
|
+
// Calculate delay
|
|
562
|
+
let delay;
|
|
563
|
+
// For 429, respect Retry-After header
|
|
564
|
+
if (response.status === 429) {
|
|
565
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
566
|
+
if (retryAfter) {
|
|
567
|
+
const seconds = parseInt(retryAfter, 10);
|
|
568
|
+
if (!isNaN(seconds)) {
|
|
569
|
+
delay = seconds * 1000;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
delay = calculateBackoffDelay(retryConfig, attempt);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
delay = calculateBackoffDelay(retryConfig, attempt);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
delay = calculateBackoffDelay(retryConfig, attempt);
|
|
581
|
+
}
|
|
582
|
+
// Notify hooks of retry
|
|
583
|
+
if (hooks?.onRetry) {
|
|
584
|
+
const info = {
|
|
585
|
+
method: request.method,
|
|
586
|
+
url: request.url,
|
|
587
|
+
attempt: attempt + 1,
|
|
588
|
+
};
|
|
589
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText || "Request failed"}`);
|
|
590
|
+
try {
|
|
591
|
+
hooks.onRetry(info, attempt + 1, error, delay);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Hooks should not interrupt the retry
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Wait before retry
|
|
598
|
+
await sleep(delay);
|
|
599
|
+
// Get cached body for methods that may have one
|
|
600
|
+
let body = null;
|
|
601
|
+
if (requestId && bodyCache.has(requestId)) {
|
|
602
|
+
const cachedBody = bodyCache.get(requestId);
|
|
603
|
+
if (cachedBody) {
|
|
604
|
+
body = cachedBody;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Create retry request with fresh body
|
|
608
|
+
const retryRequest = new Request(request.url, {
|
|
609
|
+
method: request.method,
|
|
610
|
+
headers: new Headers(request.headers),
|
|
611
|
+
body,
|
|
612
|
+
signal: request.signal,
|
|
613
|
+
});
|
|
614
|
+
retryRequest.headers.set("X-Retry-Attempt", String(attempt + 1));
|
|
615
|
+
// Retry using native fetch
|
|
616
|
+
return fetch(retryRequest);
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function calculateBackoffDelay(config, attempt) {
|
|
621
|
+
const base = config.baseDelayMs;
|
|
622
|
+
let delay;
|
|
623
|
+
switch (config.backoff) {
|
|
624
|
+
case "exponential":
|
|
625
|
+
delay = base * Math.pow(2, attempt);
|
|
626
|
+
break;
|
|
627
|
+
case "linear":
|
|
628
|
+
delay = base * (attempt + 1);
|
|
629
|
+
break;
|
|
630
|
+
case "constant":
|
|
631
|
+
default:
|
|
632
|
+
delay = base;
|
|
633
|
+
}
|
|
634
|
+
// Add jitter (0-100ms)
|
|
635
|
+
const jitter = Math.random() * MAX_JITTER_MS;
|
|
636
|
+
return delay + jitter;
|
|
637
|
+
}
|
|
638
|
+
function sleep(ms) {
|
|
639
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
640
|
+
}
|
|
641
|
+
// =============================================================================
|
|
642
|
+
// Pagination Helper
|
|
643
|
+
// =============================================================================
|
|
644
|
+
/**
|
|
645
|
+
* Fetches all pages of a paginated resource using Link header pagination.
|
|
646
|
+
* Automatically follows rel="next" links until no more pages exist.
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* ```ts
|
|
650
|
+
* const response = await client.GET("/projects.json");
|
|
651
|
+
*
|
|
652
|
+
* const allProjects = await fetchAllPages(
|
|
653
|
+
* response.response,
|
|
654
|
+
* (r) => r.json()
|
|
655
|
+
* );
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
export async function fetchAllPages(initialResponse, parse, authHeader) {
|
|
659
|
+
const results = [];
|
|
660
|
+
let response = initialResponse;
|
|
661
|
+
while (true) {
|
|
662
|
+
const items = await parse(response.clone());
|
|
663
|
+
results.push(...items);
|
|
664
|
+
const rawNextUrl = parseNextLink(response.headers.get("Link"));
|
|
665
|
+
if (!rawNextUrl)
|
|
666
|
+
break;
|
|
667
|
+
// Resolve relative URLs against the current page URL (handles path-relative links)
|
|
668
|
+
const nextUrl = resolveURL(response.url, rawNextUrl);
|
|
669
|
+
// Validate same-origin to prevent SSRF / token leakage via poisoned Link headers
|
|
670
|
+
if (!isSameOrigin(nextUrl, initialResponse.url)) {
|
|
671
|
+
throw new Error(`Pagination Link header points to different origin: ${nextUrl}`);
|
|
672
|
+
}
|
|
673
|
+
const headers = { Accept: "application/json" };
|
|
674
|
+
if (authHeader) {
|
|
675
|
+
headers["Authorization"] = authHeader;
|
|
676
|
+
}
|
|
677
|
+
response = await fetch(nextUrl, { headers });
|
|
678
|
+
}
|
|
679
|
+
return results;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Async generator that yields pages of results one at a time.
|
|
683
|
+
* Useful for processing large datasets without loading everything into memory.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```ts
|
|
687
|
+
* for await (const page of paginateAll(response.response, (r) => r.json())) {
|
|
688
|
+
* console.log(`Processing ${page.length} items`);
|
|
689
|
+
* }
|
|
690
|
+
* ```
|
|
691
|
+
*/
|
|
692
|
+
export async function* paginateAll(initialResponse, parse, authHeader) {
|
|
693
|
+
let response = initialResponse;
|
|
694
|
+
while (true) {
|
|
695
|
+
const items = await parse(response.clone());
|
|
696
|
+
yield items;
|
|
697
|
+
const rawNextUrl = parseNextLink(response.headers.get("Link"));
|
|
698
|
+
if (!rawNextUrl)
|
|
699
|
+
break;
|
|
700
|
+
// Resolve relative URLs against the current page URL (handles path-relative links)
|
|
701
|
+
const nextUrl = resolveURL(response.url, rawNextUrl);
|
|
702
|
+
// Validate same-origin to prevent SSRF / token leakage via poisoned Link headers
|
|
703
|
+
if (!isSameOrigin(nextUrl, initialResponse.url)) {
|
|
704
|
+
throw new Error(`Pagination Link header points to different origin: ${nextUrl}`);
|
|
705
|
+
}
|
|
706
|
+
const headers = { Accept: "application/json" };
|
|
707
|
+
if (authHeader) {
|
|
708
|
+
headers["Authorization"] = authHeader;
|
|
709
|
+
}
|
|
710
|
+
response = await fetch(nextUrl, { headers });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// Re-export pagination utilities (defined in pagination-utils.ts to avoid circular deps)
|
|
714
|
+
export { parseNextLink, resolveURL, isSameOrigin };
|
|
715
|
+
//# sourceMappingURL=client.js.map
|