@wefunder/sdk 0.1.0-beta.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 +221 -0
- package/dist/index.cjs +1264 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4567 -0
- package/dist/index.d.ts +4567 -0
- package/dist/index.js +1221 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wefunder, Inc.
|
|
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,221 @@
|
|
|
1
|
+
# @wefunder/sdk (beta)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Wefunder/wefunder-node/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Official TypeScript SDK for the [Wefunder API](https://docs.wefunder.com/api-reference).
|
|
6
|
+
|
|
7
|
+
> **Beta.** The package is `0.x` — breaking changes are possible while we
|
|
8
|
+
> stabilize. Feedback welcome.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @wefunder/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Node 18+ (uses the global `fetch`). ESM and CommonJS both supported.
|
|
15
|
+
|
|
16
|
+
### Scope & versioning
|
|
17
|
+
|
|
18
|
+
- **Surface:** this release covers the **stable + beta** public API (offerings,
|
|
19
|
+
investments, campaigns, syndicates, intents, attribution). Preview-only endpoints
|
|
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.
|
|
25
|
+
|
|
26
|
+
## Quickstart (server-to-server)
|
|
27
|
+
|
|
28
|
+
The fastest path: a `client_credentials` grant with a sandbox token, no user redirect.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Wefunder } from "@wefunder/sdk";
|
|
32
|
+
|
|
33
|
+
const wf = await Wefunder.fromClientCredentials({
|
|
34
|
+
clientId: process.env.WEFUNDER_CLIENT_ID!,
|
|
35
|
+
clientSecret: process.env.WEFUNDER_CLIENT_SECRET!,
|
|
36
|
+
scopes: ["read:public"],
|
|
37
|
+
});
|
|
38
|
+
|
|
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
|
+
const page = await wf.offerings.list();
|
|
42
|
+
console.log(`${page.data?.length} offerings`);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **`wf.users.me()` won't work with `client_credentials`.** `/users/me` requires
|
|
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
|
|
51
|
+
|
|
52
|
+
The SDK supports both OAuth 2.0 grants the API offers.
|
|
53
|
+
|
|
54
|
+
### `client_credentials` (server-side)
|
|
55
|
+
|
|
56
|
+
`Wefunder.fromClientCredentials({ clientId, clientSecret, scopes })` — see above.
|
|
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)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { generatePkce, createAuthorizationUrl, exchangeCode, Wefunder } from "@wefunder/sdk";
|
|
65
|
+
|
|
66
|
+
// 1. Before redirecting, generate PKCE + a state token and stash them in the session.
|
|
67
|
+
const pkce = generatePkce();
|
|
68
|
+
const url = createAuthorizationUrl({
|
|
69
|
+
clientId, redirectUri, scopes: ["read:investments"], state, pkce,
|
|
70
|
+
});
|
|
71
|
+
// redirect the user to `url`
|
|
72
|
+
|
|
73
|
+
// 2. On the callback, exchange the code (+ verifier) for tokens.
|
|
74
|
+
const tokens = await exchangeCode({
|
|
75
|
+
clientId, code, redirectUri, codeVerifier: pkce.codeVerifier,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 3. Build a client. Pass clientId so it can auto-refresh on expiry.
|
|
79
|
+
const wf = new Wefunder({ tokens, clientId, onTokenRefresh: (t) => saveToDb(t) });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Refresh tokens rotate — persist every refresh
|
|
83
|
+
|
|
84
|
+
Wefunder **rotates** refresh tokens: each refresh returns a *new* refresh token and
|
|
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:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const wf = new Wefunder({
|
|
91
|
+
tokens,
|
|
92
|
+
clientId,
|
|
93
|
+
store: {
|
|
94
|
+
load: () => db.loadTokens(),
|
|
95
|
+
save: (t) => db.saveTokens(t), // called on every rotation
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Hosts (advanced)
|
|
101
|
+
|
|
102
|
+
OAuth uses two hosts, independently overridable:
|
|
103
|
+
|
|
104
|
+
- **authorize host** — the browser consent redirect (`createAuthorizationUrl`). Defaults to `https://wefunder.com/oauth`.
|
|
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`).
|
|
106
|
+
|
|
107
|
+
The **API base** is `WefunderOptions.baseUrl` (default `https://api.wefunder.com/api/v2`). When Wefunder ships version-free URLs, the canonical base drops `/api/v2`; the current path stays as a back-compat alias, so no change is required on your side.
|
|
108
|
+
|
|
109
|
+
## Pagination
|
|
110
|
+
|
|
111
|
+
List endpoints auto-paginate. The cursor is opaque — you never construct it. List
|
|
112
|
+
methods take the endpoint's documented query params, and they're preserved across pages.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// Stream lazily (one page fetched at a time):
|
|
116
|
+
for await (const inv of wf.investments.all()) {
|
|
117
|
+
console.log(inv.id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Query params are forwarded — e.g. sort the offerings browser (sort is preserved
|
|
121
|
+
// on every page):
|
|
122
|
+
for await (const offering of wf.offerings.all({ sort: "most_raised" })) {
|
|
123
|
+
console.log(offering.id);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Or collect everything:
|
|
127
|
+
const all = await wf.investments.collect();
|
|
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);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Errors
|
|
135
|
+
|
|
136
|
+
Failed requests throw `WefunderError` with the fields from the API's error envelope,
|
|
137
|
+
including the `request_id` (read from the response body) — quote it in support tickets.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { WefunderError } from "@wefunder/sdk";
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await wf.syndicates.get(123);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err instanceof WefunderError) {
|
|
146
|
+
console.error(err.status, err.type, err.message, err.requestId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Idempotent `GET`s are retried automatically on transient `5xx`/network errors and on
|
|
152
|
+
`429` (honoring `X-RateLimit-Reset`). Writes are never auto-retried.
|
|
153
|
+
|
|
154
|
+
## Webhooks
|
|
155
|
+
|
|
156
|
+
Verify and parse webhook deliveries. Pass the **raw** request body (not a re-serialized
|
|
157
|
+
object) and the headers:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { constructEvent } from "@wefunder/sdk";
|
|
161
|
+
|
|
162
|
+
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
|
|
163
|
+
let event;
|
|
164
|
+
try {
|
|
165
|
+
event = constructEvent(req.body.toString("utf8"), req.headers, process.env.WEBHOOK_SECRET!);
|
|
166
|
+
} catch {
|
|
167
|
+
return res.status(400).send("invalid signature");
|
|
168
|
+
}
|
|
169
|
+
// event.event, event.deliveryId, event.data
|
|
170
|
+
res.sendStatus(200);
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Escape hatch: `wf.raw`
|
|
175
|
+
|
|
176
|
+
Ergonomic namespaces cover the common GA resources. Every generated operation is also
|
|
177
|
+
available, pre-bound, under `wf.raw`. Raw ops return the low-level `{ data, error,
|
|
178
|
+
response }` result; wrap them in `wf.unwrap(...)` to get the same typed-error +
|
|
179
|
+
envelope handling the namespaces use (a `WefunderError` with `request_id` on failure):
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const members = await wf.unwrap(wf.raw.listSyndicateMembers({ path: { syndicate_id: 1 } }));
|
|
183
|
+
|
|
184
|
+
// Or handle the raw result yourself:
|
|
185
|
+
const res = await wf.raw.listSyndicateMembers({ path: { syndicate_id: 1 } });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm install
|
|
192
|
+
npm run generate # regenerate src/generated from spec/openapi.yaml
|
|
193
|
+
npm run typecheck
|
|
194
|
+
npm test # hermetic unit tests (no network)
|
|
195
|
+
npm run test:e2e # live sandbox E2E — needs WEFUNDER_CLIENT_ID/SECRET (or a .env); auto-skips otherwise
|
|
196
|
+
npm run build
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The live E2E hits `api.wefunder.com` with a sandbox app's `client_credentials`. Put
|
|
200
|
+
the credentials in a gitignored `.env` (`WEFUNDER_CLIENT_ID=` / `WEFUNDER_CLIENT_SECRET=`).
|
|
201
|
+
|
|
202
|
+
The typed layer in `src/generated/` is produced by `@hey-api/openapi-ts` from
|
|
203
|
+
`spec/openapi.yaml` and is never hand-edited. The hand-written shell in `src/` wraps it.
|
|
204
|
+
|
|
205
|
+
### Syncing the spec (maintainers)
|
|
206
|
+
|
|
207
|
+
`spec/openapi.yaml` is a vendored copy of the **public tier** (stable + beta) of the
|
|
208
|
+
canonical Wefunder swagger. Preview/internal operations are excluded by design. To
|
|
209
|
+
refresh it from a local wefunder checkout:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
WEFUNDER_REPO=/path/to/wefunder npm run sync-spec
|
|
213
|
+
npm run generate
|
|
214
|
+
git add spec src/generated # commit both together
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`sync-spec` delegates filtering to the wefunder repo's own `build-filtered-spec.js`,
|
|
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.)
|