@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.d.ts CHANGED
@@ -1,15 +1,76 @@
1
1
  import * as http from 'node:http';
2
2
 
3
3
  /**
4
- * Configuration for the BlueprintClient.
4
+ * Tokens managed by an OAuth-credential client, persisted via {@link TokenStorage}.
5
5
  */
6
- interface BlueprintConfig {
6
+ interface StoredTokens {
7
+ /** Current OAuth access token. */
8
+ accessToken: string;
9
+ /**
10
+ * Refresh token used to obtain a new access token. `null` for tokens issued
11
+ * via the client_credentials grant, which have no refresh token — the
12
+ * client re-authenticates with clientId/clientSecret instead.
13
+ */
14
+ refreshToken: string | null;
15
+ /** Access-token expiry as epoch milliseconds. */
16
+ expiresAt: number;
17
+ }
18
+ /**
19
+ * Pluggable persistence for OAuth-managed tokens. Implement this to back the
20
+ * client with your own store (encrypted disk, database, secrets manager, ...).
21
+ * The default is {@link MemoryTokenStorage}.
22
+ */
23
+ interface TokenStorage {
24
+ /** Load the currently stored tokens, or `null` if none are stored. */
25
+ getTokens(): Promise<StoredTokens | null>;
26
+ /** Persist the given tokens (called after every refresh). */
27
+ setTokens(tokens: StoredTokens): Promise<void>;
28
+ }
29
+ /**
30
+ * Default in-memory {@link TokenStorage}. Tokens live only for the process
31
+ * lifetime — provide your own implementation to persist across restarts.
32
+ */
33
+ declare class MemoryTokenStorage implements TokenStorage {
34
+ private tokens;
35
+ constructor(seed?: StoredTokens | null);
36
+ getTokens(): Promise<StoredTokens | null>;
37
+ setTokens(tokens: StoredTokens): Promise<void>;
38
+ }
39
+ /**
40
+ * Configuration for the PlanesClient.
41
+ *
42
+ * Exactly one authentication mode must be supplied:
43
+ * - `accessToken` — a pre-obtained OAuth access token (not auto-refreshed)
44
+ * - `apiKey` — a `pk_live_` API key
45
+ * - `clientId` + `clientSecret` — OAuth-managed: the client obtains/refreshes
46
+ * access tokens automatically using `tokenStorage`
47
+ */
48
+ interface PlanesConfig {
7
49
  /** API base URL (defaults to https://api.planescrm.com/api/v1) */
8
50
  baseUrl?: string;
9
- /** OAuth access token (mutually exclusive with apiKey) */
51
+ /** OAuth access token (mutually exclusive with apiKey / clientId+clientSecret) */
10
52
  accessToken?: string;
11
- /** API key (mutually exclusive with accessToken) */
53
+ /** API key (mutually exclusive with accessToken / clientId+clientSecret) */
12
54
  apiKey?: string;
55
+ /** OAuth client ID — combined with `clientSecret`, enables the OAuth-managed path */
56
+ clientId?: string;
57
+ /** OAuth client secret — combined with `clientId`, enables the OAuth-managed path */
58
+ clientSecret?: string;
59
+ /**
60
+ * Scopes to request when the OAuth-managed path falls back to the
61
+ * client_credentials grant (no stored refresh token).
62
+ */
63
+ scopes?: string[];
64
+ /**
65
+ * Token store for the OAuth-managed path. Seed it with tokens obtained from
66
+ * an authorization-code exchange. Defaults to {@link MemoryTokenStorage}.
67
+ */
68
+ tokenStorage?: TokenStorage;
69
+ /**
70
+ * Refresh access tokens proactively when they are within this many
71
+ * milliseconds of expiry (OAuth-managed path only). Default 60000 (60s).
72
+ */
73
+ tokenRefreshSkewMs?: number;
13
74
  /** Request timeout in milliseconds (default 30000) */
14
75
  timeout?: number;
15
76
  /** Automatically retry on 429 rate limit responses (default true) */
@@ -20,6 +81,7 @@ interface BlueprintConfig {
20
81
  declare const DEFAULT_BASE_URL = "https://api.planescrm.com/api/v1";
21
82
  declare const DEFAULT_TIMEOUT = 30000;
22
83
  declare const DEFAULT_MAX_RETRIES = 3;
84
+ declare const DEFAULT_TOKEN_REFRESH_SKEW_MS = 60000;
23
85
 
24
86
  interface HttpRequestOptions {
25
87
  method: string;
@@ -39,9 +101,25 @@ declare class HttpClient {
39
101
  private readonly timeout;
40
102
  private readonly retryOnRateLimit;
41
103
  private readonly maxRetries;
42
- constructor(config: BlueprintConfig);
104
+ private readonly oauth;
105
+ /** Single-flight guard so concurrent requests share one refresh. */
106
+ private refreshInFlight;
107
+ constructor(config: PlanesConfig);
43
108
  request<T>(options: HttpRequestOptions): Promise<T>;
44
109
  getAuthHeaders(): Record<string, string>;
110
+ /**
111
+ * Resolve the Authorization header for the current request. For the static
112
+ * accessToken/apiKey modes this is {@link getAuthHeaders}. For the
113
+ * OAuth-managed path it reads the stored token, proactively refreshing when
114
+ * the token is within the skew window of expiry.
115
+ */
116
+ private resolveAuthHeaders;
117
+ /**
118
+ * Refresh the OAuth access token via `refreshAccessToken` and persist the new
119
+ * tokens through the configured {@link TokenStorage}. Concurrent callers share
120
+ * a single in-flight refresh.
121
+ */
122
+ private refreshTokens;
45
123
  private buildUrl;
46
124
  private parseRetryAfter;
47
125
  private mapError;
@@ -151,15 +229,397 @@ declare class InventoryResource extends BaseResource {
151
229
  }
152
230
 
153
231
  /**
154
- * BlueprintClient — typed API client for the Planes CRM platform.
232
+ * Wire shapes for the Contacts + Contact Channels API.
233
+ *
234
+ * Mirrors `createContactSchema`/`updateContactSchema` and
235
+ * `createContactChannelSchema`/`updateContactChannelSchema` in
236
+ * `packages/shared/src/validation/contact.ts`, plus the actual Drizzle-row
237
+ * response shapes returned by `ContactsService` / `ContactChannelsService`.
238
+ * Types are redefined here (not imported) so the published package carries no
239
+ * runtime or type dependency on `@blueprint/shared`. Internal-only columns
240
+ * (tenantId, deletedAt, createdBy, …) are intentionally omitted — see
241
+ * `CustomerDto` for the house style.
242
+ */
243
+ type ContactSource = 'manual' | 'import' | 'convert_from_customer';
244
+ interface ContactDto {
245
+ id: string;
246
+ firstName: string;
247
+ lastName: string | null;
248
+ notes: string | null;
249
+ customFields: Record<string, unknown> | null;
250
+ source: ContactSource | null;
251
+ createdAt: string;
252
+ updatedAt: string;
253
+ }
254
+ interface CreateContactDto {
255
+ firstName: string;
256
+ lastName?: string | null;
257
+ notes?: string | null;
258
+ customFields?: Record<string, unknown> | null;
259
+ source?: ContactSource | null;
260
+ }
261
+ interface UpdateContactDto {
262
+ firstName?: string;
263
+ lastName?: string | null;
264
+ notes?: string | null;
265
+ customFields?: Record<string, unknown> | null;
266
+ source?: ContactSource | null;
267
+ }
268
+ type ContactChannelType = 'email' | 'phone' | 'url';
269
+ type ContactChannelPhoneType = 'mobile' | 'landline' | 'voip' | 'unknown';
270
+ interface ContactChannelDto {
271
+ id: string;
272
+ contactId: string;
273
+ channelType: ContactChannelType;
274
+ label: string | null;
275
+ value: string;
276
+ isPrimary: boolean;
277
+ phoneType: ContactChannelPhoneType | null;
278
+ smsCapable: boolean | null;
279
+ dnc: boolean | null;
280
+ marketingOptOut: boolean | null;
281
+ createdAt: string;
282
+ updatedAt: string;
283
+ }
284
+ interface CreateContactChannelDto {
285
+ channelType: ContactChannelType;
286
+ label?: string | null;
287
+ value: string;
288
+ isPrimary?: boolean;
289
+ dnc?: boolean | null;
290
+ marketingOptOut?: boolean | null;
291
+ }
292
+ interface UpdateContactChannelDto {
293
+ label?: string | null;
294
+ value?: string;
295
+ isPrimary?: boolean;
296
+ dnc?: boolean | null;
297
+ marketingOptOut?: boolean | null;
298
+ }
299
+ /** A channel elsewhere in the tenant with the same normalized value. */
300
+ interface ContactChannelDuplicateMatch {
301
+ contactId: string;
302
+ channelId: string;
303
+ value: string;
304
+ }
305
+ /**
306
+ * `POST /contacts/:id/channels` return shape — `ContactChannelsService.addChannel()`
307
+ * always returns this wrapper, never a flat channel. `duplicateOf` is the
308
+ * dedup signal (empty array when there's no match) — the point of this API
309
+ * for connectors doing identity resolution (e.g. Messenger).
310
+ */
311
+ interface AddContactChannelResult {
312
+ channel: ContactChannelDto;
313
+ duplicateOf: ContactChannelDuplicateMatch[];
314
+ }
315
+ interface ContactMergeResult {
316
+ winnerId: string;
317
+ loserId: string;
318
+ }
319
+
320
+ /**
321
+ * `/contacts/:id/channels` — POST/PATCH/DELETE only. There is no list/get
322
+ * route: reads come embedded in the contact detail response (`GET
323
+ * /contacts/:id` returns `{ ...contact, channels, links, mergedInto }`), so
324
+ * this does NOT extend `BaseResource`.
325
+ */
326
+ declare class ContactChannelResource {
327
+ private readonly http;
328
+ private readonly basePath;
329
+ constructor(http: HttpClient, contactId: string);
330
+ /** Returns `{ channel, duplicateOf }` — `duplicateOf` is the dedup signal. */
331
+ create(data: CreateContactChannelDto): Promise<AddContactChannelResult>;
332
+ update(channelId: string, data: UpdateContactChannelDto): Promise<ContactChannelDto>;
333
+ delete(channelId: string): Promise<void>;
334
+ }
335
+ declare class ContactResource extends BaseResource<ContactDto, CreateContactDto, UpdateContactDto> {
336
+ constructor(http: ConstructorParameters<typeof BaseResource>[0]);
337
+ merge(winnerId: string, loserId: string): Promise<ContactMergeResult>;
338
+ channels(contactId: string): ContactChannelResource;
339
+ }
340
+
341
+ /**
342
+ * Wire shapes for the generic activity/timeline API (E31-F1).
343
+ *
344
+ * Mirrors `recordActivitySchema` (write) and `ActivityRecord` (read) in
345
+ * `packages/shared/src/validation/activity.ts` — both snake_case on the wire.
346
+ * Types are redefined here (not imported) so the published package carries no
347
+ * runtime or type dependency on `@blueprint/shared`.
348
+ */
349
+ interface CreateActivityDto {
350
+ entity_type: string;
351
+ entity_id?: string | null;
352
+ /** The contact a channel-targeted send was addressed to, if any. */
353
+ contact_id?: string | null;
354
+ source: string;
355
+ type: string;
356
+ occurred_at?: string;
357
+ actor?: string | null;
358
+ /** Required — a short human-readable description of the event. */
359
+ summary: string;
360
+ external_ref?: string | null;
361
+ match_key?: string | null;
362
+ payload?: Record<string, unknown>;
363
+ payload_version?: number;
364
+ }
365
+ interface ActivityDto {
366
+ id: string;
367
+ type: string;
368
+ source: string;
369
+ occurred_at: string;
370
+ actor: string | null;
371
+ summary: string;
372
+ entity_type: string;
373
+ entity_id: string | null;
374
+ contact_id: string | null;
375
+ payload: Record<string, unknown>;
376
+ }
377
+
378
+ declare class ActivityResource {
379
+ private readonly http;
380
+ constructor(http: HttpClient);
381
+ create(data: CreateActivityDto): Promise<ActivityDto>;
382
+ list(params?: ListParams): Promise<PaginatedResponse<ActivityDto>>;
383
+ listForEntity(entityType: string, entityId: string, params?: ListParams): Promise<PaginatedResponse<ActivityDto>>;
384
+ }
385
+
386
+ /**
387
+ * Wire shapes for the Tags & entity-tagging API.
388
+ *
389
+ * Mirrors `createTagSchema`/`updateTagSchema`/`setEntityTagsSchema` in
390
+ * `packages/shared/src/validation/tags.ts`, plus `TagsService`'s actual
391
+ * response shapes — snake_case, per `toDto()`/`list()`. Types are redefined
392
+ * here (not imported) so the published package carries no runtime or type
393
+ * dependency on `@blueprint/shared`.
394
+ */
395
+ interface TagDto {
396
+ id: string;
397
+ name: string;
398
+ color: string | null;
399
+ created_at: string;
400
+ updated_at: string;
401
+ /** Only present on `list()` — `create()`/`update()` omit it. */
402
+ usage_count?: number;
403
+ }
404
+ interface CreateTagDto {
405
+ name: string;
406
+ color?: string | null;
407
+ }
408
+ interface UpdateTagDto {
409
+ name?: string;
410
+ color?: string | null;
411
+ }
412
+ /**
413
+ * Declarative set-tags input. Provide `tag_ids` and/or `tag_names` — at least
414
+ * one must be present (the server rejects neither); an empty array clears
415
+ * that side of the set.
416
+ */
417
+ interface SetEntityTagsDto {
418
+ tag_ids?: string[];
419
+ tag_names?: string[];
420
+ }
421
+ interface EntityTagDto {
422
+ id: string;
423
+ name: string;
424
+ color: string | null;
425
+ }
426
+ interface TagEntityDto {
427
+ entity_type: string;
428
+ entity_id: string;
429
+ }
430
+ interface TagEntitiesPage {
431
+ data: TagEntityDto[];
432
+ page: number;
433
+ limit: number;
434
+ total: number;
435
+ }
436
+
437
+ declare class TagResource extends BaseResource<TagDto, CreateTagDto, UpdateTagDto> {
438
+ constructor(http: ConstructorParameters<typeof BaseResource>[0]);
439
+ /** `TagsService.setEntityTags()` returns the final tag set wrapped in `{ tags }`. */
440
+ setEntityTags(entityType: string, entityId: string, data: SetEntityTagsDto): Promise<{
441
+ tags: TagDto[];
442
+ }>;
443
+ getEntityTags(entityType: string, entityId: string): Promise<EntityTagDto[]>;
444
+ getEntities(tagId: string, params?: Record<string, unknown>): Promise<TagEntitiesPage>;
445
+ }
446
+
447
+ /**
448
+ * The kinds of change a webhook event can describe.
449
+ *
450
+ * Mirrored (self-contained, NOT imported) from
451
+ * `packages/shared/src/types/entity-events.ts` — the SDK must not depend on
452
+ * `@blueprint/shared`. Keep this union byte-identical to that source of truth.
453
+ */
454
+ type EntityEventType = 'created' | 'updated' | 'deleted' | 'status_changed' | 'entity.tagged' | 'entity.untagged' | 'list.member_added' | 'list.member_removed' | 'entity.assigned' | 'entity.unassigned' | 'team.member_added' | 'team.member_removed' | 'task.created' | 'task.updated' | 'task.completed' | 'task.cancelled' | 'comment.created';
455
+ /**
456
+ * The entity kinds a webhook event can target.
457
+ *
458
+ * Mirrored (self-contained, NOT imported) from
459
+ * `packages/shared/src/types/entity-events.ts`. Keep byte-identical.
460
+ */
461
+ type EntityType = 'customer' | 'opportunity' | 'job' | 'estimate' | 'contract' | 'property' | 'inventory_item' | 'agent_execution' | 'calendar_event' | 'invoice' | 'payment';
462
+ /**
463
+ * A single entity-change event as emitted onto the Planes event bus and
464
+ * delivered inside a {@link WebhookDelivery}.
465
+ *
466
+ * Mirrored from `EntityEvent` in
467
+ * `packages/shared/src/types/entity-events.ts`.
468
+ */
469
+ interface EntityEvent {
470
+ /** What happened (e.g. 'created', 'status_changed'). */
471
+ event_type: EntityEventType;
472
+ /** The kind of record this event is about. */
473
+ entity_type: EntityType;
474
+ /** The affected record's ID. */
475
+ entity_id: string;
476
+ /** Tenant ID that owns this event. */
477
+ tenant_id: string;
478
+ /** Per-field before/after values for the change. */
479
+ changes: Record<string, {
480
+ old: unknown;
481
+ new: unknown;
482
+ }>;
483
+ /** Full record snapshot at the time of the event. */
484
+ snapshot: Record<string, unknown>;
485
+ /** ISO 8601 timestamp of when the event occurred. */
486
+ timestamp: string;
487
+ /** Correlation ID linking related events/actions. */
488
+ correlation_id: string;
489
+ }
490
+ /**
491
+ * The webhook delivery envelope Planes POSTs to a subscriber's URL.
492
+ *
493
+ * This is the outermost body: a delivery ID, the schema `version`, the wrapped
494
+ * {@link EntityEvent}, and the subscription that matched. The entity body lives
495
+ * at `event.snapshot` — there is no top-level `data`.
496
+ */
497
+ interface WebhookDelivery {
498
+ /** Delivery ID (the delivery-log row id). */
499
+ id: string;
500
+ /**
501
+ * Webhook payload schema version (from the `X-Planes-Webhook-Version`
502
+ * header / envelope, emitted API-side per E39-F1-S2). v1 requires no
503
+ * branching; surfaced here so consumers can switch on it in the future.
504
+ */
505
+ version: number;
506
+ /** The entity-change event that triggered this delivery. */
507
+ event: EntityEvent;
508
+ /** ID of the webhook subscription that matched and produced this delivery. */
509
+ subscription_id: string;
510
+ }
511
+ /**
512
+ * Verify an HMAC-SHA256 webhook signature using timing-safe comparison.
513
+ *
514
+ * The signature header format is: `sha256=<hex-digest>`
515
+ *
516
+ * @param payload - The raw request body (string or Buffer)
517
+ * @param signature - The X-Planes-Signature header value
518
+ * @param secret - The webhook signing secret
519
+ * @returns true if the signature is valid, false otherwise
520
+ */
521
+ declare function verifyWebhookSignature(payload: string | Buffer, signature: string, secret: string): boolean;
522
+ /**
523
+ * Parse and verify a webhook delivery payload.
524
+ *
525
+ * @param body - The raw request body as a string
526
+ * @param signature - The X-Planes-Signature header value
527
+ * @param secret - The webhook signing secret
528
+ * @returns The parsed and verified {@link WebhookDelivery} envelope
529
+ * @throws WebhookVerificationError if signature is invalid
530
+ */
531
+ declare function parseWebhookEvent(body: string, signature: string, secret: string): WebhookDelivery;
532
+
533
+ /**
534
+ * A handler invoked for a matching webhook delivery. May be sync or async;
535
+ * async handlers are awaited by {@link WebhookRouter.process}.
536
+ */
537
+ type WebhookHandler = (delivery: WebhookDelivery) => void | Promise<void>;
538
+ /** Input to {@link WebhookRouter.process} — the raw request pieces. */
539
+ interface WebhookProcessInput {
540
+ /** The raw request body as a string (verbatim — not re-serialized JSON). */
541
+ body: string;
542
+ /** The `X-Planes-Signature` header value (`sha256=<hex>`). */
543
+ signature: string;
544
+ /** The webhook signing secret for this subscription. */
545
+ secret: string;
546
+ }
547
+ /**
548
+ * Ergonomic event router over the corrected {@link WebhookDelivery} envelope.
549
+ *
550
+ * The platform models an event as two fields — `event.entity_type` and
551
+ * `event.event_type` — which this router joins into a dotted composite key
552
+ * `` `${entity_type}.${event_type}` `` (e.g. `opportunity.created`). Handlers
553
+ * subscribe to that key, to an entity-scoped wildcard `` `${entity_type}.*` ``
554
+ * (e.g. `opportunity.*`), or to the global wildcard `*`.
555
+ *
556
+ * The router is a pure dispatcher: it verifies + parses the delivery, then
557
+ * fans it out to matching handlers. It holds no connection and no auth — the
558
+ * signing secret is supplied per {@link process} call — so the same client's
559
+ * `webhooks` router works across subscriptions. Wire it into your own HTTP
560
+ * handler.
561
+ *
562
+ * @example
563
+ * ```ts
564
+ * client.webhooks
565
+ * .on('opportunity.created', (d) => console.log(d.event.snapshot))
566
+ * .on('opportunity.*', (d) => audit(d))
567
+ * .on('*', (d) => metrics.count(d.event.entity_type));
568
+ *
569
+ * // inside your POST handler:
570
+ * await client.webhooks.process({ body: rawBody, signature, secret });
571
+ * ```
572
+ *
573
+ * ### Dotted `event_type` composite keys
574
+ *
575
+ * Four event types are themselves dotted — `entity.tagged`, `entity.untagged`,
576
+ * `list.member_added`, `list.member_removed`. Joined with their entity these
577
+ * yield keys with more than one dot, e.g. `customer.entity.tagged`. This router
578
+ * matches on the literal `` `${entity_type}.${event_type}` `` string, so
579
+ * subscribe with the full key (`'customer.entity.tagged'`). The entity wildcard
580
+ * (`'customer.*'`) and global wildcard (`'*'`) still match these deliveries;
581
+ * only mid-segment wildcards like `'customer.entity.*'` are intentionally
582
+ * unsupported.
583
+ */
584
+ declare class WebhookRouter {
585
+ /** pattern (composite key / entity wildcard / '*') → registered handlers. */
586
+ private readonly handlers;
587
+ /**
588
+ * Register a handler for an event pattern.
589
+ *
590
+ * @param pattern - `"{entity_type}.{event_type}"` (e.g. `opportunity.created`),
591
+ * an entity wildcard `"{entity_type}.*"` (e.g. `opportunity.*`), or the
592
+ * global wildcard `"*"`. Matching is exact-string against the derived key;
593
+ * see the class docs for the dotted-`event_type` shape.
594
+ * @param handler - Invoked with the full {@link WebhookDelivery} on a match.
595
+ * @returns `this`, for chaining.
596
+ */
597
+ on(pattern: string, handler: WebhookHandler): this;
598
+ /**
599
+ * Verify, parse, and dispatch a webhook delivery.
600
+ *
601
+ * The signature is verified first: on failure this throws
602
+ * {@link WebhookVerificationError} and dispatches nothing. On success it
603
+ * dispatches to every matching handler — specific key, then entity wildcard,
604
+ * then global — awaiting async handlers before resolving.
605
+ *
606
+ * @param input - The raw `{ body, signature, secret }`.
607
+ * @returns The parsed {@link WebhookDelivery}.
608
+ * @throws WebhookVerificationError if the signature is invalid.
609
+ */
610
+ process(input: WebhookProcessInput): Promise<WebhookDelivery>;
611
+ }
612
+
613
+ /**
614
+ * PlanesClient — typed API client for the Planes CRM platform.
155
615
  *
156
616
  * @example
157
617
  * ```ts
158
- * const client = new BlueprintClient({ accessToken: 'tok_...' });
618
+ * const client = new PlanesClient({ accessToken: 'tok_...' });
159
619
  * const opps = await client.opportunities.list({ page: 1, limit: 20 });
160
620
  * ```
161
621
  */
162
- declare class BlueprintClient {
622
+ declare class PlanesClient {
163
623
  private readonly http;
164
624
  private _opportunities?;
165
625
  private _customers?;
@@ -170,7 +630,11 @@ declare class BlueprintClient {
170
630
  private _estimates?;
171
631
  private _invoices?;
172
632
  private _inventory?;
173
- constructor(config: BlueprintConfig);
633
+ private _contacts?;
634
+ private _activities?;
635
+ private _tags?;
636
+ private _webhooks?;
637
+ constructor(config: PlanesConfig);
174
638
  get opportunities(): OpportunityResource;
175
639
  get customers(): CustomerResource;
176
640
  get properties(): PropertyResource;
@@ -180,12 +644,22 @@ declare class BlueprintClient {
180
644
  get estimates(): EstimateResource;
181
645
  get invoices(): InvoiceResource;
182
646
  get inventory(): InventoryResource;
647
+ get contacts(): ContactResource;
648
+ get activities(): ActivityResource;
649
+ get tags(): TagResource;
650
+ /**
651
+ * Ergonomic webhook event router — `client.webhooks.on(pattern, handler)`
652
+ * plus `client.webhooks.process({ body, signature, secret })`. Auth-independent
653
+ * (the signing secret is supplied per `.process()` call), so it is created
654
+ * lazily without the HTTP client. See {@link WebhookRouter}.
655
+ */
656
+ get webhooks(): WebhookRouter;
183
657
  }
184
658
 
185
659
  /**
186
660
  * Base error class for all Planes SDK errors.
187
661
  */
188
- declare class BlueprintError extends Error {
662
+ declare class PlanesError extends Error {
189
663
  readonly status: number;
190
664
  readonly code: string;
191
665
  constructor(message: string, status: number, code: string);
@@ -193,19 +667,19 @@ declare class BlueprintError extends Error {
193
667
  /**
194
668
  * Thrown when authentication fails (401).
195
669
  */
196
- declare class AuthenticationError extends BlueprintError {
670
+ declare class AuthenticationError extends PlanesError {
197
671
  constructor(message?: string);
198
672
  }
199
673
  /**
200
674
  * Thrown when the user lacks permission (403).
201
675
  */
202
- declare class AuthorizationError extends BlueprintError {
676
+ declare class AuthorizationError extends PlanesError {
203
677
  constructor(message?: string);
204
678
  }
205
679
  /**
206
680
  * Thrown when a resource is not found (404).
207
681
  */
208
- declare class NotFoundError extends BlueprintError {
682
+ declare class NotFoundError extends PlanesError {
209
683
  constructor(message?: string);
210
684
  }
211
685
  /** Field-level validation error detail. */
@@ -217,33 +691,33 @@ interface FieldError {
217
691
  /**
218
692
  * Thrown for validation failures (400) with field-level error details.
219
693
  */
220
- declare class ValidationError extends BlueprintError {
694
+ declare class ValidationError extends PlanesError {
221
695
  readonly fieldErrors: FieldError[];
222
696
  constructor(message?: string, fieldErrors?: FieldError[]);
223
697
  }
224
698
  /**
225
699
  * Thrown when rate limited (429). Includes retryAfter in seconds.
226
700
  */
227
- declare class RateLimitError extends BlueprintError {
701
+ declare class RateLimitError extends PlanesError {
228
702
  readonly retryAfter: number;
229
703
  constructor(retryAfter?: number, message?: string);
230
704
  }
231
705
  /**
232
706
  * Thrown for server errors (500+).
233
707
  */
234
- declare class ServerError extends BlueprintError {
708
+ declare class ServerError extends PlanesError {
235
709
  constructor(message?: string);
236
710
  }
237
711
  /**
238
712
  * Thrown when webhook signature verification fails.
239
713
  */
240
- declare class WebhookVerificationError extends BlueprintError {
714
+ declare class WebhookVerificationError extends PlanesError {
241
715
  constructor(message?: string);
242
716
  }
243
717
  /**
244
718
  * Thrown for OAuth flow errors.
245
719
  */
246
- declare class OAuthError extends BlueprintError {
720
+ declare class OAuthError extends PlanesError {
247
721
  readonly errorCode: string;
248
722
  constructor(errorCode: string, message?: string);
249
723
  }
@@ -251,54 +725,130 @@ declare class OAuthError extends BlueprintError {
251
725
  /**
252
726
  * ActionHandler, ActionContext, and ActionResult types for the SDK.
253
727
  *
254
- * These mirror the types in @blueprint/shared/types/agent-execution.ts
255
- * but are self-contained to avoid requiring @blueprint/shared as a dependency.
256
- * The SDK is a standalone package for external developers.
728
+ * These model the REAL over-the-wire contract that the Planes platform's
729
+ * external-agent dispatcher POSTs to a registered agent endpoint
730
+ * (`apps/api/src/modules/marketplace/handlers/external-agent.handler.ts`).
731
+ * They are self-contained — the SDK never imports `@blueprint/shared`, so
732
+ * external developers can depend on this package alone.
733
+ *
734
+ * ## Wire contract (source of truth: external-agent.handler.ts)
735
+ *
736
+ * The platform POSTs a FLATTENED, WRAPPED envelope — NOT the platform's internal
737
+ * nested `ActionContext`:
738
+ *
739
+ * ```json
740
+ * {
741
+ * "action_context": {
742
+ * "action_id": "act-1",
743
+ * "action_type": "enrich-opportunity",
744
+ * "entity_type": "opportunity",
745
+ * "entity_id": "opp-1",
746
+ * "tenant_id": "tenant-1",
747
+ * "data": { "...": "entity snapshot" },
748
+ * "stage": { "id": "...", "name": "...", "rules": null, "agent_role": null, "agent_instructions": null } | null,
749
+ * "metadata": { "trigger": { ... }, "execution": { ... } }
750
+ * },
751
+ * "callback_token": "jwt.callback.token",
752
+ * "callback_base_url": "https://…/api/v1"
753
+ * }
754
+ * ```
755
+ *
756
+ * ### Field convention
757
+ * The SDK mirrors the wire's **snake_case** verbatim (no camelCase translation),
758
+ * so a parsed request body maps 1:1 onto {@link ActionContext}. The one shaping
759
+ * the SDK server performs is to LIFT `callback_token` / `callback_base_url` (and
760
+ * the `X-Planes-Request-Id` header as {@link ActionContext.request_id}) up onto
761
+ * the same object handed to the handler — so a handler receives one flat context.
762
+ *
763
+ * ### Result delivery
764
+ * The platform reads the handler's {@link ActionResult} from the **synchronous
765
+ * HTTP response body** (`await response.json()` in the handler, dispatching on
766
+ * `status`). There is currently no async callback endpoint — `callback_token` /
767
+ * `callback_base_url` are minted for the agent to call back INTO the Planes API,
768
+ * not for the platform to poll. Handlers therefore just `return` their result.
769
+ *
770
+ * ### Not on the wire (intentionally absent vs. the platform's internal type)
771
+ * `permissions`, `agent_installation_id`, and `action.config` are NOT sent to
772
+ * external agents, so they are not modeled here.
257
773
  */
258
774
  /** Trigger types for action execution. */
259
775
  type TriggerType = 'workflow_transition' | 'schedule' | 'event' | 'agent';
260
- /** Permission levels (1 = most restricted, 4 = most powerful). */
776
+ /**
777
+ * Permission levels (1 = most restricted, 4 = most powerful).
778
+ *
779
+ * Not part of the external wire envelope (the platform does not send permission
780
+ * metadata to external agents); retained as a public type for callers that
781
+ * reason about permission levels elsewhere.
782
+ */
261
783
  type PermissionLevel = 1 | 2 | 3 | 4;
784
+ /** How this action was triggered (wire: `action_context.metadata.trigger`). */
785
+ interface ActionTrigger {
786
+ type: TriggerType;
787
+ source_id: string;
788
+ triggered_by: string;
789
+ }
790
+ /** Execution/correlation metadata (wire: `action_context.metadata.execution`). */
791
+ interface ActionExecution {
792
+ id: string;
793
+ correlation_id: string;
794
+ attempt: number;
795
+ timeout_ms: number;
796
+ }
797
+ /** Trigger + execution metadata (wire: `action_context.metadata`). */
798
+ interface ActionMetadata {
799
+ trigger: ActionTrigger;
800
+ execution: ActionExecution;
801
+ }
802
+ /**
803
+ * Destination stage metadata — present for `workflow_transition` triggers,
804
+ * `null` otherwise (wire: `action_context.stage`).
805
+ */
806
+ interface ActionStage {
807
+ id: string;
808
+ name: string;
809
+ rules: string | null;
810
+ agent_role: string | null;
811
+ agent_instructions: string | null;
812
+ }
262
813
  /**
263
814
  * ActionContext envelope — passed to handlers on each invocation.
815
+ *
816
+ * Shape matches the platform's flattened `action_context` (see file header),
817
+ * with `callback_token`, `callback_base_url`, and `request_id` lifted onto the
818
+ * same object by the SDK server for handler convenience.
264
819
  */
265
820
  interface ActionContext {
266
- trigger: {
267
- type: TriggerType;
268
- source_id: string;
269
- triggered_by: string;
270
- };
271
- entity: {
272
- type: string;
273
- id: string;
274
- snapshot: Record<string, unknown>;
275
- };
276
- action: {
277
- id: string;
278
- type: string;
279
- config: Record<string, unknown>;
280
- };
281
- tenant: {
282
- id: string;
283
- scope_paths?: string[];
284
- };
285
- /** Agent installation ID — set for agent-type actions. */
286
- agent_installation_id?: string;
287
- permissions: {
288
- level: PermissionLevel;
289
- allowed_actions: string[];
290
- spending_limit?: number;
291
- spending_limit_daily?: number;
292
- requires_approval: string[];
293
- max_actions_per_execution?: number;
294
- max_execution_time_ms?: number;
295
- };
296
- execution: {
297
- id: string;
298
- correlation_id: string;
299
- attempt: number;
300
- timeout_ms: number;
301
- };
821
+ /** Unique ID of the action being executed (wire: `action_context.action_id`). */
822
+ action_id: string;
823
+ /** The action type — the SDK routes to a handler by this value. */
824
+ action_type: string;
825
+ /** The entity type this action operates on, e.g. `'opportunity'`. */
826
+ entity_type: string;
827
+ /** The entity's ID. */
828
+ entity_id: string;
829
+ /** The tenant (company) ID this action runs for. */
830
+ tenant_id: string;
831
+ /** The entity snapshot captured at trigger time (wire: `action_context.data`). */
832
+ data: Record<string, unknown>;
833
+ /** Destination stage metadata for workflow transitions; `null` otherwise. */
834
+ stage: ActionStage | null;
835
+ /** Trigger + execution metadata. */
836
+ metadata: ActionMetadata;
837
+ /**
838
+ * Short-lived (5-min) JWT for calling back INTO the Planes API at
839
+ * {@link callback_base_url}. Lifted from the envelope's top-level `callback_token`.
840
+ */
841
+ callback_token: string;
842
+ /**
843
+ * Base URL (e.g. `https://…/api/v1`) for authenticated callbacks using
844
+ * {@link callback_token}. Lifted from the envelope's top-level `callback_base_url`.
845
+ */
846
+ callback_base_url: string;
847
+ /**
848
+ * Per-invocation correlation ID from the `X-Planes-Request-Id` request header,
849
+ * for logging/idempotency. `undefined` if the header was absent.
850
+ */
851
+ request_id?: string;
302
852
  }
303
853
  /** A follow-up action request (used with delegated status). */
304
854
  interface ActionRequest {
@@ -308,6 +858,10 @@ interface ActionRequest {
308
858
  }
309
859
  /**
310
860
  * ActionResult — the return value from a handler's execute() method.
861
+ *
862
+ * The platform reads this from the synchronous HTTP response body and dispatches
863
+ * on `status` (`completed` → `output`, `failed` → `error`/`retryable`,
864
+ * `needs_human` → `reason`/`context`, `delegated` → `follow_up_actions`).
311
865
  */
312
866
  type ActionResult = {
313
867
  status: 'completed';
@@ -332,14 +886,15 @@ interface ActionHandler {
332
886
  execute(ctx: ActionContext): Promise<ActionResult>;
333
887
  }
334
888
  /**
335
- * Generic typed version of ActionHandler for specific input/output shapes.
889
+ * Generic typed version of ActionHandler for a specific entity-data/output shape.
890
+ *
891
+ * `TData` narrows {@link ActionContext.data} (the entity snapshot) — the external
892
+ * wire carries no per-action `config`, so typed handler input is the entity data.
336
893
  */
337
- interface AgentHandler<TInput = Record<string, unknown>, TOutput = unknown> {
894
+ interface AgentHandler<TData = Record<string, unknown>, TOutput = unknown> {
338
895
  readonly type: string;
339
896
  execute(ctx: ActionContext & {
340
- action: {
341
- config: TInput;
342
- };
897
+ data: TData;
343
898
  }): Promise<{
344
899
  status: 'completed';
345
900
  output?: TOutput;
@@ -362,7 +917,7 @@ interface AgentHandler<TInput = Record<string, unknown>, TOutput = unknown> {
362
917
  type HandlerMap = Record<string, ActionHandler>;
363
918
 
364
919
  /**
365
- * Options for Blueprint.register().
920
+ * Options for Planes.register() and AgentApp.listen().
366
921
  */
367
922
  interface RegisterOptions {
368
923
  /** Port to listen on (default 3000). */
@@ -375,16 +930,16 @@ interface RegisterOptions {
375
930
  onError?: (error: Error) => void;
376
931
  }
377
932
  /**
378
- * Blueprint agent registration namespace.
933
+ * Planes agent registration namespace.
379
934
  * Provides a convenient way to register action handlers as an HTTP server.
380
935
  */
381
- declare const Blueprint: {
936
+ declare const Planes: {
382
937
  /**
383
938
  * Register action handlers and start an HTTP server to receive action requests.
384
939
  *
385
940
  * @example
386
941
  * ```ts
387
- * Blueprint.register([
942
+ * Planes.register([
388
943
  * {
389
944
  * type: 'send_email',
390
945
  * async execute(ctx) {
@@ -397,48 +952,49 @@ declare const Blueprint: {
397
952
  */
398
953
  register(handlers: ActionHandler[], options?: RegisterOptions): http.Server;
399
954
  };
400
-
401
- /**
402
- * Typed webhook event payload from Planes.
403
- */
404
- interface WebhookEvent<T = unknown> {
405
- /** Event ID (e.g. evt_abc123). */
406
- id: string;
407
- /** Event type (e.g. opportunity.created). */
408
- type: string;
409
- /** Tenant ID that owns this event. */
410
- tenant_id: string;
411
- /** ISO 8601 timestamp of when the event occurred. */
412
- timestamp: string;
413
- /** Entity snapshot at the time of the event. */
414
- data: T;
415
- /** Metadata about the event trigger. */
416
- metadata: {
417
- correlation_id: string;
418
- triggered_by: 'workflow' | 'api' | 'user';
419
- };
420
- }
421
955
  /**
422
- * Verify an HMAC-SHA256 webhook signature using timing-safe comparison.
956
+ * Builder-style agent app. Register action handlers incrementally by type,
957
+ * then start the server with {@link AgentApp.listen}.
423
958
  *
424
- * The signature header format is: `sha256=<hex-digest>`
959
+ * This is ergonomic sugar over {@link Planes.register} — both share the same
960
+ * underlying server/dispatch implementation ({@link createActionServer}).
425
961
  *
426
- * @param payload - The raw request body (string or Buffer)
427
- * @param signature - The X-Blueprint-Signature header value
428
- * @param secret - The webhook signing secret
429
- * @returns true if the signature is valid, false otherwise
962
+ * @example
963
+ * ```ts
964
+ * createAgentApp()
965
+ * .registerAction('enrich-opportunity', async (ctx) => {
966
+ * // ... enrichment logic
967
+ * return { status: 'completed', output: { enriched: true } };
968
+ * })
969
+ * .listen({ port: 3000 });
970
+ * ```
430
971
  */
431
- declare function verifyWebhookSignature(payload: string | Buffer, signature: string, secret: string): boolean;
972
+ declare class AgentApp {
973
+ private readonly handlers;
974
+ /**
975
+ * Register a handler for an action type. Accepts a bare `execute` function
976
+ * (the caller does not build the `{ type, execute }` object).
977
+ *
978
+ * @param type - The action type this handler responds to.
979
+ * @param execute - The handler function invoked with the {@link ActionContext}.
980
+ * @returns `this`, for chaining.
981
+ */
982
+ registerAction(type: string, execute: ActionHandler['execute']): this;
983
+ /**
984
+ * Start an HTTP server that dispatches incoming action requests to the
985
+ * registered handlers. Delegates to the same server factory as
986
+ * {@link Planes.register}.
987
+ *
988
+ * The handler set is snapshotted at call time, so registering further actions
989
+ * after `listen()` does not mutate the running server (parity with
990
+ * `Planes.register`, which snapshots its array).
991
+ */
992
+ listen(options?: RegisterOptions): http.Server;
993
+ }
432
994
  /**
433
- * Parse and verify a webhook event payload.
434
- *
435
- * @param body - The raw request body as a string
436
- * @param signature - The X-Blueprint-Signature header value
437
- * @param secret - The webhook signing secret
438
- * @returns The parsed and typed webhook event
439
- * @throws WebhookVerificationError if signature is invalid
995
+ * Create a new {@link AgentApp} builder.
440
996
  */
441
- declare function parseWebhookEvent<T = unknown>(body: string, signature: string, secret: string): WebhookEvent<T>;
997
+ declare function createAgentApp(): AgentApp;
442
998
 
443
999
  /**
444
1000
  * Options for building an OAuth authorization URL.
@@ -484,11 +1040,25 @@ interface RefreshOptions {
484
1040
  refreshToken: string;
485
1041
  }
486
1042
  /**
487
- * OAuth token response.
1043
+ * Options for the client_credentials grant (M2M auth — no user/tenant context).
1044
+ */
1045
+ interface ClientCredentialsOptions {
1046
+ /** Planes API base URL. */
1047
+ baseUrl: string;
1048
+ /** OAuth client ID. */
1049
+ clientId: string;
1050
+ /** OAuth client secret. */
1051
+ clientSecret: string;
1052
+ /** Requested OAuth scopes. */
1053
+ scopes: string[];
1054
+ }
1055
+ /**
1056
+ * OAuth token response. `refresh_token` is absent for the client_credentials
1057
+ * grant (there is no user session to refresh on behalf of).
488
1058
  */
489
1059
  interface TokenResponse {
490
1060
  access_token: string;
491
- refresh_token: string;
1061
+ refresh_token?: string;
492
1062
  expires_in: number;
493
1063
  token_type: string;
494
1064
  scope: string;
@@ -516,6 +1086,15 @@ declare function exchangeCode(options: ExchangeCodeOptions): Promise<TokenRespon
516
1086
  * @throws OAuthError on failure
517
1087
  */
518
1088
  declare function refreshAccessToken(options: RefreshOptions): Promise<TokenResponse>;
1089
+ /**
1090
+ * Obtain an access token via the client_credentials grant — the M2M auth
1091
+ * pattern for connectors and external services with no user/tenant context.
1092
+ *
1093
+ * @param options - Client credentials options
1094
+ * @returns Token response (no refresh_token)
1095
+ * @throws OAuthError on failure
1096
+ */
1097
+ declare function clientCredentialsGrant(options: ClientCredentialsOptions): Promise<TokenResponse>;
519
1098
 
520
1099
  interface OpportunityDto {
521
1100
  id: string;
@@ -593,4 +1172,4 @@ interface CustomersGeneratedResource {
593
1172
  createCustomer(data: CreateCustomerDto): Promise<CustomerDto>;
594
1173
  }
595
1174
 
596
- export { type ActionContext, type ActionHandler, type ActionRequest, type ActionResult, type AgentHandler, AuthenticationError, AuthorizationError, type AuthorizeOptions, BaseResource, Blueprint, BlueprintClient, type BlueprintConfig, BlueprintError, CalendarEventResource, ContractResource, type CreateCustomerDto, type CreateOpportunityDto, type CustomerDto, CustomerResource, type CustomersGeneratedResource, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, EstimateResource, type ExchangeCodeOptions, type FieldError, type HandlerMap, InventoryResource, InvoiceResource, JobResource, type ListParams, NotFoundError, OAuthError, type OpportunitiesGeneratedResource, type OpportunityDto, OpportunityResource, type PaginatedCustomerResponse, type PaginatedOpportunityResponse, type PaginatedResponse, type PaginationMeta, type PermissionLevel, PropertyResource, RateLimitError, type RefreshOptions, type RegisterOptions, ServerError, type TokenResponse, type TriggerType, type UpdateOpportunityDto, ValidationError, type WebhookEvent, WebhookVerificationError, buildAuthorizeUrl, exchangeCode, parseWebhookEvent, refreshAccessToken, verifyWebhookSignature };
1175
+ export { type ActionContext, type ActionHandler, type ActionRequest, type ActionResult, type ActivityDto, ActivityResource, AgentApp, type AgentHandler, AuthenticationError, AuthorizationError, type AuthorizeOptions, BaseResource, CalendarEventResource, type ClientCredentialsOptions, type ContactChannelDto, ContactChannelResource, type ContactDto, type ContactMergeResult, ContactResource, ContractResource, type CreateActivityDto, type CreateContactChannelDto, type CreateContactDto, type CreateCustomerDto, type CreateOpportunityDto, type CreateTagDto, type CustomerDto, CustomerResource, type CustomersGeneratedResource, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, DEFAULT_TOKEN_REFRESH_SKEW_MS, type EntityEvent, type EntityEventType, type EntityTagDto, type EntityType, EstimateResource, type ExchangeCodeOptions, type FieldError, type HandlerMap, InventoryResource, InvoiceResource, JobResource, type ListParams, MemoryTokenStorage, NotFoundError, OAuthError, type OpportunitiesGeneratedResource, type OpportunityDto, OpportunityResource, type PaginatedCustomerResponse, type PaginatedOpportunityResponse, type PaginatedResponse, type PaginationMeta, type PermissionLevel, Planes, PlanesClient, type PlanesConfig, PlanesError, PropertyResource, RateLimitError, type RefreshOptions, type RegisterOptions, ServerError, type SetEntityTagsDto, type StoredTokens, type TagDto, type TagEntityDto, TagResource, type TokenResponse, type TokenStorage, type TriggerType, type UpdateContactChannelDto, type UpdateContactDto, type UpdateOpportunityDto, type UpdateTagDto, ValidationError, type WebhookDelivery, type WebhookHandler, type WebhookProcessInput, WebhookRouter, WebhookVerificationError, buildAuthorizeUrl, clientCredentialsGrant, createAgentApp, exchangeCode, parseWebhookEvent, refreshAccessToken, verifyWebhookSignature };