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