@wefunder/sdk 0.1.0-beta.8 → 0.1.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -146
- package/dist/index.cjs +430 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5651 -2045
- package/dist/index.d.ts +5651 -2045
- package/dist/index.js +418 -39
- package/dist/index.js.map +1 -1
- package/examples_manifest.json +31 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
# @wefunder/sdk
|
|
1
|
+
# @wefunder/sdk
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Wefunder/wefunder-node/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The official TypeScript SDK for the [Wefunder API](https://docs.wefunder.com/api-reference).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The SDK is currently in beta. Releases may include breaking changes until the API reaches `1.0`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
12
|
npm install @wefunder/sdk@beta
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
Node 20 or newer is required. Both ESM and CommonJS are supported.
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
Wefunder supports two OAuth grants:
|
|
18
20
|
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
(the partner SPV / sandbox-simulation surface) are intentionally **not** included yet.
|
|
22
|
-
- **API version:** the SDK sends `Wefunder-Version: 2025-01-15` on every request,
|
|
23
|
-
forward-compatible with Wefunder's dated-version model. **The API does not resolve
|
|
24
|
-
this header yet**, so version pinning is not enforced server-side until that ships —
|
|
25
|
-
the header is correct in shape and will start taking effect transparently.
|
|
21
|
+
- Use `client_credentials` for server-to-server access to public data.
|
|
22
|
+
- Use `authorization_code` with PKCE when acting on behalf of a user.
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
### Server-to-server
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
Create a client with your application's client ID and secret:
|
|
30
27
|
|
|
31
28
|
```ts
|
|
32
29
|
import { Wefunder } from "@wefunder/sdk";
|
|
@@ -37,226 +34,256 @@ const wf = await Wefunder.fromClientCredentials({
|
|
|
37
34
|
scopes: ["read:public"],
|
|
38
35
|
});
|
|
39
36
|
|
|
40
|
-
// A client_credentials token can only hold `read:public` — it acts as your app,
|
|
41
|
-
// with no user. So it can browse public offerings, but NOT user-scoped data.
|
|
42
37
|
const page = await wf.offerings.list();
|
|
43
|
-
console.log(`${page.data?.length} offerings`);
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
> `read:profile`, a user-context scope — calling it with a `client_credentials`
|
|
48
|
-
> token throws `WefunderError` (`403 insufficient_scope`). To read user data, use
|
|
49
|
-
> the `authorization_code` + PKCE flow below and request `read:profile`.
|
|
50
|
-
|
|
51
|
-
## Authentication
|
|
40
|
+
Client-credentials tokens represent the application, not a user. They cannot be used for user-scoped endpoints such as `wf.users.me()` or `wf.portfolio.get()`.
|
|
52
41
|
|
|
53
|
-
The SDK
|
|
42
|
+
The SDK obtains a new token automatically when a client-credentials token expires.
|
|
54
43
|
|
|
55
|
-
###
|
|
44
|
+
### User authorization with PKCE
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
These tokens are short-lived and have no refresh token, but the client keeps the
|
|
59
|
-
grant inputs and **auto-re-mints** on expiry or a `401` — so a long-lived server can
|
|
60
|
-
hold one `wf` and never hand-roll token recovery.
|
|
61
|
-
|
|
62
|
-
### `authorization_code` + PKCE (acting on behalf of a user)
|
|
46
|
+
Generate the authorization URL on your server. Store the state and PKCE verifier in the user's session before redirecting them:
|
|
63
47
|
|
|
64
48
|
```ts
|
|
65
|
-
import {
|
|
49
|
+
import { createAuthorizationUrl, generatePkce } from "@wefunder/sdk";
|
|
50
|
+
import { randomBytes } from "node:crypto";
|
|
66
51
|
|
|
67
|
-
// 1. Before redirecting, generate PKCE + a state token and stash them in the session.
|
|
68
52
|
const pkce = generatePkce();
|
|
69
|
-
const
|
|
70
|
-
|
|
53
|
+
const state = randomBytes(32).toString("base64url");
|
|
54
|
+
|
|
55
|
+
await saveOAuthAttempt({ state, codeVerifier: pkce.codeVerifier });
|
|
56
|
+
|
|
57
|
+
const authorizationUrl = createAuthorizationUrl({
|
|
58
|
+
clientId,
|
|
59
|
+
redirectUri,
|
|
60
|
+
scopes: ["read:investments"],
|
|
61
|
+
state,
|
|
62
|
+
pkce,
|
|
71
63
|
});
|
|
72
|
-
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
On the callback, validate the state and exchange the authorization code:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { exchangeCode, Wefunder } from "@wefunder/sdk";
|
|
70
|
+
|
|
71
|
+
const attempt = await consumeOAuthAttempt(state);
|
|
72
|
+
if (!attempt) throw new Error("Invalid OAuth state");
|
|
73
73
|
|
|
74
|
-
// 2. On the callback, exchange the code (+ verifier) for tokens.
|
|
75
74
|
const tokens = await exchangeCode({
|
|
76
|
-
clientId,
|
|
75
|
+
clientId,
|
|
76
|
+
clientSecret, // optional for public clients
|
|
77
|
+
code,
|
|
78
|
+
redirectUri,
|
|
79
|
+
codeVerifier: attempt.codeVerifier,
|
|
77
80
|
});
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const wf = new Wefunder({
|
|
83
|
+
tokens,
|
|
84
|
+
clientId,
|
|
85
|
+
clientSecret,
|
|
86
|
+
store: {
|
|
87
|
+
save: (nextTokens) => saveTokens(nextTokens),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
81
90
|
```
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
Keep access tokens, refresh tokens, OAuth state, and PKCE verifiers on the server. Encrypt persisted tokens at rest.
|
|
93
|
+
|
|
94
|
+
### Refresh tokens
|
|
95
|
+
|
|
96
|
+
Refresh tokens rotate. When the SDK refreshes an access token, it calls `store.save()` with the new token set before continuing the request. Persist the entire token set each time.
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
invalidates the old one. The SDK refreshes automatically (proactively before expiry,
|
|
87
|
-
and on a `401`), coalescing concurrent refreshes into one. You just have to persist
|
|
88
|
-
the rotated token so it survives a restart:
|
|
98
|
+
Load the saved tokens yourself when constructing a client after a process restart:
|
|
89
99
|
|
|
90
100
|
```ts
|
|
101
|
+
const tokens = await loadTokens();
|
|
102
|
+
|
|
91
103
|
const wf = new Wefunder({
|
|
92
104
|
tokens,
|
|
93
105
|
clientId,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
save: (t) => db.saveTokens(t), // called on every rotation
|
|
97
|
-
},
|
|
106
|
+
clientSecret,
|
|
107
|
+
store: { save: saveTokens },
|
|
98
108
|
});
|
|
99
109
|
```
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
If several application instances can use the same OAuth connection, serialize refreshes for that connection. This prevents two instances from trying to rotate the same refresh token at once.
|
|
102
112
|
|
|
103
|
-
|
|
113
|
+
## Calling the API
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
- **authorize host** — the browser consent redirect (`createAuthorizationUrl`). Picked from the `client_id`: `pk_test_` → `https://oauth.wefunder-sandbox.com/oauth` (sandbox consent), otherwise `https://wefunder.com/oauth` (live). Override via `authorizeBaseUrl`.
|
|
115
|
+
Common resources are available through typed namespaces:
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
```ts
|
|
118
|
+
const offerings = await wf.offerings.list({ sort: "newest" });
|
|
119
|
+
const investments = await wf.investments.list({ company_id: "co_example" });
|
|
120
|
+
const portfolio = await wf.portfolio.get();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Namespaces: `users`, `offerings`, `investments`, `portfolio`, `campaigns`, `syndicates`, `intents`, `attribution`, and `webhookEndpoints`.
|
|
124
|
+
|
|
125
|
+
`wf.investments` is the Investment Delta API. `list()` without a cursor bootstraps; pass `updated_since` or the `meta.next_cursor` you saved from your last page to receive only records that changed since then. `next_cursor` is always present, even on the final page, so persist it after every sync.
|
|
126
|
+
|
|
127
|
+
The methods available to a client depend on its OAuth scopes. Consult the [API reference](https://docs.wefunder.com/api-reference) for the scope required by each endpoint.
|
|
128
|
+
|
|
129
|
+
The API base URL is `https://api.wefunder.com`. Paths are version-free; the SDK sends the API version in the `Wefunder-Version` request header.
|
|
109
130
|
|
|
110
131
|
## Pagination
|
|
111
132
|
|
|
112
|
-
List
|
|
113
|
-
methods take the endpoint's documented query params, and they're preserved across pages.
|
|
133
|
+
List namespaces provide three ways to work with paginated results:
|
|
114
134
|
|
|
115
135
|
```ts
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
136
|
+
// Fetch one page and inspect its cursor.
|
|
137
|
+
const page = await wf.offerings.list({ sort: "newest" });
|
|
138
|
+
console.log(page.data, page.meta?.next_cursor);
|
|
120
139
|
|
|
121
|
-
//
|
|
122
|
-
// on every page):
|
|
140
|
+
// Fetch pages lazily.
|
|
123
141
|
for await (const offering of wf.offerings.all({ sort: "most_raised" })) {
|
|
124
142
|
console.log(offering.id);
|
|
125
143
|
}
|
|
126
144
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
// Or drive pages yourself (gives you `meta`):
|
|
131
|
-
const page = await wf.offerings.list({ sort: "newest" });
|
|
132
|
-
console.log(page.data, page.meta?.next_cursor);
|
|
145
|
+
// Fetch all results into an array.
|
|
146
|
+
const investments = await wf.investments.collect();
|
|
133
147
|
```
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
Cursors are opaque. Pass the value returned by the API without modifying it.
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
## Errors and retries
|
|
152
|
+
|
|
153
|
+
API failures throw `WefunderError`:
|
|
139
154
|
|
|
140
155
|
```ts
|
|
141
|
-
|
|
142
|
-
console.log(summary.attributes?.total_current_value_cents);
|
|
156
|
+
import { WefunderError } from "@wefunder/sdk";
|
|
143
157
|
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
try {
|
|
159
|
+
await wf.syndicates.get("syn_example");
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof WefunderError) {
|
|
162
|
+
console.error(error.status, error.type, error.message, error.requestId);
|
|
163
|
+
}
|
|
146
164
|
}
|
|
147
165
|
```
|
|
148
166
|
|
|
149
|
-
|
|
167
|
+
The SDK retries idempotent `GET` requests after transient network errors, `5xx` responses, and rate limits. Write requests are not retried automatically.
|
|
168
|
+
|
|
169
|
+
## Webhooks
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
Webhooks deliver platform events (`investment.executed`, `offering.opened`, `investment.changed`, …) to an HTTPS endpoint you register. Every delivery is signed; the SDK verifies the signature, parses the envelope, and gives you a typed event.
|
|
172
|
+
|
|
173
|
+
### 1. Register an endpoint
|
|
174
|
+
|
|
175
|
+
Endpoints belong to your application and are managed through the live API (scope `write:webhooks`, org owner/admin/developer role). The signing secret is returned only on create and rotate, so store it immediately.
|
|
153
176
|
|
|
154
177
|
```ts
|
|
155
|
-
|
|
178
|
+
const endpoint = await wf.webhookEndpoints.create({
|
|
179
|
+
url: "https://yourapp.com/webhooks/wefunder", // public HTTPS; localhost and private IPs are rejected
|
|
180
|
+
events: ["offering.opened", "investment.executed"],
|
|
181
|
+
mode: "live", // "test" endpoints receive sandbox events
|
|
182
|
+
});
|
|
156
183
|
|
|
157
|
-
|
|
158
|
-
await wf.syndicates.get(123);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
if (err instanceof WefunderError) {
|
|
161
|
-
console.error(err.status, err.type, err.message, err.requestId);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
184
|
+
await saveSecret(endpoint.attributes!.secret!);
|
|
164
185
|
```
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
`429` (honoring `X-RateLimit-Reset`). Writes are never auto-retried.
|
|
187
|
+
`wf.webhookEndpoints` also provides `list`, `get`, `update`, `remove`, `rotateSecret`, `reenable`, and `test`.
|
|
168
188
|
|
|
169
|
-
|
|
189
|
+
### 2. Verify and handle deliveries
|
|
170
190
|
|
|
171
|
-
|
|
172
|
-
object) and the headers:
|
|
191
|
+
Pass the raw request body, the request headers, and your secret to `constructEvent`. It throws `WebhookSignatureError` (with a `reason`) when a delivery is not authentic.
|
|
173
192
|
|
|
174
193
|
```ts
|
|
175
|
-
import { constructEvent } from "@wefunder/sdk";
|
|
194
|
+
import { constructEvent, dispatchWebhook, WebhookSignatureError } from "@wefunder/sdk";
|
|
176
195
|
|
|
177
|
-
app.post("/webhooks", express.raw({ type: "
|
|
196
|
+
app.post("/webhooks/wefunder", express.raw({ type: "*/*" }), async (req, res) => {
|
|
178
197
|
let event;
|
|
179
198
|
try {
|
|
180
|
-
event = constructEvent(req.body
|
|
181
|
-
} catch {
|
|
182
|
-
return res.status(400).send(
|
|
199
|
+
event = constructEvent(req.body, req.headers, process.env.WEFUNDER_WEBHOOK_SECRET!);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err instanceof WebhookSignatureError) return res.status(400).send(err.reason);
|
|
202
|
+
throw err;
|
|
183
203
|
}
|
|
184
|
-
|
|
185
|
-
res.sendStatus(200);
|
|
204
|
+
|
|
205
|
+
res.sendStatus(200); // acknowledge first, then do the work
|
|
206
|
+
|
|
207
|
+
await dispatchWebhook(event, {
|
|
208
|
+
"investment.executed": async (e) => recordFunding(e.data.id, e.data.amounts.committed),
|
|
209
|
+
"offering.opened": async (e) => announce(e.data.company.name),
|
|
210
|
+
default: (e) => console.log("unhandled", e.event),
|
|
211
|
+
});
|
|
186
212
|
});
|
|
187
213
|
```
|
|
188
214
|
|
|
189
|
-
|
|
215
|
+
`event` is a discriminated union, so narrowing on `event.event` types `event.data` for you. For fetch-style servers (Next.js route handlers, Hono, Cloudflare Workers), use `constructEventFromRequest(request, secret)` instead.
|
|
216
|
+
|
|
217
|
+
Deliveries are at-least-once and unordered. Deduplicate on `event.id`, and where a payload carries `occurred_at`, keep the state from the latest one you have seen.
|
|
190
218
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
envelope handling the namespaces use (a `WefunderError` with `request_id` on failure):
|
|
219
|
+
### 3. Test your handler
|
|
220
|
+
|
|
221
|
+
`wf.webhookEndpoints.test(endpoint.id)` sends a real, signed example event to your endpoint and reports the outcome inline. To unit-test your handler without the API, sign a fixture yourself:
|
|
195
222
|
|
|
196
223
|
```ts
|
|
197
|
-
|
|
224
|
+
import { signWebhook } from "@wefunder/sdk";
|
|
198
225
|
|
|
199
|
-
|
|
200
|
-
const
|
|
226
|
+
const body = JSON.stringify({ id: "evt_1", event: "offering.opened", created_at: "…", mode: "test", data: {…} });
|
|
227
|
+
const header = signWebhook({ payload: body, secret });
|
|
228
|
+
// POST `body` to your handler with `Wefunder-Signature: ${header}`
|
|
201
229
|
```
|
|
202
230
|
|
|
231
|
+
### Secret rotation
|
|
232
|
+
|
|
233
|
+
`wf.webhookEndpoints.rotateSecret(id)` returns a new secret; the old one keeps signing for 24 hours, and deliveries carry a `v1` for each. `constructEvent` accepts either, so you can roll the new secret out to your servers without dropping an event.
|
|
234
|
+
|
|
235
|
+
### Signature scheme
|
|
236
|
+
|
|
237
|
+
Each delivery carries `Wefunder-Signature: t=<unix seconds>,v1=<hex>` where `v1` is `HMAC-SHA256(secret, "<t>.<raw body>")`. Requests whose `t` is more than five minutes from now are rejected (`toleranceSeconds` adjusts this). `verifyWebhook` and `checkWebhookSignature` expose the check without parsing, and `constructEvent` still accepts the retired attribution-webhook headers (`X-Wefunder-Signature`/`X-Wefunder-Timestamp`).
|
|
238
|
+
|
|
239
|
+
## Generated operations
|
|
240
|
+
|
|
241
|
+
Typed namespaces cover the most common resources. Every operation in the public OpenAPI specification is also available under `wf.raw`.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const members = await wf.unwrap(
|
|
245
|
+
wf.raw.listSyndicateMembers({
|
|
246
|
+
path: { syndicate_id: "syn_example" },
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Raw operations return `{ data, error, response }`. Passing the result to `wf.unwrap()` applies the same error handling used by the resource namespaces.
|
|
252
|
+
|
|
203
253
|
## Development
|
|
204
254
|
|
|
205
255
|
```bash
|
|
206
256
|
npm install
|
|
207
|
-
npm run generate # regenerate src/generated from spec/openapi.yaml
|
|
208
257
|
npm run typecheck
|
|
209
|
-
npm
|
|
210
|
-
npm
|
|
258
|
+
npm run typecheck:examples
|
|
259
|
+
npm test
|
|
211
260
|
npm run build
|
|
212
261
|
```
|
|
213
262
|
|
|
214
|
-
|
|
215
|
-
the credentials in a gitignored `.env` (`WEFUNDER_CLIENT_ID=` / `WEFUNDER_CLIENT_SECRET=`).
|
|
263
|
+
`npm run test:e2e` runs against the sandbox when `WEFUNDER_CLIENT_ID` and `WEFUNDER_CLIENT_SECRET` are set.
|
|
216
264
|
|
|
217
|
-
|
|
218
|
-
`spec/openapi.yaml` and is never hand-edited. The hand-written shell in `src/` wraps it.
|
|
265
|
+
Generated files in `src/generated/` come from `spec/openapi.yaml` and should not be edited by hand.
|
|
219
266
|
|
|
220
|
-
###
|
|
267
|
+
### Updating the API specification
|
|
221
268
|
|
|
222
|
-
|
|
223
|
-
canonical Wefunder swagger. Preview/internal operations are excluded by design. To
|
|
224
|
-
refresh it from a local wefunder checkout:
|
|
269
|
+
Run the sync command against a local checkout of the Wefunder application, then regenerate the client:
|
|
225
270
|
|
|
226
271
|
```bash
|
|
227
|
-
|
|
272
|
+
npm run sync-spec -- /path/to/wefunder
|
|
228
273
|
npm run generate
|
|
229
|
-
|
|
274
|
+
npm run typecheck
|
|
275
|
+
npm test
|
|
230
276
|
```
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
so the public-tier definition can't drift between the two repos. CI's
|
|
234
|
-
`generated code matches spec` job verifies `src/generated` matches the committed spec;
|
|
235
|
-
it cannot reach the private canonical swagger, so run `sync-spec` before cutting a
|
|
236
|
-
release. (`npm test` stays hermetic.)
|
|
278
|
+
Commit the specification and generated client together.
|
|
237
279
|
|
|
238
|
-
### Releasing
|
|
280
|
+
### Releasing
|
|
239
281
|
|
|
240
|
-
Releases
|
|
241
|
-
version and push the tag; the `Release` workflow (`.github/workflows/release.yml`,
|
|
242
|
-
triggered on `v*` tags) runs the `prepublishOnly` gate and publishes:
|
|
282
|
+
Releases are published by GitHub Actions. From a clean `main` branch:
|
|
243
283
|
|
|
244
284
|
```bash
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
git push --follow-tags # pushes the commit + tag → CI publishes
|
|
285
|
+
npm version prerelease --preid beta
|
|
286
|
+
git push --follow-tags
|
|
248
287
|
```
|
|
249
288
|
|
|
250
|
-
The workflow
|
|
251
|
-
versions (e.g. `0.1.0-beta.N`) to the **`beta`** dist-tag (npm requires an explicit tag
|
|
252
|
-
for prereleases), stable versions to `latest`. For a stable release use
|
|
253
|
-
`npm version patch|minor|major` (no `--preid`).
|
|
254
|
-
|
|
255
|
-
**Auth is npm Trusted Publishing (OIDC) — no token to store.** npm exchanges the
|
|
256
|
-
workflow's GitHub OIDC token for a short-lived credential at publish time, and
|
|
257
|
-
provenance is generated automatically (verified-build badge on npmjs.com).
|
|
258
|
-
|
|
259
|
-
**One-time setup:** on npmjs.com, `@wefunder/sdk` → **Settings → Trusted Publishing →
|
|
260
|
-
GitHub Actions**, with org/user `Wefunder`, repository `wefunder-node`, workflow
|
|
261
|
-
`release.yml` (leave Environment blank). No repo secret needed. (Requires this public
|
|
262
|
-
repo + public package — both true.)
|
|
289
|
+
The release workflow runs the package checks and publishes prereleases to npm's `beta` tag.
|