@wefunder/sdk 0.1.0-beta.10 → 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
@@ -33,10 +33,12 @@ __export(index_exports, {
33
33
  REQUEST_ID_HEADER: () => REQUEST_ID_HEADER,
34
34
  SANDBOX_AUTHORIZE_BASE_URL: () => SANDBOX_AUTHORIZE_BASE_URL,
35
35
  SIGNATURE_HEADER: () => SIGNATURE_HEADER,
36
+ TokenManager: () => TokenManager,
36
37
  WebhookSignatureError: () => WebhookSignatureError,
37
38
  Wefunder: () => Wefunder,
38
39
  WefunderAuthError: () => WefunderAuthError,
39
40
  WefunderError: () => WefunderError,
41
+ WefunderTokenPersistenceError: () => WefunderTokenPersistenceError,
40
42
  checkWebhookSignature: () => checkWebhookSignature,
41
43
  clientCredentialsGrant: () => clientCredentialsGrant,
42
44
  collect: () => collect,
@@ -1074,6 +1076,20 @@ var WefunderAuthError = class extends WefunderError {
1074
1076
  };
1075
1077
 
1076
1078
  // src/token-manager.ts
1079
+ var WefunderTokenPersistenceError = class extends Error {
1080
+ tokens;
1081
+ constructor(tokens, cause) {
1082
+ super(
1083
+ `Token store failed to save the rotated token set: ${cause instanceof Error ? cause.message : String(cause)}`,
1084
+ { cause }
1085
+ );
1086
+ this.name = "WefunderTokenPersistenceError";
1087
+ this.tokens = tokens;
1088
+ }
1089
+ };
1090
+ function sameTokenSet(a, b) {
1091
+ return a.accessToken === b.accessToken && a.refreshToken === b.refreshToken;
1092
+ }
1077
1093
  var TokenManager = class {
1078
1094
  #tokens;
1079
1095
  #clientId;
@@ -1086,6 +1102,7 @@ var TokenManager = class {
1086
1102
  #now;
1087
1103
  #leeway;
1088
1104
  #inflight;
1105
+ #pending;
1089
1106
  constructor(opts) {
1090
1107
  this.#tokens = opts.tokens;
1091
1108
  this.#clientId = opts.clientId;
@@ -1098,15 +1115,40 @@ var TokenManager = class {
1098
1115
  this.#now = opts.now ?? Date.now;
1099
1116
  this.#leeway = opts.expiryLeewayMs ?? 3e4;
1100
1117
  }
1118
+ /** The durable, in-use token set. A rotated set that could not be persisted sits in `pendingTokens`. */
1101
1119
  get current() {
1102
1120
  return this.#tokens;
1103
1121
  }
1104
1122
  /** True if the manager can recover an expired token (rotate a refresh token or re-mint). */
1105
1123
  get canRefresh() {
1106
- return Boolean(this.#tokens.refreshToken && this.#clientId || this.#reMint);
1124
+ return Boolean(
1125
+ this.#tokens.refreshToken && this.#clientId || this.#reMint
1126
+ );
1127
+ }
1128
+ /** A rotated set awaiting a successful `store.save` (see `WefunderTokenPersistenceError`). */
1129
+ get pendingTokens() {
1130
+ return this.#pending;
1107
1131
  }
1108
- /** Returns a valid access token, refreshing proactively if it's expired/near-expiry. */
1132
+ /**
1133
+ * Tell the manager you persisted `tokens` (the set from a `WefunderTokenPersistenceError`)
1134
+ * yourself. Publishes it only if it is still the pending set — a stale acknowledgment (the
1135
+ * manager has since rotated again) is a no-op and returns false, so an older save can never
1136
+ * publish a newer, unsaved set.
1137
+ */
1138
+ async markPersisted(tokens) {
1139
+ const pending = this.#pending;
1140
+ if (!pending || !sameTokenSet(pending, tokens)) return false;
1141
+ this.#pending = void 0;
1142
+ this.#tokens = pending;
1143
+ return true;
1144
+ }
1145
+ /**
1146
+ * Returns a valid access token, refreshing proactively if it's expired/near-expiry. If a
1147
+ * rotated set is pending persistence, the save is retried first — no request uses an
1148
+ * undurable token.
1149
+ */
1109
1150
  async getAccessToken() {
1151
+ if (this.#pending) await this.refresh();
1110
1152
  const { expiresAt } = this.#tokens;
1111
1153
  if (expiresAt !== void 0 && this.#now() >= expiresAt - this.#leeway && this.canRefresh) {
1112
1154
  await this.refresh();
@@ -1120,6 +1162,14 @@ var TokenManager = class {
1120
1162
  */
1121
1163
  async refresh() {
1122
1164
  if (this.#inflight) return this.#inflight;
1165
+ if (this.#pending) {
1166
+ this.#inflight = this.#publishPending();
1167
+ try {
1168
+ return await this.#inflight;
1169
+ } finally {
1170
+ this.#inflight = void 0;
1171
+ }
1172
+ }
1123
1173
  const strategy = this.#recoveryStrategy();
1124
1174
  if (!strategy) {
1125
1175
  throw new WefunderAuthError(
@@ -1127,11 +1177,8 @@ var TokenManager = class {
1127
1177
  );
1128
1178
  }
1129
1179
  this.#inflight = (async () => {
1130
- const next = await strategy();
1131
- this.#tokens = next;
1132
- await this.#store?.save(next);
1133
- await this.#onTokenRefresh?.(next);
1134
- return next;
1180
+ this.#pending = await strategy();
1181
+ return this.#publishPending();
1135
1182
  })();
1136
1183
  try {
1137
1184
  return await this.#inflight;
@@ -1139,6 +1186,22 @@ var TokenManager = class {
1139
1186
  this.#inflight = void 0;
1140
1187
  }
1141
1188
  }
1189
+ // Persist BEFORE publishing: no caller may use the rotated token until it is durable, and a
1190
+ // failed save must not leave the process working in memory but unable to reconnect after a
1191
+ // restart. On failure the set stays pending and WefunderTokenPersistenceError is thrown; the
1192
+ // next call retries the save.
1193
+ async #publishPending() {
1194
+ const tokens = this.#pending;
1195
+ try {
1196
+ await this.#store?.save(tokens);
1197
+ await this.#onTokenRefresh?.(tokens);
1198
+ } catch (err) {
1199
+ throw new WefunderTokenPersistenceError(tokens, err);
1200
+ }
1201
+ this.#pending = void 0;
1202
+ this.#tokens = tokens;
1203
+ return tokens;
1204
+ }
1142
1205
  // Pick the recovery strategy: refresh_token rotation, else cc re-mint, else none.
1143
1206
  #recoveryStrategy() {
1144
1207
  const refreshTokenValue = this.#tokens.refreshToken;
@@ -1831,12 +1894,22 @@ var Wefunder = class _Wefunder {
1831
1894
  });
1832
1895
  }
1833
1896
  /** The live token set (e.g. to persist after construction). */
1897
+ /** A rotated set awaiting a successful save (see `WefunderTokenPersistenceError`). */
1898
+ get pendingTokens() {
1899
+ return this.#tokens.pendingTokens;
1900
+ }
1901
+ /** Acknowledge an out-of-band save of `tokens`; false if that set is no longer pending. */
1902
+ markPersisted(tokens) {
1903
+ return this.#tokens.markPersisted(tokens);
1904
+ }
1834
1905
  get tokens() {
1835
1906
  return this.#tokens.current;
1836
1907
  }
1837
1908
  // ---- unwrap: turn the {data,error,response} result into data-or-throw ----
1838
1909
  async #unwrap(p) {
1839
1910
  const { data, error, response } = await p;
1911
+ if (error instanceof WefunderTokenPersistenceError || error instanceof WefunderError)
1912
+ throw error;
1840
1913
  if (response && response.ok && error === void 0) return data;
1841
1914
  const status = response?.status ?? 0;
1842
1915
  const env = error ?? {};
@@ -1845,7 +1918,10 @@ var Wefunder = class _Wefunder {
1845
1918
  type: env.error?.type ?? "api_error",
1846
1919
  message: env.error?.message ?? response?.statusText ?? "Request failed",
1847
1920
  // X-Wf-Request-Id header is primary (present even on non-JSON edge errors).
1848
- requestId: requestIdFrom(response, env.error?.request_id ?? env.request_id),
1921
+ requestId: requestIdFrom(
1922
+ response,
1923
+ env.error?.request_id ?? env.request_id
1924
+ ),
1849
1925
  details: env.error?.details,
1850
1926
  remediation: env.error?.remediation
1851
1927
  });
@@ -1871,6 +1947,32 @@ var Wefunder = class _Wefunder {
1871
1947
  unwrap(p) {
1872
1948
  return this.#unwrap(p);
1873
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
+ }
1874
1976
  // Generic page helper: forwards the endpoint's full query (cursor + documented
1875
1977
  // params like `sort`), not just the cursor. (Stress-test finding B.)
1876
1978
  #page(fn) {
@@ -1881,11 +1983,23 @@ var Wefunder = class _Wefunder {
1881
1983
  me: () => this.#unwrapData(getCurrentUser({ client: this.#client }))
1882
1984
  };
1883
1985
  offerings = {
1884
- list: this.#page(listOfferings),
1986
+ list: this.#page(
1987
+ listOfferings
1988
+ ),
1885
1989
  all: (query) => paginate((cursor) => this.offerings.list({ ...query, cursor })),
1886
1990
  collect: (query) => collect((cursor) => this.offerings.list({ ...query, cursor })),
1887
1991
  get: (externalId) => this.#unwrapData(
1888
- getOffering({ client: this.#client, path: { external_id: externalId } })
1992
+ getOffering({
1993
+ client: this.#client,
1994
+ path: { external_id: externalId }
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
+ })
1889
2003
  )
1890
2004
  };
1891
2005
  /**
@@ -1894,20 +2008,40 @@ var Wefunder = class _Wefunder {
1894
2008
  * from your last page — it is always present — and resume from it next time.
1895
2009
  */
1896
2010
  investments = {
1897
- list: this.#page(listInvestments),
1898
- all: (query) => paginate((cursor) => this.investments.list({ ...query, cursor })),
1899
- collect: (query) => collect((cursor) => this.investments.list({ ...query, cursor })),
2011
+ list: this.#page(
2012
+ listInvestments
2013
+ ),
2014
+ all: (query) => paginate(
2015
+ (cursor) => this.investments.list({
2016
+ ...query,
2017
+ cursor
2018
+ })
2019
+ ),
2020
+ collect: (query) => collect(
2021
+ (cursor) => this.investments.list({
2022
+ ...query,
2023
+ cursor
2024
+ })
2025
+ ),
1900
2026
  /** The current record (not the published projection) for one investment (`inv_…`). */
1901
- get: (id) => this.#unwrapData(getInvestment({ client: this.#client, path: { id } }))
2027
+ get: (id) => this.#unwrapData(
2028
+ getInvestment({ client: this.#client, path: { id } })
2029
+ )
1902
2030
  };
1903
2031
  portfolio = {
1904
- get: (query) => this.#unwrapData(getPortfolio({ client: this.#client, query })),
2032
+ get: (query) => this.#unwrapData(
2033
+ getPortfolio({ client: this.#client, query })
2034
+ ),
1905
2035
  positions: {
1906
2036
  list: this.#page(
1907
2037
  listPortfolioPositions
1908
2038
  ),
1909
- all: (query) => paginate((cursor) => this.portfolio.positions.list({ ...query, cursor })),
1910
- collect: (query) => collect((cursor) => this.portfolio.positions.list({ ...query, cursor }))
2039
+ all: (query) => paginate(
2040
+ (cursor) => this.portfolio.positions.list({ ...query, cursor })
2041
+ ),
2042
+ collect: (query) => collect(
2043
+ (cursor) => this.portfolio.positions.list({ ...query, cursor })
2044
+ )
1911
2045
  }
1912
2046
  };
1913
2047
  campaigns = {
@@ -1916,17 +2050,91 @@ var Wefunder = class _Wefunder {
1916
2050
  collect: () => collect((cursor) => this.campaigns.list({ cursor }))
1917
2051
  };
1918
2052
  syndicates = {
1919
- list: this.#page(listSyndicates),
2053
+ list: this.#page(
2054
+ listSyndicates
2055
+ ),
1920
2056
  all: (query) => paginate((cursor) => this.syndicates.list({ ...query, cursor })),
1921
- get: (id) => this.#unwrapData(getSyndicate({ client: this.#client, path: { id } }))
2057
+ get: (id) => this.#unwrapData(
2058
+ getSyndicate({ client: this.#client, path: { id } })
2059
+ )
1922
2060
  };
1923
2061
  intents = {
1924
2062
  list: this.#page(listIntents),
1925
2063
  all: (query) => paginate((cursor) => this.intents.list({ ...query, cursor })),
1926
- get: (id) => this.#unwrapData(getIntent({ client: this.#client, path: { id } }))
2064
+ get: (id) => this.#unwrapData(
2065
+ getIntent({ client: this.#client, path: { id } })
2066
+ )
1927
2067
  };
1928
2068
  attribution = {
1929
- me: () => this.#unwrapData(getAttributionMe({ client: this.#client }))
2069
+ me: () => this.#unwrapData(
2070
+ getAttributionMe({ client: this.#client })
2071
+ )
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
+ )
1930
2138
  };
1931
2139
  /**
1932
2140
  * Webhook endpoints (`/webhook_endpoints`, scopes `read:webhooks` / `write:webhooks`).
@@ -1937,30 +2145,50 @@ var Wefunder = class _Wefunder {
1937
2145
  */
1938
2146
  webhookEndpoints = {
1939
2147
  /** All of your app's endpoints (secret omitted). `meta.quota` is the per-app limit. */
1940
- list: () => this.#unwrap(listWebhookEndpoints({ client: this.#client })),
2148
+ list: () => this.#unwrap(
2149
+ listWebhookEndpoints({ client: this.#client })
2150
+ ),
1941
2151
  get: (id) => this.#unwrapData(
1942
- getWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2152
+ getWebhookEndpoint({
2153
+ client: this.#client,
2154
+ path: { external_id: id }
2155
+ })
1943
2156
  ),
1944
2157
  /** Store `attributes.secret` from the result — it is never shown again. */
1945
- create: (input) => this.#unwrapData(createWebhookEndpoint({ client: this.#client, body: input })),
2158
+ create: (input) => this.#unwrapData(
2159
+ createWebhookEndpoint({ client: this.#client, body: input })
2160
+ ),
1946
2161
  /** `events` replaces the subscription list wholesale (no merge); omit to leave unchanged. */
1947
2162
  update: (id, input) => this.#unwrapData(
1948
- updateWebhookEndpoint({ client: this.#client, path: { external_id: id }, body: input })
2163
+ updateWebhookEndpoint({
2164
+ client: this.#client,
2165
+ path: { external_id: id },
2166
+ body: input
2167
+ })
1949
2168
  ),
1950
2169
  /** Stops deliveries immediately. Removal is permanent — create a new endpoint to resume. */
1951
2170
  remove: (id) => this.#unwrapData(
1952
- deleteWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2171
+ deleteWebhookEndpoint({
2172
+ client: this.#client,
2173
+ path: { external_id: id }
2174
+ })
1953
2175
  ),
1954
2176
  /**
1955
2177
  * New secret (shown once). The old one keeps signing for a 24h overlap — deliveries
1956
2178
  * carry a `v1` for both, and `constructEvent` accepts either.
1957
2179
  */
1958
2180
  rotateSecret: (id) => this.#unwrapData(
1959
- rotateWebhookEndpointSecret({ client: this.#client, path: { external_id: id } })
2181
+ rotateWebhookEndpointSecret({
2182
+ client: this.#client,
2183
+ path: { external_id: id }
2184
+ })
1960
2185
  ),
1961
2186
  /** Recover an auto-disabled endpoint after fixing your server. Idempotent. */
1962
2187
  reenable: (id) => this.#unwrapData(
1963
- reenableWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2188
+ reenableWebhookEndpoint({
2189
+ client: this.#client,
2190
+ path: { external_id: id }
2191
+ })
1964
2192
  ),
1965
2193
  /**
1966
2194
  * POST a real, signed example event at the endpoint (production envelope + signature)
@@ -2156,10 +2384,12 @@ async function dispatchWebhook(event, handlers) {
2156
2384
  REQUEST_ID_HEADER,
2157
2385
  SANDBOX_AUTHORIZE_BASE_URL,
2158
2386
  SIGNATURE_HEADER,
2387
+ TokenManager,
2159
2388
  WebhookSignatureError,
2160
2389
  Wefunder,
2161
2390
  WefunderAuthError,
2162
2391
  WefunderError,
2392
+ WefunderTokenPersistenceError,
2163
2393
  checkWebhookSignature,
2164
2394
  clientCredentialsGrant,
2165
2395
  collect,