@planescrm/sdk 0.2.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/README.md +155 -0
- package/dist/index.d.mts +596 -0
- package/dist/index.d.ts +596 -0
- package/dist/index.js +646 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +581 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/errors.ts","../src/http.ts","../src/resources/base-resource.ts","../src/resources/opportunities.ts","../src/resources/customers.ts","../src/resources/properties.ts","../src/resources/calendar-events.ts","../src/resources/contracts.ts","../src/resources/jobs.ts","../src/resources/estimates.ts","../src/resources/invoices.ts","../src/resources/inventory.ts","../src/client.ts","../src/agent.ts","../src/webhooks.ts","../src/oauth.ts"],"sourcesContent":["// Client\nexport { BlueprintClient } from './client';\n\n// Config\nexport type { BlueprintConfig } from './config';\nexport { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from './config';\n\n// Errors\nexport {\n BlueprintError,\n AuthenticationError,\n AuthorizationError,\n NotFoundError,\n ValidationError,\n RateLimitError,\n ServerError,\n WebhookVerificationError,\n OAuthError,\n} from './errors';\nexport type { FieldError } from './errors';\n\n// Types\nexport type { PaginatedResponse, ListParams } from './types';\n\n// Resources\nexport {\n BaseResource,\n OpportunityResource,\n CustomerResource,\n PropertyResource,\n CalendarEventResource,\n ContractResource,\n JobResource,\n EstimateResource,\n InvoiceResource,\n InventoryResource,\n} from './resources';\n\n// Agent types\nexport type {\n ActionHandler,\n ActionContext,\n ActionResult,\n ActionRequest,\n AgentHandler,\n HandlerMap,\n TriggerType,\n PermissionLevel,\n} from './types/agent-types';\n\n// Agent registration\nexport { Blueprint } from './agent';\nexport type { RegisterOptions } from './agent';\n\n// Webhooks\nexport { verifyWebhookSignature, parseWebhookEvent } from './webhooks';\nexport type { WebhookEvent } from './webhooks';\n\n// OAuth\nexport { buildAuthorizeUrl, exchangeCode, refreshAccessToken } from './oauth';\nexport type {\n AuthorizeOptions,\n ExchangeCodeOptions,\n RefreshOptions,\n TokenResponse,\n} from './oauth';\n\n// Generated types (from OpenAPI spec)\nexport * from './generated';\n","/**\n * Configuration for the BlueprintClient.\n */\nexport interface BlueprintConfig {\n /** API base URL (defaults to https://api.planescrm.com/api/v1) */\n baseUrl?: string;\n /** OAuth access token (mutually exclusive with apiKey) */\n accessToken?: string;\n /** API key (mutually exclusive with accessToken) */\n apiKey?: string;\n /** Request timeout in milliseconds (default 30000) */\n timeout?: number;\n /** Automatically retry on 429 rate limit responses (default true) */\n retryOnRateLimit?: boolean;\n /** Maximum number of retries for rate-limited requests (default 3) */\n maxRetries?: number;\n}\n\nexport const DEFAULT_BASE_URL = 'https://api.planescrm.com/api/v1';\nexport const DEFAULT_TIMEOUT = 30_000;\nexport const DEFAULT_MAX_RETRIES = 3;\n","/**\n * Base error class for all Planes SDK errors.\n */\nexport class BlueprintError extends Error {\n public readonly status: number;\n public readonly code: string;\n\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = 'BlueprintError';\n this.status = status;\n this.code = code;\n }\n}\n\n/**\n * Thrown when authentication fails (401).\n */\nexport class AuthenticationError extends BlueprintError {\n constructor(message = 'Authentication failed') {\n super(message, 401, 'authentication_error');\n this.name = 'AuthenticationError';\n }\n}\n\n/**\n * Thrown when the user lacks permission (403).\n */\nexport class AuthorizationError extends BlueprintError {\n constructor(message = 'Insufficient permissions') {\n super(message, 403, 'authorization_error');\n this.name = 'AuthorizationError';\n }\n}\n\n/**\n * Thrown when a resource is not found (404).\n */\nexport class NotFoundError extends BlueprintError {\n constructor(message = 'Resource not found') {\n super(message, 404, 'not_found');\n this.name = 'NotFoundError';\n }\n}\n\n/** Field-level validation error detail. */\nexport interface FieldError {\n field: string;\n message: string;\n code?: string;\n}\n\n/**\n * Thrown for validation failures (400) with field-level error details.\n */\nexport class ValidationError extends BlueprintError {\n public readonly fieldErrors: FieldError[];\n\n constructor(message = 'Validation failed', fieldErrors: FieldError[] = []) {\n super(message, 400, 'validation_error');\n this.name = 'ValidationError';\n this.fieldErrors = fieldErrors;\n }\n}\n\n/**\n * Thrown when rate limited (429). Includes retryAfter in seconds.\n */\nexport class RateLimitError extends BlueprintError {\n public readonly retryAfter: number;\n\n constructor(retryAfter = 1, message = 'Rate limit exceeded') {\n super(message, 429, 'rate_limit_error');\n this.name = 'RateLimitError';\n this.retryAfter = retryAfter;\n }\n}\n\n/**\n * Thrown for server errors (500+).\n */\nexport class ServerError extends BlueprintError {\n constructor(message = 'Internal server error') {\n super(message, 500, 'server_error');\n this.name = 'ServerError';\n }\n}\n\n/**\n * Thrown when webhook signature verification fails.\n */\nexport class WebhookVerificationError extends BlueprintError {\n constructor(message = 'Webhook signature verification failed') {\n super(message, 400, 'webhook_verification_error');\n this.name = 'WebhookVerificationError';\n }\n}\n\n/**\n * Thrown for OAuth flow errors.\n */\nexport class OAuthError extends BlueprintError {\n public readonly errorCode: string;\n\n constructor(errorCode: string, message = 'OAuth error') {\n super(message, 400, 'oauth_error');\n this.name = 'OAuthError';\n this.errorCode = errorCode;\n }\n}\n","import {\n BlueprintConfig,\n DEFAULT_BASE_URL,\n DEFAULT_TIMEOUT,\n DEFAULT_MAX_RETRIES,\n} from './config';\nimport {\n BlueprintError,\n AuthenticationError,\n AuthorizationError,\n NotFoundError,\n ValidationError,\n RateLimitError,\n ServerError,\n} from './errors';\nimport type { FieldError } from './errors';\n\nexport interface HttpRequestOptions {\n method: string;\n path: string;\n body?: unknown;\n params?: Record<string, unknown>;\n headers?: Record<string, string>;\n}\n\n/**\n * Internal HTTP client wrapper using native fetch.\n * Handles authentication, rate limit retries, and error mapping.\n */\nexport class HttpClient {\n private readonly baseUrl: string;\n private readonly accessToken: string | undefined;\n private readonly apiKey: string | undefined;\n private readonly timeout: number;\n private readonly retryOnRateLimit: boolean;\n private readonly maxRetries: number;\n\n constructor(config: BlueprintConfig) {\n this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.accessToken = config.accessToken;\n this.apiKey = config.apiKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.retryOnRateLimit = config.retryOnRateLimit !== false;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n }\n\n async request<T>(options: HttpRequestOptions): Promise<T> {\n const url = this.buildUrl(options.path, options.params);\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.getAuthHeaders(),\n ...options.headers,\n };\n\n const fetchOptions: RequestInit = {\n method: options.method,\n headers,\n signal: AbortSignal.timeout(this.timeout),\n };\n\n if (options.body !== undefined) {\n fetchOptions.body = JSON.stringify(options.body);\n }\n\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const response = await fetch(url, fetchOptions);\n\n if (response.ok) {\n // 204 No Content\n if (response.status === 204) {\n return undefined as T;\n }\n return (await response.json()) as T;\n }\n\n if (response.status === 429 && this.retryOnRateLimit && attempt < this.maxRetries) {\n const retryAfter = this.parseRetryAfter(response);\n await this.sleep(retryAfter * 1000);\n continue;\n }\n\n lastError = await this.mapError(response);\n\n // Only retry on 429 when enabled; all other errors fail immediately\n if (response.status !== 429 || !this.retryOnRateLimit) {\n throw lastError;\n }\n }\n\n // All retries exhausted for 429\n throw lastError ?? new RateLimitError();\n }\n\n getAuthHeaders(): Record<string, string> {\n if (this.accessToken) {\n return { Authorization: `Bearer ${this.accessToken}` };\n }\n if (this.apiKey) {\n return { 'X-API-Key': this.apiKey };\n }\n return {};\n }\n\n private buildUrl(path: string, params?: Record<string, unknown>): string {\n const url = new URL(`${this.baseUrl}${path}`);\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n return url.toString();\n }\n\n private parseRetryAfter(response: Response): number {\n const header = response.headers.get('Retry-After');\n if (header) {\n const seconds = Number(header);\n if (!Number.isNaN(seconds) && seconds > 0) {\n return seconds;\n }\n }\n return 1; // Default 1 second\n }\n\n private async mapError(response: Response): Promise<BlueprintError> {\n let body: Record<string, unknown> = {};\n try {\n body = (await response.json()) as Record<string, unknown>;\n } catch {\n // Ignore JSON parse errors\n }\n\n const message = (body['message'] as string) ?? response.statusText ?? 'Request failed';\n\n switch (response.status) {\n case 400: {\n const fieldErrors = (body['errors'] as FieldError[] | undefined) ?? [];\n return new ValidationError(message, fieldErrors);\n }\n case 401:\n return new AuthenticationError(message);\n case 403:\n return new AuthorizationError(message);\n case 404:\n return new NotFoundError(message);\n case 429: {\n const retryAfter = this.parseRetryAfter(response);\n return new RateLimitError(retryAfter, message);\n }\n default:\n if (response.status >= 500) {\n return new ServerError(message);\n }\n return new BlueprintError(message, response.status, 'unknown_error');\n }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","import { HttpClient } from '../http';\nimport type { PaginatedResponse, ListParams } from '../types';\n\n/**\n * Abstract base resource with standard CRUD methods.\n * Generic parameters:\n * - TEntity: the entity response type\n * - TCreate: the create request body type\n * - TUpdate: the update request body type\n */\nexport abstract class BaseResource<\n TEntity = Record<string, unknown>,\n TCreate = Record<string, unknown>,\n TUpdate = Record<string, unknown>,\n> {\n constructor(\n protected readonly http: HttpClient,\n protected readonly basePath: string,\n ) {}\n\n /**\n * List entities with optional filters and pagination.\n */\n async list(params?: ListParams): Promise<PaginatedResponse<TEntity>> {\n return this.http.request<PaginatedResponse<TEntity>>({\n method: 'GET',\n path: this.basePath,\n params: params as Record<string, unknown>,\n });\n }\n\n /**\n * Get a single entity by ID.\n */\n async get(id: string): Promise<TEntity> {\n return this.http.request<TEntity>({\n method: 'GET',\n path: `${this.basePath}/${id}`,\n });\n }\n\n /**\n * Create a new entity.\n */\n async create(data: TCreate): Promise<TEntity> {\n return this.http.request<TEntity>({\n method: 'POST',\n path: this.basePath,\n body: data,\n });\n }\n\n /**\n * Update an existing entity.\n */\n async update(id: string, data: TUpdate): Promise<TEntity> {\n return this.http.request<TEntity>({\n method: 'PATCH',\n path: `${this.basePath}/${id}`,\n body: data,\n });\n }\n\n /**\n * Delete an entity by ID.\n */\n async delete(id: string): Promise<void> {\n await this.http.request<void>({\n method: 'DELETE',\n path: `${this.basePath}/${id}`,\n });\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Opportunity resource — CRUD operations on /opportunities. */\nexport class OpportunityResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/opportunities');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Customer resource — CRUD operations on /customers. */\nexport class CustomerResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/customers');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Property resource — CRUD operations on /properties. */\nexport class PropertyResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/properties');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Calendar event resource — CRUD operations on /calendar-events. */\nexport class CalendarEventResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/calendar-events');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Contract resource — CRUD operations on /contracts. */\nexport class ContractResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/contracts');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Job resource — CRUD operations on /jobs. */\nexport class JobResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/jobs');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Estimate resource — CRUD operations on /estimates. */\nexport class EstimateResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/estimates');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Invoice resource — CRUD operations on /invoices. */\nexport class InvoiceResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/invoices');\n }\n}\n","import { BaseResource } from './base-resource';\n\n/** Inventory resource — CRUD operations on /inventory/items. */\nexport class InventoryResource extends BaseResource {\n constructor(http: ConstructorParameters<typeof BaseResource>[0]) {\n super(http, '/inventory/items');\n }\n}\n","import type { BlueprintConfig } from './config';\nimport { HttpClient } from './http';\nimport { OpportunityResource } from './resources/opportunities';\nimport { CustomerResource } from './resources/customers';\nimport { PropertyResource } from './resources/properties';\nimport { CalendarEventResource } from './resources/calendar-events';\nimport { ContractResource } from './resources/contracts';\nimport { JobResource } from './resources/jobs';\nimport { EstimateResource } from './resources/estimates';\nimport { InvoiceResource } from './resources/invoices';\nimport { InventoryResource } from './resources/inventory';\n\n/**\n * BlueprintClient — typed API client for the Planes CRM platform.\n *\n * @example\n * ```ts\n * const client = new BlueprintClient({ accessToken: 'tok_...' });\n * const opps = await client.opportunities.list({ page: 1, limit: 20 });\n * ```\n */\nexport class BlueprintClient {\n private readonly http: HttpClient;\n private _opportunities?: OpportunityResource;\n private _customers?: CustomerResource;\n private _properties?: PropertyResource;\n private _calendarEvents?: CalendarEventResource;\n private _contracts?: ContractResource;\n private _jobs?: JobResource;\n private _estimates?: EstimateResource;\n private _invoices?: InvoiceResource;\n private _inventory?: InventoryResource;\n\n constructor(config: BlueprintConfig) {\n if (!config.accessToken && !config.apiKey) {\n throw new Error('BlueprintClient requires either accessToken or apiKey');\n }\n if (config.accessToken && config.apiKey) {\n throw new Error('BlueprintClient requires either accessToken or apiKey, not both');\n }\n this.http = new HttpClient(config);\n }\n\n get opportunities(): OpportunityResource {\n if (!this._opportunities) {\n this._opportunities = new OpportunityResource(this.http);\n }\n return this._opportunities;\n }\n\n get customers(): CustomerResource {\n if (!this._customers) {\n this._customers = new CustomerResource(this.http);\n }\n return this._customers;\n }\n\n get properties(): PropertyResource {\n if (!this._properties) {\n this._properties = new PropertyResource(this.http);\n }\n return this._properties;\n }\n\n get calendarEvents(): CalendarEventResource {\n if (!this._calendarEvents) {\n this._calendarEvents = new CalendarEventResource(this.http);\n }\n return this._calendarEvents;\n }\n\n get contracts(): ContractResource {\n if (!this._contracts) {\n this._contracts = new ContractResource(this.http);\n }\n return this._contracts;\n }\n\n get jobs(): JobResource {\n if (!this._jobs) {\n this._jobs = new JobResource(this.http);\n }\n return this._jobs;\n }\n\n get estimates(): EstimateResource {\n if (!this._estimates) {\n this._estimates = new EstimateResource(this.http);\n }\n return this._estimates;\n }\n\n get invoices(): InvoiceResource {\n if (!this._invoices) {\n this._invoices = new InvoiceResource(this.http);\n }\n return this._invoices;\n }\n\n get inventory(): InventoryResource {\n if (!this._inventory) {\n this._inventory = new InventoryResource(this.http);\n }\n return this._inventory;\n }\n}\n","import * as http from 'node:http';\nimport type { ActionHandler, ActionContext, ActionResult } from './types/agent-types';\n\n/**\n * Options for Blueprint.register().\n */\nexport interface RegisterOptions {\n /** Port to listen on (default 3000). */\n port?: number;\n /** HTTP path to listen on (default '/actions'). */\n path?: string;\n /** Whether to verify incoming request signatures (default true). */\n verifySignature?: boolean;\n /** Error callback for unhandled errors. */\n onError?: (error: Error) => void;\n}\n\n/**\n * Blueprint agent registration namespace.\n * Provides a convenient way to register action handlers as an HTTP server.\n */\nexport const Blueprint = {\n /**\n * Register action handlers and start an HTTP server to receive action requests.\n *\n * @example\n * ```ts\n * Blueprint.register([\n * {\n * type: 'send_email',\n * async execute(ctx) {\n * // ... send email logic\n * return { status: 'completed', output: { sent: true } };\n * },\n * },\n * ], { port: 3000 });\n * ```\n */\n register(handlers: ActionHandler[], options?: RegisterOptions): http.Server {\n const port = options?.port ?? 3000;\n const basePath = options?.path ?? '/actions';\n const onError = options?.onError;\n\n // Build handler map for O(1) lookup\n const handlerMap = new Map<string, ActionHandler>();\n for (const handler of handlers) {\n handlerMap.set(handler.type, handler);\n }\n\n const server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {\n // Only handle POST to the configured path\n if (req.method !== 'POST' || req.url !== basePath) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n return;\n }\n\n try {\n const body = await readBody(req);\n\n let context: ActionContext;\n try {\n context = JSON.parse(body) as ActionContext;\n } catch {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Malformed request body' }));\n return;\n }\n\n // Validate minimum required fields\n if (!context.action?.type) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Missing action.type in request body' }));\n return;\n }\n\n const handler = handlerMap.get(context.action.type);\n if (!handler) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: `No handler registered for action type: ${context.action.type}`,\n }),\n );\n return;\n }\n\n let result: ActionResult;\n try {\n result = await handler.execute(context);\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : 'Unknown handler error';\n if (onError && err instanceof Error) {\n onError(err);\n }\n result = {\n status: 'failed',\n error: errorMessage,\n retryable: false,\n };\n }\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(result));\n } catch (err) {\n if (onError && err instanceof Error) {\n onError(err);\n }\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Internal server error' }));\n }\n });\n\n server.listen(port);\n\n // Graceful shutdown\n const shutdown = () => {\n server.close();\n };\n process.on('SIGTERM', shutdown);\n process.on('SIGINT', shutdown);\n\n return server;\n },\n};\n\nfunction readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString()));\n req.on('error', reject);\n });\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport { WebhookVerificationError } from './errors';\n\n/**\n * Typed webhook event payload from Planes.\n */\nexport interface WebhookEvent<T = unknown> {\n /** Event ID (e.g. evt_abc123). */\n id: string;\n /** Event type (e.g. opportunity.created). */\n type: string;\n /** Tenant ID that owns this event. */\n tenant_id: string;\n /** ISO 8601 timestamp of when the event occurred. */\n timestamp: string;\n /** Entity snapshot at the time of the event. */\n data: T;\n /** Metadata about the event trigger. */\n metadata: {\n correlation_id: string;\n triggered_by: 'workflow' | 'api' | 'user';\n };\n}\n\n/**\n * Verify an HMAC-SHA256 webhook signature using timing-safe comparison.\n *\n * The signature header format is: `sha256=<hex-digest>`\n *\n * @param payload - The raw request body (string or Buffer)\n * @param signature - The X-Blueprint-Signature header value\n * @param secret - The webhook signing secret\n * @returns true if the signature is valid, false otherwise\n */\nexport function verifyWebhookSignature(\n payload: string | Buffer,\n signature: string,\n secret: string,\n): boolean {\n if (!signature || !secret || !payload) {\n return false;\n }\n\n // Expect format: sha256=<hex>\n const prefix = 'sha256=';\n if (!signature.startsWith(prefix)) {\n return false;\n }\n\n const receivedHex = signature.slice(prefix.length);\n if (!receivedHex) {\n return false;\n }\n\n const expectedHmac = createHmac('sha256', secret)\n .update(payload)\n .digest('hex');\n\n // Convert to buffers for timing-safe comparison\n const receivedBuf = Buffer.from(receivedHex, 'hex');\n const expectedBuf = Buffer.from(expectedHmac, 'hex');\n\n if (receivedBuf.length !== expectedBuf.length) {\n return false;\n }\n\n return timingSafeEqual(receivedBuf, expectedBuf);\n}\n\n/**\n * Parse and verify a webhook event payload.\n *\n * @param body - The raw request body as a string\n * @param signature - The X-Blueprint-Signature header value\n * @param secret - The webhook signing secret\n * @returns The parsed and typed webhook event\n * @throws WebhookVerificationError if signature is invalid\n */\nexport function parseWebhookEvent<T = unknown>(\n body: string,\n signature: string,\n secret: string,\n): WebhookEvent<T> {\n if (!verifyWebhookSignature(body, signature, secret)) {\n throw new WebhookVerificationError();\n }\n\n return JSON.parse(body) as WebhookEvent<T>;\n}\n","import { randomBytes } from 'node:crypto';\nimport { OAuthError } from './errors';\n\n/**\n * Options for building an OAuth authorization URL.\n */\nexport interface AuthorizeOptions {\n /** Planes API base URL (e.g. https://api.planescrm.com/api/v1). */\n baseUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** Redirect URI after authorization. */\n redirectUri: string;\n /** Requested OAuth scopes. */\n scopes: string[];\n /** CSRF state parameter (auto-generated if not provided). */\n state?: string;\n}\n\n/**\n * Options for exchanging an authorization code for tokens.\n */\nexport interface ExchangeCodeOptions {\n /** Planes API base URL. */\n baseUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** OAuth client secret. */\n clientSecret: string;\n /** Authorization code from the callback. */\n code: string;\n /** Redirect URI (must match the one used in authorization). */\n redirectUri: string;\n}\n\n/**\n * Options for refreshing an access token.\n */\nexport interface RefreshOptions {\n /** Planes API base URL. */\n baseUrl: string;\n /** OAuth client ID. */\n clientId: string;\n /** OAuth client secret. */\n clientSecret: string;\n /** Refresh token. */\n refreshToken: string;\n}\n\n/**\n * OAuth token response.\n */\nexport interface TokenResponse {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n token_type: string;\n scope: string;\n}\n\n/**\n * Build an OAuth authorization URL for the Planes platform.\n *\n * @param options - Authorization options\n * @returns The full authorization URL to redirect the user to\n */\nexport function buildAuthorizeUrl(options: AuthorizeOptions): string {\n const state = options.state ?? randomBytes(16).toString('hex');\n const baseUrl = options.baseUrl.replace(/\\/+$/, '');\n\n const url = new URL(`${baseUrl}/oauth/authorize`);\n url.searchParams.set('client_id', options.clientId);\n url.searchParams.set('redirect_uri', options.redirectUri);\n url.searchParams.set('scope', options.scopes.join(' '));\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('state', state);\n\n return url.toString();\n}\n\n/**\n * Exchange an authorization code for access and refresh tokens.\n *\n * @param options - Exchange options\n * @returns Token response with access_token, refresh_token, etc.\n * @throws OAuthError on failure\n */\nexport async function exchangeCode(options: ExchangeCodeOptions): Promise<TokenResponse> {\n const baseUrl = options.baseUrl.replace(/\\/+$/, '');\n const url = `${baseUrl}/oauth/token`;\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: options.clientId,\n client_secret: options.clientSecret,\n code: options.code,\n redirect_uri: options.redirectUri,\n });\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorBody = await response.json().catch(() => ({})) as Record<string, unknown>;\n const errorCode = (errorBody['error'] as string) ?? 'unknown_error';\n const errorDescription = (errorBody['error_description'] as string) ?? 'Token exchange failed';\n throw new OAuthError(errorCode, errorDescription);\n }\n\n return (await response.json()) as TokenResponse;\n}\n\n/**\n * Refresh an access token using a refresh token.\n *\n * @param options - Refresh options\n * @returns New token response\n * @throws OAuthError on failure\n */\nexport async function refreshAccessToken(options: RefreshOptions): Promise<TokenResponse> {\n const baseUrl = options.baseUrl.replace(/\\/+$/, '');\n const url = `${baseUrl}/oauth/token`;\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: options.clientId,\n client_secret: options.clientSecret,\n refresh_token: options.refreshToken,\n });\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorBody = await response.json().catch(() => ({})) as Record<string, unknown>;\n const errorCode = (errorBody['error'] as string) ?? 'unknown_error';\n const errorDescription = (errorBody['error_description'] as string) ?? 'Token refresh failed';\n throw new OAuthError(errorCode, errorDescription);\n }\n\n return (await response.json()) as TokenResponse;\n}\n"],"mappings":"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","names":["http","http","http","http","http","http","http","http","http","http","import_node_crypto"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://api.planescrm.com/api/v1";
|
|
3
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
4
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var BlueprintError = class extends Error {
|
|
8
|
+
constructor(message, status, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "BlueprintError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var AuthenticationError = class extends BlueprintError {
|
|
16
|
+
constructor(message = "Authentication failed") {
|
|
17
|
+
super(message, 401, "authentication_error");
|
|
18
|
+
this.name = "AuthenticationError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var AuthorizationError = class extends BlueprintError {
|
|
22
|
+
constructor(message = "Insufficient permissions") {
|
|
23
|
+
super(message, 403, "authorization_error");
|
|
24
|
+
this.name = "AuthorizationError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var NotFoundError = class extends BlueprintError {
|
|
28
|
+
constructor(message = "Resource not found") {
|
|
29
|
+
super(message, 404, "not_found");
|
|
30
|
+
this.name = "NotFoundError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var ValidationError = class extends BlueprintError {
|
|
34
|
+
constructor(message = "Validation failed", fieldErrors = []) {
|
|
35
|
+
super(message, 400, "validation_error");
|
|
36
|
+
this.name = "ValidationError";
|
|
37
|
+
this.fieldErrors = fieldErrors;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var RateLimitError = class extends BlueprintError {
|
|
41
|
+
constructor(retryAfter = 1, message = "Rate limit exceeded") {
|
|
42
|
+
super(message, 429, "rate_limit_error");
|
|
43
|
+
this.name = "RateLimitError";
|
|
44
|
+
this.retryAfter = retryAfter;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var ServerError = class extends BlueprintError {
|
|
48
|
+
constructor(message = "Internal server error") {
|
|
49
|
+
super(message, 500, "server_error");
|
|
50
|
+
this.name = "ServerError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var WebhookVerificationError = class extends BlueprintError {
|
|
54
|
+
constructor(message = "Webhook signature verification failed") {
|
|
55
|
+
super(message, 400, "webhook_verification_error");
|
|
56
|
+
this.name = "WebhookVerificationError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var OAuthError = class extends BlueprintError {
|
|
60
|
+
constructor(errorCode, message = "OAuth error") {
|
|
61
|
+
super(message, 400, "oauth_error");
|
|
62
|
+
this.name = "OAuthError";
|
|
63
|
+
this.errorCode = errorCode;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/http.ts
|
|
68
|
+
var HttpClient = class {
|
|
69
|
+
constructor(config) {
|
|
70
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
71
|
+
this.accessToken = config.accessToken;
|
|
72
|
+
this.apiKey = config.apiKey;
|
|
73
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
74
|
+
this.retryOnRateLimit = config.retryOnRateLimit !== false;
|
|
75
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
76
|
+
}
|
|
77
|
+
async request(options) {
|
|
78
|
+
const url = this.buildUrl(options.path, options.params);
|
|
79
|
+
const headers = {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
...this.getAuthHeaders(),
|
|
82
|
+
...options.headers
|
|
83
|
+
};
|
|
84
|
+
const fetchOptions = {
|
|
85
|
+
method: options.method,
|
|
86
|
+
headers,
|
|
87
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
88
|
+
};
|
|
89
|
+
if (options.body !== void 0) {
|
|
90
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
91
|
+
}
|
|
92
|
+
let lastError;
|
|
93
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
94
|
+
const response = await fetch(url, fetchOptions);
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
if (response.status === 204) {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
return await response.json();
|
|
100
|
+
}
|
|
101
|
+
if (response.status === 429 && this.retryOnRateLimit && attempt < this.maxRetries) {
|
|
102
|
+
const retryAfter = this.parseRetryAfter(response);
|
|
103
|
+
await this.sleep(retryAfter * 1e3);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
lastError = await this.mapError(response);
|
|
107
|
+
if (response.status !== 429 || !this.retryOnRateLimit) {
|
|
108
|
+
throw lastError;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw lastError ?? new RateLimitError();
|
|
112
|
+
}
|
|
113
|
+
getAuthHeaders() {
|
|
114
|
+
if (this.accessToken) {
|
|
115
|
+
return { Authorization: `Bearer ${this.accessToken}` };
|
|
116
|
+
}
|
|
117
|
+
if (this.apiKey) {
|
|
118
|
+
return { "X-API-Key": this.apiKey };
|
|
119
|
+
}
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
buildUrl(path, params) {
|
|
123
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
124
|
+
if (params) {
|
|
125
|
+
for (const [key, value] of Object.entries(params)) {
|
|
126
|
+
if (value !== void 0 && value !== null) {
|
|
127
|
+
url.searchParams.set(key, String(value));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return url.toString();
|
|
132
|
+
}
|
|
133
|
+
parseRetryAfter(response) {
|
|
134
|
+
const header = response.headers.get("Retry-After");
|
|
135
|
+
if (header) {
|
|
136
|
+
const seconds = Number(header);
|
|
137
|
+
if (!Number.isNaN(seconds) && seconds > 0) {
|
|
138
|
+
return seconds;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
async mapError(response) {
|
|
144
|
+
let body = {};
|
|
145
|
+
try {
|
|
146
|
+
body = await response.json();
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
const message = body["message"] ?? response.statusText ?? "Request failed";
|
|
150
|
+
switch (response.status) {
|
|
151
|
+
case 400: {
|
|
152
|
+
const fieldErrors = body["errors"] ?? [];
|
|
153
|
+
return new ValidationError(message, fieldErrors);
|
|
154
|
+
}
|
|
155
|
+
case 401:
|
|
156
|
+
return new AuthenticationError(message);
|
|
157
|
+
case 403:
|
|
158
|
+
return new AuthorizationError(message);
|
|
159
|
+
case 404:
|
|
160
|
+
return new NotFoundError(message);
|
|
161
|
+
case 429: {
|
|
162
|
+
const retryAfter = this.parseRetryAfter(response);
|
|
163
|
+
return new RateLimitError(retryAfter, message);
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
if (response.status >= 500) {
|
|
167
|
+
return new ServerError(message);
|
|
168
|
+
}
|
|
169
|
+
return new BlueprintError(message, response.status, "unknown_error");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
sleep(ms) {
|
|
173
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// src/resources/base-resource.ts
|
|
178
|
+
var BaseResource = class {
|
|
179
|
+
constructor(http2, basePath) {
|
|
180
|
+
this.http = http2;
|
|
181
|
+
this.basePath = basePath;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* List entities with optional filters and pagination.
|
|
185
|
+
*/
|
|
186
|
+
async list(params) {
|
|
187
|
+
return this.http.request({
|
|
188
|
+
method: "GET",
|
|
189
|
+
path: this.basePath,
|
|
190
|
+
params
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get a single entity by ID.
|
|
195
|
+
*/
|
|
196
|
+
async get(id) {
|
|
197
|
+
return this.http.request({
|
|
198
|
+
method: "GET",
|
|
199
|
+
path: `${this.basePath}/${id}`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create a new entity.
|
|
204
|
+
*/
|
|
205
|
+
async create(data) {
|
|
206
|
+
return this.http.request({
|
|
207
|
+
method: "POST",
|
|
208
|
+
path: this.basePath,
|
|
209
|
+
body: data
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Update an existing entity.
|
|
214
|
+
*/
|
|
215
|
+
async update(id, data) {
|
|
216
|
+
return this.http.request({
|
|
217
|
+
method: "PATCH",
|
|
218
|
+
path: `${this.basePath}/${id}`,
|
|
219
|
+
body: data
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Delete an entity by ID.
|
|
224
|
+
*/
|
|
225
|
+
async delete(id) {
|
|
226
|
+
await this.http.request({
|
|
227
|
+
method: "DELETE",
|
|
228
|
+
path: `${this.basePath}/${id}`
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/resources/opportunities.ts
|
|
234
|
+
var OpportunityResource = class extends BaseResource {
|
|
235
|
+
constructor(http2) {
|
|
236
|
+
super(http2, "/opportunities");
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/resources/customers.ts
|
|
241
|
+
var CustomerResource = class extends BaseResource {
|
|
242
|
+
constructor(http2) {
|
|
243
|
+
super(http2, "/customers");
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/resources/properties.ts
|
|
248
|
+
var PropertyResource = class extends BaseResource {
|
|
249
|
+
constructor(http2) {
|
|
250
|
+
super(http2, "/properties");
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/resources/calendar-events.ts
|
|
255
|
+
var CalendarEventResource = class extends BaseResource {
|
|
256
|
+
constructor(http2) {
|
|
257
|
+
super(http2, "/calendar-events");
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/resources/contracts.ts
|
|
262
|
+
var ContractResource = class extends BaseResource {
|
|
263
|
+
constructor(http2) {
|
|
264
|
+
super(http2, "/contracts");
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/resources/jobs.ts
|
|
269
|
+
var JobResource = class extends BaseResource {
|
|
270
|
+
constructor(http2) {
|
|
271
|
+
super(http2, "/jobs");
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/resources/estimates.ts
|
|
276
|
+
var EstimateResource = class extends BaseResource {
|
|
277
|
+
constructor(http2) {
|
|
278
|
+
super(http2, "/estimates");
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/resources/invoices.ts
|
|
283
|
+
var InvoiceResource = class extends BaseResource {
|
|
284
|
+
constructor(http2) {
|
|
285
|
+
super(http2, "/invoices");
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/resources/inventory.ts
|
|
290
|
+
var InventoryResource = class extends BaseResource {
|
|
291
|
+
constructor(http2) {
|
|
292
|
+
super(http2, "/inventory/items");
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/client.ts
|
|
297
|
+
var BlueprintClient = class {
|
|
298
|
+
constructor(config) {
|
|
299
|
+
if (!config.accessToken && !config.apiKey) {
|
|
300
|
+
throw new Error("BlueprintClient requires either accessToken or apiKey");
|
|
301
|
+
}
|
|
302
|
+
if (config.accessToken && config.apiKey) {
|
|
303
|
+
throw new Error("BlueprintClient requires either accessToken or apiKey, not both");
|
|
304
|
+
}
|
|
305
|
+
this.http = new HttpClient(config);
|
|
306
|
+
}
|
|
307
|
+
get opportunities() {
|
|
308
|
+
if (!this._opportunities) {
|
|
309
|
+
this._opportunities = new OpportunityResource(this.http);
|
|
310
|
+
}
|
|
311
|
+
return this._opportunities;
|
|
312
|
+
}
|
|
313
|
+
get customers() {
|
|
314
|
+
if (!this._customers) {
|
|
315
|
+
this._customers = new CustomerResource(this.http);
|
|
316
|
+
}
|
|
317
|
+
return this._customers;
|
|
318
|
+
}
|
|
319
|
+
get properties() {
|
|
320
|
+
if (!this._properties) {
|
|
321
|
+
this._properties = new PropertyResource(this.http);
|
|
322
|
+
}
|
|
323
|
+
return this._properties;
|
|
324
|
+
}
|
|
325
|
+
get calendarEvents() {
|
|
326
|
+
if (!this._calendarEvents) {
|
|
327
|
+
this._calendarEvents = new CalendarEventResource(this.http);
|
|
328
|
+
}
|
|
329
|
+
return this._calendarEvents;
|
|
330
|
+
}
|
|
331
|
+
get contracts() {
|
|
332
|
+
if (!this._contracts) {
|
|
333
|
+
this._contracts = new ContractResource(this.http);
|
|
334
|
+
}
|
|
335
|
+
return this._contracts;
|
|
336
|
+
}
|
|
337
|
+
get jobs() {
|
|
338
|
+
if (!this._jobs) {
|
|
339
|
+
this._jobs = new JobResource(this.http);
|
|
340
|
+
}
|
|
341
|
+
return this._jobs;
|
|
342
|
+
}
|
|
343
|
+
get estimates() {
|
|
344
|
+
if (!this._estimates) {
|
|
345
|
+
this._estimates = new EstimateResource(this.http);
|
|
346
|
+
}
|
|
347
|
+
return this._estimates;
|
|
348
|
+
}
|
|
349
|
+
get invoices() {
|
|
350
|
+
if (!this._invoices) {
|
|
351
|
+
this._invoices = new InvoiceResource(this.http);
|
|
352
|
+
}
|
|
353
|
+
return this._invoices;
|
|
354
|
+
}
|
|
355
|
+
get inventory() {
|
|
356
|
+
if (!this._inventory) {
|
|
357
|
+
this._inventory = new InventoryResource(this.http);
|
|
358
|
+
}
|
|
359
|
+
return this._inventory;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/agent.ts
|
|
364
|
+
import * as http from "http";
|
|
365
|
+
var Blueprint = {
|
|
366
|
+
/**
|
|
367
|
+
* Register action handlers and start an HTTP server to receive action requests.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```ts
|
|
371
|
+
* Blueprint.register([
|
|
372
|
+
* {
|
|
373
|
+
* type: 'send_email',
|
|
374
|
+
* async execute(ctx) {
|
|
375
|
+
* // ... send email logic
|
|
376
|
+
* return { status: 'completed', output: { sent: true } };
|
|
377
|
+
* },
|
|
378
|
+
* },
|
|
379
|
+
* ], { port: 3000 });
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
register(handlers, options) {
|
|
383
|
+
const port = options?.port ?? 3e3;
|
|
384
|
+
const basePath = options?.path ?? "/actions";
|
|
385
|
+
const onError = options?.onError;
|
|
386
|
+
const handlerMap = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const handler of handlers) {
|
|
388
|
+
handlerMap.set(handler.type, handler);
|
|
389
|
+
}
|
|
390
|
+
const server = http.createServer(async (req, res) => {
|
|
391
|
+
if (req.method !== "POST" || req.url !== basePath) {
|
|
392
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
393
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const body = await readBody(req);
|
|
398
|
+
let context;
|
|
399
|
+
try {
|
|
400
|
+
context = JSON.parse(body);
|
|
401
|
+
} catch {
|
|
402
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
403
|
+
res.end(JSON.stringify({ error: "Malformed request body" }));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (!context.action?.type) {
|
|
407
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
408
|
+
res.end(JSON.stringify({ error: "Missing action.type in request body" }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const handler = handlerMap.get(context.action.type);
|
|
412
|
+
if (!handler) {
|
|
413
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
414
|
+
res.end(
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
error: `No handler registered for action type: ${context.action.type}`
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let result;
|
|
422
|
+
try {
|
|
423
|
+
result = await handler.execute(context);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown handler error";
|
|
426
|
+
if (onError && err instanceof Error) {
|
|
427
|
+
onError(err);
|
|
428
|
+
}
|
|
429
|
+
result = {
|
|
430
|
+
status: "failed",
|
|
431
|
+
error: errorMessage,
|
|
432
|
+
retryable: false
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
436
|
+
res.end(JSON.stringify(result));
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (onError && err instanceof Error) {
|
|
439
|
+
onError(err);
|
|
440
|
+
}
|
|
441
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
442
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
server.listen(port);
|
|
446
|
+
const shutdown = () => {
|
|
447
|
+
server.close();
|
|
448
|
+
};
|
|
449
|
+
process.on("SIGTERM", shutdown);
|
|
450
|
+
process.on("SIGINT", shutdown);
|
|
451
|
+
return server;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
function readBody(req) {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const chunks = [];
|
|
457
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
458
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
459
|
+
req.on("error", reject);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/webhooks.ts
|
|
464
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
465
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
466
|
+
if (!signature || !secret || !payload) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const prefix = "sha256=";
|
|
470
|
+
if (!signature.startsWith(prefix)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const receivedHex = signature.slice(prefix.length);
|
|
474
|
+
if (!receivedHex) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const expectedHmac = createHmac("sha256", secret).update(payload).digest("hex");
|
|
478
|
+
const receivedBuf = Buffer.from(receivedHex, "hex");
|
|
479
|
+
const expectedBuf = Buffer.from(expectedHmac, "hex");
|
|
480
|
+
if (receivedBuf.length !== expectedBuf.length) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
return timingSafeEqual(receivedBuf, expectedBuf);
|
|
484
|
+
}
|
|
485
|
+
function parseWebhookEvent(body, signature, secret) {
|
|
486
|
+
if (!verifyWebhookSignature(body, signature, secret)) {
|
|
487
|
+
throw new WebhookVerificationError();
|
|
488
|
+
}
|
|
489
|
+
return JSON.parse(body);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/oauth.ts
|
|
493
|
+
import { randomBytes } from "crypto";
|
|
494
|
+
function buildAuthorizeUrl(options) {
|
|
495
|
+
const state = options.state ?? randomBytes(16).toString("hex");
|
|
496
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
497
|
+
const url = new URL(`${baseUrl}/oauth/authorize`);
|
|
498
|
+
url.searchParams.set("client_id", options.clientId);
|
|
499
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
500
|
+
url.searchParams.set("scope", options.scopes.join(" "));
|
|
501
|
+
url.searchParams.set("response_type", "code");
|
|
502
|
+
url.searchParams.set("state", state);
|
|
503
|
+
return url.toString();
|
|
504
|
+
}
|
|
505
|
+
async function exchangeCode(options) {
|
|
506
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
507
|
+
const url = `${baseUrl}/oauth/token`;
|
|
508
|
+
const body = new URLSearchParams({
|
|
509
|
+
grant_type: "authorization_code",
|
|
510
|
+
client_id: options.clientId,
|
|
511
|
+
client_secret: options.clientSecret,
|
|
512
|
+
code: options.code,
|
|
513
|
+
redirect_uri: options.redirectUri
|
|
514
|
+
});
|
|
515
|
+
const response = await fetch(url, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
518
|
+
body: body.toString()
|
|
519
|
+
});
|
|
520
|
+
if (!response.ok) {
|
|
521
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
522
|
+
const errorCode = errorBody["error"] ?? "unknown_error";
|
|
523
|
+
const errorDescription = errorBody["error_description"] ?? "Token exchange failed";
|
|
524
|
+
throw new OAuthError(errorCode, errorDescription);
|
|
525
|
+
}
|
|
526
|
+
return await response.json();
|
|
527
|
+
}
|
|
528
|
+
async function refreshAccessToken(options) {
|
|
529
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
530
|
+
const url = `${baseUrl}/oauth/token`;
|
|
531
|
+
const body = new URLSearchParams({
|
|
532
|
+
grant_type: "refresh_token",
|
|
533
|
+
client_id: options.clientId,
|
|
534
|
+
client_secret: options.clientSecret,
|
|
535
|
+
refresh_token: options.refreshToken
|
|
536
|
+
});
|
|
537
|
+
const response = await fetch(url, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
540
|
+
body: body.toString()
|
|
541
|
+
});
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
544
|
+
const errorCode = errorBody["error"] ?? "unknown_error";
|
|
545
|
+
const errorDescription = errorBody["error_description"] ?? "Token refresh failed";
|
|
546
|
+
throw new OAuthError(errorCode, errorDescription);
|
|
547
|
+
}
|
|
548
|
+
return await response.json();
|
|
549
|
+
}
|
|
550
|
+
export {
|
|
551
|
+
AuthenticationError,
|
|
552
|
+
AuthorizationError,
|
|
553
|
+
BaseResource,
|
|
554
|
+
Blueprint,
|
|
555
|
+
BlueprintClient,
|
|
556
|
+
BlueprintError,
|
|
557
|
+
CalendarEventResource,
|
|
558
|
+
ContractResource,
|
|
559
|
+
CustomerResource,
|
|
560
|
+
DEFAULT_BASE_URL,
|
|
561
|
+
DEFAULT_MAX_RETRIES,
|
|
562
|
+
DEFAULT_TIMEOUT,
|
|
563
|
+
EstimateResource,
|
|
564
|
+
InventoryResource,
|
|
565
|
+
InvoiceResource,
|
|
566
|
+
JobResource,
|
|
567
|
+
NotFoundError,
|
|
568
|
+
OAuthError,
|
|
569
|
+
OpportunityResource,
|
|
570
|
+
PropertyResource,
|
|
571
|
+
RateLimitError,
|
|
572
|
+
ServerError,
|
|
573
|
+
ValidationError,
|
|
574
|
+
WebhookVerificationError,
|
|
575
|
+
buildAuthorizeUrl,
|
|
576
|
+
exchangeCode,
|
|
577
|
+
parseWebhookEvent,
|
|
578
|
+
refreshAccessToken,
|
|
579
|
+
verifyWebhookSignature
|
|
580
|
+
};
|
|
581
|
+
//# sourceMappingURL=index.mjs.map
|