@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 +69 -20
- package/dist/index.cjs +258 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -1
- package/dist/index.d.ts +134 -1
- package/dist/index.js +256 -28
- package/dist/index.js.map +1 -1
- package/examples_manifest.json +9 -9
- package/package.json +1 -1
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 {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
|
|
1131
|
-
this.#
|
|
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(
|
|
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(
|
|
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({
|
|
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(
|
|
1898
|
-
|
|
1899
|
-
|
|
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(
|
|
2027
|
+
get: (id) => this.#unwrapData(
|
|
2028
|
+
getInvestment({ client: this.#client, path: { id } })
|
|
2029
|
+
)
|
|
1902
2030
|
};
|
|
1903
2031
|
portfolio = {
|
|
1904
|
-
get: (query) => this.#unwrapData(
|
|
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(
|
|
1910
|
-
|
|
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(
|
|
2053
|
+
list: this.#page(
|
|
2054
|
+
listSyndicates
|
|
2055
|
+
),
|
|
1920
2056
|
all: (query) => paginate((cursor) => this.syndicates.list({ ...query, cursor })),
|
|
1921
|
-
get: (id) => this.#unwrapData(
|
|
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(
|
|
2064
|
+
get: (id) => this.#unwrapData(
|
|
2065
|
+
getIntent({ client: this.#client, path: { id } })
|
|
2066
|
+
)
|
|
1927
2067
|
};
|
|
1928
2068
|
attribution = {
|
|
1929
|
-
me: () => this.#unwrapData(
|
|
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(
|
|
2148
|
+
list: () => this.#unwrap(
|
|
2149
|
+
listWebhookEndpoints({ client: this.#client })
|
|
2150
|
+
),
|
|
1941
2151
|
get: (id) => this.#unwrapData(
|
|
1942
|
-
getWebhookEndpoint({
|
|
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(
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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,
|