@wefunder/sdk 0.1.0-beta.11 → 0.1.0-beta.12

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 CHANGED
@@ -120,7 +120,9 @@ const investments = await wf.investments.list({ company_id: "co_example" });
120
120
  const portfolio = await wf.portfolio.get();
121
121
  ```
122
122
 
123
- Namespaces: `users`, `offerings`, `investments`, `portfolio`, `campaigns`, `syndicates`, `intents`, `attribution`, and `webhookEndpoints`.
123
+ Namespaces: `users`, `offerings`, `investments`, `portfolio`, `campaigns`, `syndicates`, `intents`, `attribution`, `installations`, and `webhookEndpoints`.
124
+
125
+ `wf.offerings.stats(id)` returns an offering's aggregate investment stats. `wf.installations` lets your app act as a company or syndicate: `eligibleTargets()`, `create()`, `mintToken()`, `list()`, `get()`, `revoke()`, and `installOrMintToken()`, which handles the API's 409 `already_installed` answer by minting a token for the existing install with the same scopes.
124
126
 
125
127
  `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
128
 
@@ -191,25 +193,39 @@ await saveSecret(endpoint.attributes!.secret!);
191
193
  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.
192
194
 
193
195
  ```ts
194
- import { constructEvent, dispatchWebhook, WebhookSignatureError } from "@wefunder/sdk";
195
-
196
- app.post("/webhooks/wefunder", express.raw({ type: "*/*" }), async (req, res) => {
197
- let event;
198
- try {
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;
203
- }
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
- });
212
- });
196
+ import {
197
+ constructEvent,
198
+ dispatchWebhook,
199
+ WebhookSignatureError,
200
+ } from "@wefunder/sdk";
201
+
202
+ app.post(
203
+ "/webhooks/wefunder",
204
+ express.raw({ type: "*/*" }),
205
+ async (req, res) => {
206
+ let event;
207
+ try {
208
+ event = constructEvent(
209
+ req.body,
210
+ req.headers,
211
+ process.env.WEFUNDER_WEBHOOK_SECRET!,
212
+ );
213
+ } catch (err) {
214
+ if (err instanceof WebhookSignatureError)
215
+ return res.status(400).send(err.reason);
216
+ throw err;
217
+ }
218
+
219
+ res.sendStatus(200); // acknowledge first, then do the work
220
+
221
+ await dispatchWebhook(event, {
222
+ "investment.executed": async (e) =>
223
+ recordFunding(e.data.id, e.data.amounts.committed),
224
+ "offering.opened": async (e) => announce(e.data.company.name),
225
+ default: (e) => console.log("unhandled", e.event),
226
+ });
227
+ },
228
+ );
213
229
  ```
214
230
 
215
231
  `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.
@@ -250,6 +266,39 @@ const members = await wf.unwrap(
250
266
 
251
267
  Raw operations return `{ data, error, response }`. Passing the result to `wf.unwrap()` applies the same error handling used by the resource namespaces.
252
268
 
269
+ ## Escape hatch: `wf.request` (any path)
270
+
271
+ For endpoints outside the generated surface entirely — preview-tier operations
272
+ (e.g. partner SPVs) or operations newer than your installed SDK version —
273
+ `wf.request()` calls any API path with the SDK's full envelope: bearer auth
274
+ (including refresh / client_credentials re-mint on 401), the pinned
275
+ `Wefunder-Version` header, the retry policy, and a typed `WefunderError` on
276
+ failure. It is untyped by design; preview endpoints can change at any time.
277
+
278
+ ```ts
279
+ // GET with query params
280
+ const spvs = await wf.request("GET", "/partner/spvs", { query: { limit: 10 } });
281
+
282
+ // POST with a JSON body and an idempotency key
283
+ const session = await wf.request(
284
+ "POST",
285
+ `/partner/spvs/${spvId}/investment_sessions`,
286
+ {
287
+ body: {
288
+ investment_session: {
289
+ email: "alex@example.com",
290
+ allocation_cents: 500_000,
291
+ },
292
+ },
293
+ headers: { "Idempotency-Key": "invite-alex-1" },
294
+ },
295
+ );
296
+ ```
297
+
298
+ The returned body is passed through as-is (no `{ data }` unwrapping — envelope
299
+ shapes vary across unshipped endpoints). When an operation graduates to the
300
+ generated surface, switch to `wf.raw.<opId>` (typed) or its namespace method.
301
+
253
302
  ## Development
254
303
 
255
304
  ```bash
package/dist/index.cjs CHANGED
@@ -1947,6 +1947,32 @@ var Wefunder = class _Wefunder {
1947
1947
  unwrap(p) {
1948
1948
  return this.#unwrap(p);
1949
1949
  }
1950
+ /**
1951
+ * Untyped escape hatch: call ANY API path with the SDK's full envelope —
1952
+ * bearer auth (incl. refresh / client_credentials re-mint on 401), the pinned
1953
+ * `Wefunder-Version` header, the retry policy, and a typed `WefunderError` on
1954
+ * failure. For endpoints outside the generated surface: preview-tier ops
1955
+ * (e.g. partner SPVs) or ops newer than this SDK build.
1956
+ *
1957
+ * `path` is relative to the client's `baseUrl` (e.g. `"/partner/spvs"`).
1958
+ * Returns the raw response body (no `{ data }` unwrapping — envelopes vary
1959
+ * across unshipped endpoints).
1960
+ */
1961
+ async request(method, path, opts = {}) {
1962
+ return this.#unwrap(
1963
+ this.#client.request({
1964
+ method,
1965
+ url: path,
1966
+ // Same security descriptor the generated ops pass — this is what makes
1967
+ // the client attach (and on 401, refresh/re-mint) the bearer token.
1968
+ security: [{ scheme: "bearer", type: "http" }],
1969
+ query: opts.query,
1970
+ body: opts.body,
1971
+ // JSON by default when a body is present; caller headers win.
1972
+ headers: opts.body !== void 0 ? { "Content-Type": "application/json", ...opts.headers } : opts.headers
1973
+ })
1974
+ );
1975
+ }
1950
1976
  // Generic page helper: forwards the endpoint's full query (cursor + documented
1951
1977
  // params like `sort`), not just the cursor. (Stress-test finding B.)
1952
1978
  #page(fn) {
@@ -1967,6 +1993,13 @@ var Wefunder = class _Wefunder {
1967
1993
  client: this.#client,
1968
1994
  path: { external_id: externalId }
1969
1995
  })
1996
+ ),
1997
+ /** Aggregate investment stats (count / committed / raised, by status) for one offering. */
1998
+ stats: (externalId) => this.#unwrapData(
1999
+ getOfferingStats({
2000
+ client: this.#client,
2001
+ path: { offering_id: externalId }
2002
+ })
1970
2003
  )
1971
2004
  };
1972
2005
  /**
@@ -2037,6 +2070,72 @@ var Wefunder = class _Wefunder {
2037
2070
  getAttributionMe({ client: this.#client })
2038
2071
  )
2039
2072
  };
2073
+ /**
2074
+ * Installations (`/installations`, scopes `read:installations` / `write:installations`;
2075
+ * write does not imply read). An install lets your app act AS a company or syndicate:
2076
+ * `create` and `mintToken` return a company-owned token (shown once, no expiry, no
2077
+ * refresh) — build a second client with it. Installing is also what makes a company
2078
+ * or syndicate an audience for your webhooks.
2079
+ */
2080
+ installations = {
2081
+ /** Companies / syndicates the token's user may install your app on (empty for an investor). */
2082
+ eligibleTargets: (query) => this.#unwrapData(
2083
+ listEligibleInstallTargets({ client: this.#client, query })
2084
+ ).then((rows) => rows ?? []),
2085
+ /** Every install of your app (`meta.count`). */
2086
+ list: () => this.#unwrap(
2087
+ listInstallations({ client: this.#client })
2088
+ ),
2089
+ get: (id) => this.#unwrapData(
2090
+ getInstallation({
2091
+ client: this.#client,
2092
+ path: { external_id: id }
2093
+ })
2094
+ ),
2095
+ /**
2096
+ * Install on a target. Store `token.access_token` — it is never shown again. If the
2097
+ * app is already installed there the API answers 409 `already_installed` with
2098
+ * `details.installation` = the existing id; mint a token for that instead
2099
+ * (see `installOrMintToken`).
2100
+ */
2101
+ create: (input) => this.#unwrap(
2102
+ createInstallation({ client: this.#client, body: input })
2103
+ ),
2104
+ /**
2105
+ * A fresh company-owned token for an existing install. `scopes` narrows within the
2106
+ * install's ceiling; omit it for the ceiling, and note an explicit `[]` grants nothing.
2107
+ */
2108
+ mintToken: (id, scopes) => this.#unwrap(
2109
+ createInstallationToken({
2110
+ client: this.#client,
2111
+ path: { external_id: id },
2112
+ body: scopes ? { scopes } : void 0
2113
+ })
2114
+ ),
2115
+ /**
2116
+ * `create`, falling back to `mintToken` for the existing install on 409
2117
+ * `already_installed`. The mint re-requests `input.scopes` so a retry never widens the
2118
+ * grant. Any other error (revoked install, missing scope) still throws.
2119
+ */
2120
+ installOrMintToken: async (input) => {
2121
+ try {
2122
+ return await this.installations.create(input);
2123
+ } catch (err) {
2124
+ if (!(err instanceof WefunderError) || err.type !== "already_installed")
2125
+ throw err;
2126
+ const existingId = err.details?.installation;
2127
+ if (!existingId) throw err;
2128
+ return this.installations.mintToken(existingId, input.scopes);
2129
+ }
2130
+ },
2131
+ /** Revoke an install: its tokens stop working at once. Returns the install, now `revoked`. */
2132
+ revoke: (id) => this.#unwrapData(
2133
+ revokeInstallation({
2134
+ client: this.#client,
2135
+ path: { external_id: id }
2136
+ })
2137
+ )
2138
+ };
2040
2139
  /**
2041
2140
  * Webhook endpoints (`/webhook_endpoints`, scopes `read:webhooks` / `write:webhooks`).
2042
2141
  * Endpoints belong to your application and are managed through the LIVE API —