@withone/connect 0.17.0 → 0.17.1

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
@@ -281,7 +281,7 @@ const reply = await oneConnect.runAction(userId, {
281
281
  actionId: action._id,
282
282
  body: { limit: 10 },
283
283
  });
284
- // { status, ok, blockedByGrant, data }
284
+ // { status, ok, blockedByGrant, refusal, data }
285
285
  ```
286
286
 
287
287
  Each connection carries its `access`: `{ policy: "full" }`, `{ policy: "methods", methods }` or `{ policy: "actions", actions }`. Plan from it before you call.
@@ -306,7 +306,15 @@ You never set an auth header: the client adds the connect key and the user's id
306
306
 
307
307
  ## 6 · Errors
308
308
 
309
- A `403` with `blockedByGrant: true` means the call is outside what the user granted. The provider was never called; don't retry it.
309
+ When One itself turns a call down, `runAction` says why in `refusal`:
310
+
311
+ | `refusal` | Status | What to do |
312
+ |---|---|---|
313
+ | `"grant"` (`blockedByGrant: true`) | `403` | The call is outside what the user granted. The provider was never called; don't retry it. |
314
+ | `"plan_limit"` | `402` | A plan limit, not the grant. Wait and retry later; don't ask the user to reconnect. |
315
+ | `"rate_limit"` | `429` | Too many calls. Wait and retry. |
316
+ | `"one_error"` | other, e.g. `5xx` | Retry with a backoff. |
317
+ | `null` | any | Not One's refusal: the provider's own answer, or a success. |
310
318
 
311
319
  Everything else is a `OneConnectError` with a `code`:
312
320
 
@@ -393,6 +401,7 @@ The job renews both tokens when either is within 3 days of expiring. Without it,
393
401
  | Every call fails with `request_failed`: "One did not accept the connect key" | The key is not this app's, or it was revoked. Create a key on the app's page and update `ONE_CONNECT_KEY`. |
394
402
  | Every user's calls throw `reconnect_required` | The app is deactivated. Check that it is active in the dashboard. |
395
403
  | A call returns `403` with `blockedByGrant: true` | The action is outside what the user granted. Ask for it in your permission set; users see new tools the next time they connect. |
404
+ | A call returns `402` with `refusal: "plan_limit"` | A plan limit was hit, not the grant. Wait and retry later; don't ask the user to reconnect. |
396
405
 
397
406
  ## Supported versions
398
407
 
@@ -185,6 +185,18 @@ function tokenScopes(accessToken) {
185
185
  * method and path the SDK runs it with.
186
186
  */
187
187
 
188
+ /**
189
+ * Why One itself turned a call down, when it did. A provider's own error
190
+ * (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
191
+ *
192
+ * - `grant`: the call is outside what the user granted (403). Do not retry.
193
+ * - `plan_limit`: a plan limit was hit (402, "Please try again later or
194
+ * upgrade your plan"). Not about the grant: wait and retry later.
195
+ * - `rate_limit`: too many calls (429). Wait and retry later.
196
+ * - `one_error`: any other error from One (for example a 5xx). Retry with
197
+ * a backoff.
198
+ */
199
+
188
200
  /**
189
201
  * - `not_connected`: nothing is stored for this user.
190
202
  * - `reconnect_required` (key mode): One will not act for this user.
@@ -510,6 +522,23 @@ function createTokenCredential(tokenStore, postToken) {
510
522
  * The Next.js and Node adapters turn the first two into route handlers.
511
523
  * See `./key` and `./token` for what each mode stores and sends.
512
524
  */
525
+
526
+ /**
527
+ * Why One itself refused a call, from its reply. One's own errors carry a
528
+ * correlationId; a provider's error does not, so it reads `null`. Only a
529
+ * 403 is the grant: One also answers a plan limit (402), a rate limit
530
+ * (429) and its own failures (5xx) with a correlationId, and none of those
531
+ * mean the call is outside what the user granted.
532
+ */
533
+ function oneRefusal(response, data) {
534
+ if (response.ok) return null;
535
+ const fromOne = typeof data === "object" && data !== null && "correlationId" in data;
536
+ if (!fromOne) return null;
537
+ if (response.status === 403) return "grant";
538
+ if (response.status === 402) return "plan_limit";
539
+ if (response.status === 429) return "rate_limit";
540
+ return "one_error";
541
+ }
513
542
  const CATALOG_PAGE_SIZE = 100;
514
543
  const CATALOG_MAX_PAGES = 20;
515
544
  const SEARCH_DEFAULT_LIMIT = 5;
@@ -860,6 +889,7 @@ function createOneConnect(config) {
860
889
  status: response.status,
861
890
  ok: false,
862
891
  blockedByGrant: true,
892
+ refusal: "grant",
863
893
  data: text
864
894
  };
865
895
  throw refused;
@@ -873,13 +903,12 @@ function createOneConnect(config) {
873
903
  } catch {
874
904
  /* the provider answered with something other than JSON */
875
905
  }
876
- // One's own refusals carry a correlationId; a provider's error does
877
- // not. A One-shaped 403 is the grant working, not the provider.
878
- const blockedByGrant = !response.ok && typeof data === "object" && data !== null && "correlationId" in data;
906
+ const refusal = oneRefusal(response, data);
879
907
  return {
880
908
  status: response.status,
881
909
  ok: response.ok,
882
- blockedByGrant,
910
+ blockedByGrant: refusal === "grant",
911
+ refusal,
883
912
  data
884
913
  };
885
914
  };
@@ -183,6 +183,18 @@ function tokenScopes(accessToken) {
183
183
  * method and path the SDK runs it with.
184
184
  */
185
185
 
186
+ /**
187
+ * Why One itself turned a call down, when it did. A provider's own error
188
+ * (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
189
+ *
190
+ * - `grant`: the call is outside what the user granted (403). Do not retry.
191
+ * - `plan_limit`: a plan limit was hit (402, "Please try again later or
192
+ * upgrade your plan"). Not about the grant: wait and retry later.
193
+ * - `rate_limit`: too many calls (429). Wait and retry later.
194
+ * - `one_error`: any other error from One (for example a 5xx). Retry with
195
+ * a backoff.
196
+ */
197
+
186
198
  /**
187
199
  * - `not_connected`: nothing is stored for this user.
188
200
  * - `reconnect_required` (key mode): One will not act for this user.
@@ -508,6 +520,23 @@ function createTokenCredential(tokenStore, postToken) {
508
520
  * The Next.js and Node adapters turn the first two into route handlers.
509
521
  * See `./key` and `./token` for what each mode stores and sends.
510
522
  */
523
+
524
+ /**
525
+ * Why One itself refused a call, from its reply. One's own errors carry a
526
+ * correlationId; a provider's error does not, so it reads `null`. Only a
527
+ * 403 is the grant: One also answers a plan limit (402), a rate limit
528
+ * (429) and its own failures (5xx) with a correlationId, and none of those
529
+ * mean the call is outside what the user granted.
530
+ */
531
+ function oneRefusal(response, data) {
532
+ if (response.ok) return null;
533
+ const fromOne = typeof data === "object" && data !== null && "correlationId" in data;
534
+ if (!fromOne) return null;
535
+ if (response.status === 403) return "grant";
536
+ if (response.status === 402) return "plan_limit";
537
+ if (response.status === 429) return "rate_limit";
538
+ return "one_error";
539
+ }
511
540
  const CATALOG_PAGE_SIZE = 100;
512
541
  const CATALOG_MAX_PAGES = 20;
513
542
  const SEARCH_DEFAULT_LIMIT = 5;
@@ -858,6 +887,7 @@ function createOneConnect(config) {
858
887
  status: response.status,
859
888
  ok: false,
860
889
  blockedByGrant: true,
890
+ refusal: "grant",
861
891
  data: text
862
892
  };
863
893
  throw refused;
@@ -871,13 +901,12 @@ function createOneConnect(config) {
871
901
  } catch {
872
902
  /* the provider answered with something other than JSON */
873
903
  }
874
- // One's own refusals carry a correlationId; a provider's error does
875
- // not. A One-shaped 403 is the grant working, not the provider.
876
- const blockedByGrant = !response.ok && typeof data === "object" && data !== null && "correlationId" in data;
904
+ const refusal = oneRefusal(response, data);
877
905
  return {
878
906
  status: response.status,
879
907
  ok: response.ok,
880
- blockedByGrant,
908
+ blockedByGrant: refusal === "grant",
909
+ refusal,
881
910
  data
882
911
  };
883
912
  };
@@ -260,13 +260,30 @@ export interface RunActionInput {
260
260
  * notation); "multipart" for file-style uploads. */
261
261
  encoding?: ActionBodyEncoding;
262
262
  }
263
+ /**
264
+ * Why One itself turned a call down, when it did. A provider's own error
265
+ * (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
266
+ *
267
+ * - `grant`: the call is outside what the user granted (403). Do not retry.
268
+ * - `plan_limit`: a plan limit was hit (402, "Please try again later or
269
+ * upgrade your plan"). Not about the grant: wait and retry later.
270
+ * - `rate_limit`: too many calls (429). Wait and retry later.
271
+ * - `one_error`: any other error from One (for example a 5xx). Retry with
272
+ * a backoff.
273
+ */
274
+ export type OneRefusal = "grant" | "plan_limit" | "rate_limit" | "one_error";
263
275
  export interface RunActionResult {
264
276
  status: number;
265
277
  ok: boolean;
266
- /** True when One refused the call because it is outside the grant.
267
- * The provider was never called. Do not retry. Key mode sets it; in
268
- * token mode it stays false today, so treat any 403 there as refused. */
278
+ /** True only when One refused the call because it is outside the grant
279
+ * (a 403). The provider was never called. Do not retry. Key mode sets
280
+ * it; in token mode it stays false today, so treat any 403 there as
281
+ * refused. One's other refusals (plan limit, rate limit, its own
282
+ * errors) are not about the grant: see `refusal`. */
269
283
  blockedByGrant: boolean;
284
+ /** Why One turned the call down, or `null` when it didn't (the call
285
+ * succeeded, or the provider answered with its own error). */
286
+ refusal: OneRefusal | null;
270
287
  data: unknown;
271
288
  }
272
289
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/connect",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "One Connect for your app: the button your users press, the two backend routes as one import, and a server client that calls One with the grant. Users keep their connections in One; your app holds only what they granted.",
5
5
  "files": [
6
6
  "dist",
@@ -214,7 +214,7 @@ const reply = await oneConnect.runAction(userId, {
214
214
  encoding: "json", // or "form" / "multipart", as the guide says
215
215
  headers: {}, // only when the guide names one
216
216
  });
217
- // { status, ok, blockedByGrant, data }
217
+ // { status, ok, blockedByGrant, refusal, data }
218
218
  ```
219
219
 
220
220
  Each connection's `access` is `{ policy: "full" }`, `{ policy: "methods",
@@ -230,8 +230,18 @@ user's id to every call it makes.
230
230
 
231
231
  ## 8 - Errors
232
232
 
233
- A `403` reply with `blockedByGrant: true` means the call is outside what
234
- the user granted. Do not retry it.
233
+ `refusal` says why One itself turned a call down:
234
+
235
+ - `"grant"` (`403`, `blockedByGrant: true`): outside what the user granted.
236
+ Do not retry it.
237
+ - `"plan_limit"` (`402`): a plan limit, not the grant. Wait and retry later.
238
+ Do not ask the user to reconnect.
239
+ - `"rate_limit"` (`429`): too many calls. Wait and retry.
240
+ - `"one_error"` (other, e.g. `5xx`): retry with a backoff.
241
+ - `null`: not One's refusal; the provider's own answer, or a success.
242
+
243
+ Only a `403` means "outside the grant". Never treat a `402`, `429` or `5xx`
244
+ as a missing permission.
235
245
 
236
246
  Errors are `OneConnectError` with a `code`. Handle them where the app calls One:
237
247
 
@@ -59,6 +59,7 @@ import {
59
59
  type ReachableConnection,
60
60
  type RefreshIfExpiringOptions,
61
61
  type RunActionInput,
62
+ type OneRefusal,
62
63
  type RunActionResult,
63
64
  type SearchActionsOptions,
64
65
  type StartAuthorizationInput,
@@ -69,6 +70,24 @@ export * from "./types";
69
70
  export { encodeUserReference, parseUserReference } from "./key";
70
71
  export { refreshTokenExpiresAt, tenancyHeaders, tokenScopes } from "./oauth";
71
72
 
73
+ /**
74
+ * Why One itself refused a call, from its reply. One's own errors carry a
75
+ * correlationId; a provider's error does not, so it reads `null`. Only a
76
+ * 403 is the grant: One also answers a plan limit (402), a rate limit
77
+ * (429) and its own failures (5xx) with a correlationId, and none of those
78
+ * mean the call is outside what the user granted.
79
+ */
80
+ function oneRefusal(response: Response, data: unknown): OneRefusal | null {
81
+ if (response.ok) return null;
82
+ const fromOne =
83
+ typeof data === "object" && data !== null && "correlationId" in (data as Record<string, unknown>);
84
+ if (!fromOne) return null;
85
+ if (response.status === 403) return "grant";
86
+ if (response.status === 402) return "plan_limit";
87
+ if (response.status === 429) return "rate_limit";
88
+ return "one_error";
89
+ }
90
+
72
91
  const CATALOG_PAGE_SIZE = 100;
73
92
  const CATALOG_MAX_PAGES = 20;
74
93
  const SEARCH_DEFAULT_LIMIT = 5;
@@ -624,7 +643,7 @@ export function createOneConnect(
624
643
  // tells them apart: if that still answers, the consent stands
625
644
  // and it was this call One refused.
626
645
  if (await consentStands(userId, refused))
627
- return { status: response.status, ok: false, blockedByGrant: true, data: text };
646
+ return { status: response.status, ok: false, blockedByGrant: true, refusal: "grant", data: text };
628
647
  throw refused;
629
648
  }
630
649
  // One refusing the credential is not an answer to the action.
@@ -636,14 +655,8 @@ export function createOneConnect(
636
655
  } catch {
637
656
  /* the provider answered with something other than JSON */
638
657
  }
639
- // One's own refusals carry a correlationId; a provider's error does
640
- // not. A One-shaped 403 is the grant working, not the provider.
641
- const blockedByGrant =
642
- !response.ok &&
643
- typeof data === "object" &&
644
- data !== null &&
645
- "correlationId" in (data as Record<string, unknown>);
646
- return { status: response.status, ok: response.ok, blockedByGrant, data };
658
+ const refusal = oneRefusal(response, data);
659
+ return { status: response.status, ok: response.ok, blockedByGrant: refusal === "grant", refusal, data };
647
660
  };
648
661
 
649
662
  const shared = {
@@ -279,13 +279,31 @@ export interface RunActionInput {
279
279
  encoding?: ActionBodyEncoding;
280
280
  }
281
281
 
282
+ /**
283
+ * Why One itself turned a call down, when it did. A provider's own error
284
+ * (Gmail's 404, Stripe's 400) is not One's refusal and reads `null`.
285
+ *
286
+ * - `grant`: the call is outside what the user granted (403). Do not retry.
287
+ * - `plan_limit`: a plan limit was hit (402, "Please try again later or
288
+ * upgrade your plan"). Not about the grant: wait and retry later.
289
+ * - `rate_limit`: too many calls (429). Wait and retry later.
290
+ * - `one_error`: any other error from One (for example a 5xx). Retry with
291
+ * a backoff.
292
+ */
293
+ export type OneRefusal = "grant" | "plan_limit" | "rate_limit" | "one_error";
294
+
282
295
  export interface RunActionResult {
283
296
  status: number;
284
297
  ok: boolean;
285
- /** True when One refused the call because it is outside the grant.
286
- * The provider was never called. Do not retry. Key mode sets it; in
287
- * token mode it stays false today, so treat any 403 there as refused. */
298
+ /** True only when One refused the call because it is outside the grant
299
+ * (a 403). The provider was never called. Do not retry. Key mode sets
300
+ * it; in token mode it stays false today, so treat any 403 there as
301
+ * refused. One's other refusals (plan limit, rate limit, its own
302
+ * errors) are not about the grant: see `refusal`. */
288
303
  blockedByGrant: boolean;
304
+ /** Why One turned the call down, or `null` when it didn't (the call
305
+ * succeeded, or the provider answered with its own error). */
306
+ refusal: OneRefusal | null;
289
307
  data: unknown;
290
308
  }
291
309