@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 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
+ [![CI](https://github.com/Wefunder/wefunder-node/actions/workflows/ci.yml/badge.svg)](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.)