@churchapps/integration-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/index.cjs +429 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +366 -0
- package/dist/index.d.ts +366 -0
- package/dist/index.js +382 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ChurchApps
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @churchapps/integration-sdk
|
|
2
|
+
|
|
3
|
+
Toolkit for building [B1.church](https://b1.church) integrations — verify inbound
|
|
4
|
+
webhooks, call the B1 Api with a typed REST client, and complete OAuth flows.
|
|
5
|
+
|
|
6
|
+
Requires **Node 18+** (uses the built-in `crypto` and global `fetch`). Zero runtime
|
|
7
|
+
dependencies; `express` is an optional peer (only the webhook middleware needs it).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @churchapps/integration-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Webhooks
|
|
14
|
+
|
|
15
|
+
B1 signs every webhook delivery with an HMAC-SHA256 over the **raw request body**,
|
|
16
|
+
sent in the `X-B1-Signature` header. Verify *before* the body is JSON-parsed and
|
|
17
|
+
re-stringified — that would change byte order and break the signature.
|
|
18
|
+
|
|
19
|
+
### With Express
|
|
20
|
+
|
|
21
|
+
Capture the raw body with `express.json`'s `verify` hook, then mount the middleware:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import express from "express";
|
|
25
|
+
import { b1WebhookMiddleware } from "@churchapps/integration-sdk";
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));
|
|
29
|
+
|
|
30
|
+
app.post("/webhooks/b1", b1WebhookMiddleware({ secret: process.env.B1_WEBHOOK_SECRET! }), (req, res) => {
|
|
31
|
+
const env = req.b1Webhook!; // typed B1WebhookEnvelope
|
|
32
|
+
switch (env.event) {
|
|
33
|
+
case "donation.created":
|
|
34
|
+
console.log("new gift", env.data.amount); // data narrowed to DonationWebhookData
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
res.sendStatus(200);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`express.raw({ type: "application/json" })` is also accepted. A failed verification
|
|
42
|
+
responds `401` (override with `onInvalid`).
|
|
43
|
+
|
|
44
|
+
### Without a framework
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { WebhookVerifier } from "@churchapps/integration-sdk";
|
|
48
|
+
|
|
49
|
+
const ok = WebhookVerifier.verify(secret, rawBody, signatureHeader);
|
|
50
|
+
const envelope = WebhookVerifier.verifyAndParse(secret, rawBody, signatureHeader); // throws on mismatch
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## REST client
|
|
54
|
+
|
|
55
|
+
Authenticates with a `cak_` API key (created in B1Admin → Settings → Developer).
|
|
56
|
+
The Api is one host with per-module path prefixes; use the module helpers or a full
|
|
57
|
+
path. Non-2xx responses throw `B1ApiError`.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { B1RestClient, B1ApiError } from "@churchapps/integration-sdk";
|
|
61
|
+
|
|
62
|
+
const client = new B1RestClient({ apiKey: process.env.B1_API_KEY! });
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const people = await client.membership<Person[]>("/people");
|
|
66
|
+
await client.giving("/donations", { method: "POST", body: { amount: 50 } });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof B1ApiError) console.error(err.status, err.body);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Module helpers: `membership`, `giving`, `attendance`, `content`, `messaging`,
|
|
73
|
+
`doing`, `reporting`. Pass `baseUrl` to target staging.
|
|
74
|
+
|
|
75
|
+
## OAuth
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { B1OAuthClient } from "@churchapps/integration-sdk";
|
|
79
|
+
|
|
80
|
+
const oauth = new B1OAuthClient({ clientId, clientSecret });
|
|
81
|
+
|
|
82
|
+
const token = await oauth.exchangeCode({ code, redirectUri });
|
|
83
|
+
const fresh = await oauth.refresh(token.refresh_token);
|
|
84
|
+
|
|
85
|
+
// Device flow (RFC 8628):
|
|
86
|
+
const device = await oauth.startDeviceFlow(["people:read"]);
|
|
87
|
+
console.log(`Visit ${device.verification_uri} and enter ${device.user_code}`);
|
|
88
|
+
const deviceToken = await oauth.awaitDeviceToken({
|
|
89
|
+
deviceCode: device.device_code, interval: device.interval, expiresIn: device.expires_in
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Base URLs
|
|
94
|
+
|
|
95
|
+
`B1_BASE_URLS.prod` — `https://api.b1.church` (default) ·
|
|
96
|
+
`B1_BASE_URLS.staging` — `https://api.staging.b1.church`.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT © ChurchApps
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
B1ApiError: () => B1ApiError,
|
|
34
|
+
B1OAuthClient: () => B1OAuthClient,
|
|
35
|
+
B1OAuthError: () => B1OAuthError,
|
|
36
|
+
B1RestClient: () => B1RestClient,
|
|
37
|
+
B1_BASE_URLS: () => B1_BASE_URLS,
|
|
38
|
+
B1_SCOPES: () => B1_SCOPES,
|
|
39
|
+
VERSION: () => VERSION,
|
|
40
|
+
WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
|
|
41
|
+
WebhookVerificationError: () => WebhookVerificationError,
|
|
42
|
+
WebhookVerifier: () => WebhookVerifier,
|
|
43
|
+
b1WebhookMiddleware: () => b1WebhookMiddleware
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/types/webhooks.ts
|
|
48
|
+
var WEBHOOK_HEADERS = {
|
|
49
|
+
signature: "X-B1-Signature",
|
|
50
|
+
event: "X-B1-Event",
|
|
51
|
+
deliveryId: "X-B1-Delivery-Id",
|
|
52
|
+
timestamp: "X-B1-Timestamp"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/types/oauth.ts
|
|
56
|
+
var B1_SCOPES = [
|
|
57
|
+
"people:read",
|
|
58
|
+
"people:write",
|
|
59
|
+
"groups:read",
|
|
60
|
+
"groups:write",
|
|
61
|
+
"donations:read",
|
|
62
|
+
"donations:write",
|
|
63
|
+
"attendance:read",
|
|
64
|
+
"attendance:write",
|
|
65
|
+
"forms:write",
|
|
66
|
+
"content:read",
|
|
67
|
+
"content:write",
|
|
68
|
+
"messaging:read",
|
|
69
|
+
"messaging:write",
|
|
70
|
+
"roles:read",
|
|
71
|
+
"roles:write",
|
|
72
|
+
"settings:read",
|
|
73
|
+
"settings:write",
|
|
74
|
+
"offline_access"
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// src/types/rest.ts
|
|
78
|
+
var B1_BASE_URLS = {
|
|
79
|
+
prod: "https://api.b1.church",
|
|
80
|
+
staging: "https://api.staging.b1.church"
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/webhooks/WebhookVerifier.ts
|
|
84
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
85
|
+
var WebhookVerificationError = class extends Error {
|
|
86
|
+
constructor(message) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = "WebhookVerificationError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var WebhookVerifier = class _WebhookVerifier {
|
|
92
|
+
/** Computes the `X-B1-Signature` value for a raw body. */
|
|
93
|
+
static sign(secret, rawBody) {
|
|
94
|
+
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
95
|
+
return "sha256=" + import_crypto.default.createHmac("sha256", secret).update(body, "utf8").digest("hex");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns `true` when `signatureHeader` matches the body. Never throws —
|
|
99
|
+
* a missing, empty, or malformed header simply returns `false`.
|
|
100
|
+
*/
|
|
101
|
+
static verify(secret, rawBody, signatureHeader) {
|
|
102
|
+
if (!signatureHeader) return false;
|
|
103
|
+
const expected = _WebhookVerifier.sign(secret, rawBody);
|
|
104
|
+
const a = Buffer.from(signatureHeader, "utf8");
|
|
105
|
+
const b = Buffer.from(expected, "utf8");
|
|
106
|
+
if (a.length !== b.length) {
|
|
107
|
+
import_crypto.default.timingSafeEqual(b, b);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return import_crypto.default.timingSafeEqual(a, b);
|
|
111
|
+
}
|
|
112
|
+
/** Parses a raw body into a typed envelope (no verification). */
|
|
113
|
+
static parseEnvelope(rawBody) {
|
|
114
|
+
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
115
|
+
return JSON.parse(body);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Verifies the signature, then parses the body into a typed envelope.
|
|
119
|
+
* Throws `WebhookVerificationError` if the signature does not match.
|
|
120
|
+
*/
|
|
121
|
+
static verifyAndParse(secret, rawBody, signatureHeader) {
|
|
122
|
+
if (!_WebhookVerifier.verify(secret, rawBody, signatureHeader)) {
|
|
123
|
+
throw new WebhookVerificationError("Webhook signature verification failed");
|
|
124
|
+
}
|
|
125
|
+
return _WebhookVerifier.parseEnvelope(rawBody);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/webhooks/expressMiddleware.ts
|
|
130
|
+
function b1WebhookMiddleware(options) {
|
|
131
|
+
return (req, res, next) => {
|
|
132
|
+
const raw = resolveRawBody(req);
|
|
133
|
+
if (raw === void 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
'b1WebhookMiddleware: no raw request body found. Mount express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }) or express.raw({ type: "application/json" }) before this middleware.'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const secret = typeof options.secret === "function" ? options.secret(req) : options.secret;
|
|
139
|
+
const signature = req.header(WEBHOOK_HEADERS.signature);
|
|
140
|
+
if (!WebhookVerifier.verify(secret, raw, signature)) {
|
|
141
|
+
if (options.onInvalid) options.onInvalid(req, res);
|
|
142
|
+
else res.status(401).json({ error: "invalid webhook signature" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const envelope = WebhookVerifier.parseEnvelope(raw);
|
|
146
|
+
req.b1Webhook = envelope;
|
|
147
|
+
if (Buffer.isBuffer(req.body)) req.body = envelope;
|
|
148
|
+
next();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function resolveRawBody(req) {
|
|
152
|
+
if (req.rawBody !== void 0) return req.rawBody;
|
|
153
|
+
if (Buffer.isBuffer(req.body)) return req.body;
|
|
154
|
+
return void 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/rest/B1ApiError.ts
|
|
158
|
+
var B1ApiError = class extends Error {
|
|
159
|
+
constructor(opts) {
|
|
160
|
+
super(`B1 Api ${opts.method} ${opts.url} failed: ${opts.status} ${opts.statusText}`);
|
|
161
|
+
this.name = "B1ApiError";
|
|
162
|
+
this.status = opts.status;
|
|
163
|
+
this.statusText = opts.statusText;
|
|
164
|
+
this.body = opts.body;
|
|
165
|
+
this.method = opts.method;
|
|
166
|
+
this.url = opts.url;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/rest/B1RestClient.ts
|
|
171
|
+
var B1RestClient = class {
|
|
172
|
+
constructor(options) {
|
|
173
|
+
if (!options.apiKey) throw new Error("B1RestClient: apiKey is required");
|
|
174
|
+
this.apiKey = options.apiKey;
|
|
175
|
+
this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\/+$/, "");
|
|
176
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
177
|
+
if (!f) throw new Error("B1RestClient: no fetch available \u2014 pass options.fetch (Node 18+ has global fetch)");
|
|
178
|
+
this.fetchImpl = f;
|
|
179
|
+
}
|
|
180
|
+
/** Issues a request against a full Api path (e.g. `/membership/people`). */
|
|
181
|
+
async request(path, options = {}) {
|
|
182
|
+
const method = options.method ?? "GET";
|
|
183
|
+
const url = this.buildUrl(path, options.query);
|
|
184
|
+
const headers = {
|
|
185
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
186
|
+
Accept: "application/json",
|
|
187
|
+
...options.headers
|
|
188
|
+
};
|
|
189
|
+
const hasBody = options.body !== void 0 && options.body !== null;
|
|
190
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
191
|
+
let response;
|
|
192
|
+
try {
|
|
193
|
+
response = await this.fetchImpl(url, {
|
|
194
|
+
method,
|
|
195
|
+
headers,
|
|
196
|
+
...hasBody ? { body: JSON.stringify(options.body) } : {}
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new B1ApiError({
|
|
200
|
+
status: 0,
|
|
201
|
+
statusText: err instanceof Error ? err.message : "network error",
|
|
202
|
+
body: null,
|
|
203
|
+
method,
|
|
204
|
+
url
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const text = await response.text();
|
|
208
|
+
const body = parseBody(text);
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new B1ApiError({ status: response.status, statusText: response.statusText, body, method, url });
|
|
211
|
+
}
|
|
212
|
+
return body;
|
|
213
|
+
}
|
|
214
|
+
/** Request against the `/membership` module. */
|
|
215
|
+
membership(path, options) {
|
|
216
|
+
return this.module("membership", path, options);
|
|
217
|
+
}
|
|
218
|
+
/** Request against the `/giving` module. */
|
|
219
|
+
giving(path, options) {
|
|
220
|
+
return this.module("giving", path, options);
|
|
221
|
+
}
|
|
222
|
+
/** Request against the `/attendance` module. */
|
|
223
|
+
attendance(path, options) {
|
|
224
|
+
return this.module("attendance", path, options);
|
|
225
|
+
}
|
|
226
|
+
/** Request against the `/content` module. */
|
|
227
|
+
content(path, options) {
|
|
228
|
+
return this.module("content", path, options);
|
|
229
|
+
}
|
|
230
|
+
/** Request against the `/messaging` module. */
|
|
231
|
+
messaging(path, options) {
|
|
232
|
+
return this.module("messaging", path, options);
|
|
233
|
+
}
|
|
234
|
+
/** Request against the `/doing` module. */
|
|
235
|
+
doing(path, options) {
|
|
236
|
+
return this.module("doing", path, options);
|
|
237
|
+
}
|
|
238
|
+
/** Request against the `/reporting` module. */
|
|
239
|
+
reporting(path, options) {
|
|
240
|
+
return this.module("reporting", path, options);
|
|
241
|
+
}
|
|
242
|
+
module(module2, path, options) {
|
|
243
|
+
const sub = path.startsWith("/") ? path : `/${path}`;
|
|
244
|
+
return this.request(`/${module2}${sub}`, options);
|
|
245
|
+
}
|
|
246
|
+
buildUrl(path, query) {
|
|
247
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
248
|
+
let url = `${this.baseUrl}${p}`;
|
|
249
|
+
if (query) {
|
|
250
|
+
const params = new URLSearchParams();
|
|
251
|
+
for (const [key, value] of Object.entries(query)) {
|
|
252
|
+
if (value !== void 0 && value !== null) params.append(key, String(value));
|
|
253
|
+
}
|
|
254
|
+
const qs = params.toString();
|
|
255
|
+
if (qs) url += `?${qs}`;
|
|
256
|
+
}
|
|
257
|
+
return url;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
function parseBody(text) {
|
|
261
|
+
if (!text) return void 0;
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(text);
|
|
264
|
+
} catch {
|
|
265
|
+
return text;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/oauth/B1OAuthClient.ts
|
|
270
|
+
var B1OAuthError = class extends Error {
|
|
271
|
+
constructor(error, errorDescription, status) {
|
|
272
|
+
super(errorDescription ? `${error}: ${errorDescription}` : error);
|
|
273
|
+
this.name = "B1OAuthError";
|
|
274
|
+
this.error = error;
|
|
275
|
+
this.errorDescription = errorDescription;
|
|
276
|
+
this.status = status;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var B1OAuthClient = class {
|
|
280
|
+
constructor(options) {
|
|
281
|
+
if (!options.clientId) throw new Error("B1OAuthClient: clientId is required");
|
|
282
|
+
this.clientId = options.clientId;
|
|
283
|
+
this.clientSecret = options.clientSecret;
|
|
284
|
+
this.baseUrl = (options.baseUrl ?? B1_BASE_URLS.prod).replace(/\/+$/, "");
|
|
285
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
286
|
+
if (!f) throw new Error("B1OAuthClient: no fetch available \u2014 pass options.fetch");
|
|
287
|
+
this.fetchImpl = f;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Requests an authorization code. B1's `/authorize` endpoint is an
|
|
291
|
+
* authenticated POST, so this needs the *user's* access token (a JWT).
|
|
292
|
+
*/
|
|
293
|
+
async getAuthorizationCode(params) {
|
|
294
|
+
return this.post(
|
|
295
|
+
"/authorize",
|
|
296
|
+
{
|
|
297
|
+
client_id: this.clientId,
|
|
298
|
+
redirect_uri: params.redirectUri,
|
|
299
|
+
response_type: "code",
|
|
300
|
+
scope: scopeString(params.scope),
|
|
301
|
+
state: params.state
|
|
302
|
+
},
|
|
303
|
+
{ Authorization: `Bearer ${params.userAccessToken}` }
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
/** Exchanges an authorization code for tokens. */
|
|
307
|
+
async exchangeCode(params) {
|
|
308
|
+
return this.post("/token", {
|
|
309
|
+
grant_type: "authorization_code",
|
|
310
|
+
client_id: this.clientId,
|
|
311
|
+
client_secret: this.clientSecret,
|
|
312
|
+
code: params.code,
|
|
313
|
+
redirect_uri: params.redirectUri
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** Exchanges a refresh token for a fresh access token. */
|
|
317
|
+
async refresh(refreshToken) {
|
|
318
|
+
return this.post("/token", {
|
|
319
|
+
grant_type: "refresh_token",
|
|
320
|
+
client_id: this.clientId,
|
|
321
|
+
client_secret: this.clientSecret,
|
|
322
|
+
refresh_token: refreshToken
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/** Starts the device flow — returns a user code + verification URI. */
|
|
326
|
+
async startDeviceFlow(scope) {
|
|
327
|
+
return this.post("/device/authorize", {
|
|
328
|
+
client_id: this.clientId,
|
|
329
|
+
scope: scope ? scopeString(scope) : void 0
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/** Polls once for a device-flow token. Never throws on a pending/expired/denied state. */
|
|
333
|
+
async pollDeviceToken(deviceCode) {
|
|
334
|
+
const res = await this.rawPost("/token", {
|
|
335
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
336
|
+
client_id: this.clientId,
|
|
337
|
+
device_code: deviceCode
|
|
338
|
+
});
|
|
339
|
+
if (res.ok) return { status: "approved", token: res.body };
|
|
340
|
+
const error = typeof res.body === "object" && res.body ? res.body.error : void 0;
|
|
341
|
+
if (error === "authorization_pending") return { status: "pending" };
|
|
342
|
+
if (error === "expired_token") return { status: "expired" };
|
|
343
|
+
if (error === "access_denied") return { status: "denied" };
|
|
344
|
+
throw new B1OAuthError(error ?? "invalid_grant", res.body?.error_description, res.status);
|
|
345
|
+
}
|
|
346
|
+
/** Polls until the device flow is approved, denied, or expires. */
|
|
347
|
+
async awaitDeviceToken(options) {
|
|
348
|
+
const deadline = Date.now() + options.expiresIn * 1e3;
|
|
349
|
+
let intervalMs = Math.max(1, options.interval) * 1e3;
|
|
350
|
+
while (Date.now() < deadline) {
|
|
351
|
+
if (options.signal?.aborted) throw new B1OAuthError("access_denied", "polling aborted", 0);
|
|
352
|
+
await delay(intervalMs, options.signal);
|
|
353
|
+
const result = await this.pollDeviceToken(options.deviceCode);
|
|
354
|
+
if (result.status === "approved") return result.token;
|
|
355
|
+
if (result.status === "denied") throw new B1OAuthError("access_denied", "user denied the request", 400);
|
|
356
|
+
if (result.status === "expired") throw new B1OAuthError("expired_token", "device code expired", 400);
|
|
357
|
+
intervalMs += 1e3;
|
|
358
|
+
}
|
|
359
|
+
throw new B1OAuthError("expired_token", "device code expired", 400);
|
|
360
|
+
}
|
|
361
|
+
/** Looks up a pending device authorization by its user code (for an approval UI). */
|
|
362
|
+
async getPendingDevice(userCode) {
|
|
363
|
+
const url = `${this.baseUrl}/membership/oauth/device/pending/${encodeURIComponent(userCode)}`;
|
|
364
|
+
const res = await this.fetchImpl(url, { headers: { Accept: "application/json" } });
|
|
365
|
+
const body = await readJson(res);
|
|
366
|
+
if (!res.ok) throw new B1OAuthError(body?.error ?? "not_found", body?.error_description, res.status);
|
|
367
|
+
return body;
|
|
368
|
+
}
|
|
369
|
+
async post(path, body, extraHeaders) {
|
|
370
|
+
const res = await this.rawPost(path, body, extraHeaders);
|
|
371
|
+
if (!res.ok) {
|
|
372
|
+
const b = res.body;
|
|
373
|
+
throw new B1OAuthError(b?.error ?? "oauth_error", b?.error_description, res.status);
|
|
374
|
+
}
|
|
375
|
+
return res.body;
|
|
376
|
+
}
|
|
377
|
+
async rawPost(path, body, extraHeaders) {
|
|
378
|
+
const url = `${this.baseUrl}/membership/oauth${path}`;
|
|
379
|
+
const clean = {};
|
|
380
|
+
for (const [k, v] of Object.entries(body)) {
|
|
381
|
+
if (v !== void 0 && v !== null) clean[k] = v;
|
|
382
|
+
}
|
|
383
|
+
const res = await this.fetchImpl(url, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...extraHeaders },
|
|
386
|
+
body: JSON.stringify(clean)
|
|
387
|
+
});
|
|
388
|
+
return { ok: res.ok, status: res.status, body: await readJson(res) };
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function scopeString(scope) {
|
|
392
|
+
return Array.isArray(scope) ? scope.join(" ") : scope;
|
|
393
|
+
}
|
|
394
|
+
async function readJson(res) {
|
|
395
|
+
const text = await res.text();
|
|
396
|
+
if (!text) return void 0;
|
|
397
|
+
try {
|
|
398
|
+
return JSON.parse(text);
|
|
399
|
+
} catch {
|
|
400
|
+
return text;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function delay(ms, signal) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
const timer = setTimeout(resolve, ms);
|
|
406
|
+
signal?.addEventListener("abort", () => {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
reject(new B1OAuthError("access_denied", "polling aborted", 0));
|
|
409
|
+
}, { once: true });
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/index.ts
|
|
414
|
+
var VERSION = "0.1.0";
|
|
415
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
416
|
+
0 && (module.exports = {
|
|
417
|
+
B1ApiError,
|
|
418
|
+
B1OAuthClient,
|
|
419
|
+
B1OAuthError,
|
|
420
|
+
B1RestClient,
|
|
421
|
+
B1_BASE_URLS,
|
|
422
|
+
B1_SCOPES,
|
|
423
|
+
VERSION,
|
|
424
|
+
WEBHOOK_HEADERS,
|
|
425
|
+
WebhookVerificationError,
|
|
426
|
+
WebhookVerifier,
|
|
427
|
+
b1WebhookMiddleware
|
|
428
|
+
});
|
|
429
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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"]}
|