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

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.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
  });
@@ -1881,11 +1957,16 @@ var Wefunder = class _Wefunder {
1881
1957
  me: () => this.#unwrapData(getCurrentUser({ client: this.#client }))
1882
1958
  };
1883
1959
  offerings = {
1884
- list: this.#page(listOfferings),
1960
+ list: this.#page(
1961
+ listOfferings
1962
+ ),
1885
1963
  all: (query) => paginate((cursor) => this.offerings.list({ ...query, cursor })),
1886
1964
  collect: (query) => collect((cursor) => this.offerings.list({ ...query, cursor })),
1887
1965
  get: (externalId) => this.#unwrapData(
1888
- getOffering({ client: this.#client, path: { external_id: externalId } })
1966
+ getOffering({
1967
+ client: this.#client,
1968
+ path: { external_id: externalId }
1969
+ })
1889
1970
  )
1890
1971
  };
1891
1972
  /**
@@ -1894,20 +1975,40 @@ var Wefunder = class _Wefunder {
1894
1975
  * from your last page — it is always present — and resume from it next time.
1895
1976
  */
1896
1977
  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 })),
1978
+ list: this.#page(
1979
+ listInvestments
1980
+ ),
1981
+ all: (query) => paginate(
1982
+ (cursor) => this.investments.list({
1983
+ ...query,
1984
+ cursor
1985
+ })
1986
+ ),
1987
+ collect: (query) => collect(
1988
+ (cursor) => this.investments.list({
1989
+ ...query,
1990
+ cursor
1991
+ })
1992
+ ),
1900
1993
  /** The current record (not the published projection) for one investment (`inv_…`). */
1901
- get: (id) => this.#unwrapData(getInvestment({ client: this.#client, path: { id } }))
1994
+ get: (id) => this.#unwrapData(
1995
+ getInvestment({ client: this.#client, path: { id } })
1996
+ )
1902
1997
  };
1903
1998
  portfolio = {
1904
- get: (query) => this.#unwrapData(getPortfolio({ client: this.#client, query })),
1999
+ get: (query) => this.#unwrapData(
2000
+ getPortfolio({ client: this.#client, query })
2001
+ ),
1905
2002
  positions: {
1906
2003
  list: this.#page(
1907
2004
  listPortfolioPositions
1908
2005
  ),
1909
- all: (query) => paginate((cursor) => this.portfolio.positions.list({ ...query, cursor })),
1910
- collect: (query) => collect((cursor) => this.portfolio.positions.list({ ...query, cursor }))
2006
+ all: (query) => paginate(
2007
+ (cursor) => this.portfolio.positions.list({ ...query, cursor })
2008
+ ),
2009
+ collect: (query) => collect(
2010
+ (cursor) => this.portfolio.positions.list({ ...query, cursor })
2011
+ )
1911
2012
  }
1912
2013
  };
1913
2014
  campaigns = {
@@ -1916,17 +2017,25 @@ var Wefunder = class _Wefunder {
1916
2017
  collect: () => collect((cursor) => this.campaigns.list({ cursor }))
1917
2018
  };
1918
2019
  syndicates = {
1919
- list: this.#page(listSyndicates),
2020
+ list: this.#page(
2021
+ listSyndicates
2022
+ ),
1920
2023
  all: (query) => paginate((cursor) => this.syndicates.list({ ...query, cursor })),
1921
- get: (id) => this.#unwrapData(getSyndicate({ client: this.#client, path: { id } }))
2024
+ get: (id) => this.#unwrapData(
2025
+ getSyndicate({ client: this.#client, path: { id } })
2026
+ )
1922
2027
  };
1923
2028
  intents = {
1924
2029
  list: this.#page(listIntents),
1925
2030
  all: (query) => paginate((cursor) => this.intents.list({ ...query, cursor })),
1926
- get: (id) => this.#unwrapData(getIntent({ client: this.#client, path: { id } }))
2031
+ get: (id) => this.#unwrapData(
2032
+ getIntent({ client: this.#client, path: { id } })
2033
+ )
1927
2034
  };
1928
2035
  attribution = {
1929
- me: () => this.#unwrapData(getAttributionMe({ client: this.#client }))
2036
+ me: () => this.#unwrapData(
2037
+ getAttributionMe({ client: this.#client })
2038
+ )
1930
2039
  };
1931
2040
  /**
1932
2041
  * Webhook endpoints (`/webhook_endpoints`, scopes `read:webhooks` / `write:webhooks`).
@@ -1937,30 +2046,50 @@ var Wefunder = class _Wefunder {
1937
2046
  */
1938
2047
  webhookEndpoints = {
1939
2048
  /** All of your app's endpoints (secret omitted). `meta.quota` is the per-app limit. */
1940
- list: () => this.#unwrap(listWebhookEndpoints({ client: this.#client })),
2049
+ list: () => this.#unwrap(
2050
+ listWebhookEndpoints({ client: this.#client })
2051
+ ),
1941
2052
  get: (id) => this.#unwrapData(
1942
- getWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2053
+ getWebhookEndpoint({
2054
+ client: this.#client,
2055
+ path: { external_id: id }
2056
+ })
1943
2057
  ),
1944
2058
  /** Store `attributes.secret` from the result — it is never shown again. */
1945
- create: (input) => this.#unwrapData(createWebhookEndpoint({ client: this.#client, body: input })),
2059
+ create: (input) => this.#unwrapData(
2060
+ createWebhookEndpoint({ client: this.#client, body: input })
2061
+ ),
1946
2062
  /** `events` replaces the subscription list wholesale (no merge); omit to leave unchanged. */
1947
2063
  update: (id, input) => this.#unwrapData(
1948
- updateWebhookEndpoint({ client: this.#client, path: { external_id: id }, body: input })
2064
+ updateWebhookEndpoint({
2065
+ client: this.#client,
2066
+ path: { external_id: id },
2067
+ body: input
2068
+ })
1949
2069
  ),
1950
2070
  /** Stops deliveries immediately. Removal is permanent — create a new endpoint to resume. */
1951
2071
  remove: (id) => this.#unwrapData(
1952
- deleteWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2072
+ deleteWebhookEndpoint({
2073
+ client: this.#client,
2074
+ path: { external_id: id }
2075
+ })
1953
2076
  ),
1954
2077
  /**
1955
2078
  * New secret (shown once). The old one keeps signing for a 24h overlap — deliveries
1956
2079
  * carry a `v1` for both, and `constructEvent` accepts either.
1957
2080
  */
1958
2081
  rotateSecret: (id) => this.#unwrapData(
1959
- rotateWebhookEndpointSecret({ client: this.#client, path: { external_id: id } })
2082
+ rotateWebhookEndpointSecret({
2083
+ client: this.#client,
2084
+ path: { external_id: id }
2085
+ })
1960
2086
  ),
1961
2087
  /** Recover an auto-disabled endpoint after fixing your server. Idempotent. */
1962
2088
  reenable: (id) => this.#unwrapData(
1963
- reenableWebhookEndpoint({ client: this.#client, path: { external_id: id } })
2089
+ reenableWebhookEndpoint({
2090
+ client: this.#client,
2091
+ path: { external_id: id }
2092
+ })
1964
2093
  ),
1965
2094
  /**
1966
2095
  * POST a real, signed example event at the endpoint (production envelope + signature)
@@ -2156,10 +2285,12 @@ async function dispatchWebhook(event, handlers) {
2156
2285
  REQUEST_ID_HEADER,
2157
2286
  SANDBOX_AUTHORIZE_BASE_URL,
2158
2287
  SIGNATURE_HEADER,
2288
+ TokenManager,
2159
2289
  WebhookSignatureError,
2160
2290
  Wefunder,
2161
2291
  WefunderAuthError,
2162
2292
  WefunderError,
2293
+ WefunderTokenPersistenceError,
2163
2294
  checkWebhookSignature,
2164
2295
  clientCredentialsGrant,
2165
2296
  collect,