@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/dist/index.js CHANGED
@@ -1022,6 +1022,20 @@ var WefunderAuthError = class extends WefunderError {
1022
1022
  };
1023
1023
 
1024
1024
  // src/token-manager.ts
1025
+ var WefunderTokenPersistenceError = class extends Error {
1026
+ tokens;
1027
+ constructor(tokens, cause) {
1028
+ super(
1029
+ `Token store failed to save the rotated token set: ${cause instanceof Error ? cause.message : String(cause)}`,
1030
+ { cause }
1031
+ );
1032
+ this.name = "WefunderTokenPersistenceError";
1033
+ this.tokens = tokens;
1034
+ }
1035
+ };
1036
+ function sameTokenSet(a, b) {
1037
+ return a.accessToken === b.accessToken && a.refreshToken === b.refreshToken;
1038
+ }
1025
1039
  var TokenManager = class {
1026
1040
  #tokens;
1027
1041
  #clientId;
@@ -1034,6 +1048,7 @@ var TokenManager = class {
1034
1048
  #now;
1035
1049
  #leeway;
1036
1050
  #inflight;
1051
+ #pending;
1037
1052
  constructor(opts) {
1038
1053
  this.#tokens = opts.tokens;
1039
1054
  this.#clientId = opts.clientId;
@@ -1046,15 +1061,40 @@ var TokenManager = class {
1046
1061
  this.#now = opts.now ?? Date.now;
1047
1062
  this.#leeway = opts.expiryLeewayMs ?? 3e4;
1048
1063
  }
1064
+ /** The durable, in-use token set. A rotated set that could not be persisted sits in `pendingTokens`. */
1049
1065
  get current() {
1050
1066
  return this.#tokens;
1051
1067
  }
1052
1068
  /** True if the manager can recover an expired token (rotate a refresh token or re-mint). */
1053
1069
  get canRefresh() {
1054
- return Boolean(this.#tokens.refreshToken && this.#clientId || this.#reMint);
1070
+ return Boolean(
1071
+ this.#tokens.refreshToken && this.#clientId || this.#reMint
1072
+ );
1073
+ }
1074
+ /** A rotated set awaiting a successful `store.save` (see `WefunderTokenPersistenceError`). */
1075
+ get pendingTokens() {
1076
+ return this.#pending;
1055
1077
  }
1056
- /** Returns a valid access token, refreshing proactively if it's expired/near-expiry. */
1078
+ /**
1079
+ * Tell the manager you persisted `tokens` (the set from a `WefunderTokenPersistenceError`)
1080
+ * yourself. Publishes it only if it is still the pending set — a stale acknowledgment (the
1081
+ * manager has since rotated again) is a no-op and returns false, so an older save can never
1082
+ * publish a newer, unsaved set.
1083
+ */
1084
+ async markPersisted(tokens) {
1085
+ const pending = this.#pending;
1086
+ if (!pending || !sameTokenSet(pending, tokens)) return false;
1087
+ this.#pending = void 0;
1088
+ this.#tokens = pending;
1089
+ return true;
1090
+ }
1091
+ /**
1092
+ * Returns a valid access token, refreshing proactively if it's expired/near-expiry. If a
1093
+ * rotated set is pending persistence, the save is retried first — no request uses an
1094
+ * undurable token.
1095
+ */
1057
1096
  async getAccessToken() {
1097
+ if (this.#pending) await this.refresh();
1058
1098
  const { expiresAt } = this.#tokens;
1059
1099
  if (expiresAt !== void 0 && this.#now() >= expiresAt - this.#leeway && this.canRefresh) {
1060
1100
  await this.refresh();
@@ -1068,6 +1108,14 @@ var TokenManager = class {
1068
1108
  */
1069
1109
  async refresh() {
1070
1110
  if (this.#inflight) return this.#inflight;
1111
+ if (this.#pending) {
1112
+ this.#inflight = this.#publishPending();
1113
+ try {
1114
+ return await this.#inflight;
1115
+ } finally {
1116
+ this.#inflight = void 0;
1117
+ }
1118
+ }
1071
1119
  const strategy = this.#recoveryStrategy();
1072
1120
  if (!strategy) {
1073
1121
  throw new WefunderAuthError(
@@ -1075,11 +1123,8 @@ var TokenManager = class {
1075
1123
  );
1076
1124
  }
1077
1125
  this.#inflight = (async () => {
1078
- const next = await strategy();
1079
- this.#tokens = next;
1080
- await this.#store?.save(next);
1081
- await this.#onTokenRefresh?.(next);
1082
- return next;
1126
+ this.#pending = await strategy();
1127
+ return this.#publishPending();
1083
1128
  })();
1084
1129
  try {
1085
1130
  return await this.#inflight;
@@ -1087,6 +1132,22 @@ var TokenManager = class {
1087
1132
  this.#inflight = void 0;
1088
1133
  }
1089
1134
  }
1135
+ // Persist BEFORE publishing: no caller may use the rotated token until it is durable, and a
1136
+ // failed save must not leave the process working in memory but unable to reconnect after a
1137
+ // restart. On failure the set stays pending and WefunderTokenPersistenceError is thrown; the
1138
+ // next call retries the save.
1139
+ async #publishPending() {
1140
+ const tokens = this.#pending;
1141
+ try {
1142
+ await this.#store?.save(tokens);
1143
+ await this.#onTokenRefresh?.(tokens);
1144
+ } catch (err) {
1145
+ throw new WefunderTokenPersistenceError(tokens, err);
1146
+ }
1147
+ this.#pending = void 0;
1148
+ this.#tokens = tokens;
1149
+ return tokens;
1150
+ }
1090
1151
  // Pick the recovery strategy: refresh_token rotation, else cc re-mint, else none.
1091
1152
  #recoveryStrategy() {
1092
1153
  const refreshTokenValue = this.#tokens.refreshToken;
@@ -1779,12 +1840,22 @@ var Wefunder = class _Wefunder {
1779
1840
  });
1780
1841
  }
1781
1842
  /** The live token set (e.g. to persist after construction). */
1843
+ /** A rotated set awaiting a successful save (see `WefunderTokenPersistenceError`). */
1844
+ get pendingTokens() {
1845
+ return this.#tokens.pendingTokens;
1846
+ }
1847
+ /** Acknowledge an out-of-band save of `tokens`; false if that set is no longer pending. */
1848
+ markPersisted(tokens) {
1849
+ return this.#tokens.markPersisted(tokens);
1850
+ }
1782
1851
  get tokens() {
1783
1852
  return this.#tokens.current;
1784
1853
  }
1785
1854
  // ---- unwrap: turn the {data,error,response} result into data-or-throw ----
1786
1855
  async #unwrap(p) {
1787
1856
  const { data, error, response } = await p;
1857
+ if (error instanceof WefunderTokenPersistenceError || error instanceof WefunderError)
1858
+ throw error;
1788
1859
  if (response && response.ok && error === void 0) return data;
1789
1860
  const status = response?.status ?? 0;
1790
1861
  const env = error ?? {};
@@ -1793,7 +1864,10 @@ var Wefunder = class _Wefunder {
1793
1864
  type: env.error?.type ?? "api_error",
1794
1865
  message: env.error?.message ?? response?.statusText ?? "Request failed",
1795
1866
  // X-Wf-Request-Id header is primary (present even on non-JSON edge errors).
1796
- requestId: requestIdFrom(response, env.error?.request_id ?? env.request_id),
1867
+ requestId: requestIdFrom(
1868
+ response,
1869
+ env.error?.request_id ?? env.request_id
1870
+ ),
1797
1871
  details: env.error?.details,
1798
1872
  remediation: env.error?.remediation
1799
1873
  });
@@ -1819,6 +1893,32 @@ var Wefunder = class _Wefunder {
1819
1893
  unwrap(p) {
1820
1894
  return this.#unwrap(p);
1821
1895
  }
1896
+ /**
1897
+ * Untyped escape hatch: call ANY API path with the SDK's full envelope —
1898
+ * bearer auth (incl. refresh / client_credentials re-mint on 401), the pinned
1899
+ * `Wefunder-Version` header, the retry policy, and a typed `WefunderError` on
1900
+ * failure. For endpoints outside the generated surface: preview-tier ops
1901
+ * (e.g. partner SPVs) or ops newer than this SDK build.
1902
+ *
1903
+ * `path` is relative to the client's `baseUrl` (e.g. `"/partner/spvs"`).
1904
+ * Returns the raw response body (no `{ data }` unwrapping — envelopes vary
1905
+ * across unshipped endpoints).
1906
+ */
1907
+ async request(method, path, opts = {}) {
1908
+ return this.#unwrap(
1909
+ this.#client.request({
1910
+ method,
1911
+ url: path,
1912
+ // Same security descriptor the generated ops pass — this is what makes
1913
+ // the client attach (and on 401, refresh/re-mint) the bearer token.
1914
+ security: [{ scheme: "bearer", type: "http" }],
1915
+ query: opts.query,
1916
+ body: opts.body,
1917
+ // JSON by default when a body is present; caller headers win.
1918
+ headers: opts.body !== void 0 ? { "Content-Type": "application/json", ...opts.headers } : opts.headers
1919
+ })
1920
+ );
1921
+ }
1822
1922
  // Generic page helper: forwards the endpoint's full query (cursor + documented
1823
1923
  // params like `sort`), not just the cursor. (Stress-test finding B.)
1824
1924
  #page(fn) {
@@ -1829,11 +1929,23 @@ var Wefunder = class _Wefunder {
1829
1929
  me: () => this.#unwrapData(getCurrentUser({ client: this.#client }))
1830
1930
  };
1831
1931
  offerings = {
1832
- list: this.#page(listOfferings),
1932
+ list: this.#page(
1933
+ listOfferings
1934
+ ),
1833
1935
  all: (query) => paginate((cursor) => this.offerings.list({ ...query, cursor })),
1834
1936
  collect: (query) => collect((cursor) => this.offerings.list({ ...query, cursor })),
1835
1937
  get: (externalId) => this.#unwrapData(
1836
- getOffering({ client: this.#client, path: { external_id: externalId } })
1938
+ getOffering({
1939
+ client: this.#client,
1940
+ path: { external_id: externalId }
1941
+ })
1942
+ ),
1943
+ /** Aggregate investment stats (count / committed / raised, by status) for one offering. */
1944
+ stats: (externalId) => this.#unwrapData(
1945
+ getOfferingStats({
1946
+ client: this.#client,
1947
+ path: { offering_id: externalId }
1948
+ })
1837
1949
  )
1838
1950
  };
1839
1951
  /**
@@ -1842,20 +1954,40 @@ var Wefunder = class _Wefunder {
1842
1954
  * from your last page — it is always present — and resume from it next time.
1843
1955
  */
1844
1956
  investments = {
1845
- list: this.#page(listInvestments),
1846
- all: (query) => paginate((cursor) => this.investments.list({ ...query, cursor })),
1847
- collect: (query) => collect((cursor) => this.investments.list({ ...query, cursor })),
1957
+ list: this.#page(
1958
+ listInvestments
1959
+ ),
1960
+ all: (query) => paginate(
1961
+ (cursor) => this.investments.list({
1962
+ ...query,
1963
+ cursor
1964
+ })
1965
+ ),
1966
+ collect: (query) => collect(
1967
+ (cursor) => this.investments.list({
1968
+ ...query,
1969
+ cursor
1970
+ })
1971
+ ),
1848
1972
  /** The current record (not the published projection) for one investment (`inv_…`). */
1849
- get: (id) => this.#unwrapData(getInvestment({ client: this.#client, path: { id } }))
1973
+ get: (id) => this.#unwrapData(
1974
+ getInvestment({ client: this.#client, path: { id } })
1975
+ )
1850
1976
  };
1851
1977
  portfolio = {
1852
- get: (query) => this.#unwrapData(getPortfolio({ client: this.#client, query })),
1978
+ get: (query) => this.#unwrapData(
1979
+ getPortfolio({ client: this.#client, query })
1980
+ ),
1853
1981
  positions: {
1854
1982
  list: this.#page(
1855
1983
  listPortfolioPositions
1856
1984
  ),
1857
- all: (query) => paginate((cursor) => this.portfolio.positions.list({ ...query, cursor })),
1858
- collect: (query) => collect((cursor) => this.portfolio.positions.list({ ...query, cursor }))
1985
+ all: (query) => paginate(
1986
+ (cursor) => this.portfolio.positions.list({ ...query, cursor })
1987
+ ),
1988
+ collect: (query) => collect(
1989
+ (cursor) => this.portfolio.positions.list({ ...query, cursor })
1990
+ )
1859
1991
  }
1860
1992
  };
1861
1993
  campaigns = {
@@ -1864,17 +1996,91 @@ var Wefunder = class _Wefunder {
1864
1996
  collect: () => collect((cursor) => this.campaigns.list({ cursor }))
1865
1997
  };
1866
1998
  syndicates = {
1867
- list: this.#page(listSyndicates),
1999
+ list: this.#page(
2000
+ listSyndicates
2001
+ ),
1868
2002
  all: (query) => paginate((cursor) => this.syndicates.list({ ...query, cursor })),
1869
- get: (id) => this.#unwrapData(getSyndicate({ client: this.#client, path: { id } }))
2003
+ get: (id) => this.#unwrapData(
2004
+ getSyndicate({ client: this.#client, path: { id } })
2005
+ )
1870
2006
  };
1871
2007
  intents = {
1872
2008
  list: this.#page(listIntents),
1873
2009
  all: (query) => paginate((cursor) => this.intents.list({ ...query, cursor })),
1874
- get: (id) => this.#unwrapData(getIntent({ client: this.#client, path: { id } }))
2010
+ get: (id) => this.#unwrapData(
2011
+ getIntent({ client: this.#client, path: { id } })
2012
+ )
1875
2013
  };
1876
2014
  attribution = {
1877
- me: () => this.#unwrapData(getAttributionMe({ client: this.#client }))
2015
+ me: () => this.#unwrapData(
2016
+ getAttributionMe({ client: this.#client })
2017
+ )
2018
+ };
2019
+ /**
2020
+ * Installations (`/installations`, scopes `read:installations` / `write:installations`;
2021
+ * write does not imply read). An install lets your app act AS a company or syndicate:
2022
+ * `create` and `mintToken` return a company-owned token (shown once, no expiry, no
2023
+ * refresh) — build a second client with it. Installing is also what makes a company
2024
+ * or syndicate an audience for your webhooks.
2025
+ */
2026
+ installations = {
2027
+ /** Companies / syndicates the token's user may install your app on (empty for an investor). */
2028
+ eligibleTargets: (query) => this.#unwrapData(
2029
+ listEligibleInstallTargets({ client: this.#client, query })
2030
+ ).then((rows) => rows ?? []),
2031
+ /** Every install of your app (`meta.count`). */
2032
+ list: () => this.#unwrap(
2033
+ listInstallations({ client: this.#client })
2034
+ ),
2035
+ get: (id) => this.#unwrapData(
2036
+ getInstallation({
2037
+ client: this.#client,
2038
+ path: { external_id: id }
2039
+ })
2040
+ ),
2041
+ /**
2042
+ * Install on a target. Store `token.access_token` — it is never shown again. If the
2043
+ * app is already installed there the API answers 409 `already_installed` with
2044
+ * `details.installation` = the existing id; mint a token for that instead
2045
+ * (see `installOrMintToken`).
2046
+ */
2047
+ create: (input) => this.#unwrap(
2048
+ createInstallation({ client: this.#client, body: input })
2049
+ ),
2050
+ /**
2051
+ * A fresh company-owned token for an existing install. `scopes` narrows within the
2052
+ * install's ceiling; omit it for the ceiling, and note an explicit `[]` grants nothing.
2053
+ */
2054
+ mintToken: (id, scopes) => this.#unwrap(
2055
+ createInstallationToken({
2056
+ client: this.#client,
2057
+ path: { external_id: id },
2058
+ body: scopes ? { scopes } : void 0
2059
+ })
2060
+ ),
2061
+ /**
2062
+ * `create`, falling back to `mintToken` for the existing install on 409
2063
+ * `already_installed`. The mint re-requests `input.scopes` so a retry never widens the
2064
+ * grant. Any other error (revoked install, missing scope) still throws.
2065
+ */
2066
+ installOrMintToken: async (input) => {
2067
+ try {
2068
+ return await this.installations.create(input);
2069
+ } catch (err) {
2070
+ if (!(err instanceof WefunderError) || err.type !== "already_installed")
2071
+ throw err;
2072
+ const existingId = err.details?.installation;
2073
+ if (!existingId) throw err;
2074
+ return this.installations.mintToken(existingId, input.scopes);
2075
+ }
2076
+ },
2077
+ /** Revoke an install: its tokens stop working at once. Returns the install, now `revoked`. */
2078
+ revoke: (id) => this.#unwrapData(
2079
+ revokeInstallation({
2080
+ client: this.#client,
2081
+ path: { external_id: id }
2082
+ })
2083
+ )
1878
2084
  };
1879
2085
  /**
1880
2086
  * Webhook endpoints (`/webhook_endpoints`, scopes `read:webhooks` / `write:webhooks`).
@@ -1885,30 +2091,50 @@ var Wefunder = class _Wefunder {
1885
2091
  */
1886
2092
  webhookEndpoints = {
1887
2093
  /** All of your app's endpoints (secret omitted). `meta.quota` is the per-app limit. */
1888
- list: () => this.#unwrap(listWebhookEndpoints({ client: this.#client })),
2094
+ list: () => this.#unwrap(
2095
+ listWebhookEndpoints({ client: this.#client })
2096
+ ),
1889
2097
  get: (id) => this.#unwrapData(
1890
- getWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2098
+ getWebhookEndpoint({
2099
+ client: this.#client,
2100
+ path: { external_id: id }
2101
+ })
1891
2102
  ),
1892
2103
  /** Store `attributes.secret` from the result — it is never shown again. */
1893
- create: (input) => this.#unwrapData(createWebhookEndpoint({ client: this.#client, body: input })),
2104
+ create: (input) => this.#unwrapData(
2105
+ createWebhookEndpoint({ client: this.#client, body: input })
2106
+ ),
1894
2107
  /** `events` replaces the subscription list wholesale (no merge); omit to leave unchanged. */
1895
2108
  update: (id, input) => this.#unwrapData(
1896
- updateWebhookEndpoint({ client: this.#client, path: { external_id: id }, body: input })
2109
+ updateWebhookEndpoint({
2110
+ client: this.#client,
2111
+ path: { external_id: id },
2112
+ body: input
2113
+ })
1897
2114
  ),
1898
2115
  /** Stops deliveries immediately. Removal is permanent — create a new endpoint to resume. */
1899
2116
  remove: (id) => this.#unwrapData(
1900
- deleteWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2117
+ deleteWebhookEndpoint({
2118
+ client: this.#client,
2119
+ path: { external_id: id }
2120
+ })
1901
2121
  ),
1902
2122
  /**
1903
2123
  * New secret (shown once). The old one keeps signing for a 24h overlap — deliveries
1904
2124
  * carry a `v1` for both, and `constructEvent` accepts either.
1905
2125
  */
1906
2126
  rotateSecret: (id) => this.#unwrapData(
1907
- rotateWebhookEndpointSecret({ client: this.#client, path: { external_id: id } })
2127
+ rotateWebhookEndpointSecret({
2128
+ client: this.#client,
2129
+ path: { external_id: id }
2130
+ })
1908
2131
  ),
1909
2132
  /** Recover an auto-disabled endpoint after fixing your server. Idempotent. */
1910
2133
  reenable: (id) => this.#unwrapData(
1911
- reenableWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2134
+ reenableWebhookEndpoint({
2135
+ client: this.#client,
2136
+ path: { external_id: id }
2137
+ })
1912
2138
  ),
1913
2139
  /**
1914
2140
  * POST a real, signed example event at the endpoint (production envelope + signature)
@@ -2103,10 +2329,12 @@ export {
2103
2329
  REQUEST_ID_HEADER,
2104
2330
  SANDBOX_AUTHORIZE_BASE_URL,
2105
2331
  SIGNATURE_HEADER,
2332
+ TokenManager,
2106
2333
  WebhookSignatureError,
2107
2334
  Wefunder,
2108
2335
  WefunderAuthError,
2109
2336
  WefunderError,
2337
+ WefunderTokenPersistenceError,
2110
2338
  checkWebhookSignature,
2111
2339
  clientCredentialsGrant,
2112
2340
  collect,