@planescrm/sdk 0.2.0 → 0.4.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/dist/index.mjs CHANGED
@@ -1,62 +1,74 @@
1
1
  // src/config.ts
2
+ var MemoryTokenStorage = class {
3
+ constructor(seed) {
4
+ this.tokens = seed ?? null;
5
+ }
6
+ async getTokens() {
7
+ return this.tokens;
8
+ }
9
+ async setTokens(tokens) {
10
+ this.tokens = tokens;
11
+ }
12
+ };
2
13
  var DEFAULT_BASE_URL = "https://api.planescrm.com/api/v1";
3
14
  var DEFAULT_TIMEOUT = 3e4;
4
15
  var DEFAULT_MAX_RETRIES = 3;
16
+ var DEFAULT_TOKEN_REFRESH_SKEW_MS = 6e4;
5
17
 
6
18
  // src/errors.ts
7
- var BlueprintError = class extends Error {
19
+ var PlanesError = class extends Error {
8
20
  constructor(message, status, code) {
9
21
  super(message);
10
- this.name = "BlueprintError";
22
+ this.name = "PlanesError";
11
23
  this.status = status;
12
24
  this.code = code;
13
25
  }
14
26
  };
15
- var AuthenticationError = class extends BlueprintError {
27
+ var AuthenticationError = class extends PlanesError {
16
28
  constructor(message = "Authentication failed") {
17
29
  super(message, 401, "authentication_error");
18
30
  this.name = "AuthenticationError";
19
31
  }
20
32
  };
21
- var AuthorizationError = class extends BlueprintError {
33
+ var AuthorizationError = class extends PlanesError {
22
34
  constructor(message = "Insufficient permissions") {
23
35
  super(message, 403, "authorization_error");
24
36
  this.name = "AuthorizationError";
25
37
  }
26
38
  };
27
- var NotFoundError = class extends BlueprintError {
39
+ var NotFoundError = class extends PlanesError {
28
40
  constructor(message = "Resource not found") {
29
41
  super(message, 404, "not_found");
30
42
  this.name = "NotFoundError";
31
43
  }
32
44
  };
33
- var ValidationError = class extends BlueprintError {
45
+ var ValidationError = class extends PlanesError {
34
46
  constructor(message = "Validation failed", fieldErrors = []) {
35
47
  super(message, 400, "validation_error");
36
48
  this.name = "ValidationError";
37
49
  this.fieldErrors = fieldErrors;
38
50
  }
39
51
  };
40
- var RateLimitError = class extends BlueprintError {
52
+ var RateLimitError = class extends PlanesError {
41
53
  constructor(retryAfter = 1, message = "Rate limit exceeded") {
42
54
  super(message, 429, "rate_limit_error");
43
55
  this.name = "RateLimitError";
44
56
  this.retryAfter = retryAfter;
45
57
  }
46
58
  };
47
- var ServerError = class extends BlueprintError {
59
+ var ServerError = class extends PlanesError {
48
60
  constructor(message = "Internal server error") {
49
61
  super(message, 500, "server_error");
50
62
  this.name = "ServerError";
51
63
  }
52
64
  };
53
- var WebhookVerificationError = class extends BlueprintError {
65
+ var WebhookVerificationError = class extends PlanesError {
54
66
  constructor(message = "Webhook signature verification failed") {
55
67
  super(message, 400, "webhook_verification_error");
56
68
  this.name = "WebhookVerificationError";
57
69
  }
58
70
  };
59
- var OAuthError = class extends BlueprintError {
71
+ var OAuthError = class extends PlanesError {
60
72
  constructor(errorCode, message = "OAuth error") {
61
73
  super(message, 400, "oauth_error");
62
74
  this.name = "OAuthError";
@@ -64,6 +76,87 @@ var OAuthError = class extends BlueprintError {
64
76
  }
65
77
  };
66
78
 
79
+ // src/oauth.ts
80
+ import { randomBytes } from "crypto";
81
+ function buildAuthorizeUrl(options) {
82
+ const state = options.state ?? randomBytes(16).toString("hex");
83
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
84
+ const url = new URL(`${baseUrl}/oauth/authorize`);
85
+ url.searchParams.set("client_id", options.clientId);
86
+ url.searchParams.set("redirect_uri", options.redirectUri);
87
+ url.searchParams.set("scope", options.scopes.join(" "));
88
+ url.searchParams.set("response_type", "code");
89
+ url.searchParams.set("state", state);
90
+ return url.toString();
91
+ }
92
+ async function exchangeCode(options) {
93
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
94
+ const url = `${baseUrl}/oauth/token`;
95
+ const body = new URLSearchParams({
96
+ grant_type: "authorization_code",
97
+ client_id: options.clientId,
98
+ client_secret: options.clientSecret,
99
+ code: options.code,
100
+ redirect_uri: options.redirectUri
101
+ });
102
+ const response = await fetch(url, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
105
+ body: body.toString()
106
+ });
107
+ if (!response.ok) {
108
+ const errorBody = await response.json().catch(() => ({}));
109
+ const errorCode = errorBody["error"] ?? "unknown_error";
110
+ const errorDescription = errorBody["error_description"] ?? "Token exchange failed";
111
+ throw new OAuthError(errorCode, errorDescription);
112
+ }
113
+ return await response.json();
114
+ }
115
+ async function refreshAccessToken(options) {
116
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
117
+ const url = `${baseUrl}/oauth/token`;
118
+ const body = new URLSearchParams({
119
+ grant_type: "refresh_token",
120
+ client_id: options.clientId,
121
+ client_secret: options.clientSecret,
122
+ refresh_token: options.refreshToken
123
+ });
124
+ const response = await fetch(url, {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
127
+ body: body.toString()
128
+ });
129
+ if (!response.ok) {
130
+ const errorBody = await response.json().catch(() => ({}));
131
+ const errorCode = errorBody["error"] ?? "unknown_error";
132
+ const errorDescription = errorBody["error_description"] ?? "Token refresh failed";
133
+ throw new OAuthError(errorCode, errorDescription);
134
+ }
135
+ return await response.json();
136
+ }
137
+ async function clientCredentialsGrant(options) {
138
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
139
+ const url = `${baseUrl}/oauth/token`;
140
+ const body = new URLSearchParams({
141
+ grant_type: "client_credentials",
142
+ client_id: options.clientId,
143
+ client_secret: options.clientSecret,
144
+ scope: options.scopes.join(" ")
145
+ });
146
+ const response = await fetch(url, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
149
+ body: body.toString()
150
+ });
151
+ if (!response.ok) {
152
+ const errorBody = await response.json().catch(() => ({}));
153
+ const errorCode = errorBody["error"] ?? "unknown_error";
154
+ const errorDescription = errorBody["error_description"] ?? "Client credentials grant failed";
155
+ throw new OAuthError(errorCode, errorDescription);
156
+ }
157
+ return await response.json();
158
+ }
159
+
67
160
  // src/http.ts
68
161
  var HttpClient = class {
69
162
  constructor(config) {
@@ -73,24 +166,35 @@ var HttpClient = class {
73
166
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
74
167
  this.retryOnRateLimit = config.retryOnRateLimit !== false;
75
168
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
169
+ if (config.clientId && config.clientSecret) {
170
+ this.oauth = {
171
+ clientId: config.clientId,
172
+ clientSecret: config.clientSecret,
173
+ tokenStorage: config.tokenStorage ?? new MemoryTokenStorage(),
174
+ skewMs: config.tokenRefreshSkewMs ?? DEFAULT_TOKEN_REFRESH_SKEW_MS,
175
+ scopes: config.scopes
176
+ };
177
+ }
76
178
  }
77
179
  async request(options) {
78
180
  const url = this.buildUrl(options.path, options.params);
79
- const headers = {
80
- "Content-Type": "application/json",
81
- ...this.getAuthHeaders(),
82
- ...options.headers
83
- };
84
- const fetchOptions = {
85
- method: options.method,
86
- headers,
87
- signal: AbortSignal.timeout(this.timeout)
88
- };
89
- if (options.body !== void 0) {
90
- fetchOptions.body = JSON.stringify(options.body);
91
- }
181
+ const bodyStr = options.body !== void 0 ? JSON.stringify(options.body) : void 0;
92
182
  let lastError;
183
+ let didRefreshOn401 = false;
93
184
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
185
+ const headers = {
186
+ "Content-Type": "application/json",
187
+ ...await this.resolveAuthHeaders(),
188
+ ...options.headers
189
+ };
190
+ const fetchOptions = {
191
+ method: options.method,
192
+ headers,
193
+ signal: AbortSignal.timeout(this.timeout)
194
+ };
195
+ if (bodyStr !== void 0) {
196
+ fetchOptions.body = bodyStr;
197
+ }
94
198
  const response = await fetch(url, fetchOptions);
95
199
  if (response.ok) {
96
200
  if (response.status === 204) {
@@ -103,6 +207,12 @@ var HttpClient = class {
103
207
  await this.sleep(retryAfter * 1e3);
104
208
  continue;
105
209
  }
210
+ if (response.status === 401 && this.oauth && !didRefreshOn401) {
211
+ didRefreshOn401 = true;
212
+ await this.refreshTokens();
213
+ attempt--;
214
+ continue;
215
+ }
106
216
  lastError = await this.mapError(response);
107
217
  if (response.status !== 429 || !this.retryOnRateLimit) {
108
218
  throw lastError;
@@ -119,6 +229,76 @@ var HttpClient = class {
119
229
  }
120
230
  return {};
121
231
  }
232
+ /**
233
+ * Resolve the Authorization header for the current request. For the static
234
+ * accessToken/apiKey modes this is {@link getAuthHeaders}. For the
235
+ * OAuth-managed path it reads the stored token, proactively refreshing when
236
+ * the token is within the skew window of expiry.
237
+ */
238
+ async resolveAuthHeaders() {
239
+ if (!this.oauth) {
240
+ return this.getAuthHeaders();
241
+ }
242
+ const tokens = await this.oauth.tokenStorage.getTokens();
243
+ if (!tokens) {
244
+ return {};
245
+ }
246
+ if (tokens.expiresAt - Date.now() <= this.oauth.skewMs) {
247
+ const refreshed = await this.refreshTokens();
248
+ return { Authorization: `Bearer ${refreshed.accessToken}` };
249
+ }
250
+ return { Authorization: `Bearer ${tokens.accessToken}` };
251
+ }
252
+ /**
253
+ * Refresh the OAuth access token via `refreshAccessToken` and persist the new
254
+ * tokens through the configured {@link TokenStorage}. Concurrent callers share
255
+ * a single in-flight refresh.
256
+ */
257
+ async refreshTokens() {
258
+ if (!this.oauth) {
259
+ throw new AuthenticationError("Token refresh requires the OAuth clientId/clientSecret path");
260
+ }
261
+ if (this.refreshInFlight) {
262
+ return this.refreshInFlight;
263
+ }
264
+ const oauth = this.oauth;
265
+ this.refreshInFlight = (async () => {
266
+ const current = await oauth.tokenStorage.getTokens();
267
+ if (!current?.refreshToken) {
268
+ const response2 = await clientCredentialsGrant({
269
+ baseUrl: this.baseUrl,
270
+ clientId: oauth.clientId,
271
+ clientSecret: oauth.clientSecret,
272
+ scopes: oauth.scopes ?? []
273
+ });
274
+ const next2 = {
275
+ accessToken: response2.access_token,
276
+ refreshToken: response2.refresh_token ?? null,
277
+ expiresAt: Date.now() + response2.expires_in * 1e3
278
+ };
279
+ await oauth.tokenStorage.setTokens(next2);
280
+ return next2;
281
+ }
282
+ const response = await refreshAccessToken({
283
+ baseUrl: this.baseUrl,
284
+ clientId: oauth.clientId,
285
+ clientSecret: oauth.clientSecret,
286
+ refreshToken: current.refreshToken
287
+ });
288
+ const next = {
289
+ accessToken: response.access_token,
290
+ refreshToken: response.refresh_token || current.refreshToken,
291
+ expiresAt: Date.now() + response.expires_in * 1e3
292
+ };
293
+ await oauth.tokenStorage.setTokens(next);
294
+ return next;
295
+ })();
296
+ try {
297
+ return await this.refreshInFlight;
298
+ } finally {
299
+ this.refreshInFlight = void 0;
300
+ }
301
+ }
122
302
  buildUrl(path, params) {
123
303
  const url = new URL(`${this.baseUrl}${path}`);
124
304
  if (params) {
@@ -166,7 +346,7 @@ var HttpClient = class {
166
346
  if (response.status >= 500) {
167
347
  return new ServerError(message);
168
348
  }
169
- return new BlueprintError(message, response.status, "unknown_error");
349
+ return new PlanesError(message, response.status, "unknown_error");
170
350
  }
171
351
  }
172
352
  sleep(ms) {
@@ -293,14 +473,207 @@ var InventoryResource = class extends BaseResource {
293
473
  }
294
474
  };
295
475
 
476
+ // src/resources/contacts.ts
477
+ var ContactChannelResource = class {
478
+ constructor(http2, contactId) {
479
+ this.http = http2;
480
+ this.basePath = `/contacts/${contactId}/channels`;
481
+ }
482
+ /** Returns `{ channel, duplicateOf }` — `duplicateOf` is the dedup signal. */
483
+ async create(data) {
484
+ return this.http.request({
485
+ method: "POST",
486
+ path: this.basePath,
487
+ body: data
488
+ });
489
+ }
490
+ async update(channelId, data) {
491
+ return this.http.request({
492
+ method: "PATCH",
493
+ path: `${this.basePath}/${channelId}`,
494
+ body: data
495
+ });
496
+ }
497
+ async delete(channelId) {
498
+ await this.http.request({
499
+ method: "DELETE",
500
+ path: `${this.basePath}/${channelId}`
501
+ });
502
+ }
503
+ };
504
+ var ContactResource = class extends BaseResource {
505
+ constructor(http2) {
506
+ super(http2, "/contacts");
507
+ }
508
+ async merge(winnerId, loserId) {
509
+ return this.http.request({
510
+ method: "POST",
511
+ path: `/contacts/${winnerId}/merge/${loserId}`
512
+ });
513
+ }
514
+ channels(contactId) {
515
+ return new ContactChannelResource(this.http, contactId);
516
+ }
517
+ };
518
+
519
+ // src/resources/activities.ts
520
+ var ActivityResource = class {
521
+ constructor(http2) {
522
+ this.http = http2;
523
+ }
524
+ async create(data) {
525
+ return this.http.request({
526
+ method: "POST",
527
+ path: "/activities",
528
+ body: data
529
+ });
530
+ }
531
+ async list(params) {
532
+ return this.http.request({
533
+ method: "GET",
534
+ path: "/activities",
535
+ params
536
+ });
537
+ }
538
+ async listForEntity(entityType, entityId, params) {
539
+ return this.http.request({
540
+ method: "GET",
541
+ path: `/${entityType}/${entityId}/activities`,
542
+ params
543
+ });
544
+ }
545
+ };
546
+
547
+ // src/resources/tags.ts
548
+ var TagResource = class extends BaseResource {
549
+ constructor(http2) {
550
+ super(http2, "/tags");
551
+ }
552
+ /** `TagsService.setEntityTags()` returns the final tag set wrapped in `{ tags }`. */
553
+ async setEntityTags(entityType, entityId, data) {
554
+ return this.http.request({
555
+ method: "PUT",
556
+ path: `/entities/${entityType}/${entityId}/tags`,
557
+ body: data
558
+ });
559
+ }
560
+ async getEntityTags(entityType, entityId) {
561
+ return this.http.request({
562
+ method: "GET",
563
+ path: `/entities/${entityType}/${entityId}/tags`
564
+ });
565
+ }
566
+ async getEntities(tagId, params) {
567
+ return this.http.request({
568
+ method: "GET",
569
+ path: `/tags/${tagId}/entities`,
570
+ params
571
+ });
572
+ }
573
+ };
574
+
575
+ // src/webhooks.ts
576
+ import { createHmac, timingSafeEqual } from "crypto";
577
+ function verifyWebhookSignature(payload, signature, secret) {
578
+ if (!signature || !secret || !payload) {
579
+ return false;
580
+ }
581
+ const prefix = "sha256=";
582
+ if (!signature.startsWith(prefix)) {
583
+ return false;
584
+ }
585
+ const receivedHex = signature.slice(prefix.length);
586
+ if (!receivedHex) {
587
+ return false;
588
+ }
589
+ const expectedHmac = createHmac("sha256", secret).update(payload).digest("hex");
590
+ const receivedBuf = Buffer.from(receivedHex, "hex");
591
+ const expectedBuf = Buffer.from(expectedHmac, "hex");
592
+ if (receivedBuf.length !== expectedBuf.length) {
593
+ return false;
594
+ }
595
+ return timingSafeEqual(receivedBuf, expectedBuf);
596
+ }
597
+ function parseWebhookEvent(body, signature, secret) {
598
+ if (!verifyWebhookSignature(body, signature, secret)) {
599
+ throw new WebhookVerificationError();
600
+ }
601
+ return JSON.parse(body);
602
+ }
603
+
604
+ // src/webhook-router.ts
605
+ var WebhookRouter = class {
606
+ constructor() {
607
+ /** pattern (composite key / entity wildcard / '*') → registered handlers. */
608
+ this.handlers = /* @__PURE__ */ new Map();
609
+ }
610
+ /**
611
+ * Register a handler for an event pattern.
612
+ *
613
+ * @param pattern - `"{entity_type}.{event_type}"` (e.g. `opportunity.created`),
614
+ * an entity wildcard `"{entity_type}.*"` (e.g. `opportunity.*`), or the
615
+ * global wildcard `"*"`. Matching is exact-string against the derived key;
616
+ * see the class docs for the dotted-`event_type` shape.
617
+ * @param handler - Invoked with the full {@link WebhookDelivery} on a match.
618
+ * @returns `this`, for chaining.
619
+ */
620
+ on(pattern, handler) {
621
+ const existing = this.handlers.get(pattern);
622
+ if (existing) {
623
+ existing.push(handler);
624
+ } else {
625
+ this.handlers.set(pattern, [handler]);
626
+ }
627
+ return this;
628
+ }
629
+ /**
630
+ * Verify, parse, and dispatch a webhook delivery.
631
+ *
632
+ * The signature is verified first: on failure this throws
633
+ * {@link WebhookVerificationError} and dispatches nothing. On success it
634
+ * dispatches to every matching handler — specific key, then entity wildcard,
635
+ * then global — awaiting async handlers before resolving.
636
+ *
637
+ * @param input - The raw `{ body, signature, secret }`.
638
+ * @returns The parsed {@link WebhookDelivery}.
639
+ * @throws WebhookVerificationError if the signature is invalid.
640
+ */
641
+ async process(input) {
642
+ const delivery = parseWebhookEvent(input.body, input.signature, input.secret);
643
+ const { entity_type, event_type } = delivery.event;
644
+ const patterns = [`${entity_type}.${event_type}`, `${entity_type}.*`, "*"];
645
+ for (const pattern of patterns) {
646
+ const matched = this.handlers.get(pattern);
647
+ if (!matched) continue;
648
+ for (const handler of matched) {
649
+ await handler(delivery);
650
+ }
651
+ }
652
+ return delivery;
653
+ }
654
+ };
655
+
296
656
  // src/client.ts
297
- var BlueprintClient = class {
657
+ var PlanesClient = class {
298
658
  constructor(config) {
299
- if (!config.accessToken && !config.apiKey) {
300
- throw new Error("BlueprintClient requires either accessToken or apiKey");
659
+ const hasOAuthCreds = Boolean(config.clientId) || Boolean(config.clientSecret);
660
+ const modes = [
661
+ config.accessToken ? "accessToken" : null,
662
+ config.apiKey ? "apiKey" : null,
663
+ hasOAuthCreds ? "clientId+clientSecret" : null
664
+ ].filter((m) => m !== null);
665
+ if (modes.length === 0) {
666
+ throw new Error(
667
+ "PlanesClient requires one auth mode: accessToken, apiKey, or clientId + clientSecret"
668
+ );
669
+ }
670
+ if (modes.length > 1) {
671
+ throw new Error(
672
+ `PlanesClient requires exactly one auth mode, not multiple (got: ${modes.join(", ")})`
673
+ );
301
674
  }
302
- if (config.accessToken && config.apiKey) {
303
- throw new Error("BlueprintClient requires either accessToken or apiKey, not both");
675
+ if (hasOAuthCreds && (!config.clientId || !config.clientSecret)) {
676
+ throw new Error("PlanesClient OAuth mode requires both clientId and clientSecret");
304
677
  }
305
678
  this.http = new HttpClient(config);
306
679
  }
@@ -358,17 +731,134 @@ var BlueprintClient = class {
358
731
  }
359
732
  return this._inventory;
360
733
  }
734
+ get contacts() {
735
+ if (!this._contacts) {
736
+ this._contacts = new ContactResource(this.http);
737
+ }
738
+ return this._contacts;
739
+ }
740
+ get activities() {
741
+ if (!this._activities) {
742
+ this._activities = new ActivityResource(this.http);
743
+ }
744
+ return this._activities;
745
+ }
746
+ get tags() {
747
+ if (!this._tags) {
748
+ this._tags = new TagResource(this.http);
749
+ }
750
+ return this._tags;
751
+ }
752
+ /**
753
+ * Ergonomic webhook event router — `client.webhooks.on(pattern, handler)`
754
+ * plus `client.webhooks.process({ body, signature, secret })`. Auth-independent
755
+ * (the signing secret is supplied per `.process()` call), so it is created
756
+ * lazily without the HTTP client. See {@link WebhookRouter}.
757
+ */
758
+ get webhooks() {
759
+ if (!this._webhooks) {
760
+ this._webhooks = new WebhookRouter();
761
+ }
762
+ return this._webhooks;
763
+ }
361
764
  };
362
765
 
363
766
  // src/agent.ts
364
767
  import * as http from "http";
365
- var Blueprint = {
768
+ function createActionServer(handlerMap, options) {
769
+ const port = options?.port ?? 3e3;
770
+ const basePath = options?.path ?? "/actions";
771
+ const onError = options?.onError;
772
+ const server = http.createServer(async (req, res) => {
773
+ if (req.method !== "POST" || req.url !== basePath) {
774
+ res.writeHead(404, { "Content-Type": "application/json" });
775
+ res.end(JSON.stringify({ error: "Not found" }));
776
+ return;
777
+ }
778
+ try {
779
+ const body = await readBody(req);
780
+ let envelope;
781
+ try {
782
+ envelope = JSON.parse(body);
783
+ } catch {
784
+ res.writeHead(400, { "Content-Type": "application/json" });
785
+ res.end(JSON.stringify({ error: "Malformed request body" }));
786
+ return;
787
+ }
788
+ const wire = envelope?.action_context;
789
+ const actionType = wire?.action_type;
790
+ if (!wire || !actionType) {
791
+ res.writeHead(400, { "Content-Type": "application/json" });
792
+ res.end(
793
+ JSON.stringify({ error: "Missing action_context.action_type in request body" })
794
+ );
795
+ return;
796
+ }
797
+ const handler = handlerMap.get(actionType);
798
+ if (!handler) {
799
+ res.writeHead(404, { "Content-Type": "application/json" });
800
+ res.end(
801
+ JSON.stringify({
802
+ error: `No handler registered for action type: ${actionType}`
803
+ })
804
+ );
805
+ return;
806
+ }
807
+ const requestIdHeader = req.headers["x-planes-request-id"];
808
+ const requestId = Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader;
809
+ const context = {
810
+ action_id: wire.action_id,
811
+ action_type: actionType,
812
+ entity_type: wire.entity_type,
813
+ entity_id: wire.entity_id,
814
+ tenant_id: wire.tenant_id,
815
+ data: wire.data ?? {},
816
+ stage: wire.stage ?? null,
817
+ metadata: wire.metadata,
818
+ callback_token: envelope.callback_token,
819
+ callback_base_url: envelope.callback_base_url,
820
+ // Only set request_id when the header was present (exactOptionalPropertyTypes).
821
+ ...requestId !== void 0 ? { request_id: requestId } : {}
822
+ };
823
+ let result;
824
+ try {
825
+ result = await handler.execute(context);
826
+ } catch (err) {
827
+ const errorMessage = err instanceof Error ? err.message : "Unknown handler error";
828
+ if (onError && err instanceof Error) {
829
+ onError(err);
830
+ }
831
+ result = {
832
+ status: "failed",
833
+ error: errorMessage,
834
+ retryable: false
835
+ };
836
+ }
837
+ res.writeHead(200, { "Content-Type": "application/json" });
838
+ res.end(JSON.stringify(result));
839
+ } catch (err) {
840
+ if (onError && err instanceof Error) {
841
+ onError(err);
842
+ }
843
+ res.writeHead(500, { "Content-Type": "application/json" });
844
+ res.end(JSON.stringify({ error: "Internal server error" }));
845
+ }
846
+ });
847
+ server.listen(port);
848
+ const shutdown = () => {
849
+ server.close();
850
+ };
851
+ process.on("SIGTERM", shutdown);
852
+ process.on("SIGINT", shutdown);
853
+ return server;
854
+ }
855
+ var Planes = {
366
856
  /**
367
857
  * Register action handlers and start an HTTP server to receive action requests.
368
858
  *
369
859
  * @example
370
860
  * ```ts
371
- * Blueprint.register([
861
+ * Planes.register([
372
862
  * {
373
863
  * type: 'send_email',
374
864
  * async execute(ctx) {
@@ -380,77 +870,45 @@ var Blueprint = {
380
870
  * ```
381
871
  */
382
872
  register(handlers, options) {
383
- const port = options?.port ?? 3e3;
384
- const basePath = options?.path ?? "/actions";
385
- const onError = options?.onError;
386
873
  const handlerMap = /* @__PURE__ */ new Map();
387
874
  for (const handler of handlers) {
388
875
  handlerMap.set(handler.type, handler);
389
876
  }
390
- const server = http.createServer(async (req, res) => {
391
- if (req.method !== "POST" || req.url !== basePath) {
392
- res.writeHead(404, { "Content-Type": "application/json" });
393
- res.end(JSON.stringify({ error: "Not found" }));
394
- return;
395
- }
396
- try {
397
- const body = await readBody(req);
398
- let context;
399
- try {
400
- context = JSON.parse(body);
401
- } catch {
402
- res.writeHead(400, { "Content-Type": "application/json" });
403
- res.end(JSON.stringify({ error: "Malformed request body" }));
404
- return;
405
- }
406
- if (!context.action?.type) {
407
- res.writeHead(400, { "Content-Type": "application/json" });
408
- res.end(JSON.stringify({ error: "Missing action.type in request body" }));
409
- return;
410
- }
411
- const handler = handlerMap.get(context.action.type);
412
- if (!handler) {
413
- res.writeHead(404, { "Content-Type": "application/json" });
414
- res.end(
415
- JSON.stringify({
416
- error: `No handler registered for action type: ${context.action.type}`
417
- })
418
- );
419
- return;
420
- }
421
- let result;
422
- try {
423
- result = await handler.execute(context);
424
- } catch (err) {
425
- const errorMessage = err instanceof Error ? err.message : "Unknown handler error";
426
- if (onError && err instanceof Error) {
427
- onError(err);
428
- }
429
- result = {
430
- status: "failed",
431
- error: errorMessage,
432
- retryable: false
433
- };
434
- }
435
- res.writeHead(200, { "Content-Type": "application/json" });
436
- res.end(JSON.stringify(result));
437
- } catch (err) {
438
- if (onError && err instanceof Error) {
439
- onError(err);
440
- }
441
- res.writeHead(500, { "Content-Type": "application/json" });
442
- res.end(JSON.stringify({ error: "Internal server error" }));
443
- }
444
- });
445
- server.listen(port);
446
- const shutdown = () => {
447
- server.close();
448
- };
449
- process.on("SIGTERM", shutdown);
450
- process.on("SIGINT", shutdown);
451
- return server;
877
+ return createActionServer(handlerMap, options);
452
878
  }
453
879
  };
880
+ var AgentApp = class {
881
+ constructor() {
882
+ this.handlers = /* @__PURE__ */ new Map();
883
+ }
884
+ /**
885
+ * Register a handler for an action type. Accepts a bare `execute` function
886
+ * (the caller does not build the `{ type, execute }` object).
887
+ *
888
+ * @param type - The action type this handler responds to.
889
+ * @param execute - The handler function invoked with the {@link ActionContext}.
890
+ * @returns `this`, for chaining.
891
+ */
892
+ registerAction(type, execute) {
893
+ this.handlers.set(type, { type, execute });
894
+ return this;
895
+ }
896
+ /**
897
+ * Start an HTTP server that dispatches incoming action requests to the
898
+ * registered handlers. Delegates to the same server factory as
899
+ * {@link Planes.register}.
900
+ *
901
+ * The handler set is snapshotted at call time, so registering further actions
902
+ * after `listen()` does not mutate the running server (parity with
903
+ * `Planes.register`, which snapshots its array).
904
+ */
905
+ listen(options) {
906
+ return createActionServer(new Map(this.handlers), options);
907
+ }
908
+ };
909
+ function createAgentApp() {
910
+ return new AgentApp();
911
+ }
454
912
  function readBody(req) {
455
913
  return new Promise((resolve, reject) => {
456
914
  const chunks = [];
@@ -459,120 +917,42 @@ function readBody(req) {
459
917
  req.on("error", reject);
460
918
  });
461
919
  }
462
-
463
- // src/webhooks.ts
464
- import { createHmac, timingSafeEqual } from "crypto";
465
- function verifyWebhookSignature(payload, signature, secret) {
466
- if (!signature || !secret || !payload) {
467
- return false;
468
- }
469
- const prefix = "sha256=";
470
- if (!signature.startsWith(prefix)) {
471
- return false;
472
- }
473
- const receivedHex = signature.slice(prefix.length);
474
- if (!receivedHex) {
475
- return false;
476
- }
477
- const expectedHmac = createHmac("sha256", secret).update(payload).digest("hex");
478
- const receivedBuf = Buffer.from(receivedHex, "hex");
479
- const expectedBuf = Buffer.from(expectedHmac, "hex");
480
- if (receivedBuf.length !== expectedBuf.length) {
481
- return false;
482
- }
483
- return timingSafeEqual(receivedBuf, expectedBuf);
484
- }
485
- function parseWebhookEvent(body, signature, secret) {
486
- if (!verifyWebhookSignature(body, signature, secret)) {
487
- throw new WebhookVerificationError();
488
- }
489
- return JSON.parse(body);
490
- }
491
-
492
- // src/oauth.ts
493
- import { randomBytes } from "crypto";
494
- function buildAuthorizeUrl(options) {
495
- const state = options.state ?? randomBytes(16).toString("hex");
496
- const baseUrl = options.baseUrl.replace(/\/+$/, "");
497
- const url = new URL(`${baseUrl}/oauth/authorize`);
498
- url.searchParams.set("client_id", options.clientId);
499
- url.searchParams.set("redirect_uri", options.redirectUri);
500
- url.searchParams.set("scope", options.scopes.join(" "));
501
- url.searchParams.set("response_type", "code");
502
- url.searchParams.set("state", state);
503
- return url.toString();
504
- }
505
- async function exchangeCode(options) {
506
- const baseUrl = options.baseUrl.replace(/\/+$/, "");
507
- const url = `${baseUrl}/oauth/token`;
508
- const body = new URLSearchParams({
509
- grant_type: "authorization_code",
510
- client_id: options.clientId,
511
- client_secret: options.clientSecret,
512
- code: options.code,
513
- redirect_uri: options.redirectUri
514
- });
515
- const response = await fetch(url, {
516
- method: "POST",
517
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
518
- body: body.toString()
519
- });
520
- if (!response.ok) {
521
- const errorBody = await response.json().catch(() => ({}));
522
- const errorCode = errorBody["error"] ?? "unknown_error";
523
- const errorDescription = errorBody["error_description"] ?? "Token exchange failed";
524
- throw new OAuthError(errorCode, errorDescription);
525
- }
526
- return await response.json();
527
- }
528
- async function refreshAccessToken(options) {
529
- const baseUrl = options.baseUrl.replace(/\/+$/, "");
530
- const url = `${baseUrl}/oauth/token`;
531
- const body = new URLSearchParams({
532
- grant_type: "refresh_token",
533
- client_id: options.clientId,
534
- client_secret: options.clientSecret,
535
- refresh_token: options.refreshToken
536
- });
537
- const response = await fetch(url, {
538
- method: "POST",
539
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
540
- body: body.toString()
541
- });
542
- if (!response.ok) {
543
- const errorBody = await response.json().catch(() => ({}));
544
- const errorCode = errorBody["error"] ?? "unknown_error";
545
- const errorDescription = errorBody["error_description"] ?? "Token refresh failed";
546
- throw new OAuthError(errorCode, errorDescription);
547
- }
548
- return await response.json();
549
- }
550
920
  export {
921
+ ActivityResource,
922
+ AgentApp,
551
923
  AuthenticationError,
552
924
  AuthorizationError,
553
925
  BaseResource,
554
- Blueprint,
555
- BlueprintClient,
556
- BlueprintError,
557
926
  CalendarEventResource,
927
+ ContactChannelResource,
928
+ ContactResource,
558
929
  ContractResource,
559
930
  CustomerResource,
560
931
  DEFAULT_BASE_URL,
561
932
  DEFAULT_MAX_RETRIES,
562
933
  DEFAULT_TIMEOUT,
934
+ DEFAULT_TOKEN_REFRESH_SKEW_MS,
563
935
  EstimateResource,
564
936
  InventoryResource,
565
937
  InvoiceResource,
566
938
  JobResource,
939
+ MemoryTokenStorage,
567
940
  NotFoundError,
568
941
  OAuthError,
569
942
  OpportunityResource,
943
+ Planes,
944
+ PlanesClient,
945
+ PlanesError,
570
946
  PropertyResource,
571
947
  RateLimitError,
572
948
  ServerError,
949
+ TagResource,
573
950
  ValidationError,
951
+ WebhookRouter,
574
952
  WebhookVerificationError,
575
953
  buildAuthorizeUrl,
954
+ clientCredentialsGrant,
955
+ createAgentApp,
576
956
  exchangeCode,
577
957
  parseWebhookEvent,
578
958
  refreshAccessToken,