@churchapps/integration-sdk 0.2.0 → 0.2.1
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.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +65 -65
package/dist/index.cjs
CHANGED
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/types/webhooks.ts","../src/types/oauth.ts","../src/types/rest.ts","../src/webhooks/WebhookVerifier.ts","../src/webhooks/expressMiddleware.ts","../src/rest/B1ApiError.ts","../src/rest/B1RestClient.ts","../src/oauth/B1OAuthClient.ts"],"sourcesContent":["/** `@churchapps/integration-sdk` — toolkit for building B1.church integrations. */\r\nexport const VERSION = \"0.1.0\";\r\n\r\nexport * from \"./types\";\r\nexport * from \"./webhooks\";\r\nexport * from \"./rest\";\r\nexport * from \"./oauth\";\r\n","// Webhook event names, the delivery envelope, and per-event `data` shapes.\r\n// These mirror the B1 Api — `shared/webhooks/WebhookEvents.ts` for the names\r\n// and `WebhookSamplePayloads.ts` for the data shapes.\r\n\r\n/** Every webhook event B1 can emit. */\r\nexport type B1WebhookEventName =\r\n | \"person.created\" | \"person.updated\" | \"person.destroyed\"\r\n | \"group.created\" | \"group.updated\" | \"group.destroyed\"\r\n | \"group.member.added\" | \"group.member.removed\"\r\n | \"household.created\" | \"household.updated\" | \"household.destroyed\"\r\n | \"donation.created\" | \"donation.updated\"\r\n | \"attendance.recorded\"\r\n | \"session.created\"\r\n | \"form.submission.created\"\r\n | \"event.created\" | \"event.updated\" | \"event.destroyed\";\r\n\r\n/** The HTTP headers B1 sends with every webhook delivery. */\r\nexport const WEBHOOK_HEADERS = {\r\n signature: \"X-B1-Signature\",\r\n event: \"X-B1-Event\",\r\n deliveryId: \"X-B1-Delivery-Id\",\r\n timestamp: \"X-B1-Timestamp\"\r\n} as const;\r\n\r\n/** The JSON body B1 POSTs to a subscriber URL. */\r\nexport interface B1WebhookEnvelope<T = unknown> {\r\n event: B1WebhookEventName;\r\n churchId: string;\r\n /** ISO 8601 timestamp. */\r\n occurredAt: string;\r\n data: T;\r\n}\r\n\r\n// --- per-event data shapes ------------------------------------------------\r\n// `destroyed` events carry only `{ id, churchId }`, so the descriptive fields\r\n// are optional on the shared shape.\r\n\r\nexport interface PersonWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: { display?: string; first?: string; last?: string };\r\n contactInfo?: { email?: string };\r\n}\r\n\r\nexport interface GroupWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n categoryName?: string;\r\n}\r\n\r\nexport interface GroupMemberWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId: string;\r\n personId: string;\r\n}\r\n\r\nexport interface HouseholdWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n}\r\n\r\nexport interface DonationWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n batchId?: string;\r\n donationDate?: string;\r\n amount?: number;\r\n currency?: string;\r\n method?: string;\r\n status?: string;\r\n}\r\n\r\nexport interface AttendanceWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n visitDate?: string;\r\n checkinTime?: string;\r\n}\r\n\r\nexport interface SessionWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n serviceTimeId?: string;\r\n sessionDate?: string;\r\n}\r\n\r\nexport interface FormSubmissionWebhookData {\r\n id: string;\r\n churchId: string;\r\n formId?: string;\r\n contentType?: string;\r\n contentId?: string;\r\n}\r\n\r\nexport interface EventWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n title?: string;\r\n start?: string;\r\n end?: string;\r\n}\r\n\r\n/**\r\n * Discriminated union of every webhook envelope — `switch (env.event)` narrows\r\n * `data` to the matching shape.\r\n */\r\nexport type B1Webhook =\r\n | B1WebhookEnvelope<PersonWebhookData> & { event: \"person.created\" | \"person.updated\" | \"person.destroyed\" }\r\n | B1WebhookEnvelope<GroupWebhookData> & { event: \"group.created\" | \"group.updated\" | \"group.destroyed\" }\r\n | B1WebhookEnvelope<GroupMemberWebhookData> & { event: \"group.member.added\" | \"group.member.removed\" }\r\n | B1WebhookEnvelope<HouseholdWebhookData> & { event: \"household.created\" | \"household.updated\" | \"household.destroyed\" }\r\n | B1WebhookEnvelope<DonationWebhookData> & { event: \"donation.created\" | \"donation.updated\" }\r\n | B1WebhookEnvelope<AttendanceWebhookData> & { event: \"attendance.recorded\" }\r\n | B1WebhookEnvelope<SessionWebhookData> & { event: \"session.created\" }\r\n | B1WebhookEnvelope<FormSubmissionWebhookData> & { event: \"form.submission.created\" }\r\n | B1WebhookEnvelope<EventWebhookData> & { event: \"event.created\" | \"event.updated\" | \"event.destroyed\" };\r\n","// OAuth types — mirror the B1 Api scope catalog (`shared/auth/Scopes.ts`) and\r\n// the token responses from `OAuthController`.\r\n\r\n/** A recognised OAuth / API-key scope. */\r\nexport type B1KnownScope =\r\n | \"people:read\" | \"people:write\"\r\n | \"groups:read\" | \"groups:write\"\r\n | \"donations:read\" | \"donations:write\"\r\n | \"attendance:read\" | \"attendance:write\"\r\n | \"forms:write\"\r\n | \"content:read\" | \"content:write\"\r\n | \"messaging:read\" | \"messaging:write\"\r\n | \"roles:read\" | \"roles:write\"\r\n | \"settings:read\" | \"settings:write\"\r\n | \"offline_access\";\r\n\r\n/** Scope strings — known scopes get autocomplete, custom strings still allowed. */\r\nexport type B1Scope = B1KnownScope | (string & {});\r\n\r\n/** All scopes B1 recognises in its catalog (plus `offline_access`). */\r\nexport const B1_SCOPES: B1KnownScope[] = [\r\n \"people:read\",\r\n \"people:write\",\r\n \"groups:read\",\r\n \"groups:write\",\r\n \"donations:read\",\r\n \"donations:write\",\r\n \"attendance:read\",\r\n \"attendance:write\",\r\n \"forms:write\",\r\n \"content:read\",\r\n \"content:write\",\r\n \"messaging:read\",\r\n \"messaging:write\",\r\n \"roles:read\",\r\n \"roles:write\",\r\n \"settings:read\",\r\n \"settings:write\",\r\n \"offline_access\"\r\n];\r\n\r\nexport type B1GrantType =\r\n | \"authorization_code\"\r\n | \"refresh_token\"\r\n | \"urn:ietf:params:oauth:grant-type:device_code\";\r\n\r\n/** The token response from `POST /membership/oauth/token`. */\r\nexport interface B1TokenResponse {\r\n access_token: string;\r\n token_type: \"Bearer\";\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Unix timestamp (seconds) the token was created. Absent on the device grant. */\r\n created_at?: number;\r\n refresh_token: string;\r\n scope: string;\r\n}\r\n\r\n/** The response from `POST /membership/oauth/device/authorize` (RFC 8628). */\r\nexport interface B1DeviceAuthResponse {\r\n device_code: string;\r\n user_code: string;\r\n verification_uri: string;\r\n verification_uri_complete?: string;\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Recommended poll interval in seconds. */\r\n interval: number;\r\n}\r\n\r\n/** Outcome of a single device-token poll. */\r\nexport type B1DevicePollResult =\r\n | { status: \"approved\"; token: B1TokenResponse }\r\n | { status: \"pending\" }\r\n | { status: \"expired\" }\r\n | { status: \"denied\" };\r\n","// REST client types — module names, base URLs, request/option shapes.\r\n\r\n/** The B1 Api modules, each addressed by a `/<module>` path prefix. */\r\nexport type B1Module =\r\n | \"membership\"\r\n | \"giving\"\r\n | \"attendance\"\r\n | \"content\"\r\n | \"messaging\"\r\n | \"doing\"\r\n | \"reporting\";\r\n\r\n/** Known B1 Api base URLs. */\r\nexport const B1_BASE_URLS = {\r\n prod: \"https://api.b1.church\",\r\n staging: \"https://api.staging.b1.church\"\r\n} as const;\r\n\r\nexport interface B1RestClientOptions {\r\n /** A raw `cak_<prefix>.<secret>` API key, sent verbatim as a bearer token. */\r\n apiKey: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport type B1QueryValue = string | number | boolean | undefined | null;\r\n\r\nexport interface B1RequestOptions {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n query?: Record<string, B1QueryValue>;\r\n headers?: Record<string, string>;\r\n}\r\n","import crypto from \"crypto\";\r\nimport { B1WebhookEnvelope } from \"../types\";\r\n\r\n/** Thrown by `verifyAndParse` when a signature does not match. */\r\nexport class WebhookVerificationError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"WebhookVerificationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Verifies and parses inbound B1 webhook deliveries.\r\n *\r\n * The signature is an HMAC-SHA256 over the **raw request body** — verify\r\n * before any JSON parse/re-stringify, which would change byte order/whitespace.\r\n * Byte-compatible with the B1 Api `shared/webhooks/WebhookSigner.ts`.\r\n */\r\nexport class WebhookVerifier {\r\n /** Computes the `X-B1-Signature` value for a raw body. */\r\n static sign(secret: string, rawBody: string | Buffer): string {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return \"sha256=\" + crypto.createHmac(\"sha256\", secret).update(body, \"utf8\").digest(\"hex\");\r\n }\r\n\r\n /**\r\n * Returns `true` when `signatureHeader` matches the body. Never throws —\r\n * a missing, empty, or malformed header simply returns `false`.\r\n */\r\n static verify(secret: string, rawBody: string | Buffer, signatureHeader: string | null | undefined): boolean {\r\n if (!signatureHeader) return false;\r\n const expected = WebhookVerifier.sign(secret, rawBody);\r\n const a = Buffer.from(signatureHeader, \"utf8\");\r\n const b = Buffer.from(expected, \"utf8\");\r\n // timingSafeEqual throws on length mismatch — guard, but still do a\r\n // constant-time compare against `expected` so timing stays flat.\r\n if (a.length !== b.length) {\r\n crypto.timingSafeEqual(b, b);\r\n return false;\r\n }\r\n return crypto.timingSafeEqual(a, b);\r\n }\r\n\r\n /** Parses a raw body into a typed envelope (no verification). */\r\n static parseEnvelope<T = unknown>(rawBody: string | Buffer): B1WebhookEnvelope<T> {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return JSON.parse(body) as B1WebhookEnvelope<T>;\r\n }\r\n\r\n /**\r\n * Verifies the signature, then parses the body into a typed envelope.\r\n * Throws `WebhookVerificationError` if the signature does not match.\r\n */\r\n static verifyAndParse<T = unknown>(\r\n secret: string,\r\n rawBody: string | Buffer,\r\n signatureHeader: string | null | undefined\r\n ): B1WebhookEnvelope<T> {\r\n if (!WebhookVerifier.verify(secret, rawBody, signatureHeader)) {\r\n throw new WebhookVerificationError(\"Webhook signature verification failed\");\r\n }\r\n return WebhookVerifier.parseEnvelope<T>(rawBody);\r\n }\r\n}\r\n","import type { Request, RequestHandler, Response } from \"express\";\r\nimport { WebhookVerifier } from \"./WebhookVerifier\";\r\nimport { B1WebhookEnvelope, WEBHOOK_HEADERS } from \"../types\";\r\n\r\ndeclare global {\r\n // eslint-disable-next-line @typescript-eslint/no-namespace\r\n namespace Express {\r\n interface Request {\r\n /** The untouched request body — set by `express.json({ verify })`. */\r\n rawBody?: Buffer | string;\r\n /** The verified, parsed webhook envelope — set by `b1WebhookMiddleware`. */\r\n b1Webhook?: B1WebhookEnvelope;\r\n }\r\n }\r\n}\r\n\r\nexport interface B1WebhookMiddlewareOptions {\r\n /** The webhook secret, or a function resolving one per request. */\r\n secret: string | ((req: Request) => string);\r\n /** Called instead of the default 401 response when verification fails. */\r\n onInvalid?: (req: Request, res: Response) => void;\r\n}\r\n\r\n/**\r\n * Express middleware that verifies the `X-B1-Signature` header and attaches a\r\n * typed `req.b1Webhook` envelope.\r\n *\r\n * The raw request body must be available — capture it before JSON parsing:\r\n *\r\n * ```ts\r\n * app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));\r\n * app.post(\"/webhooks/b1\", b1WebhookMiddleware({ secret }), (req, res) => {\r\n * console.log(req.b1Webhook?.event);\r\n * res.sendStatus(200);\r\n * });\r\n * ```\r\n *\r\n * `express.raw({ type: \"application/json\" })` is also accepted — `req.body` is\r\n * then a Buffer the middleware verifies and parses itself.\r\n */\r\nexport function b1WebhookMiddleware(options: B1WebhookMiddlewareOptions): RequestHandler {\r\n return (req: Request, res: Response, next): void => {\r\n const raw = resolveRawBody(req);\r\n if (raw === undefined) {\r\n throw new Error(\r\n \"b1WebhookMiddleware: no raw request body found. Mount \" +\r\n \"express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) \" +\r\n \"or express.raw({ type: \\\"application/json\\\" }) before this middleware.\"\r\n );\r\n }\r\n\r\n const secret = typeof options.secret === \"function\" ? options.secret(req) : options.secret;\r\n const signature = req.header(WEBHOOK_HEADERS.signature);\r\n\r\n if (!WebhookVerifier.verify(secret, raw, signature)) {\r\n if (options.onInvalid) options.onInvalid(req, res);\r\n else res.status(401).json({ error: \"invalid webhook signature\" });\r\n return;\r\n }\r\n\r\n const envelope = WebhookVerifier.parseEnvelope(raw);\r\n req.b1Webhook = envelope;\r\n if (Buffer.isBuffer(req.body)) req.body = envelope;\r\n next();\r\n };\r\n}\r\n\r\nfunction resolveRawBody(req: Request): Buffer | string | undefined {\r\n if (req.rawBody !== undefined) return req.rawBody;\r\n if (Buffer.isBuffer(req.body)) return req.body;\r\n return undefined;\r\n}\r\n","/** Thrown by `B1RestClient` when the Api returns a non-2xx response. */\r\nexport class B1ApiError extends Error {\r\n readonly status: number;\r\n readonly statusText: string;\r\n readonly body: unknown;\r\n readonly method: string;\r\n readonly url: string;\r\n\r\n constructor(opts: { status: number; statusText: string; body: unknown; method: string; url: string }) {\r\n super(`B1 Api ${opts.method} ${opts.url} failed: ${opts.status} ${opts.statusText}`);\r\n this.name = \"B1ApiError\";\r\n this.status = opts.status;\r\n this.statusText = opts.statusText;\r\n this.body = opts.body;\r\n this.method = opts.method;\r\n this.url = opts.url;\r\n }\r\n}\r\n","import { B1ApiError } from \"./B1ApiError\";\r\nimport { B1_BASE_URLS, B1Module, B1RequestOptions, B1RestClientOptions } from \"../types\";\r\n\r\n/**\r\n * A typed REST client for the B1 Api, authenticated with a `cak_` API key.\r\n *\r\n * The Api is a single host with per-module path prefixes — use `request()`\r\n * with a full `/membership/...` path, or the module helpers which prefix it\r\n * for you.\r\n *\r\n * ```ts\r\n * const client = new B1RestClient({ apiKey: \"cak_...\" });\r\n * const people = await client.membership<Person[]>(\"/people\");\r\n * ```\r\n *\r\n * Non-2xx responses throw `B1ApiError` (carrying status + parsed body) so a\r\n * caller can distinguish 401/403/404/500.\r\n */\r\nexport class B1RestClient {\r\n private readonly apiKey: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1RestClientOptions) {\r\n if (!options.apiKey) throw new Error(\"B1RestClient: apiKey is required\");\r\n this.apiKey = options.apiKey;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1RestClient: no fetch available — pass options.fetch (Node 18+ has global fetch)\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /** Issues a request against a full Api path (e.g. `/membership/people`). */\r\n async request<T = unknown>(path: string, options: B1RequestOptions = {}): Promise<T> {\r\n const method = options.method ?? \"GET\";\r\n const url = this.buildUrl(path, options.query);\r\n\r\n const headers: Record<string, string> = {\r\n Authorization: `Bearer ${this.apiKey}`,\r\n Accept: \"application/json\",\r\n ...options.headers\r\n };\r\n const hasBody = options.body !== undefined && options.body !== null;\r\n if (hasBody) headers[\"Content-Type\"] = \"application/json\";\r\n\r\n let response: Response;\r\n try {\r\n response = await this.fetchImpl(url, {\r\n method,\r\n headers,\r\n ...(hasBody ? { body: JSON.stringify(options.body) } : {})\r\n });\r\n } catch (err) {\r\n throw new B1ApiError({\r\n status: 0,\r\n statusText: err instanceof Error ? err.message : \"network error\",\r\n body: null,\r\n method,\r\n url\r\n });\r\n }\r\n\r\n const text = await response.text();\r\n const body = parseBody(text);\r\n\r\n if (!response.ok) {\r\n throw new B1ApiError({ status: response.status, statusText: response.statusText, body, method, url });\r\n }\r\n return body as T;\r\n }\r\n\r\n /** Request against the `/membership` module. */\r\n membership<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"membership\", path, options);\r\n }\r\n\r\n /** Request against the `/giving` module. */\r\n giving<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"giving\", path, options);\r\n }\r\n\r\n /** Request against the `/attendance` module. */\r\n attendance<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"attendance\", path, options);\r\n }\r\n\r\n /** Request against the `/content` module. */\r\n content<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"content\", path, options);\r\n }\r\n\r\n /** Request against the `/messaging` module. */\r\n messaging<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"messaging\", path, options);\r\n }\r\n\r\n /** Request against the `/doing` module. */\r\n doing<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"doing\", path, options);\r\n }\r\n\r\n /** Request against the `/reporting` module. */\r\n reporting<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"reporting\", path, options);\r\n }\r\n\r\n private module<T>(module: B1Module, path: string, options?: B1RequestOptions): Promise<T> {\r\n const sub = path.startsWith(\"/\") ? path : `/${path}`;\r\n return this.request<T>(`/${module}${sub}`, options);\r\n }\r\n\r\n private buildUrl(path: string, query?: B1RequestOptions[\"query\"]): string {\r\n const p = path.startsWith(\"/\") ? path : `/${path}`;\r\n let url = `${this.baseUrl}${p}`;\r\n if (query) {\r\n const params = new URLSearchParams();\r\n for (const [key, value] of Object.entries(query)) {\r\n if (value !== undefined && value !== null) params.append(key, String(value));\r\n }\r\n const qs = params.toString();\r\n if (qs) url += `?${qs}`;\r\n }\r\n return url;\r\n }\r\n}\r\n\r\n/** Parses a response body as JSON, falling back to the raw text / undefined. */\r\nfunction parseBody(text: string): unknown {\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n","import {\r\n B1_BASE_URLS,\r\n B1DeviceAuthResponse,\r\n B1DevicePollResult,\r\n B1Scope,\r\n B1TokenResponse\r\n} from \"../types\";\r\n\r\n/** Thrown when a B1 OAuth endpoint returns an `error` response. */\r\nexport class B1OAuthError extends Error {\r\n readonly error: string;\r\n readonly errorDescription?: string;\r\n readonly status: number;\r\n\r\n constructor(error: string, errorDescription: string | undefined, status: number) {\r\n super(errorDescription ? `${error}: ${errorDescription}` : error);\r\n this.name = \"B1OAuthError\";\r\n this.error = error;\r\n this.errorDescription = errorDescription;\r\n this.status = status;\r\n }\r\n}\r\n\r\nexport interface B1OAuthClientOptions {\r\n clientId: string;\r\n /** Required for confidential clients (authorization_code grant). */\r\n clientSecret?: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport interface AwaitDeviceTokenOptions {\r\n deviceCode: string;\r\n /** Poll interval in seconds (from `B1DeviceAuthResponse.interval`). */\r\n interval: number;\r\n /** Overall timeout in seconds (from `B1DeviceAuthResponse.expires_in`). */\r\n expiresIn: number;\r\n /** Optional abort signal to cancel polling. */\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Helper for B1's OAuth flows — authorization-code, refresh-token, and the\r\n * RFC 8628 device flow — against `/membership/oauth/*`.\r\n */\r\nexport class B1OAuthClient {\r\n private readonly clientId: string;\r\n private readonly clientSecret?: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1OAuthClientOptions) {\r\n if (!options.clientId) throw new Error(\"B1OAuthClient: clientId is required\");\r\n this.clientId = options.clientId;\r\n this.clientSecret = options.clientSecret;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1OAuthClient: no fetch available — pass options.fetch\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /**\r\n * Requests an authorization code. B1's `/authorize` endpoint is an\r\n * authenticated POST, so this needs the *user's* access token (a JWT).\r\n */\r\n async getAuthorizationCode(params: {\r\n userAccessToken: string;\r\n redirectUri: string;\r\n scope: B1Scope[] | string;\r\n state?: string;\r\n }): Promise<{ code: string; state: string | null }> {\r\n return this.post(\r\n \"/authorize\",\r\n {\r\n client_id: this.clientId,\r\n redirect_uri: params.redirectUri,\r\n response_type: \"code\",\r\n scope: scopeString(params.scope),\r\n state: params.state\r\n },\r\n { Authorization: `Bearer ${params.userAccessToken}` }\r\n );\r\n }\r\n\r\n /** Exchanges an authorization code for tokens. */\r\n async exchangeCode(params: { code: string; redirectUri?: string }): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"authorization_code\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n code: params.code,\r\n redirect_uri: params.redirectUri\r\n });\r\n }\r\n\r\n /** Exchanges a refresh token for a fresh access token. */\r\n async refresh(refreshToken: string): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"refresh_token\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n refresh_token: refreshToken\r\n });\r\n }\r\n\r\n /** Starts the device flow — returns a user code + verification URI. */\r\n async startDeviceFlow(scope?: B1Scope[] | string): Promise<B1DeviceAuthResponse> {\r\n return this.post(\"/device/authorize\", {\r\n client_id: this.clientId,\r\n scope: scope ? scopeString(scope) : undefined\r\n });\r\n }\r\n\r\n /** Polls once for a device-flow token. Never throws on a pending/expired/denied state. */\r\n async pollDeviceToken(deviceCode: string): Promise<B1DevicePollResult> {\r\n const res = await this.rawPost(\"/token\", {\r\n grant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\r\n client_id: this.clientId,\r\n device_code: deviceCode\r\n });\r\n if (res.ok) return { status: \"approved\", token: res.body as B1TokenResponse };\r\n\r\n const error = typeof res.body === \"object\" && res.body ? (res.body as any).error : undefined;\r\n if (error === \"authorization_pending\") return { status: \"pending\" };\r\n if (error === \"expired_token\") return { status: \"expired\" };\r\n if (error === \"access_denied\") return { status: \"denied\" };\r\n throw new B1OAuthError(error ?? \"invalid_grant\", (res.body as any)?.error_description, res.status);\r\n }\r\n\r\n /** Polls until the device flow is approved, denied, or expires. */\r\n async awaitDeviceToken(options: AwaitDeviceTokenOptions): Promise<B1TokenResponse> {\r\n const deadline = Date.now() + options.expiresIn * 1000;\r\n let intervalMs = Math.max(1, options.interval) * 1000;\r\n\r\n while (Date.now() < deadline) {\r\n if (options.signal?.aborted) throw new B1OAuthError(\"access_denied\", \"polling aborted\", 0);\r\n await delay(intervalMs, options.signal);\r\n\r\n const result = await this.pollDeviceToken(options.deviceCode);\r\n if (result.status === \"approved\") return result.token;\r\n if (result.status === \"denied\") throw new B1OAuthError(\"access_denied\", \"user denied the request\", 400);\r\n if (result.status === \"expired\") throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n // pending — RFC 8628 \"slow_down\" backoff is conservative; nudge the interval up.\r\n intervalMs += 1000;\r\n }\r\n throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n }\r\n\r\n /** Looks up a pending device authorization by its user code (for an approval UI). */\r\n async getPendingDevice(userCode: string): Promise<unknown> {\r\n const url = `${this.baseUrl}/membership/oauth/device/pending/${encodeURIComponent(userCode)}`;\r\n const res = await this.fetchImpl(url, { headers: { Accept: \"application/json\" } });\r\n const body = await readJson(res);\r\n if (!res.ok) throw new B1OAuthError((body as any)?.error ?? \"not_found\", (body as any)?.error_description, res.status);\r\n return body;\r\n }\r\n\r\n private async post<T>(path: string, body: Record<string, unknown>, extraHeaders?: Record<string, string>): Promise<T> {\r\n const res = await this.rawPost(path, body, extraHeaders);\r\n if (!res.ok) {\r\n const b = res.body as any;\r\n throw new B1OAuthError(b?.error ?? \"oauth_error\", b?.error_description, res.status);\r\n }\r\n return res.body as T;\r\n }\r\n\r\n private async rawPost(\r\n path: string,\r\n body: Record<string, unknown>,\r\n extraHeaders?: Record<string, string>\r\n ): Promise<{ ok: boolean; status: number; body: unknown }> {\r\n const url = `${this.baseUrl}/membership/oauth${path}`;\r\n const clean: Record<string, unknown> = {};\r\n for (const [k, v] of Object.entries(body)) {\r\n if (v !== undefined && v !== null) clean[k] = v;\r\n }\r\n const res = await this.fetchImpl(url, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\", ...extraHeaders },\r\n body: JSON.stringify(clean)\r\n });\r\n return { ok: res.ok, status: res.status, body: await readJson(res) };\r\n }\r\n}\r\n\r\nfunction scopeString(scope: B1Scope[] | string): string {\r\n return Array.isArray(scope) ? scope.join(\" \") : scope;\r\n}\r\n\r\nasync function readJson(res: Response): Promise<unknown> {\r\n const text = await res.text();\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n\r\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const timer = setTimeout(resolve, ms);\r\n signal?.addEventListener(\"abort\", () => {\r\n clearTimeout(timer);\r\n reject(new B1OAuthError(\"access_denied\", \"polling aborted\", 0));\r\n }, { once: true });\r\n });\r\n}\r\n"],"mappings":"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","names":["crypto","module"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types/webhooks.ts","../src/types/oauth.ts","../src/types/rest.ts","../src/webhooks/WebhookVerifier.ts","../src/webhooks/expressMiddleware.ts","../src/rest/B1ApiError.ts","../src/rest/B1RestClient.ts","../src/oauth/B1OAuthClient.ts"],"sourcesContent":["/** `@churchapps/integration-sdk` — toolkit for building B1.church integrations. */\r\n// Injected from package.json by tsup at build time so it can't drift\r\ndeclare const __PACKAGE_VERSION__: string;\r\nexport const VERSION = __PACKAGE_VERSION__;\r\n\r\nexport * from \"./types\";\r\nexport * from \"./webhooks\";\r\nexport * from \"./rest\";\r\nexport * from \"./oauth\";\r\n","// Webhook event names, the delivery envelope, and per-event `data` shapes.\r\n// These mirror the B1 Api — `shared/webhooks/WebhookEvents.ts` for the names\r\n// and `WebhookSamplePayloads.ts` for the data shapes.\r\n\r\n/** Every webhook event B1 can emit. */\r\nexport type B1WebhookEventName =\r\n | \"person.created\" | \"person.updated\" | \"person.destroyed\"\r\n | \"group.created\" | \"group.updated\" | \"group.destroyed\"\r\n | \"group.member.added\" | \"group.member.removed\"\r\n | \"household.created\" | \"household.updated\" | \"household.destroyed\"\r\n | \"donation.created\" | \"donation.updated\"\r\n | \"attendance.recorded\"\r\n | \"session.created\"\r\n | \"form.submission.created\"\r\n | \"event.created\" | \"event.updated\" | \"event.destroyed\";\r\n\r\n/** The HTTP headers B1 sends with every webhook delivery. */\r\nexport const WEBHOOK_HEADERS = {\r\n signature: \"X-B1-Signature\",\r\n event: \"X-B1-Event\",\r\n deliveryId: \"X-B1-Delivery-Id\",\r\n timestamp: \"X-B1-Timestamp\"\r\n} as const;\r\n\r\n/** The JSON body B1 POSTs to a subscriber URL. */\r\nexport interface B1WebhookEnvelope<T = unknown> {\r\n event: B1WebhookEventName;\r\n churchId: string;\r\n /** ISO 8601 timestamp. */\r\n occurredAt: string;\r\n data: T;\r\n}\r\n\r\n// --- per-event data shapes ------------------------------------------------\r\n// `destroyed` events carry only `{ id, churchId }`, so the descriptive fields\r\n// are optional on the shared shape.\r\n\r\nexport interface PersonWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: { display?: string; first?: string; last?: string };\r\n contactInfo?: { email?: string };\r\n}\r\n\r\nexport interface GroupWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n categoryName?: string;\r\n}\r\n\r\nexport interface GroupMemberWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId: string;\r\n personId: string;\r\n}\r\n\r\nexport interface HouseholdWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n}\r\n\r\nexport interface DonationWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n batchId?: string;\r\n donationDate?: string;\r\n amount?: number;\r\n currency?: string;\r\n method?: string;\r\n status?: string;\r\n}\r\n\r\nexport interface AttendanceWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n visitDate?: string;\r\n checkinTime?: string;\r\n}\r\n\r\nexport interface SessionWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n serviceTimeId?: string;\r\n sessionDate?: string;\r\n}\r\n\r\nexport interface FormSubmissionWebhookData {\r\n id: string;\r\n churchId: string;\r\n formId?: string;\r\n contentType?: string;\r\n contentId?: string;\r\n}\r\n\r\nexport interface EventWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n title?: string;\r\n start?: string;\r\n end?: string;\r\n}\r\n\r\n/**\r\n * Discriminated union of every webhook envelope — `switch (env.event)` narrows\r\n * `data` to the matching shape.\r\n */\r\nexport type B1Webhook =\r\n | B1WebhookEnvelope<PersonWebhookData> & { event: \"person.created\" | \"person.updated\" | \"person.destroyed\" }\r\n | B1WebhookEnvelope<GroupWebhookData> & { event: \"group.created\" | \"group.updated\" | \"group.destroyed\" }\r\n | B1WebhookEnvelope<GroupMemberWebhookData> & { event: \"group.member.added\" | \"group.member.removed\" }\r\n | B1WebhookEnvelope<HouseholdWebhookData> & { event: \"household.created\" | \"household.updated\" | \"household.destroyed\" }\r\n | B1WebhookEnvelope<DonationWebhookData> & { event: \"donation.created\" | \"donation.updated\" }\r\n | B1WebhookEnvelope<AttendanceWebhookData> & { event: \"attendance.recorded\" }\r\n | B1WebhookEnvelope<SessionWebhookData> & { event: \"session.created\" }\r\n | B1WebhookEnvelope<FormSubmissionWebhookData> & { event: \"form.submission.created\" }\r\n | B1WebhookEnvelope<EventWebhookData> & { event: \"event.created\" | \"event.updated\" | \"event.destroyed\" };\r\n","// OAuth types — mirror the B1 Api scope catalog (`shared/auth/Scopes.ts`) and\r\n// the token responses from `OAuthController`.\r\n\r\n/** A recognised OAuth / API-key scope. */\r\nexport type B1KnownScope =\r\n | \"people:read\" | \"people:write\"\r\n | \"groups:read\" | \"groups:write\"\r\n | \"donations:read\" | \"donations:write\"\r\n | \"attendance:read\" | \"attendance:write\"\r\n | \"forms:write\"\r\n | \"content:read\" | \"content:write\"\r\n | \"messaging:read\" | \"messaging:write\"\r\n | \"roles:read\" | \"roles:write\"\r\n | \"settings:read\" | \"settings:write\"\r\n | \"offline_access\";\r\n\r\n/** Scope strings — known scopes get autocomplete, custom strings still allowed. */\r\nexport type B1Scope = B1KnownScope | (string & {});\r\n\r\n/** All scopes B1 recognises in its catalog (plus `offline_access`). */\r\nexport const B1_SCOPES: B1KnownScope[] = [\r\n \"people:read\",\r\n \"people:write\",\r\n \"groups:read\",\r\n \"groups:write\",\r\n \"donations:read\",\r\n \"donations:write\",\r\n \"attendance:read\",\r\n \"attendance:write\",\r\n \"forms:write\",\r\n \"content:read\",\r\n \"content:write\",\r\n \"messaging:read\",\r\n \"messaging:write\",\r\n \"roles:read\",\r\n \"roles:write\",\r\n \"settings:read\",\r\n \"settings:write\",\r\n \"offline_access\"\r\n];\r\n\r\nexport type B1GrantType =\r\n | \"authorization_code\"\r\n | \"refresh_token\"\r\n | \"urn:ietf:params:oauth:grant-type:device_code\";\r\n\r\n/** The token response from `POST /membership/oauth/token`. */\r\nexport interface B1TokenResponse {\r\n access_token: string;\r\n token_type: \"Bearer\";\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Unix timestamp (seconds) the token was created. Absent on the device grant. */\r\n created_at?: number;\r\n refresh_token: string;\r\n scope: string;\r\n}\r\n\r\n/** The response from `POST /membership/oauth/device/authorize` (RFC 8628). */\r\nexport interface B1DeviceAuthResponse {\r\n device_code: string;\r\n user_code: string;\r\n verification_uri: string;\r\n verification_uri_complete?: string;\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Recommended poll interval in seconds. */\r\n interval: number;\r\n}\r\n\r\n/** Outcome of a single device-token poll. */\r\nexport type B1DevicePollResult =\r\n | { status: \"approved\"; token: B1TokenResponse }\r\n | { status: \"pending\" }\r\n | { status: \"expired\" }\r\n | { status: \"denied\" };\r\n","// REST client types — module names, base URLs, request/option shapes.\r\n\r\n/** The B1 Api modules, each addressed by a `/<module>` path prefix. */\r\nexport type B1Module =\r\n | \"membership\"\r\n | \"giving\"\r\n | \"attendance\"\r\n | \"content\"\r\n | \"messaging\"\r\n | \"doing\"\r\n | \"reporting\";\r\n\r\n/** Known B1 Api base URLs. */\r\nexport const B1_BASE_URLS = {\r\n prod: \"https://api.b1.church\",\r\n staging: \"https://api.staging.b1.church\"\r\n} as const;\r\n\r\nexport interface B1RestClientOptions {\r\n /** A raw `cak_<prefix>.<secret>` API key, sent verbatim as a bearer token. */\r\n apiKey: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport type B1QueryValue = string | number | boolean | undefined | null;\r\n\r\nexport interface B1RequestOptions {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n query?: Record<string, B1QueryValue>;\r\n headers?: Record<string, string>;\r\n}\r\n","import crypto from \"crypto\";\r\nimport { B1WebhookEnvelope } from \"../types\";\r\n\r\n/** Thrown by `verifyAndParse` when a signature does not match. */\r\nexport class WebhookVerificationError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"WebhookVerificationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Verifies and parses inbound B1 webhook deliveries.\r\n *\r\n * The signature is an HMAC-SHA256 over the **raw request body** — verify\r\n * before any JSON parse/re-stringify, which would change byte order/whitespace.\r\n * Byte-compatible with the B1 Api `shared/webhooks/WebhookSigner.ts`.\r\n */\r\nexport class WebhookVerifier {\r\n /** Computes the `X-B1-Signature` value for a raw body. */\r\n static sign(secret: string, rawBody: string | Buffer): string {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return \"sha256=\" + crypto.createHmac(\"sha256\", secret).update(body, \"utf8\").digest(\"hex\");\r\n }\r\n\r\n /**\r\n * Returns `true` when `signatureHeader` matches the body. Never throws —\r\n * a missing, empty, or malformed header simply returns `false`.\r\n */\r\n static verify(secret: string, rawBody: string | Buffer, signatureHeader: string | null | undefined): boolean {\r\n if (!signatureHeader) return false;\r\n const expected = WebhookVerifier.sign(secret, rawBody);\r\n const a = Buffer.from(signatureHeader, \"utf8\");\r\n const b = Buffer.from(expected, \"utf8\");\r\n // timingSafeEqual throws on length mismatch — guard, but still do a\r\n // constant-time compare against `expected` so timing stays flat.\r\n if (a.length !== b.length) {\r\n crypto.timingSafeEqual(b, b);\r\n return false;\r\n }\r\n return crypto.timingSafeEqual(a, b);\r\n }\r\n\r\n /** Parses a raw body into a typed envelope (no verification). */\r\n static parseEnvelope<T = unknown>(rawBody: string | Buffer): B1WebhookEnvelope<T> {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return JSON.parse(body) as B1WebhookEnvelope<T>;\r\n }\r\n\r\n /**\r\n * Verifies the signature, then parses the body into a typed envelope.\r\n * Throws `WebhookVerificationError` if the signature does not match.\r\n */\r\n static verifyAndParse<T = unknown>(\r\n secret: string,\r\n rawBody: string | Buffer,\r\n signatureHeader: string | null | undefined\r\n ): B1WebhookEnvelope<T> {\r\n if (!WebhookVerifier.verify(secret, rawBody, signatureHeader)) {\r\n throw new WebhookVerificationError(\"Webhook signature verification failed\");\r\n }\r\n return WebhookVerifier.parseEnvelope<T>(rawBody);\r\n }\r\n}\r\n","import type { Request, RequestHandler, Response } from \"express\";\r\nimport { WebhookVerifier } from \"./WebhookVerifier\";\r\nimport { B1WebhookEnvelope, WEBHOOK_HEADERS } from \"../types\";\r\n\r\ndeclare global {\r\n // eslint-disable-next-line @typescript-eslint/no-namespace\r\n namespace Express {\r\n interface Request {\r\n /** The untouched request body — set by `express.json({ verify })`. */\r\n rawBody?: Buffer | string;\r\n /** The verified, parsed webhook envelope — set by `b1WebhookMiddleware`. */\r\n b1Webhook?: B1WebhookEnvelope;\r\n }\r\n }\r\n}\r\n\r\nexport interface B1WebhookMiddlewareOptions {\r\n /** The webhook secret, or a function resolving one per request. */\r\n secret: string | ((req: Request) => string);\r\n /** Called instead of the default 401 response when verification fails. */\r\n onInvalid?: (req: Request, res: Response) => void;\r\n}\r\n\r\n/**\r\n * Express middleware that verifies the `X-B1-Signature` header and attaches a\r\n * typed `req.b1Webhook` envelope.\r\n *\r\n * The raw request body must be available — capture it before JSON parsing:\r\n *\r\n * ```ts\r\n * app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));\r\n * app.post(\"/webhooks/b1\", b1WebhookMiddleware({ secret }), (req, res) => {\r\n * console.log(req.b1Webhook?.event);\r\n * res.sendStatus(200);\r\n * });\r\n * ```\r\n *\r\n * `express.raw({ type: \"application/json\" })` is also accepted — `req.body` is\r\n * then a Buffer the middleware verifies and parses itself.\r\n */\r\nexport function b1WebhookMiddleware(options: B1WebhookMiddlewareOptions): RequestHandler {\r\n return (req: Request, res: Response, next): void => {\r\n const raw = resolveRawBody(req);\r\n if (raw === undefined) {\r\n throw new Error(\r\n \"b1WebhookMiddleware: no raw request body found. Mount \" +\r\n \"express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) \" +\r\n \"or express.raw({ type: \\\"application/json\\\" }) before this middleware.\"\r\n );\r\n }\r\n\r\n const secret = typeof options.secret === \"function\" ? options.secret(req) : options.secret;\r\n const signature = req.header(WEBHOOK_HEADERS.signature);\r\n\r\n if (!WebhookVerifier.verify(secret, raw, signature)) {\r\n if (options.onInvalid) options.onInvalid(req, res);\r\n else res.status(401).json({ error: \"invalid webhook signature\" });\r\n return;\r\n }\r\n\r\n const envelope = WebhookVerifier.parseEnvelope(raw);\r\n req.b1Webhook = envelope;\r\n if (Buffer.isBuffer(req.body)) req.body = envelope;\r\n next();\r\n };\r\n}\r\n\r\nfunction resolveRawBody(req: Request): Buffer | string | undefined {\r\n if (req.rawBody !== undefined) return req.rawBody;\r\n if (Buffer.isBuffer(req.body)) return req.body;\r\n return undefined;\r\n}\r\n","/** Thrown by `B1RestClient` when the Api returns a non-2xx response. */\r\nexport class B1ApiError extends Error {\r\n readonly status: number;\r\n readonly statusText: string;\r\n readonly body: unknown;\r\n readonly method: string;\r\n readonly url: string;\r\n\r\n constructor(opts: { status: number; statusText: string; body: unknown; method: string; url: string }) {\r\n super(`B1 Api ${opts.method} ${opts.url} failed: ${opts.status} ${opts.statusText}`);\r\n this.name = \"B1ApiError\";\r\n this.status = opts.status;\r\n this.statusText = opts.statusText;\r\n this.body = opts.body;\r\n this.method = opts.method;\r\n this.url = opts.url;\r\n }\r\n}\r\n","import { B1ApiError } from \"./B1ApiError\";\r\nimport { B1_BASE_URLS, B1Module, B1RequestOptions, B1RestClientOptions } from \"../types\";\r\n\r\n/**\r\n * A typed REST client for the B1 Api, authenticated with a `cak_` API key.\r\n *\r\n * The Api is a single host with per-module path prefixes — use `request()`\r\n * with a full `/membership/...` path, or the module helpers which prefix it\r\n * for you.\r\n *\r\n * ```ts\r\n * const client = new B1RestClient({ apiKey: \"cak_...\" });\r\n * const people = await client.membership<Person[]>(\"/people\");\r\n * ```\r\n *\r\n * Non-2xx responses throw `B1ApiError` (carrying status + parsed body) so a\r\n * caller can distinguish 401/403/404/500.\r\n */\r\nexport class B1RestClient {\r\n private readonly apiKey: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1RestClientOptions) {\r\n if (!options.apiKey) throw new Error(\"B1RestClient: apiKey is required\");\r\n this.apiKey = options.apiKey;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1RestClient: no fetch available — pass options.fetch (Node 18+ has global fetch)\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /** Issues a request against a full Api path (e.g. `/membership/people`). */\r\n async request<T = unknown>(path: string, options: B1RequestOptions = {}): Promise<T> {\r\n const method = options.method ?? \"GET\";\r\n const url = this.buildUrl(path, options.query);\r\n\r\n const headers: Record<string, string> = {\r\n Authorization: `Bearer ${this.apiKey}`,\r\n Accept: \"application/json\",\r\n ...options.headers\r\n };\r\n const hasBody = options.body !== undefined && options.body !== null;\r\n if (hasBody) headers[\"Content-Type\"] = \"application/json\";\r\n\r\n let response: Response;\r\n try {\r\n response = await this.fetchImpl(url, {\r\n method,\r\n headers,\r\n ...(hasBody ? { body: JSON.stringify(options.body) } : {})\r\n });\r\n } catch (err) {\r\n throw new B1ApiError({\r\n status: 0,\r\n statusText: err instanceof Error ? err.message : \"network error\",\r\n body: null,\r\n method,\r\n url\r\n });\r\n }\r\n\r\n const text = await response.text();\r\n const body = parseBody(text);\r\n\r\n if (!response.ok) {\r\n throw new B1ApiError({ status: response.status, statusText: response.statusText, body, method, url });\r\n }\r\n return body as T;\r\n }\r\n\r\n /** Request against the `/membership` module. */\r\n membership<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"membership\", path, options);\r\n }\r\n\r\n /** Request against the `/giving` module. */\r\n giving<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"giving\", path, options);\r\n }\r\n\r\n /** Request against the `/attendance` module. */\r\n attendance<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"attendance\", path, options);\r\n }\r\n\r\n /** Request against the `/content` module. */\r\n content<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"content\", path, options);\r\n }\r\n\r\n /** Request against the `/messaging` module. */\r\n messaging<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"messaging\", path, options);\r\n }\r\n\r\n /** Request against the `/doing` module. */\r\n doing<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"doing\", path, options);\r\n }\r\n\r\n /** Request against the `/reporting` module. */\r\n reporting<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"reporting\", path, options);\r\n }\r\n\r\n private module<T>(module: B1Module, path: string, options?: B1RequestOptions): Promise<T> {\r\n const sub = path.startsWith(\"/\") ? path : `/${path}`;\r\n return this.request<T>(`/${module}${sub}`, options);\r\n }\r\n\r\n private buildUrl(path: string, query?: B1RequestOptions[\"query\"]): string {\r\n const p = path.startsWith(\"/\") ? path : `/${path}`;\r\n let url = `${this.baseUrl}${p}`;\r\n if (query) {\r\n const params = new URLSearchParams();\r\n for (const [key, value] of Object.entries(query)) {\r\n if (value !== undefined && value !== null) params.append(key, String(value));\r\n }\r\n const qs = params.toString();\r\n if (qs) url += `?${qs}`;\r\n }\r\n return url;\r\n }\r\n}\r\n\r\n/** Parses a response body as JSON, falling back to the raw text / undefined. */\r\nfunction parseBody(text: string): unknown {\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n","import {\r\n B1_BASE_URLS,\r\n B1DeviceAuthResponse,\r\n B1DevicePollResult,\r\n B1Scope,\r\n B1TokenResponse\r\n} from \"../types\";\r\n\r\n/** Thrown when a B1 OAuth endpoint returns an `error` response. */\r\nexport class B1OAuthError extends Error {\r\n readonly error: string;\r\n readonly errorDescription?: string;\r\n readonly status: number;\r\n\r\n constructor(error: string, errorDescription: string | undefined, status: number) {\r\n super(errorDescription ? `${error}: ${errorDescription}` : error);\r\n this.name = \"B1OAuthError\";\r\n this.error = error;\r\n this.errorDescription = errorDescription;\r\n this.status = status;\r\n }\r\n}\r\n\r\nexport interface B1OAuthClientOptions {\r\n clientId: string;\r\n /** Required for confidential clients (authorization_code grant). */\r\n clientSecret?: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport interface AwaitDeviceTokenOptions {\r\n deviceCode: string;\r\n /** Poll interval in seconds (from `B1DeviceAuthResponse.interval`). */\r\n interval: number;\r\n /** Overall timeout in seconds (from `B1DeviceAuthResponse.expires_in`). */\r\n expiresIn: number;\r\n /** Optional abort signal to cancel polling. */\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Helper for B1's OAuth flows — authorization-code, refresh-token, and the\r\n * RFC 8628 device flow — against `/membership/oauth/*`.\r\n */\r\nexport class B1OAuthClient {\r\n private readonly clientId: string;\r\n private readonly clientSecret?: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1OAuthClientOptions) {\r\n if (!options.clientId) throw new Error(\"B1OAuthClient: clientId is required\");\r\n this.clientId = options.clientId;\r\n this.clientSecret = options.clientSecret;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1OAuthClient: no fetch available — pass options.fetch\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /**\r\n * Requests an authorization code. B1's `/authorize` endpoint is an\r\n * authenticated POST, so this needs the *user's* access token (a JWT).\r\n */\r\n async getAuthorizationCode(params: {\r\n userAccessToken: string;\r\n redirectUri: string;\r\n scope: B1Scope[] | string;\r\n state?: string;\r\n }): Promise<{ code: string; state: string | null }> {\r\n return this.post(\r\n \"/authorize\",\r\n {\r\n client_id: this.clientId,\r\n redirect_uri: params.redirectUri,\r\n response_type: \"code\",\r\n scope: scopeString(params.scope),\r\n state: params.state\r\n },\r\n { Authorization: `Bearer ${params.userAccessToken}` }\r\n );\r\n }\r\n\r\n /** Exchanges an authorization code for tokens. */\r\n async exchangeCode(params: { code: string; redirectUri?: string }): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"authorization_code\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n code: params.code,\r\n redirect_uri: params.redirectUri\r\n });\r\n }\r\n\r\n /** Exchanges a refresh token for a fresh access token. */\r\n async refresh(refreshToken: string): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"refresh_token\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n refresh_token: refreshToken\r\n });\r\n }\r\n\r\n /** Starts the device flow — returns a user code + verification URI. */\r\n async startDeviceFlow(scope?: B1Scope[] | string): Promise<B1DeviceAuthResponse> {\r\n return this.post(\"/device/authorize\", {\r\n client_id: this.clientId,\r\n scope: scope ? scopeString(scope) : undefined\r\n });\r\n }\r\n\r\n /** Polls once for a device-flow token. Never throws on a pending/expired/denied state. */\r\n async pollDeviceToken(deviceCode: string): Promise<B1DevicePollResult> {\r\n const res = await this.rawPost(\"/token\", {\r\n grant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\r\n client_id: this.clientId,\r\n device_code: deviceCode\r\n });\r\n if (res.ok) return { status: \"approved\", token: res.body as B1TokenResponse };\r\n\r\n const error = typeof res.body === \"object\" && res.body ? (res.body as any).error : undefined;\r\n if (error === \"authorization_pending\") return { status: \"pending\" };\r\n if (error === \"expired_token\") return { status: \"expired\" };\r\n if (error === \"access_denied\") return { status: \"denied\" };\r\n throw new B1OAuthError(error ?? \"invalid_grant\", (res.body as any)?.error_description, res.status);\r\n }\r\n\r\n /** Polls until the device flow is approved, denied, or expires. */\r\n async awaitDeviceToken(options: AwaitDeviceTokenOptions): Promise<B1TokenResponse> {\r\n const deadline = Date.now() + options.expiresIn * 1000;\r\n let intervalMs = Math.max(1, options.interval) * 1000;\r\n\r\n while (Date.now() < deadline) {\r\n if (options.signal?.aborted) throw new B1OAuthError(\"access_denied\", \"polling aborted\", 0);\r\n await delay(intervalMs, options.signal);\r\n\r\n const result = await this.pollDeviceToken(options.deviceCode);\r\n if (result.status === \"approved\") return result.token;\r\n if (result.status === \"denied\") throw new B1OAuthError(\"access_denied\", \"user denied the request\", 400);\r\n if (result.status === \"expired\") throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n // pending — RFC 8628 \"slow_down\" backoff is conservative; nudge the interval up.\r\n intervalMs += 1000;\r\n }\r\n throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n }\r\n\r\n /** Looks up a pending device authorization by its user code (for an approval UI). */\r\n async getPendingDevice(userCode: string): Promise<unknown> {\r\n const url = `${this.baseUrl}/membership/oauth/device/pending/${encodeURIComponent(userCode)}`;\r\n const res = await this.fetchImpl(url, { headers: { Accept: \"application/json\" } });\r\n const body = await readJson(res);\r\n if (!res.ok) throw new B1OAuthError((body as any)?.error ?? \"not_found\", (body as any)?.error_description, res.status);\r\n return body;\r\n }\r\n\r\n private async post<T>(path: string, body: Record<string, unknown>, extraHeaders?: Record<string, string>): Promise<T> {\r\n const res = await this.rawPost(path, body, extraHeaders);\r\n if (!res.ok) {\r\n const b = res.body as any;\r\n throw new B1OAuthError(b?.error ?? \"oauth_error\", b?.error_description, res.status);\r\n }\r\n return res.body as T;\r\n }\r\n\r\n private async rawPost(\r\n path: string,\r\n body: Record<string, unknown>,\r\n extraHeaders?: Record<string, string>\r\n ): Promise<{ ok: boolean; status: number; body: unknown }> {\r\n const url = `${this.baseUrl}/membership/oauth${path}`;\r\n const clean: Record<string, unknown> = {};\r\n for (const [k, v] of Object.entries(body)) {\r\n if (v !== undefined && v !== null) clean[k] = v;\r\n }\r\n const res = await this.fetchImpl(url, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\", ...extraHeaders },\r\n body: JSON.stringify(clean)\r\n });\r\n return { ok: res.ok, status: res.status, body: await readJson(res) };\r\n }\r\n}\r\n\r\nfunction scopeString(scope: B1Scope[] | string): string {\r\n return Array.isArray(scope) ? scope.join(\" \") : scope;\r\n}\r\n\r\nasync function readJson(res: Response): Promise<unknown> {\r\n const text = await res.text();\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n\r\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const timer = setTimeout(resolve, ms);\r\n signal?.addEventListener(\"abort\", () => {\r\n clearTimeout(timer);\r\n reject(new B1OAuthError(\"access_denied\", \"polling aborted\", 0));\r\n }, { once: true });\r\n });\r\n}\r\n"],"mappings":"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","names":["crypto","module"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -360,7 +360,6 @@ declare class B1OAuthClient {
|
|
|
360
360
|
private rawPost;
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
|
|
364
|
-
declare const VERSION = "0.1.0";
|
|
363
|
+
declare const VERSION: string;
|
|
365
364
|
|
|
366
365
|
export { type AttendanceWebhookData, type AwaitDeviceTokenOptions, B1ApiError, type B1DeviceAuthResponse, type B1DevicePollResult, type B1GrantType, type B1KnownScope, type B1Module, B1OAuthClient, type B1OAuthClientOptions, B1OAuthError, type B1QueryValue, type B1RequestOptions, B1RestClient, type B1RestClientOptions, type B1Scope, type B1TokenResponse, type B1Webhook, type B1WebhookEnvelope, type B1WebhookEventName, type B1WebhookMiddlewareOptions, B1_BASE_URLS, B1_SCOPES, type DonationWebhookData, type EventWebhookData, type FormSubmissionWebhookData, type GroupMemberWebhookData, type GroupWebhookData, type HouseholdWebhookData, type PersonWebhookData, type SessionWebhookData, VERSION, WEBHOOK_HEADERS, WebhookVerificationError, WebhookVerifier, b1WebhookMiddleware };
|
package/dist/index.d.ts
CHANGED
|
@@ -360,7 +360,6 @@ declare class B1OAuthClient {
|
|
|
360
360
|
private rawPost;
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
|
|
364
|
-
declare const VERSION = "0.1.0";
|
|
363
|
+
declare const VERSION: string;
|
|
365
364
|
|
|
366
365
|
export { type AttendanceWebhookData, type AwaitDeviceTokenOptions, B1ApiError, type B1DeviceAuthResponse, type B1DevicePollResult, type B1GrantType, type B1KnownScope, type B1Module, B1OAuthClient, type B1OAuthClientOptions, B1OAuthError, type B1QueryValue, type B1RequestOptions, B1RestClient, type B1RestClientOptions, type B1Scope, type B1TokenResponse, type B1Webhook, type B1WebhookEnvelope, type B1WebhookEventName, type B1WebhookMiddlewareOptions, B1_BASE_URLS, B1_SCOPES, type DonationWebhookData, type EventWebhookData, type FormSubmissionWebhookData, type GroupMemberWebhookData, type GroupWebhookData, type HouseholdWebhookData, type PersonWebhookData, type SessionWebhookData, VERSION, WEBHOOK_HEADERS, WebhookVerificationError, WebhookVerifier, b1WebhookMiddleware };
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types/webhooks.ts","../src/types/oauth.ts","../src/types/rest.ts","../src/webhooks/WebhookVerifier.ts","../src/webhooks/expressMiddleware.ts","../src/rest/B1ApiError.ts","../src/rest/B1RestClient.ts","../src/oauth/B1OAuthClient.ts","../src/index.ts"],"sourcesContent":["// Webhook event names, the delivery envelope, and per-event `data` shapes.\r\n// These mirror the B1 Api — `shared/webhooks/WebhookEvents.ts` for the names\r\n// and `WebhookSamplePayloads.ts` for the data shapes.\r\n\r\n/** Every webhook event B1 can emit. */\r\nexport type B1WebhookEventName =\r\n | \"person.created\" | \"person.updated\" | \"person.destroyed\"\r\n | \"group.created\" | \"group.updated\" | \"group.destroyed\"\r\n | \"group.member.added\" | \"group.member.removed\"\r\n | \"household.created\" | \"household.updated\" | \"household.destroyed\"\r\n | \"donation.created\" | \"donation.updated\"\r\n | \"attendance.recorded\"\r\n | \"session.created\"\r\n | \"form.submission.created\"\r\n | \"event.created\" | \"event.updated\" | \"event.destroyed\";\r\n\r\n/** The HTTP headers B1 sends with every webhook delivery. */\r\nexport const WEBHOOK_HEADERS = {\r\n signature: \"X-B1-Signature\",\r\n event: \"X-B1-Event\",\r\n deliveryId: \"X-B1-Delivery-Id\",\r\n timestamp: \"X-B1-Timestamp\"\r\n} as const;\r\n\r\n/** The JSON body B1 POSTs to a subscriber URL. */\r\nexport interface B1WebhookEnvelope<T = unknown> {\r\n event: B1WebhookEventName;\r\n churchId: string;\r\n /** ISO 8601 timestamp. */\r\n occurredAt: string;\r\n data: T;\r\n}\r\n\r\n// --- per-event data shapes ------------------------------------------------\r\n// `destroyed` events carry only `{ id, churchId }`, so the descriptive fields\r\n// are optional on the shared shape.\r\n\r\nexport interface PersonWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: { display?: string; first?: string; last?: string };\r\n contactInfo?: { email?: string };\r\n}\r\n\r\nexport interface GroupWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n categoryName?: string;\r\n}\r\n\r\nexport interface GroupMemberWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId: string;\r\n personId: string;\r\n}\r\n\r\nexport interface HouseholdWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n}\r\n\r\nexport interface DonationWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n batchId?: string;\r\n donationDate?: string;\r\n amount?: number;\r\n currency?: string;\r\n method?: string;\r\n status?: string;\r\n}\r\n\r\nexport interface AttendanceWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n visitDate?: string;\r\n checkinTime?: string;\r\n}\r\n\r\nexport interface SessionWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n serviceTimeId?: string;\r\n sessionDate?: string;\r\n}\r\n\r\nexport interface FormSubmissionWebhookData {\r\n id: string;\r\n churchId: string;\r\n formId?: string;\r\n contentType?: string;\r\n contentId?: string;\r\n}\r\n\r\nexport interface EventWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n title?: string;\r\n start?: string;\r\n end?: string;\r\n}\r\n\r\n/**\r\n * Discriminated union of every webhook envelope — `switch (env.event)` narrows\r\n * `data` to the matching shape.\r\n */\r\nexport type B1Webhook =\r\n | B1WebhookEnvelope<PersonWebhookData> & { event: \"person.created\" | \"person.updated\" | \"person.destroyed\" }\r\n | B1WebhookEnvelope<GroupWebhookData> & { event: \"group.created\" | \"group.updated\" | \"group.destroyed\" }\r\n | B1WebhookEnvelope<GroupMemberWebhookData> & { event: \"group.member.added\" | \"group.member.removed\" }\r\n | B1WebhookEnvelope<HouseholdWebhookData> & { event: \"household.created\" | \"household.updated\" | \"household.destroyed\" }\r\n | B1WebhookEnvelope<DonationWebhookData> & { event: \"donation.created\" | \"donation.updated\" }\r\n | B1WebhookEnvelope<AttendanceWebhookData> & { event: \"attendance.recorded\" }\r\n | B1WebhookEnvelope<SessionWebhookData> & { event: \"session.created\" }\r\n | B1WebhookEnvelope<FormSubmissionWebhookData> & { event: \"form.submission.created\" }\r\n | B1WebhookEnvelope<EventWebhookData> & { event: \"event.created\" | \"event.updated\" | \"event.destroyed\" };\r\n","// OAuth types — mirror the B1 Api scope catalog (`shared/auth/Scopes.ts`) and\r\n// the token responses from `OAuthController`.\r\n\r\n/** A recognised OAuth / API-key scope. */\r\nexport type B1KnownScope =\r\n | \"people:read\" | \"people:write\"\r\n | \"groups:read\" | \"groups:write\"\r\n | \"donations:read\" | \"donations:write\"\r\n | \"attendance:read\" | \"attendance:write\"\r\n | \"forms:write\"\r\n | \"content:read\" | \"content:write\"\r\n | \"messaging:read\" | \"messaging:write\"\r\n | \"roles:read\" | \"roles:write\"\r\n | \"settings:read\" | \"settings:write\"\r\n | \"offline_access\";\r\n\r\n/** Scope strings — known scopes get autocomplete, custom strings still allowed. */\r\nexport type B1Scope = B1KnownScope | (string & {});\r\n\r\n/** All scopes B1 recognises in its catalog (plus `offline_access`). */\r\nexport const B1_SCOPES: B1KnownScope[] = [\r\n \"people:read\",\r\n \"people:write\",\r\n \"groups:read\",\r\n \"groups:write\",\r\n \"donations:read\",\r\n \"donations:write\",\r\n \"attendance:read\",\r\n \"attendance:write\",\r\n \"forms:write\",\r\n \"content:read\",\r\n \"content:write\",\r\n \"messaging:read\",\r\n \"messaging:write\",\r\n \"roles:read\",\r\n \"roles:write\",\r\n \"settings:read\",\r\n \"settings:write\",\r\n \"offline_access\"\r\n];\r\n\r\nexport type B1GrantType =\r\n | \"authorization_code\"\r\n | \"refresh_token\"\r\n | \"urn:ietf:params:oauth:grant-type:device_code\";\r\n\r\n/** The token response from `POST /membership/oauth/token`. */\r\nexport interface B1TokenResponse {\r\n access_token: string;\r\n token_type: \"Bearer\";\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Unix timestamp (seconds) the token was created. Absent on the device grant. */\r\n created_at?: number;\r\n refresh_token: string;\r\n scope: string;\r\n}\r\n\r\n/** The response from `POST /membership/oauth/device/authorize` (RFC 8628). */\r\nexport interface B1DeviceAuthResponse {\r\n device_code: string;\r\n user_code: string;\r\n verification_uri: string;\r\n verification_uri_complete?: string;\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Recommended poll interval in seconds. */\r\n interval: number;\r\n}\r\n\r\n/** Outcome of a single device-token poll. */\r\nexport type B1DevicePollResult =\r\n | { status: \"approved\"; token: B1TokenResponse }\r\n | { status: \"pending\" }\r\n | { status: \"expired\" }\r\n | { status: \"denied\" };\r\n","// REST client types — module names, base URLs, request/option shapes.\r\n\r\n/** The B1 Api modules, each addressed by a `/<module>` path prefix. */\r\nexport type B1Module =\r\n | \"membership\"\r\n | \"giving\"\r\n | \"attendance\"\r\n | \"content\"\r\n | \"messaging\"\r\n | \"doing\"\r\n | \"reporting\";\r\n\r\n/** Known B1 Api base URLs. */\r\nexport const B1_BASE_URLS = {\r\n prod: \"https://api.b1.church\",\r\n staging: \"https://api.staging.b1.church\"\r\n} as const;\r\n\r\nexport interface B1RestClientOptions {\r\n /** A raw `cak_<prefix>.<secret>` API key, sent verbatim as a bearer token. */\r\n apiKey: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport type B1QueryValue = string | number | boolean | undefined | null;\r\n\r\nexport interface B1RequestOptions {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n query?: Record<string, B1QueryValue>;\r\n headers?: Record<string, string>;\r\n}\r\n","import crypto from \"crypto\";\r\nimport { B1WebhookEnvelope } from \"../types\";\r\n\r\n/** Thrown by `verifyAndParse` when a signature does not match. */\r\nexport class WebhookVerificationError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"WebhookVerificationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Verifies and parses inbound B1 webhook deliveries.\r\n *\r\n * The signature is an HMAC-SHA256 over the **raw request body** — verify\r\n * before any JSON parse/re-stringify, which would change byte order/whitespace.\r\n * Byte-compatible with the B1 Api `shared/webhooks/WebhookSigner.ts`.\r\n */\r\nexport class WebhookVerifier {\r\n /** Computes the `X-B1-Signature` value for a raw body. */\r\n static sign(secret: string, rawBody: string | Buffer): string {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return \"sha256=\" + crypto.createHmac(\"sha256\", secret).update(body, \"utf8\").digest(\"hex\");\r\n }\r\n\r\n /**\r\n * Returns `true` when `signatureHeader` matches the body. Never throws —\r\n * a missing, empty, or malformed header simply returns `false`.\r\n */\r\n static verify(secret: string, rawBody: string | Buffer, signatureHeader: string | null | undefined): boolean {\r\n if (!signatureHeader) return false;\r\n const expected = WebhookVerifier.sign(secret, rawBody);\r\n const a = Buffer.from(signatureHeader, \"utf8\");\r\n const b = Buffer.from(expected, \"utf8\");\r\n // timingSafeEqual throws on length mismatch — guard, but still do a\r\n // constant-time compare against `expected` so timing stays flat.\r\n if (a.length !== b.length) {\r\n crypto.timingSafeEqual(b, b);\r\n return false;\r\n }\r\n return crypto.timingSafeEqual(a, b);\r\n }\r\n\r\n /** Parses a raw body into a typed envelope (no verification). */\r\n static parseEnvelope<T = unknown>(rawBody: string | Buffer): B1WebhookEnvelope<T> {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return JSON.parse(body) as B1WebhookEnvelope<T>;\r\n }\r\n\r\n /**\r\n * Verifies the signature, then parses the body into a typed envelope.\r\n * Throws `WebhookVerificationError` if the signature does not match.\r\n */\r\n static verifyAndParse<T = unknown>(\r\n secret: string,\r\n rawBody: string | Buffer,\r\n signatureHeader: string | null | undefined\r\n ): B1WebhookEnvelope<T> {\r\n if (!WebhookVerifier.verify(secret, rawBody, signatureHeader)) {\r\n throw new WebhookVerificationError(\"Webhook signature verification failed\");\r\n }\r\n return WebhookVerifier.parseEnvelope<T>(rawBody);\r\n }\r\n}\r\n","import type { Request, RequestHandler, Response } from \"express\";\r\nimport { WebhookVerifier } from \"./WebhookVerifier\";\r\nimport { B1WebhookEnvelope, WEBHOOK_HEADERS } from \"../types\";\r\n\r\ndeclare global {\r\n // eslint-disable-next-line @typescript-eslint/no-namespace\r\n namespace Express {\r\n interface Request {\r\n /** The untouched request body — set by `express.json({ verify })`. */\r\n rawBody?: Buffer | string;\r\n /** The verified, parsed webhook envelope — set by `b1WebhookMiddleware`. */\r\n b1Webhook?: B1WebhookEnvelope;\r\n }\r\n }\r\n}\r\n\r\nexport interface B1WebhookMiddlewareOptions {\r\n /** The webhook secret, or a function resolving one per request. */\r\n secret: string | ((req: Request) => string);\r\n /** Called instead of the default 401 response when verification fails. */\r\n onInvalid?: (req: Request, res: Response) => void;\r\n}\r\n\r\n/**\r\n * Express middleware that verifies the `X-B1-Signature` header and attaches a\r\n * typed `req.b1Webhook` envelope.\r\n *\r\n * The raw request body must be available — capture it before JSON parsing:\r\n *\r\n * ```ts\r\n * app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));\r\n * app.post(\"/webhooks/b1\", b1WebhookMiddleware({ secret }), (req, res) => {\r\n * console.log(req.b1Webhook?.event);\r\n * res.sendStatus(200);\r\n * });\r\n * ```\r\n *\r\n * `express.raw({ type: \"application/json\" })` is also accepted — `req.body` is\r\n * then a Buffer the middleware verifies and parses itself.\r\n */\r\nexport function b1WebhookMiddleware(options: B1WebhookMiddlewareOptions): RequestHandler {\r\n return (req: Request, res: Response, next): void => {\r\n const raw = resolveRawBody(req);\r\n if (raw === undefined) {\r\n throw new Error(\r\n \"b1WebhookMiddleware: no raw request body found. Mount \" +\r\n \"express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) \" +\r\n \"or express.raw({ type: \\\"application/json\\\" }) before this middleware.\"\r\n );\r\n }\r\n\r\n const secret = typeof options.secret === \"function\" ? options.secret(req) : options.secret;\r\n const signature = req.header(WEBHOOK_HEADERS.signature);\r\n\r\n if (!WebhookVerifier.verify(secret, raw, signature)) {\r\n if (options.onInvalid) options.onInvalid(req, res);\r\n else res.status(401).json({ error: \"invalid webhook signature\" });\r\n return;\r\n }\r\n\r\n const envelope = WebhookVerifier.parseEnvelope(raw);\r\n req.b1Webhook = envelope;\r\n if (Buffer.isBuffer(req.body)) req.body = envelope;\r\n next();\r\n };\r\n}\r\n\r\nfunction resolveRawBody(req: Request): Buffer | string | undefined {\r\n if (req.rawBody !== undefined) return req.rawBody;\r\n if (Buffer.isBuffer(req.body)) return req.body;\r\n return undefined;\r\n}\r\n","/** Thrown by `B1RestClient` when the Api returns a non-2xx response. */\r\nexport class B1ApiError extends Error {\r\n readonly status: number;\r\n readonly statusText: string;\r\n readonly body: unknown;\r\n readonly method: string;\r\n readonly url: string;\r\n\r\n constructor(opts: { status: number; statusText: string; body: unknown; method: string; url: string }) {\r\n super(`B1 Api ${opts.method} ${opts.url} failed: ${opts.status} ${opts.statusText}`);\r\n this.name = \"B1ApiError\";\r\n this.status = opts.status;\r\n this.statusText = opts.statusText;\r\n this.body = opts.body;\r\n this.method = opts.method;\r\n this.url = opts.url;\r\n }\r\n}\r\n","import { B1ApiError } from \"./B1ApiError\";\r\nimport { B1_BASE_URLS, B1Module, B1RequestOptions, B1RestClientOptions } from \"../types\";\r\n\r\n/**\r\n * A typed REST client for the B1 Api, authenticated with a `cak_` API key.\r\n *\r\n * The Api is a single host with per-module path prefixes — use `request()`\r\n * with a full `/membership/...` path, or the module helpers which prefix it\r\n * for you.\r\n *\r\n * ```ts\r\n * const client = new B1RestClient({ apiKey: \"cak_...\" });\r\n * const people = await client.membership<Person[]>(\"/people\");\r\n * ```\r\n *\r\n * Non-2xx responses throw `B1ApiError` (carrying status + parsed body) so a\r\n * caller can distinguish 401/403/404/500.\r\n */\r\nexport class B1RestClient {\r\n private readonly apiKey: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1RestClientOptions) {\r\n if (!options.apiKey) throw new Error(\"B1RestClient: apiKey is required\");\r\n this.apiKey = options.apiKey;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1RestClient: no fetch available — pass options.fetch (Node 18+ has global fetch)\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /** Issues a request against a full Api path (e.g. `/membership/people`). */\r\n async request<T = unknown>(path: string, options: B1RequestOptions = {}): Promise<T> {\r\n const method = options.method ?? \"GET\";\r\n const url = this.buildUrl(path, options.query);\r\n\r\n const headers: Record<string, string> = {\r\n Authorization: `Bearer ${this.apiKey}`,\r\n Accept: \"application/json\",\r\n ...options.headers\r\n };\r\n const hasBody = options.body !== undefined && options.body !== null;\r\n if (hasBody) headers[\"Content-Type\"] = \"application/json\";\r\n\r\n let response: Response;\r\n try {\r\n response = await this.fetchImpl(url, {\r\n method,\r\n headers,\r\n ...(hasBody ? { body: JSON.stringify(options.body) } : {})\r\n });\r\n } catch (err) {\r\n throw new B1ApiError({\r\n status: 0,\r\n statusText: err instanceof Error ? err.message : \"network error\",\r\n body: null,\r\n method,\r\n url\r\n });\r\n }\r\n\r\n const text = await response.text();\r\n const body = parseBody(text);\r\n\r\n if (!response.ok) {\r\n throw new B1ApiError({ status: response.status, statusText: response.statusText, body, method, url });\r\n }\r\n return body as T;\r\n }\r\n\r\n /** Request against the `/membership` module. */\r\n membership<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"membership\", path, options);\r\n }\r\n\r\n /** Request against the `/giving` module. */\r\n giving<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"giving\", path, options);\r\n }\r\n\r\n /** Request against the `/attendance` module. */\r\n attendance<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"attendance\", path, options);\r\n }\r\n\r\n /** Request against the `/content` module. */\r\n content<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"content\", path, options);\r\n }\r\n\r\n /** Request against the `/messaging` module. */\r\n messaging<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"messaging\", path, options);\r\n }\r\n\r\n /** Request against the `/doing` module. */\r\n doing<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"doing\", path, options);\r\n }\r\n\r\n /** Request against the `/reporting` module. */\r\n reporting<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"reporting\", path, options);\r\n }\r\n\r\n private module<T>(module: B1Module, path: string, options?: B1RequestOptions): Promise<T> {\r\n const sub = path.startsWith(\"/\") ? path : `/${path}`;\r\n return this.request<T>(`/${module}${sub}`, options);\r\n }\r\n\r\n private buildUrl(path: string, query?: B1RequestOptions[\"query\"]): string {\r\n const p = path.startsWith(\"/\") ? path : `/${path}`;\r\n let url = `${this.baseUrl}${p}`;\r\n if (query) {\r\n const params = new URLSearchParams();\r\n for (const [key, value] of Object.entries(query)) {\r\n if (value !== undefined && value !== null) params.append(key, String(value));\r\n }\r\n const qs = params.toString();\r\n if (qs) url += `?${qs}`;\r\n }\r\n return url;\r\n }\r\n}\r\n\r\n/** Parses a response body as JSON, falling back to the raw text / undefined. */\r\nfunction parseBody(text: string): unknown {\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n","import {\r\n B1_BASE_URLS,\r\n B1DeviceAuthResponse,\r\n B1DevicePollResult,\r\n B1Scope,\r\n B1TokenResponse\r\n} from \"../types\";\r\n\r\n/** Thrown when a B1 OAuth endpoint returns an `error` response. */\r\nexport class B1OAuthError extends Error {\r\n readonly error: string;\r\n readonly errorDescription?: string;\r\n readonly status: number;\r\n\r\n constructor(error: string, errorDescription: string | undefined, status: number) {\r\n super(errorDescription ? `${error}: ${errorDescription}` : error);\r\n this.name = \"B1OAuthError\";\r\n this.error = error;\r\n this.errorDescription = errorDescription;\r\n this.status = status;\r\n }\r\n}\r\n\r\nexport interface B1OAuthClientOptions {\r\n clientId: string;\r\n /** Required for confidential clients (authorization_code grant). */\r\n clientSecret?: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport interface AwaitDeviceTokenOptions {\r\n deviceCode: string;\r\n /** Poll interval in seconds (from `B1DeviceAuthResponse.interval`). */\r\n interval: number;\r\n /** Overall timeout in seconds (from `B1DeviceAuthResponse.expires_in`). */\r\n expiresIn: number;\r\n /** Optional abort signal to cancel polling. */\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Helper for B1's OAuth flows — authorization-code, refresh-token, and the\r\n * RFC 8628 device flow — against `/membership/oauth/*`.\r\n */\r\nexport class B1OAuthClient {\r\n private readonly clientId: string;\r\n private readonly clientSecret?: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1OAuthClientOptions) {\r\n if (!options.clientId) throw new Error(\"B1OAuthClient: clientId is required\");\r\n this.clientId = options.clientId;\r\n this.clientSecret = options.clientSecret;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1OAuthClient: no fetch available — pass options.fetch\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /**\r\n * Requests an authorization code. B1's `/authorize` endpoint is an\r\n * authenticated POST, so this needs the *user's* access token (a JWT).\r\n */\r\n async getAuthorizationCode(params: {\r\n userAccessToken: string;\r\n redirectUri: string;\r\n scope: B1Scope[] | string;\r\n state?: string;\r\n }): Promise<{ code: string; state: string | null }> {\r\n return this.post(\r\n \"/authorize\",\r\n {\r\n client_id: this.clientId,\r\n redirect_uri: params.redirectUri,\r\n response_type: \"code\",\r\n scope: scopeString(params.scope),\r\n state: params.state\r\n },\r\n { Authorization: `Bearer ${params.userAccessToken}` }\r\n );\r\n }\r\n\r\n /** Exchanges an authorization code for tokens. */\r\n async exchangeCode(params: { code: string; redirectUri?: string }): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"authorization_code\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n code: params.code,\r\n redirect_uri: params.redirectUri\r\n });\r\n }\r\n\r\n /** Exchanges a refresh token for a fresh access token. */\r\n async refresh(refreshToken: string): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"refresh_token\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n refresh_token: refreshToken\r\n });\r\n }\r\n\r\n /** Starts the device flow — returns a user code + verification URI. */\r\n async startDeviceFlow(scope?: B1Scope[] | string): Promise<B1DeviceAuthResponse> {\r\n return this.post(\"/device/authorize\", {\r\n client_id: this.clientId,\r\n scope: scope ? scopeString(scope) : undefined\r\n });\r\n }\r\n\r\n /** Polls once for a device-flow token. Never throws on a pending/expired/denied state. */\r\n async pollDeviceToken(deviceCode: string): Promise<B1DevicePollResult> {\r\n const res = await this.rawPost(\"/token\", {\r\n grant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\r\n client_id: this.clientId,\r\n device_code: deviceCode\r\n });\r\n if (res.ok) return { status: \"approved\", token: res.body as B1TokenResponse };\r\n\r\n const error = typeof res.body === \"object\" && res.body ? (res.body as any).error : undefined;\r\n if (error === \"authorization_pending\") return { status: \"pending\" };\r\n if (error === \"expired_token\") return { status: \"expired\" };\r\n if (error === \"access_denied\") return { status: \"denied\" };\r\n throw new B1OAuthError(error ?? \"invalid_grant\", (res.body as any)?.error_description, res.status);\r\n }\r\n\r\n /** Polls until the device flow is approved, denied, or expires. */\r\n async awaitDeviceToken(options: AwaitDeviceTokenOptions): Promise<B1TokenResponse> {\r\n const deadline = Date.now() + options.expiresIn * 1000;\r\n let intervalMs = Math.max(1, options.interval) * 1000;\r\n\r\n while (Date.now() < deadline) {\r\n if (options.signal?.aborted) throw new B1OAuthError(\"access_denied\", \"polling aborted\", 0);\r\n await delay(intervalMs, options.signal);\r\n\r\n const result = await this.pollDeviceToken(options.deviceCode);\r\n if (result.status === \"approved\") return result.token;\r\n if (result.status === \"denied\") throw new B1OAuthError(\"access_denied\", \"user denied the request\", 400);\r\n if (result.status === \"expired\") throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n // pending — RFC 8628 \"slow_down\" backoff is conservative; nudge the interval up.\r\n intervalMs += 1000;\r\n }\r\n throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n }\r\n\r\n /** Looks up a pending device authorization by its user code (for an approval UI). */\r\n async getPendingDevice(userCode: string): Promise<unknown> {\r\n const url = `${this.baseUrl}/membership/oauth/device/pending/${encodeURIComponent(userCode)}`;\r\n const res = await this.fetchImpl(url, { headers: { Accept: \"application/json\" } });\r\n const body = await readJson(res);\r\n if (!res.ok) throw new B1OAuthError((body as any)?.error ?? \"not_found\", (body as any)?.error_description, res.status);\r\n return body;\r\n }\r\n\r\n private async post<T>(path: string, body: Record<string, unknown>, extraHeaders?: Record<string, string>): Promise<T> {\r\n const res = await this.rawPost(path, body, extraHeaders);\r\n if (!res.ok) {\r\n const b = res.body as any;\r\n throw new B1OAuthError(b?.error ?? \"oauth_error\", b?.error_description, res.status);\r\n }\r\n return res.body as T;\r\n }\r\n\r\n private async rawPost(\r\n path: string,\r\n body: Record<string, unknown>,\r\n extraHeaders?: Record<string, string>\r\n ): Promise<{ ok: boolean; status: number; body: unknown }> {\r\n const url = `${this.baseUrl}/membership/oauth${path}`;\r\n const clean: Record<string, unknown> = {};\r\n for (const [k, v] of Object.entries(body)) {\r\n if (v !== undefined && v !== null) clean[k] = v;\r\n }\r\n const res = await this.fetchImpl(url, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\", ...extraHeaders },\r\n body: JSON.stringify(clean)\r\n });\r\n return { ok: res.ok, status: res.status, body: await readJson(res) };\r\n }\r\n}\r\n\r\nfunction scopeString(scope: B1Scope[] | string): string {\r\n return Array.isArray(scope) ? scope.join(\" \") : scope;\r\n}\r\n\r\nasync function readJson(res: Response): Promise<unknown> {\r\n const text = await res.text();\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n\r\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const timer = setTimeout(resolve, ms);\r\n signal?.addEventListener(\"abort\", () => {\r\n clearTimeout(timer);\r\n reject(new B1OAuthError(\"access_denied\", \"polling aborted\", 0));\r\n }, { once: true });\r\n });\r\n}\r\n","/** `@churchapps/integration-sdk` — toolkit for building B1.church integrations. */\r\nexport const VERSION = \"0.1.0\";\r\n\r\nexport * from \"./types\";\r\nexport * from \"./webhooks\";\r\nexport * from \"./rest\";\r\nexport * from \"./oauth\";\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/types/webhooks.ts","../src/types/oauth.ts","../src/types/rest.ts","../src/webhooks/WebhookVerifier.ts","../src/webhooks/expressMiddleware.ts","../src/rest/B1ApiError.ts","../src/rest/B1RestClient.ts","../src/oauth/B1OAuthClient.ts","../src/index.ts"],"sourcesContent":["// Webhook event names, the delivery envelope, and per-event `data` shapes.\r\n// These mirror the B1 Api — `shared/webhooks/WebhookEvents.ts` for the names\r\n// and `WebhookSamplePayloads.ts` for the data shapes.\r\n\r\n/** Every webhook event B1 can emit. */\r\nexport type B1WebhookEventName =\r\n | \"person.created\" | \"person.updated\" | \"person.destroyed\"\r\n | \"group.created\" | \"group.updated\" | \"group.destroyed\"\r\n | \"group.member.added\" | \"group.member.removed\"\r\n | \"household.created\" | \"household.updated\" | \"household.destroyed\"\r\n | \"donation.created\" | \"donation.updated\"\r\n | \"attendance.recorded\"\r\n | \"session.created\"\r\n | \"form.submission.created\"\r\n | \"event.created\" | \"event.updated\" | \"event.destroyed\";\r\n\r\n/** The HTTP headers B1 sends with every webhook delivery. */\r\nexport const WEBHOOK_HEADERS = {\r\n signature: \"X-B1-Signature\",\r\n event: \"X-B1-Event\",\r\n deliveryId: \"X-B1-Delivery-Id\",\r\n timestamp: \"X-B1-Timestamp\"\r\n} as const;\r\n\r\n/** The JSON body B1 POSTs to a subscriber URL. */\r\nexport interface B1WebhookEnvelope<T = unknown> {\r\n event: B1WebhookEventName;\r\n churchId: string;\r\n /** ISO 8601 timestamp. */\r\n occurredAt: string;\r\n data: T;\r\n}\r\n\r\n// --- per-event data shapes ------------------------------------------------\r\n// `destroyed` events carry only `{ id, churchId }`, so the descriptive fields\r\n// are optional on the shared shape.\r\n\r\nexport interface PersonWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: { display?: string; first?: string; last?: string };\r\n contactInfo?: { email?: string };\r\n}\r\n\r\nexport interface GroupWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n categoryName?: string;\r\n}\r\n\r\nexport interface GroupMemberWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId: string;\r\n personId: string;\r\n}\r\n\r\nexport interface HouseholdWebhookData {\r\n id: string;\r\n churchId: string;\r\n name?: string;\r\n}\r\n\r\nexport interface DonationWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n batchId?: string;\r\n donationDate?: string;\r\n amount?: number;\r\n currency?: string;\r\n method?: string;\r\n status?: string;\r\n}\r\n\r\nexport interface AttendanceWebhookData {\r\n id: string;\r\n churchId: string;\r\n personId?: string;\r\n visitDate?: string;\r\n checkinTime?: string;\r\n}\r\n\r\nexport interface SessionWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n serviceTimeId?: string;\r\n sessionDate?: string;\r\n}\r\n\r\nexport interface FormSubmissionWebhookData {\r\n id: string;\r\n churchId: string;\r\n formId?: string;\r\n contentType?: string;\r\n contentId?: string;\r\n}\r\n\r\nexport interface EventWebhookData {\r\n id: string;\r\n churchId: string;\r\n groupId?: string;\r\n title?: string;\r\n start?: string;\r\n end?: string;\r\n}\r\n\r\n/**\r\n * Discriminated union of every webhook envelope — `switch (env.event)` narrows\r\n * `data` to the matching shape.\r\n */\r\nexport type B1Webhook =\r\n | B1WebhookEnvelope<PersonWebhookData> & { event: \"person.created\" | \"person.updated\" | \"person.destroyed\" }\r\n | B1WebhookEnvelope<GroupWebhookData> & { event: \"group.created\" | \"group.updated\" | \"group.destroyed\" }\r\n | B1WebhookEnvelope<GroupMemberWebhookData> & { event: \"group.member.added\" | \"group.member.removed\" }\r\n | B1WebhookEnvelope<HouseholdWebhookData> & { event: \"household.created\" | \"household.updated\" | \"household.destroyed\" }\r\n | B1WebhookEnvelope<DonationWebhookData> & { event: \"donation.created\" | \"donation.updated\" }\r\n | B1WebhookEnvelope<AttendanceWebhookData> & { event: \"attendance.recorded\" }\r\n | B1WebhookEnvelope<SessionWebhookData> & { event: \"session.created\" }\r\n | B1WebhookEnvelope<FormSubmissionWebhookData> & { event: \"form.submission.created\" }\r\n | B1WebhookEnvelope<EventWebhookData> & { event: \"event.created\" | \"event.updated\" | \"event.destroyed\" };\r\n","// OAuth types — mirror the B1 Api scope catalog (`shared/auth/Scopes.ts`) and\r\n// the token responses from `OAuthController`.\r\n\r\n/** A recognised OAuth / API-key scope. */\r\nexport type B1KnownScope =\r\n | \"people:read\" | \"people:write\"\r\n | \"groups:read\" | \"groups:write\"\r\n | \"donations:read\" | \"donations:write\"\r\n | \"attendance:read\" | \"attendance:write\"\r\n | \"forms:write\"\r\n | \"content:read\" | \"content:write\"\r\n | \"messaging:read\" | \"messaging:write\"\r\n | \"roles:read\" | \"roles:write\"\r\n | \"settings:read\" | \"settings:write\"\r\n | \"offline_access\";\r\n\r\n/** Scope strings — known scopes get autocomplete, custom strings still allowed. */\r\nexport type B1Scope = B1KnownScope | (string & {});\r\n\r\n/** All scopes B1 recognises in its catalog (plus `offline_access`). */\r\nexport const B1_SCOPES: B1KnownScope[] = [\r\n \"people:read\",\r\n \"people:write\",\r\n \"groups:read\",\r\n \"groups:write\",\r\n \"donations:read\",\r\n \"donations:write\",\r\n \"attendance:read\",\r\n \"attendance:write\",\r\n \"forms:write\",\r\n \"content:read\",\r\n \"content:write\",\r\n \"messaging:read\",\r\n \"messaging:write\",\r\n \"roles:read\",\r\n \"roles:write\",\r\n \"settings:read\",\r\n \"settings:write\",\r\n \"offline_access\"\r\n];\r\n\r\nexport type B1GrantType =\r\n | \"authorization_code\"\r\n | \"refresh_token\"\r\n | \"urn:ietf:params:oauth:grant-type:device_code\";\r\n\r\n/** The token response from `POST /membership/oauth/token`. */\r\nexport interface B1TokenResponse {\r\n access_token: string;\r\n token_type: \"Bearer\";\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Unix timestamp (seconds) the token was created. Absent on the device grant. */\r\n created_at?: number;\r\n refresh_token: string;\r\n scope: string;\r\n}\r\n\r\n/** The response from `POST /membership/oauth/device/authorize` (RFC 8628). */\r\nexport interface B1DeviceAuthResponse {\r\n device_code: string;\r\n user_code: string;\r\n verification_uri: string;\r\n verification_uri_complete?: string;\r\n /** Lifetime in seconds. */\r\n expires_in: number;\r\n /** Recommended poll interval in seconds. */\r\n interval: number;\r\n}\r\n\r\n/** Outcome of a single device-token poll. */\r\nexport type B1DevicePollResult =\r\n | { status: \"approved\"; token: B1TokenResponse }\r\n | { status: \"pending\" }\r\n | { status: \"expired\" }\r\n | { status: \"denied\" };\r\n","// REST client types — module names, base URLs, request/option shapes.\r\n\r\n/** The B1 Api modules, each addressed by a `/<module>` path prefix. */\r\nexport type B1Module =\r\n | \"membership\"\r\n | \"giving\"\r\n | \"attendance\"\r\n | \"content\"\r\n | \"messaging\"\r\n | \"doing\"\r\n | \"reporting\";\r\n\r\n/** Known B1 Api base URLs. */\r\nexport const B1_BASE_URLS = {\r\n prod: \"https://api.b1.church\",\r\n staging: \"https://api.staging.b1.church\"\r\n} as const;\r\n\r\nexport interface B1RestClientOptions {\r\n /** A raw `cak_<prefix>.<secret>` API key, sent verbatim as a bearer token. */\r\n apiKey: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport type B1QueryValue = string | number | boolean | undefined | null;\r\n\r\nexport interface B1RequestOptions {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n query?: Record<string, B1QueryValue>;\r\n headers?: Record<string, string>;\r\n}\r\n","import crypto from \"crypto\";\r\nimport { B1WebhookEnvelope } from \"../types\";\r\n\r\n/** Thrown by `verifyAndParse` when a signature does not match. */\r\nexport class WebhookVerificationError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"WebhookVerificationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Verifies and parses inbound B1 webhook deliveries.\r\n *\r\n * The signature is an HMAC-SHA256 over the **raw request body** — verify\r\n * before any JSON parse/re-stringify, which would change byte order/whitespace.\r\n * Byte-compatible with the B1 Api `shared/webhooks/WebhookSigner.ts`.\r\n */\r\nexport class WebhookVerifier {\r\n /** Computes the `X-B1-Signature` value for a raw body. */\r\n static sign(secret: string, rawBody: string | Buffer): string {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return \"sha256=\" + crypto.createHmac(\"sha256\", secret).update(body, \"utf8\").digest(\"hex\");\r\n }\r\n\r\n /**\r\n * Returns `true` when `signatureHeader` matches the body. Never throws —\r\n * a missing, empty, or malformed header simply returns `false`.\r\n */\r\n static verify(secret: string, rawBody: string | Buffer, signatureHeader: string | null | undefined): boolean {\r\n if (!signatureHeader) return false;\r\n const expected = WebhookVerifier.sign(secret, rawBody);\r\n const a = Buffer.from(signatureHeader, \"utf8\");\r\n const b = Buffer.from(expected, \"utf8\");\r\n // timingSafeEqual throws on length mismatch — guard, but still do a\r\n // constant-time compare against `expected` so timing stays flat.\r\n if (a.length !== b.length) {\r\n crypto.timingSafeEqual(b, b);\r\n return false;\r\n }\r\n return crypto.timingSafeEqual(a, b);\r\n }\r\n\r\n /** Parses a raw body into a typed envelope (no verification). */\r\n static parseEnvelope<T = unknown>(rawBody: string | Buffer): B1WebhookEnvelope<T> {\r\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\r\n return JSON.parse(body) as B1WebhookEnvelope<T>;\r\n }\r\n\r\n /**\r\n * Verifies the signature, then parses the body into a typed envelope.\r\n * Throws `WebhookVerificationError` if the signature does not match.\r\n */\r\n static verifyAndParse<T = unknown>(\r\n secret: string,\r\n rawBody: string | Buffer,\r\n signatureHeader: string | null | undefined\r\n ): B1WebhookEnvelope<T> {\r\n if (!WebhookVerifier.verify(secret, rawBody, signatureHeader)) {\r\n throw new WebhookVerificationError(\"Webhook signature verification failed\");\r\n }\r\n return WebhookVerifier.parseEnvelope<T>(rawBody);\r\n }\r\n}\r\n","import type { Request, RequestHandler, Response } from \"express\";\r\nimport { WebhookVerifier } from \"./WebhookVerifier\";\r\nimport { B1WebhookEnvelope, WEBHOOK_HEADERS } from \"../types\";\r\n\r\ndeclare global {\r\n // eslint-disable-next-line @typescript-eslint/no-namespace\r\n namespace Express {\r\n interface Request {\r\n /** The untouched request body — set by `express.json({ verify })`. */\r\n rawBody?: Buffer | string;\r\n /** The verified, parsed webhook envelope — set by `b1WebhookMiddleware`. */\r\n b1Webhook?: B1WebhookEnvelope;\r\n }\r\n }\r\n}\r\n\r\nexport interface B1WebhookMiddlewareOptions {\r\n /** The webhook secret, or a function resolving one per request. */\r\n secret: string | ((req: Request) => string);\r\n /** Called instead of the default 401 response when verification fails. */\r\n onInvalid?: (req: Request, res: Response) => void;\r\n}\r\n\r\n/**\r\n * Express middleware that verifies the `X-B1-Signature` header and attaches a\r\n * typed `req.b1Webhook` envelope.\r\n *\r\n * The raw request body must be available — capture it before JSON parsing:\r\n *\r\n * ```ts\r\n * app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));\r\n * app.post(\"/webhooks/b1\", b1WebhookMiddleware({ secret }), (req, res) => {\r\n * console.log(req.b1Webhook?.event);\r\n * res.sendStatus(200);\r\n * });\r\n * ```\r\n *\r\n * `express.raw({ type: \"application/json\" })` is also accepted — `req.body` is\r\n * then a Buffer the middleware verifies and parses itself.\r\n */\r\nexport function b1WebhookMiddleware(options: B1WebhookMiddlewareOptions): RequestHandler {\r\n return (req: Request, res: Response, next): void => {\r\n const raw = resolveRawBody(req);\r\n if (raw === undefined) {\r\n throw new Error(\r\n \"b1WebhookMiddleware: no raw request body found. Mount \" +\r\n \"express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) \" +\r\n \"or express.raw({ type: \\\"application/json\\\" }) before this middleware.\"\r\n );\r\n }\r\n\r\n const secret = typeof options.secret === \"function\" ? options.secret(req) : options.secret;\r\n const signature = req.header(WEBHOOK_HEADERS.signature);\r\n\r\n if (!WebhookVerifier.verify(secret, raw, signature)) {\r\n if (options.onInvalid) options.onInvalid(req, res);\r\n else res.status(401).json({ error: \"invalid webhook signature\" });\r\n return;\r\n }\r\n\r\n const envelope = WebhookVerifier.parseEnvelope(raw);\r\n req.b1Webhook = envelope;\r\n if (Buffer.isBuffer(req.body)) req.body = envelope;\r\n next();\r\n };\r\n}\r\n\r\nfunction resolveRawBody(req: Request): Buffer | string | undefined {\r\n if (req.rawBody !== undefined) return req.rawBody;\r\n if (Buffer.isBuffer(req.body)) return req.body;\r\n return undefined;\r\n}\r\n","/** Thrown by `B1RestClient` when the Api returns a non-2xx response. */\r\nexport class B1ApiError extends Error {\r\n readonly status: number;\r\n readonly statusText: string;\r\n readonly body: unknown;\r\n readonly method: string;\r\n readonly url: string;\r\n\r\n constructor(opts: { status: number; statusText: string; body: unknown; method: string; url: string }) {\r\n super(`B1 Api ${opts.method} ${opts.url} failed: ${opts.status} ${opts.statusText}`);\r\n this.name = \"B1ApiError\";\r\n this.status = opts.status;\r\n this.statusText = opts.statusText;\r\n this.body = opts.body;\r\n this.method = opts.method;\r\n this.url = opts.url;\r\n }\r\n}\r\n","import { B1ApiError } from \"./B1ApiError\";\r\nimport { B1_BASE_URLS, B1Module, B1RequestOptions, B1RestClientOptions } from \"../types\";\r\n\r\n/**\r\n * A typed REST client for the B1 Api, authenticated with a `cak_` API key.\r\n *\r\n * The Api is a single host with per-module path prefixes — use `request()`\r\n * with a full `/membership/...` path, or the module helpers which prefix it\r\n * for you.\r\n *\r\n * ```ts\r\n * const client = new B1RestClient({ apiKey: \"cak_...\" });\r\n * const people = await client.membership<Person[]>(\"/people\");\r\n * ```\r\n *\r\n * Non-2xx responses throw `B1ApiError` (carrying status + parsed body) so a\r\n * caller can distinguish 401/403/404/500.\r\n */\r\nexport class B1RestClient {\r\n private readonly apiKey: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1RestClientOptions) {\r\n if (!options.apiKey) throw new Error(\"B1RestClient: apiKey is required\");\r\n this.apiKey = options.apiKey;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1RestClient: no fetch available — pass options.fetch (Node 18+ has global fetch)\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /** Issues a request against a full Api path (e.g. `/membership/people`). */\r\n async request<T = unknown>(path: string, options: B1RequestOptions = {}): Promise<T> {\r\n const method = options.method ?? \"GET\";\r\n const url = this.buildUrl(path, options.query);\r\n\r\n const headers: Record<string, string> = {\r\n Authorization: `Bearer ${this.apiKey}`,\r\n Accept: \"application/json\",\r\n ...options.headers\r\n };\r\n const hasBody = options.body !== undefined && options.body !== null;\r\n if (hasBody) headers[\"Content-Type\"] = \"application/json\";\r\n\r\n let response: Response;\r\n try {\r\n response = await this.fetchImpl(url, {\r\n method,\r\n headers,\r\n ...(hasBody ? { body: JSON.stringify(options.body) } : {})\r\n });\r\n } catch (err) {\r\n throw new B1ApiError({\r\n status: 0,\r\n statusText: err instanceof Error ? err.message : \"network error\",\r\n body: null,\r\n method,\r\n url\r\n });\r\n }\r\n\r\n const text = await response.text();\r\n const body = parseBody(text);\r\n\r\n if (!response.ok) {\r\n throw new B1ApiError({ status: response.status, statusText: response.statusText, body, method, url });\r\n }\r\n return body as T;\r\n }\r\n\r\n /** Request against the `/membership` module. */\r\n membership<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"membership\", path, options);\r\n }\r\n\r\n /** Request against the `/giving` module. */\r\n giving<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"giving\", path, options);\r\n }\r\n\r\n /** Request against the `/attendance` module. */\r\n attendance<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"attendance\", path, options);\r\n }\r\n\r\n /** Request against the `/content` module. */\r\n content<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"content\", path, options);\r\n }\r\n\r\n /** Request against the `/messaging` module. */\r\n messaging<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"messaging\", path, options);\r\n }\r\n\r\n /** Request against the `/doing` module. */\r\n doing<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"doing\", path, options);\r\n }\r\n\r\n /** Request against the `/reporting` module. */\r\n reporting<T = unknown>(path: string, options?: B1RequestOptions): Promise<T> {\r\n return this.module<T>(\"reporting\", path, options);\r\n }\r\n\r\n private module<T>(module: B1Module, path: string, options?: B1RequestOptions): Promise<T> {\r\n const sub = path.startsWith(\"/\") ? path : `/${path}`;\r\n return this.request<T>(`/${module}${sub}`, options);\r\n }\r\n\r\n private buildUrl(path: string, query?: B1RequestOptions[\"query\"]): string {\r\n const p = path.startsWith(\"/\") ? path : `/${path}`;\r\n let url = `${this.baseUrl}${p}`;\r\n if (query) {\r\n const params = new URLSearchParams();\r\n for (const [key, value] of Object.entries(query)) {\r\n if (value !== undefined && value !== null) params.append(key, String(value));\r\n }\r\n const qs = params.toString();\r\n if (qs) url += `?${qs}`;\r\n }\r\n return url;\r\n }\r\n}\r\n\r\n/** Parses a response body as JSON, falling back to the raw text / undefined. */\r\nfunction parseBody(text: string): unknown {\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n","import {\r\n B1_BASE_URLS,\r\n B1DeviceAuthResponse,\r\n B1DevicePollResult,\r\n B1Scope,\r\n B1TokenResponse\r\n} from \"../types\";\r\n\r\n/** Thrown when a B1 OAuth endpoint returns an `error` response. */\r\nexport class B1OAuthError extends Error {\r\n readonly error: string;\r\n readonly errorDescription?: string;\r\n readonly status: number;\r\n\r\n constructor(error: string, errorDescription: string | undefined, status: number) {\r\n super(errorDescription ? `${error}: ${errorDescription}` : error);\r\n this.name = \"B1OAuthError\";\r\n this.error = error;\r\n this.errorDescription = errorDescription;\r\n this.status = status;\r\n }\r\n}\r\n\r\nexport interface B1OAuthClientOptions {\r\n clientId: string;\r\n /** Required for confidential clients (authorization_code grant). */\r\n clientSecret?: string;\r\n /** Base URL — defaults to production. */\r\n baseUrl?: string;\r\n /** Override `fetch` (for tests or non-global-fetch runtimes). */\r\n fetch?: typeof fetch;\r\n}\r\n\r\nexport interface AwaitDeviceTokenOptions {\r\n deviceCode: string;\r\n /** Poll interval in seconds (from `B1DeviceAuthResponse.interval`). */\r\n interval: number;\r\n /** Overall timeout in seconds (from `B1DeviceAuthResponse.expires_in`). */\r\n expiresIn: number;\r\n /** Optional abort signal to cancel polling. */\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Helper for B1's OAuth flows — authorization-code, refresh-token, and the\r\n * RFC 8628 device flow — against `/membership/oauth/*`.\r\n */\r\nexport class B1OAuthClient {\r\n private readonly clientId: string;\r\n private readonly clientSecret?: string;\r\n private readonly baseUrl: string;\r\n private readonly fetchImpl: typeof fetch;\r\n\r\n constructor(options: B1OAuthClientOptions) {\r\n if (!options.clientId) throw new Error(\"B1OAuthClient: clientId is required\");\r\n this.clientId = options.clientId;\r\n this.clientSecret = options.clientSecret;\r\n this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\\/+$/, \"\");\r\n const f = options.fetch ?? globalThis.fetch;\r\n if (!f) throw new Error(\"B1OAuthClient: no fetch available — pass options.fetch\");\r\n this.fetchImpl = f;\r\n }\r\n\r\n /**\r\n * Requests an authorization code. B1's `/authorize` endpoint is an\r\n * authenticated POST, so this needs the *user's* access token (a JWT).\r\n */\r\n async getAuthorizationCode(params: {\r\n userAccessToken: string;\r\n redirectUri: string;\r\n scope: B1Scope[] | string;\r\n state?: string;\r\n }): Promise<{ code: string; state: string | null }> {\r\n return this.post(\r\n \"/authorize\",\r\n {\r\n client_id: this.clientId,\r\n redirect_uri: params.redirectUri,\r\n response_type: \"code\",\r\n scope: scopeString(params.scope),\r\n state: params.state\r\n },\r\n { Authorization: `Bearer ${params.userAccessToken}` }\r\n );\r\n }\r\n\r\n /** Exchanges an authorization code for tokens. */\r\n async exchangeCode(params: { code: string; redirectUri?: string }): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"authorization_code\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n code: params.code,\r\n redirect_uri: params.redirectUri\r\n });\r\n }\r\n\r\n /** Exchanges a refresh token for a fresh access token. */\r\n async refresh(refreshToken: string): Promise<B1TokenResponse> {\r\n return this.post(\"/token\", {\r\n grant_type: \"refresh_token\",\r\n client_id: this.clientId,\r\n client_secret: this.clientSecret,\r\n refresh_token: refreshToken\r\n });\r\n }\r\n\r\n /** Starts the device flow — returns a user code + verification URI. */\r\n async startDeviceFlow(scope?: B1Scope[] | string): Promise<B1DeviceAuthResponse> {\r\n return this.post(\"/device/authorize\", {\r\n client_id: this.clientId,\r\n scope: scope ? scopeString(scope) : undefined\r\n });\r\n }\r\n\r\n /** Polls once for a device-flow token. Never throws on a pending/expired/denied state. */\r\n async pollDeviceToken(deviceCode: string): Promise<B1DevicePollResult> {\r\n const res = await this.rawPost(\"/token\", {\r\n grant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\r\n client_id: this.clientId,\r\n device_code: deviceCode\r\n });\r\n if (res.ok) return { status: \"approved\", token: res.body as B1TokenResponse };\r\n\r\n const error = typeof res.body === \"object\" && res.body ? (res.body as any).error : undefined;\r\n if (error === \"authorization_pending\") return { status: \"pending\" };\r\n if (error === \"expired_token\") return { status: \"expired\" };\r\n if (error === \"access_denied\") return { status: \"denied\" };\r\n throw new B1OAuthError(error ?? \"invalid_grant\", (res.body as any)?.error_description, res.status);\r\n }\r\n\r\n /** Polls until the device flow is approved, denied, or expires. */\r\n async awaitDeviceToken(options: AwaitDeviceTokenOptions): Promise<B1TokenResponse> {\r\n const deadline = Date.now() + options.expiresIn * 1000;\r\n let intervalMs = Math.max(1, options.interval) * 1000;\r\n\r\n while (Date.now() < deadline) {\r\n if (options.signal?.aborted) throw new B1OAuthError(\"access_denied\", \"polling aborted\", 0);\r\n await delay(intervalMs, options.signal);\r\n\r\n const result = await this.pollDeviceToken(options.deviceCode);\r\n if (result.status === \"approved\") return result.token;\r\n if (result.status === \"denied\") throw new B1OAuthError(\"access_denied\", \"user denied the request\", 400);\r\n if (result.status === \"expired\") throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n // pending — RFC 8628 \"slow_down\" backoff is conservative; nudge the interval up.\r\n intervalMs += 1000;\r\n }\r\n throw new B1OAuthError(\"expired_token\", \"device code expired\", 400);\r\n }\r\n\r\n /** Looks up a pending device authorization by its user code (for an approval UI). */\r\n async getPendingDevice(userCode: string): Promise<unknown> {\r\n const url = `${this.baseUrl}/membership/oauth/device/pending/${encodeURIComponent(userCode)}`;\r\n const res = await this.fetchImpl(url, { headers: { Accept: \"application/json\" } });\r\n const body = await readJson(res);\r\n if (!res.ok) throw new B1OAuthError((body as any)?.error ?? \"not_found\", (body as any)?.error_description, res.status);\r\n return body;\r\n }\r\n\r\n private async post<T>(path: string, body: Record<string, unknown>, extraHeaders?: Record<string, string>): Promise<T> {\r\n const res = await this.rawPost(path, body, extraHeaders);\r\n if (!res.ok) {\r\n const b = res.body as any;\r\n throw new B1OAuthError(b?.error ?? \"oauth_error\", b?.error_description, res.status);\r\n }\r\n return res.body as T;\r\n }\r\n\r\n private async rawPost(\r\n path: string,\r\n body: Record<string, unknown>,\r\n extraHeaders?: Record<string, string>\r\n ): Promise<{ ok: boolean; status: number; body: unknown }> {\r\n const url = `${this.baseUrl}/membership/oauth${path}`;\r\n const clean: Record<string, unknown> = {};\r\n for (const [k, v] of Object.entries(body)) {\r\n if (v !== undefined && v !== null) clean[k] = v;\r\n }\r\n const res = await this.fetchImpl(url, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\", ...extraHeaders },\r\n body: JSON.stringify(clean)\r\n });\r\n return { ok: res.ok, status: res.status, body: await readJson(res) };\r\n }\r\n}\r\n\r\nfunction scopeString(scope: B1Scope[] | string): string {\r\n return Array.isArray(scope) ? scope.join(\" \") : scope;\r\n}\r\n\r\nasync function readJson(res: Response): Promise<unknown> {\r\n const text = await res.text();\r\n if (!text) return undefined;\r\n try {\r\n return JSON.parse(text);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n\r\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const timer = setTimeout(resolve, ms);\r\n signal?.addEventListener(\"abort\", () => {\r\n clearTimeout(timer);\r\n reject(new B1OAuthError(\"access_denied\", \"polling aborted\", 0));\r\n }, { once: true });\r\n });\r\n}\r\n","/** `@churchapps/integration-sdk` — toolkit for building B1.church integrations. */\r\n// Injected from package.json by tsup at build time so it can't drift\r\ndeclare const __PACKAGE_VERSION__: string;\r\nexport const VERSION = __PACKAGE_VERSION__;\r\n\r\nexport * from \"./types\";\r\nexport * from \"./webhooks\";\r\nexport * from \"./rest\";\r\nexport * from \"./oauth\";\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@churchapps/integration-sdk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "SDK for building B1.church integrations — webhook verification, typed REST client, OAuth helpers",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"require": "./dist/index.cjs"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsup",
|
|
20
|
-
"test": "vitest run",
|
|
21
|
-
"test:watch": "vitest",
|
|
22
|
-
"prepublishOnly": "
|
|
23
|
-
"lint": "eslint --fix src/",
|
|
24
|
-
"lint:check": "eslint src/"
|
|
25
|
-
},
|
|
26
|
-
"keywords": [
|
|
27
|
-
"churchapps",
|
|
28
|
-
"b1",
|
|
29
|
-
"integration",
|
|
30
|
-
"webhook",
|
|
31
|
-
"oauth",
|
|
32
|
-
"sdk"
|
|
33
|
-
],
|
|
34
|
-
"author": "ChurchApps",
|
|
35
|
-
"license": "MIT",
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
38
|
-
},
|
|
39
|
-
"repository": {
|
|
40
|
-
"type": "git",
|
|
41
|
-
"url": "https://github.com/ChurchApps/Packages.git",
|
|
42
|
-
"directory": "integration-sdk"
|
|
43
|
-
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"express": ">=4"
|
|
46
|
-
},
|
|
47
|
-
"peerDependenciesMeta": {
|
|
48
|
-
"express": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@eslint/js": "^9.28.0",
|
|
54
|
-
"@types/express": "^5.0.0",
|
|
55
|
-
"@types/node": "^22.0.0",
|
|
56
|
-
"eslint": "^9.28.0",
|
|
57
|
-
"eslint-plugin-unused-imports": "^4.4.1",
|
|
58
|
-
"express": "^4.21.0",
|
|
59
|
-
"globals": "^16.2.0",
|
|
60
|
-
"tsup": "^8.0.0",
|
|
61
|
-
"typescript": "^5.9.2",
|
|
62
|
-
"typescript-eslint": "^8.35.0",
|
|
63
|
-
"vitest": "^2.1.0"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@churchapps/integration-sdk",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "SDK for building B1.church integrations — webhook verification, typed REST client, OAuth helpers",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"prepublishOnly": "yarn build",
|
|
23
|
+
"lint": "eslint --fix src/",
|
|
24
|
+
"lint:check": "eslint src/"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"churchapps",
|
|
28
|
+
"b1",
|
|
29
|
+
"integration",
|
|
30
|
+
"webhook",
|
|
31
|
+
"oauth",
|
|
32
|
+
"sdk"
|
|
33
|
+
],
|
|
34
|
+
"author": "ChurchApps",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/ChurchApps/Packages.git",
|
|
42
|
+
"directory": "integration-sdk"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"express": ">=4"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"express": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^9.28.0",
|
|
54
|
+
"@types/express": "^5.0.0",
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
56
|
+
"eslint": "^9.28.0",
|
|
57
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
58
|
+
"express": "^4.21.0",
|
|
59
|
+
"globals": "^16.2.0",
|
|
60
|
+
"tsup": "^8.0.0",
|
|
61
|
+
"typescript": "^5.9.2",
|
|
62
|
+
"typescript-eslint": "^8.35.0",
|
|
63
|
+
"vitest": "^2.1.0"
|
|
64
|
+
}
|
|
65
|
+
}
|