@wefunder/sdk 0.1.0-beta.1 → 0.1.0-beta.11
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 +190 -114
- package/dist/index.cjs +1734 -687
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7070 -1940
- package/dist/index.d.ts +7070 -1940
- package/dist/index.js +1719 -684
- package/dist/index.js.map +1 -1
- package/examples_manifest.json +67 -0
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -1,31 +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
|
-
npm install @wefunder/sdk
|
|
12
|
+
npm install @wefunder/sdk@beta
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
Node
|
|
15
|
+
Node 20 or newer is required. Both ESM and CommonJS are supported.
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
Wefunder supports two OAuth grants:
|
|
17
20
|
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
(the partner SPV / sandbox-simulation surface) are intentionally **not** included yet.
|
|
21
|
-
- **API version:** the SDK sends `Wefunder-Version: 2025-01-15` on every request,
|
|
22
|
-
forward-compatible with Wefunder's dated-version model. **The API does not resolve
|
|
23
|
-
this header yet**, so version pinning is not enforced server-side until that ships —
|
|
24
|
-
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.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
### Server-to-server
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
Create a client with your application's client ID and secret:
|
|
29
27
|
|
|
30
28
|
```ts
|
|
31
29
|
import { Wefunder } from "@wefunder/sdk";
|
|
@@ -36,186 +34,264 @@ const wf = await Wefunder.fromClientCredentials({
|
|
|
36
34
|
scopes: ["read:public"],
|
|
37
35
|
});
|
|
38
36
|
|
|
39
|
-
// A client_credentials token can only hold `read:public` — it acts as your app,
|
|
40
|
-
// with no user. So it can browse public offerings, but NOT user-scoped data.
|
|
41
37
|
const page = await wf.offerings.list();
|
|
42
|
-
console.log(`${page.data?.length} offerings`);
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
> `read:profile`, a user-context scope — calling it with a `client_credentials`
|
|
47
|
-
> token throws `WefunderError` (`403 insufficient_scope`). To read user data, use
|
|
48
|
-
> the `authorization_code` + PKCE flow below and request `read:profile`.
|
|
49
|
-
|
|
50
|
-
## 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()`.
|
|
51
41
|
|
|
52
|
-
The SDK
|
|
42
|
+
The SDK obtains a new token automatically when a client-credentials token expires.
|
|
53
43
|
|
|
54
|
-
###
|
|
44
|
+
### User authorization with PKCE
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
These tokens are short-lived and have no refresh token, but the client keeps the
|
|
58
|
-
grant inputs and **auto-re-mints** on expiry or a `401` — so a long-lived server can
|
|
59
|
-
hold one `wf` and never hand-roll token recovery.
|
|
60
|
-
|
|
61
|
-
### `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:
|
|
62
47
|
|
|
63
48
|
```ts
|
|
64
|
-
import {
|
|
49
|
+
import { createAuthorizationUrl, generatePkce } from "@wefunder/sdk";
|
|
50
|
+
import { randomBytes } from "node:crypto";
|
|
65
51
|
|
|
66
|
-
// 1. Before redirecting, generate PKCE + a state token and stash them in the session.
|
|
67
52
|
const pkce = generatePkce();
|
|
68
|
-
const
|
|
69
|
-
|
|
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,
|
|
70
63
|
});
|
|
71
|
-
|
|
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");
|
|
72
73
|
|
|
73
|
-
// 2. On the callback, exchange the code (+ verifier) for tokens.
|
|
74
74
|
const tokens = await exchangeCode({
|
|
75
|
-
clientId,
|
|
75
|
+
clientId,
|
|
76
|
+
clientSecret, // optional for public clients
|
|
77
|
+
code,
|
|
78
|
+
redirectUri,
|
|
79
|
+
codeVerifier: attempt.codeVerifier,
|
|
76
80
|
});
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const wf = new Wefunder({
|
|
83
|
+
tokens,
|
|
84
|
+
clientId,
|
|
85
|
+
clientSecret,
|
|
86
|
+
store: {
|
|
87
|
+
save: (nextTokens) => saveTokens(nextTokens),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
|
|
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.
|
|
83
97
|
|
|
84
|
-
|
|
85
|
-
invalidates the old one. The SDK refreshes automatically (proactively before expiry,
|
|
86
|
-
and on a `401`), coalescing concurrent refreshes into one. You just have to persist
|
|
87
|
-
the rotated token so it survives a restart:
|
|
98
|
+
Load the saved tokens yourself when constructing a client after a process restart:
|
|
88
99
|
|
|
89
100
|
```ts
|
|
101
|
+
const tokens = await loadTokens();
|
|
102
|
+
|
|
90
103
|
const wf = new Wefunder({
|
|
91
104
|
tokens,
|
|
92
105
|
clientId,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
save: (t) => db.saveTokens(t), // called on every rotation
|
|
96
|
-
},
|
|
106
|
+
clientSecret,
|
|
107
|
+
store: { save: saveTokens },
|
|
97
108
|
});
|
|
98
109
|
```
|
|
99
110
|
|
|
100
|
-
|
|
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.
|
|
112
|
+
|
|
113
|
+
## Calling the API
|
|
114
|
+
|
|
115
|
+
Common resources are available through typed namespaces:
|
|
116
|
+
|
|
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`.
|
|
101
124
|
|
|
102
|
-
|
|
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.
|
|
103
126
|
|
|
104
|
-
|
|
105
|
-
- **token host** — `/token` + refresh (`fromClientCredentials`, `exchangeCode`, refresh). Defaults to `https://wefunder.com/oauth` today; it will move to `https://api.wefunder.com/oauth` when Wefunder's edge gateway ships. Override via `tokenBaseUrl` (or set both at once with `oauthBaseUrl`).
|
|
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.
|
|
106
128
|
|
|
107
|
-
The
|
|
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.
|
|
108
130
|
|
|
109
131
|
## Pagination
|
|
110
132
|
|
|
111
|
-
List
|
|
112
|
-
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:
|
|
113
134
|
|
|
114
135
|
```ts
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
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);
|
|
119
139
|
|
|
120
|
-
//
|
|
121
|
-
// on every page):
|
|
140
|
+
// Fetch pages lazily.
|
|
122
141
|
for await (const offering of wf.offerings.all({ sort: "most_raised" })) {
|
|
123
142
|
console.log(offering.id);
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
// Or drive pages yourself (gives you `meta`):
|
|
130
|
-
const page = await wf.offerings.list({ sort: "newest" });
|
|
131
|
-
console.log(page.data, page.meta?.next_cursor);
|
|
145
|
+
// Fetch all results into an array.
|
|
146
|
+
const investments = await wf.investments.collect();
|
|
132
147
|
```
|
|
133
148
|
|
|
134
|
-
|
|
149
|
+
Cursors are opaque. Pass the value returned by the API without modifying it.
|
|
150
|
+
|
|
151
|
+
## Errors and retries
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
including the `request_id` (read from the response body) — quote it in support tickets.
|
|
153
|
+
API failures throw `WefunderError`:
|
|
138
154
|
|
|
139
155
|
```ts
|
|
140
156
|
import { WefunderError } from "@wefunder/sdk";
|
|
141
157
|
|
|
142
158
|
try {
|
|
143
|
-
await wf.syndicates.get(
|
|
144
|
-
} catch (
|
|
145
|
-
if (
|
|
146
|
-
console.error(
|
|
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);
|
|
147
163
|
}
|
|
148
164
|
}
|
|
149
165
|
```
|
|
150
166
|
|
|
151
|
-
|
|
152
|
-
`429` (honoring `X-RateLimit-Reset`). Writes are never auto-retried.
|
|
167
|
+
The SDK retries idempotent `GET` requests after transient network errors, `5xx` responses, and rate limits. Write requests are not retried automatically.
|
|
153
168
|
|
|
154
169
|
## Webhooks
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
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
|
+
});
|
|
183
|
+
|
|
184
|
+
await saveSecret(endpoint.attributes!.secret!);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`wf.webhookEndpoints` also provides `list`, `get`, `update`, `remove`, `rotateSecret`, `reenable`, and `test`.
|
|
188
|
+
|
|
189
|
+
### 2. Verify and handle deliveries
|
|
190
|
+
|
|
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.
|
|
158
192
|
|
|
159
193
|
```ts
|
|
160
|
-
import { constructEvent } from "@wefunder/sdk";
|
|
194
|
+
import { constructEvent, dispatchWebhook, WebhookSignatureError } from "@wefunder/sdk";
|
|
161
195
|
|
|
162
|
-
app.post("/webhooks", express.raw({ type: "
|
|
196
|
+
app.post("/webhooks/wefunder", express.raw({ type: "*/*" }), async (req, res) => {
|
|
163
197
|
let event;
|
|
164
198
|
try {
|
|
165
|
-
event = constructEvent(req.body
|
|
166
|
-
} catch {
|
|
167
|
-
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;
|
|
168
203
|
}
|
|
169
|
-
|
|
170
|
-
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
|
+
});
|
|
171
212
|
});
|
|
172
213
|
```
|
|
173
214
|
|
|
174
|
-
|
|
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.
|
|
175
218
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
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:
|
|
180
222
|
|
|
181
223
|
```ts
|
|
182
|
-
|
|
224
|
+
import { signWebhook } from "@wefunder/sdk";
|
|
225
|
+
|
|
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}`
|
|
229
|
+
```
|
|
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`).
|
|
183
238
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
);
|
|
186
249
|
```
|
|
187
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
|
+
|
|
188
253
|
## Development
|
|
189
254
|
|
|
190
255
|
```bash
|
|
191
256
|
npm install
|
|
192
|
-
npm run generate # regenerate src/generated from spec/openapi.yaml
|
|
193
257
|
npm run typecheck
|
|
194
|
-
npm
|
|
195
|
-
npm
|
|
258
|
+
npm run typecheck:examples
|
|
259
|
+
npm test
|
|
196
260
|
npm run build
|
|
197
261
|
```
|
|
198
262
|
|
|
199
|
-
|
|
200
|
-
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.
|
|
201
264
|
|
|
202
|
-
|
|
203
|
-
`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.
|
|
204
266
|
|
|
205
|
-
###
|
|
267
|
+
### Conformance vectors
|
|
206
268
|
|
|
207
|
-
`
|
|
208
|
-
|
|
209
|
-
|
|
269
|
+
`conformance/*.json` is the cross-language behavioural contract shared with the Python and Ruby
|
|
270
|
+
SDKs (signatures, token rotation, pagination, retries, errors). `test/conformance.test.ts` runs
|
|
271
|
+
every case; the vectors are frozen — fix the shell, not the vector. After deliberately changing
|
|
272
|
+
one, run `npm run build:conformance` to refresh `conformance/manifest.json`. See
|
|
273
|
+
[`conformance/README.md`](conformance/README.md).
|
|
274
|
+
|
|
275
|
+
### Updating the API specification
|
|
276
|
+
|
|
277
|
+
Run the sync command against a local checkout of the Wefunder application, then regenerate the client:
|
|
210
278
|
|
|
211
279
|
```bash
|
|
212
|
-
|
|
280
|
+
npm run sync-spec -- /path/to/wefunder
|
|
213
281
|
npm run generate
|
|
214
|
-
|
|
282
|
+
npm run typecheck
|
|
283
|
+
npm test
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Commit the specification and generated client together.
|
|
287
|
+
|
|
288
|
+
### Releasing
|
|
289
|
+
|
|
290
|
+
Releases are published by GitHub Actions. From a clean `main` branch:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
npm version prerelease --preid beta
|
|
294
|
+
git push --follow-tags
|
|
215
295
|
```
|
|
216
296
|
|
|
217
|
-
|
|
218
|
-
so the public-tier definition can't drift between the two repos. CI's
|
|
219
|
-
`generated code matches spec` job verifies `src/generated` matches the committed spec;
|
|
220
|
-
it cannot reach the private canonical swagger, so run `sync-spec` before cutting a
|
|
221
|
-
release. (`npm test` stays hermetic.)
|
|
297
|
+
The release workflow runs the package checks and publishes prereleases to npm's `beta` tag.
|