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