@planescrm/sdk 0.2.0 → 0.3.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.d.mts +651 -104
- package/dist/index.d.ts +651 -104
- package/dist/index.js +543 -190
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +531 -187
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -2
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
|
|
19
|
+
var PlanesError = class extends Error {
|
|
8
20
|
constructor(message, status, code) {
|
|
9
21
|
super(message);
|
|
10
|
-
this.name = "
|
|
22
|
+
this.name = "PlanesError";
|
|
11
23
|
this.status = status;
|
|
12
24
|
this.code = code;
|
|
13
25
|
}
|
|
14
26
|
};
|
|
15
|
-
var AuthenticationError = class extends
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,65 @@ 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
|
+
|
|
67
138
|
// src/http.ts
|
|
68
139
|
var HttpClient = class {
|
|
69
140
|
constructor(config) {
|
|
@@ -73,24 +144,34 @@ var HttpClient = class {
|
|
|
73
144
|
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
74
145
|
this.retryOnRateLimit = config.retryOnRateLimit !== false;
|
|
75
146
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
147
|
+
if (config.clientId && config.clientSecret) {
|
|
148
|
+
this.oauth = {
|
|
149
|
+
clientId: config.clientId,
|
|
150
|
+
clientSecret: config.clientSecret,
|
|
151
|
+
tokenStorage: config.tokenStorage ?? new MemoryTokenStorage(),
|
|
152
|
+
skewMs: config.tokenRefreshSkewMs ?? DEFAULT_TOKEN_REFRESH_SKEW_MS
|
|
153
|
+
};
|
|
154
|
+
}
|
|
76
155
|
}
|
|
77
156
|
async request(options) {
|
|
78
157
|
const url = this.buildUrl(options.path, options.params);
|
|
79
|
-
const
|
|
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
|
-
}
|
|
158
|
+
const bodyStr = options.body !== void 0 ? JSON.stringify(options.body) : void 0;
|
|
92
159
|
let lastError;
|
|
160
|
+
let didRefreshOn401 = false;
|
|
93
161
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
162
|
+
const headers = {
|
|
163
|
+
"Content-Type": "application/json",
|
|
164
|
+
...await this.resolveAuthHeaders(),
|
|
165
|
+
...options.headers
|
|
166
|
+
};
|
|
167
|
+
const fetchOptions = {
|
|
168
|
+
method: options.method,
|
|
169
|
+
headers,
|
|
170
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
171
|
+
};
|
|
172
|
+
if (bodyStr !== void 0) {
|
|
173
|
+
fetchOptions.body = bodyStr;
|
|
174
|
+
}
|
|
94
175
|
const response = await fetch(url, fetchOptions);
|
|
95
176
|
if (response.ok) {
|
|
96
177
|
if (response.status === 204) {
|
|
@@ -103,6 +184,12 @@ var HttpClient = class {
|
|
|
103
184
|
await this.sleep(retryAfter * 1e3);
|
|
104
185
|
continue;
|
|
105
186
|
}
|
|
187
|
+
if (response.status === 401 && this.oauth && !didRefreshOn401) {
|
|
188
|
+
didRefreshOn401 = true;
|
|
189
|
+
await this.refreshTokens();
|
|
190
|
+
attempt--;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
106
193
|
lastError = await this.mapError(response);
|
|
107
194
|
if (response.status !== 429 || !this.retryOnRateLimit) {
|
|
108
195
|
throw lastError;
|
|
@@ -119,6 +206,64 @@ var HttpClient = class {
|
|
|
119
206
|
}
|
|
120
207
|
return {};
|
|
121
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve the Authorization header for the current request. For the static
|
|
211
|
+
* accessToken/apiKey modes this is {@link getAuthHeaders}. For the
|
|
212
|
+
* OAuth-managed path it reads the stored token, proactively refreshing when
|
|
213
|
+
* the token is within the skew window of expiry.
|
|
214
|
+
*/
|
|
215
|
+
async resolveAuthHeaders() {
|
|
216
|
+
if (!this.oauth) {
|
|
217
|
+
return this.getAuthHeaders();
|
|
218
|
+
}
|
|
219
|
+
const tokens = await this.oauth.tokenStorage.getTokens();
|
|
220
|
+
if (!tokens) {
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
if (tokens.expiresAt - Date.now() <= this.oauth.skewMs) {
|
|
224
|
+
const refreshed = await this.refreshTokens();
|
|
225
|
+
return { Authorization: `Bearer ${refreshed.accessToken}` };
|
|
226
|
+
}
|
|
227
|
+
return { Authorization: `Bearer ${tokens.accessToken}` };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Refresh the OAuth access token via `refreshAccessToken` and persist the new
|
|
231
|
+
* tokens through the configured {@link TokenStorage}. Concurrent callers share
|
|
232
|
+
* a single in-flight refresh.
|
|
233
|
+
*/
|
|
234
|
+
async refreshTokens() {
|
|
235
|
+
if (!this.oauth) {
|
|
236
|
+
throw new AuthenticationError("Token refresh requires the OAuth clientId/clientSecret path");
|
|
237
|
+
}
|
|
238
|
+
if (this.refreshInFlight) {
|
|
239
|
+
return this.refreshInFlight;
|
|
240
|
+
}
|
|
241
|
+
const oauth = this.oauth;
|
|
242
|
+
this.refreshInFlight = (async () => {
|
|
243
|
+
const current = await oauth.tokenStorage.getTokens();
|
|
244
|
+
if (!current?.refreshToken) {
|
|
245
|
+
throw new AuthenticationError("No refresh token available to refresh the access token");
|
|
246
|
+
}
|
|
247
|
+
const response = await refreshAccessToken({
|
|
248
|
+
baseUrl: this.baseUrl,
|
|
249
|
+
clientId: oauth.clientId,
|
|
250
|
+
clientSecret: oauth.clientSecret,
|
|
251
|
+
refreshToken: current.refreshToken
|
|
252
|
+
});
|
|
253
|
+
const next = {
|
|
254
|
+
accessToken: response.access_token,
|
|
255
|
+
refreshToken: response.refresh_token || current.refreshToken,
|
|
256
|
+
expiresAt: Date.now() + response.expires_in * 1e3
|
|
257
|
+
};
|
|
258
|
+
await oauth.tokenStorage.setTokens(next);
|
|
259
|
+
return next;
|
|
260
|
+
})();
|
|
261
|
+
try {
|
|
262
|
+
return await this.refreshInFlight;
|
|
263
|
+
} finally {
|
|
264
|
+
this.refreshInFlight = void 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
122
267
|
buildUrl(path, params) {
|
|
123
268
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
124
269
|
if (params) {
|
|
@@ -166,7 +311,7 @@ var HttpClient = class {
|
|
|
166
311
|
if (response.status >= 500) {
|
|
167
312
|
return new ServerError(message);
|
|
168
313
|
}
|
|
169
|
-
return new
|
|
314
|
+
return new PlanesError(message, response.status, "unknown_error");
|
|
170
315
|
}
|
|
171
316
|
}
|
|
172
317
|
sleep(ms) {
|
|
@@ -293,14 +438,207 @@ var InventoryResource = class extends BaseResource {
|
|
|
293
438
|
}
|
|
294
439
|
};
|
|
295
440
|
|
|
441
|
+
// src/resources/contacts.ts
|
|
442
|
+
var ContactChannelResource = class {
|
|
443
|
+
constructor(http2, contactId) {
|
|
444
|
+
this.http = http2;
|
|
445
|
+
this.basePath = `/contacts/${contactId}/channels`;
|
|
446
|
+
}
|
|
447
|
+
/** Returns `{ channel, duplicateOf }` — `duplicateOf` is the dedup signal. */
|
|
448
|
+
async create(data) {
|
|
449
|
+
return this.http.request({
|
|
450
|
+
method: "POST",
|
|
451
|
+
path: this.basePath,
|
|
452
|
+
body: data
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async update(channelId, data) {
|
|
456
|
+
return this.http.request({
|
|
457
|
+
method: "PATCH",
|
|
458
|
+
path: `${this.basePath}/${channelId}`,
|
|
459
|
+
body: data
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async delete(channelId) {
|
|
463
|
+
await this.http.request({
|
|
464
|
+
method: "DELETE",
|
|
465
|
+
path: `${this.basePath}/${channelId}`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var ContactResource = class extends BaseResource {
|
|
470
|
+
constructor(http2) {
|
|
471
|
+
super(http2, "/contacts");
|
|
472
|
+
}
|
|
473
|
+
async merge(winnerId, loserId) {
|
|
474
|
+
return this.http.request({
|
|
475
|
+
method: "POST",
|
|
476
|
+
path: `/contacts/${winnerId}/merge/${loserId}`
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
channels(contactId) {
|
|
480
|
+
return new ContactChannelResource(this.http, contactId);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/resources/activities.ts
|
|
485
|
+
var ActivityResource = class {
|
|
486
|
+
constructor(http2) {
|
|
487
|
+
this.http = http2;
|
|
488
|
+
}
|
|
489
|
+
async create(data) {
|
|
490
|
+
return this.http.request({
|
|
491
|
+
method: "POST",
|
|
492
|
+
path: "/activities",
|
|
493
|
+
body: data
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
async list(params) {
|
|
497
|
+
return this.http.request({
|
|
498
|
+
method: "GET",
|
|
499
|
+
path: "/activities",
|
|
500
|
+
params
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
async listForEntity(entityType, entityId, params) {
|
|
504
|
+
return this.http.request({
|
|
505
|
+
method: "GET",
|
|
506
|
+
path: `/${entityType}/${entityId}/activities`,
|
|
507
|
+
params
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/resources/tags.ts
|
|
513
|
+
var TagResource = class extends BaseResource {
|
|
514
|
+
constructor(http2) {
|
|
515
|
+
super(http2, "/tags");
|
|
516
|
+
}
|
|
517
|
+
/** `TagsService.setEntityTags()` returns the final tag set wrapped in `{ tags }`. */
|
|
518
|
+
async setEntityTags(entityType, entityId, data) {
|
|
519
|
+
return this.http.request({
|
|
520
|
+
method: "PUT",
|
|
521
|
+
path: `/entities/${entityType}/${entityId}/tags`,
|
|
522
|
+
body: data
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async getEntityTags(entityType, entityId) {
|
|
526
|
+
return this.http.request({
|
|
527
|
+
method: "GET",
|
|
528
|
+
path: `/entities/${entityType}/${entityId}/tags`
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async getEntities(tagId, params) {
|
|
532
|
+
return this.http.request({
|
|
533
|
+
method: "GET",
|
|
534
|
+
path: `/tags/${tagId}/entities`,
|
|
535
|
+
params
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/webhooks.ts
|
|
541
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
542
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
543
|
+
if (!signature || !secret || !payload) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
const prefix = "sha256=";
|
|
547
|
+
if (!signature.startsWith(prefix)) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
const receivedHex = signature.slice(prefix.length);
|
|
551
|
+
if (!receivedHex) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
const expectedHmac = createHmac("sha256", secret).update(payload).digest("hex");
|
|
555
|
+
const receivedBuf = Buffer.from(receivedHex, "hex");
|
|
556
|
+
const expectedBuf = Buffer.from(expectedHmac, "hex");
|
|
557
|
+
if (receivedBuf.length !== expectedBuf.length) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
return timingSafeEqual(receivedBuf, expectedBuf);
|
|
561
|
+
}
|
|
562
|
+
function parseWebhookEvent(body, signature, secret) {
|
|
563
|
+
if (!verifyWebhookSignature(body, signature, secret)) {
|
|
564
|
+
throw new WebhookVerificationError();
|
|
565
|
+
}
|
|
566
|
+
return JSON.parse(body);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/webhook-router.ts
|
|
570
|
+
var WebhookRouter = class {
|
|
571
|
+
constructor() {
|
|
572
|
+
/** pattern (composite key / entity wildcard / '*') → registered handlers. */
|
|
573
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Register a handler for an event pattern.
|
|
577
|
+
*
|
|
578
|
+
* @param pattern - `"{entity_type}.{event_type}"` (e.g. `opportunity.created`),
|
|
579
|
+
* an entity wildcard `"{entity_type}.*"` (e.g. `opportunity.*`), or the
|
|
580
|
+
* global wildcard `"*"`. Matching is exact-string against the derived key;
|
|
581
|
+
* see the class docs for the dotted-`event_type` shape.
|
|
582
|
+
* @param handler - Invoked with the full {@link WebhookDelivery} on a match.
|
|
583
|
+
* @returns `this`, for chaining.
|
|
584
|
+
*/
|
|
585
|
+
on(pattern, handler) {
|
|
586
|
+
const existing = this.handlers.get(pattern);
|
|
587
|
+
if (existing) {
|
|
588
|
+
existing.push(handler);
|
|
589
|
+
} else {
|
|
590
|
+
this.handlers.set(pattern, [handler]);
|
|
591
|
+
}
|
|
592
|
+
return this;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Verify, parse, and dispatch a webhook delivery.
|
|
596
|
+
*
|
|
597
|
+
* The signature is verified first: on failure this throws
|
|
598
|
+
* {@link WebhookVerificationError} and dispatches nothing. On success it
|
|
599
|
+
* dispatches to every matching handler — specific key, then entity wildcard,
|
|
600
|
+
* then global — awaiting async handlers before resolving.
|
|
601
|
+
*
|
|
602
|
+
* @param input - The raw `{ body, signature, secret }`.
|
|
603
|
+
* @returns The parsed {@link WebhookDelivery}.
|
|
604
|
+
* @throws WebhookVerificationError if the signature is invalid.
|
|
605
|
+
*/
|
|
606
|
+
async process(input) {
|
|
607
|
+
const delivery = parseWebhookEvent(input.body, input.signature, input.secret);
|
|
608
|
+
const { entity_type, event_type } = delivery.event;
|
|
609
|
+
const patterns = [`${entity_type}.${event_type}`, `${entity_type}.*`, "*"];
|
|
610
|
+
for (const pattern of patterns) {
|
|
611
|
+
const matched = this.handlers.get(pattern);
|
|
612
|
+
if (!matched) continue;
|
|
613
|
+
for (const handler of matched) {
|
|
614
|
+
await handler(delivery);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return delivery;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
296
621
|
// src/client.ts
|
|
297
|
-
var
|
|
622
|
+
var PlanesClient = class {
|
|
298
623
|
constructor(config) {
|
|
299
|
-
|
|
300
|
-
|
|
624
|
+
const hasOAuthCreds = Boolean(config.clientId) || Boolean(config.clientSecret);
|
|
625
|
+
const modes = [
|
|
626
|
+
config.accessToken ? "accessToken" : null,
|
|
627
|
+
config.apiKey ? "apiKey" : null,
|
|
628
|
+
hasOAuthCreds ? "clientId+clientSecret" : null
|
|
629
|
+
].filter((m) => m !== null);
|
|
630
|
+
if (modes.length === 0) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
"PlanesClient requires one auth mode: accessToken, apiKey, or clientId + clientSecret"
|
|
633
|
+
);
|
|
301
634
|
}
|
|
302
|
-
if (
|
|
303
|
-
throw new Error(
|
|
635
|
+
if (modes.length > 1) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`PlanesClient requires exactly one auth mode, not multiple (got: ${modes.join(", ")})`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (hasOAuthCreds && (!config.clientId || !config.clientSecret)) {
|
|
641
|
+
throw new Error("PlanesClient OAuth mode requires both clientId and clientSecret");
|
|
304
642
|
}
|
|
305
643
|
this.http = new HttpClient(config);
|
|
306
644
|
}
|
|
@@ -358,17 +696,134 @@ var BlueprintClient = class {
|
|
|
358
696
|
}
|
|
359
697
|
return this._inventory;
|
|
360
698
|
}
|
|
699
|
+
get contacts() {
|
|
700
|
+
if (!this._contacts) {
|
|
701
|
+
this._contacts = new ContactResource(this.http);
|
|
702
|
+
}
|
|
703
|
+
return this._contacts;
|
|
704
|
+
}
|
|
705
|
+
get activities() {
|
|
706
|
+
if (!this._activities) {
|
|
707
|
+
this._activities = new ActivityResource(this.http);
|
|
708
|
+
}
|
|
709
|
+
return this._activities;
|
|
710
|
+
}
|
|
711
|
+
get tags() {
|
|
712
|
+
if (!this._tags) {
|
|
713
|
+
this._tags = new TagResource(this.http);
|
|
714
|
+
}
|
|
715
|
+
return this._tags;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Ergonomic webhook event router — `client.webhooks.on(pattern, handler)`
|
|
719
|
+
* plus `client.webhooks.process({ body, signature, secret })`. Auth-independent
|
|
720
|
+
* (the signing secret is supplied per `.process()` call), so it is created
|
|
721
|
+
* lazily without the HTTP client. See {@link WebhookRouter}.
|
|
722
|
+
*/
|
|
723
|
+
get webhooks() {
|
|
724
|
+
if (!this._webhooks) {
|
|
725
|
+
this._webhooks = new WebhookRouter();
|
|
726
|
+
}
|
|
727
|
+
return this._webhooks;
|
|
728
|
+
}
|
|
361
729
|
};
|
|
362
730
|
|
|
363
731
|
// src/agent.ts
|
|
364
732
|
import * as http from "http";
|
|
365
|
-
|
|
733
|
+
function createActionServer(handlerMap, options) {
|
|
734
|
+
const port = options?.port ?? 3e3;
|
|
735
|
+
const basePath = options?.path ?? "/actions";
|
|
736
|
+
const onError = options?.onError;
|
|
737
|
+
const server = http.createServer(async (req, res) => {
|
|
738
|
+
if (req.method !== "POST" || req.url !== basePath) {
|
|
739
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
740
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const body = await readBody(req);
|
|
745
|
+
let envelope;
|
|
746
|
+
try {
|
|
747
|
+
envelope = JSON.parse(body);
|
|
748
|
+
} catch {
|
|
749
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
750
|
+
res.end(JSON.stringify({ error: "Malformed request body" }));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const wire = envelope?.action_context;
|
|
754
|
+
const actionType = wire?.action_type;
|
|
755
|
+
if (!wire || !actionType) {
|
|
756
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
757
|
+
res.end(
|
|
758
|
+
JSON.stringify({ error: "Missing action_context.action_type in request body" })
|
|
759
|
+
);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const handler = handlerMap.get(actionType);
|
|
763
|
+
if (!handler) {
|
|
764
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
765
|
+
res.end(
|
|
766
|
+
JSON.stringify({
|
|
767
|
+
error: `No handler registered for action type: ${actionType}`
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const requestIdHeader = req.headers["x-planes-request-id"];
|
|
773
|
+
const requestId = Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader;
|
|
774
|
+
const context = {
|
|
775
|
+
action_id: wire.action_id,
|
|
776
|
+
action_type: actionType,
|
|
777
|
+
entity_type: wire.entity_type,
|
|
778
|
+
entity_id: wire.entity_id,
|
|
779
|
+
tenant_id: wire.tenant_id,
|
|
780
|
+
data: wire.data ?? {},
|
|
781
|
+
stage: wire.stage ?? null,
|
|
782
|
+
metadata: wire.metadata,
|
|
783
|
+
callback_token: envelope.callback_token,
|
|
784
|
+
callback_base_url: envelope.callback_base_url,
|
|
785
|
+
// Only set request_id when the header was present (exactOptionalPropertyTypes).
|
|
786
|
+
...requestId !== void 0 ? { request_id: requestId } : {}
|
|
787
|
+
};
|
|
788
|
+
let result;
|
|
789
|
+
try {
|
|
790
|
+
result = await handler.execute(context);
|
|
791
|
+
} catch (err) {
|
|
792
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown handler error";
|
|
793
|
+
if (onError && err instanceof Error) {
|
|
794
|
+
onError(err);
|
|
795
|
+
}
|
|
796
|
+
result = {
|
|
797
|
+
status: "failed",
|
|
798
|
+
error: errorMessage,
|
|
799
|
+
retryable: false
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
803
|
+
res.end(JSON.stringify(result));
|
|
804
|
+
} catch (err) {
|
|
805
|
+
if (onError && err instanceof Error) {
|
|
806
|
+
onError(err);
|
|
807
|
+
}
|
|
808
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
809
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
server.listen(port);
|
|
813
|
+
const shutdown = () => {
|
|
814
|
+
server.close();
|
|
815
|
+
};
|
|
816
|
+
process.on("SIGTERM", shutdown);
|
|
817
|
+
process.on("SIGINT", shutdown);
|
|
818
|
+
return server;
|
|
819
|
+
}
|
|
820
|
+
var Planes = {
|
|
366
821
|
/**
|
|
367
822
|
* Register action handlers and start an HTTP server to receive action requests.
|
|
368
823
|
*
|
|
369
824
|
* @example
|
|
370
825
|
* ```ts
|
|
371
|
-
*
|
|
826
|
+
* Planes.register([
|
|
372
827
|
* {
|
|
373
828
|
* type: 'send_email',
|
|
374
829
|
* async execute(ctx) {
|
|
@@ -380,77 +835,45 @@ var Blueprint = {
|
|
|
380
835
|
* ```
|
|
381
836
|
*/
|
|
382
837
|
register(handlers, options) {
|
|
383
|
-
const port = options?.port ?? 3e3;
|
|
384
|
-
const basePath = options?.path ?? "/actions";
|
|
385
|
-
const onError = options?.onError;
|
|
386
838
|
const handlerMap = /* @__PURE__ */ new Map();
|
|
387
839
|
for (const handler of handlers) {
|
|
388
840
|
handlerMap.set(handler.type, handler);
|
|
389
841
|
}
|
|
390
|
-
|
|
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;
|
|
842
|
+
return createActionServer(handlerMap, options);
|
|
452
843
|
}
|
|
453
844
|
};
|
|
845
|
+
var AgentApp = class {
|
|
846
|
+
constructor() {
|
|
847
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Register a handler for an action type. Accepts a bare `execute` function
|
|
851
|
+
* (the caller does not build the `{ type, execute }` object).
|
|
852
|
+
*
|
|
853
|
+
* @param type - The action type this handler responds to.
|
|
854
|
+
* @param execute - The handler function invoked with the {@link ActionContext}.
|
|
855
|
+
* @returns `this`, for chaining.
|
|
856
|
+
*/
|
|
857
|
+
registerAction(type, execute) {
|
|
858
|
+
this.handlers.set(type, { type, execute });
|
|
859
|
+
return this;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Start an HTTP server that dispatches incoming action requests to the
|
|
863
|
+
* registered handlers. Delegates to the same server factory as
|
|
864
|
+
* {@link Planes.register}.
|
|
865
|
+
*
|
|
866
|
+
* The handler set is snapshotted at call time, so registering further actions
|
|
867
|
+
* after `listen()` does not mutate the running server (parity with
|
|
868
|
+
* `Planes.register`, which snapshots its array).
|
|
869
|
+
*/
|
|
870
|
+
listen(options) {
|
|
871
|
+
return createActionServer(new Map(this.handlers), options);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
function createAgentApp() {
|
|
875
|
+
return new AgentApp();
|
|
876
|
+
}
|
|
454
877
|
function readBody(req) {
|
|
455
878
|
return new Promise((resolve, reject) => {
|
|
456
879
|
const chunks = [];
|
|
@@ -459,120 +882,41 @@ function readBody(req) {
|
|
|
459
882
|
req.on("error", reject);
|
|
460
883
|
});
|
|
461
884
|
}
|
|
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
885
|
export {
|
|
886
|
+
ActivityResource,
|
|
887
|
+
AgentApp,
|
|
551
888
|
AuthenticationError,
|
|
552
889
|
AuthorizationError,
|
|
553
890
|
BaseResource,
|
|
554
|
-
Blueprint,
|
|
555
|
-
BlueprintClient,
|
|
556
|
-
BlueprintError,
|
|
557
891
|
CalendarEventResource,
|
|
892
|
+
ContactChannelResource,
|
|
893
|
+
ContactResource,
|
|
558
894
|
ContractResource,
|
|
559
895
|
CustomerResource,
|
|
560
896
|
DEFAULT_BASE_URL,
|
|
561
897
|
DEFAULT_MAX_RETRIES,
|
|
562
898
|
DEFAULT_TIMEOUT,
|
|
899
|
+
DEFAULT_TOKEN_REFRESH_SKEW_MS,
|
|
563
900
|
EstimateResource,
|
|
564
901
|
InventoryResource,
|
|
565
902
|
InvoiceResource,
|
|
566
903
|
JobResource,
|
|
904
|
+
MemoryTokenStorage,
|
|
567
905
|
NotFoundError,
|
|
568
906
|
OAuthError,
|
|
569
907
|
OpportunityResource,
|
|
908
|
+
Planes,
|
|
909
|
+
PlanesClient,
|
|
910
|
+
PlanesError,
|
|
570
911
|
PropertyResource,
|
|
571
912
|
RateLimitError,
|
|
572
913
|
ServerError,
|
|
914
|
+
TagResource,
|
|
573
915
|
ValidationError,
|
|
916
|
+
WebhookRouter,
|
|
574
917
|
WebhookVerificationError,
|
|
575
918
|
buildAuthorizeUrl,
|
|
919
|
+
createAgentApp,
|
|
576
920
|
exchangeCode,
|
|
577
921
|
parseWebhookEvent,
|
|
578
922
|
refreshAccessToken,
|