@drawbridge/drawbridge-utils 0.0.133 → 0.0.135

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.
@@ -464,8 +464,14 @@ var authToken = async ({
464
464
  });
465
465
  const body = await response.json().catch(() => ({}));
466
466
  if (!response.ok) {
467
- throw new Error(
468
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
467
+ throw Object.assign(
468
+ new Error(
469
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
470
+ ),
471
+ {
472
+ status: response.status,
473
+ ...renewing && { code: "grant_refused" }
474
+ }
469
475
  );
470
476
  }
471
477
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
@@ -521,7 +527,10 @@ var accessToken = async ({
521
527
  }
522
528
  if (!force && !isStale(settings, now)) return settings.accessToken;
523
529
  if (!settings.refreshToken) {
524
- throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
530
+ throw Object.assign(
531
+ new Error("This connection has expired and cannot be renewed automatically. Reconnect it."),
532
+ { code: "grant_refused" }
533
+ );
525
534
  }
526
535
  const minted = await manifest.hooks.auth.token({
527
536
  clientId,
@@ -2735,8 +2744,62 @@ var klaviyo_default2 = {
2735
2744
  sms: false,
2736
2745
  inbound: false,
2737
2746
  // Nothing to set up or tear down at the vendor: the grant is the whole
2738
- // integration, and revoking it is auth.disconnect's job.
2739
- lifecycle: false,
2747
+ // integration. What CAN rot is the grant itself, so health is the one
2748
+ // lifecycle hook Klaviyo carries.
2749
+ lifecycle: {
2750
+ // Nothing to set up, tear down, or re-register at the vendor — the
2751
+ // grant is the whole integration.
2752
+ cleanup: false,
2753
+ register: false,
2754
+ rehydrate: false,
2755
+ // KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
2756
+ // token before every hook run — for Klaviyo that spends the refresh
2757
+ // token, which is the only question that reaches Klaviyo (see
2758
+ // auth.probe) — so this body running at all proves the grant still
2759
+ // rotates, and the daily run keeps it inside Klaviyo's 90-day idle
2760
+ // window. A dead grant never gets here: the shell's mint throws
2761
+ // `grant_refused` and the step runner errors the connection itself.
2762
+ //
2763
+ // The one call below proves the minted token is HONOURED — mint and
2764
+ // acceptance are different facts, and /accounts is already the call
2765
+ // the connect flow makes (auth.connect), so it needs no new scope.
2766
+ health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
2767
+ const request2 = { connectionId: connection2.id };
2768
+ try {
2769
+ await api2("/accounts", { fetcher, token });
2770
+ return {
2771
+ message: "Health check passed \u2014 token minted and accepted.",
2772
+ request: request2,
2773
+ response: { pingedAt: /* @__PURE__ */ new Date() }
2774
+ };
2775
+ } catch (error) {
2776
+ if ([401, 403].includes(error.status) && read) {
2777
+ const current = await read.get({ collection: "connection", query: { id: connection2.id } });
2778
+ if (current) {
2779
+ const others = (current.errors || []).filter((entry) => entry.source !== "oauth");
2780
+ error.writes = [{
2781
+ collection: "connection",
2782
+ data: {
2783
+ $set: {
2784
+ errors: [
2785
+ ...others,
2786
+ {
2787
+ message: "Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.",
2788
+ source: "oauth"
2789
+ }
2790
+ ],
2791
+ status: "error"
2792
+ }
2793
+ },
2794
+ operation: "update",
2795
+ query: { id: connection2.id }
2796
+ }];
2797
+ }
2798
+ }
2799
+ throw error;
2800
+ }
2801
+ }
2802
+ },
2740
2803
  resources: {
2741
2804
  // The lists a merchant can sync into, for the picker on their
2742
2805
  // connection.
@@ -2812,6 +2875,19 @@ var klaviyo_default2 = {
2812
2875
  return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? data2.status : "pending";
2813
2876
  },
2814
2877
  steps: {
2878
+ connection: {
2879
+ // The daily grant check — same step type and workflow shape as
2880
+ // Shopify's, provisioned as a system workflow per connection.
2881
+ health: {
2882
+ check: () => ({
2883
+ description: "Keeps the Klaviyo grant working \u2014 spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.",
2884
+ hook: "lifecycle.health",
2885
+ key: "Klaviyo Connection Health",
2886
+ queue: "connection",
2887
+ system: true
2888
+ })
2889
+ }
2890
+ },
2815
2891
  contacts: {
2816
2892
  // A DECLARATION, not the work. It names the hook that does the work, and
2817
2893
  // says where that hook's values belong. Nested like the hooks, and the
@@ -741,8 +741,12 @@ const accessToken = async ({
741
741
 
742
742
  // Expired with no way to mint another. Said plainly rather than returning
743
743
  // a token the vendor will refuse — the merchant has to reconnect and the
744
- // error should say so.
745
- throw new Error( 'This connection has expired and cannot be renewed automatically. Reconnect it.' );
744
+ // error should say so. Same `code` as a refused refresh: both mean the
745
+ // grant is dead and only reconnecting revives it.
746
+ throw Object.assign(
747
+ new Error( 'This connection has expired and cannot be renewed automatically. Reconnect it.' ),
748
+ { code : 'grant_refused' }
749
+ );
746
750
 
747
751
  }
748
752
 
@@ -2941,8 +2945,84 @@ var klaviyo = {
2941
2945
  inbound : false,
2942
2946
 
2943
2947
  // Nothing to set up or tear down at the vendor: the grant is the whole
2944
- // integration, and revoking it is auth.disconnect's job.
2945
- lifecycle : false,
2948
+ // integration. What CAN rot is the grant itself, so health is the one
2949
+ // lifecycle hook Klaviyo carries.
2950
+ lifecycle : {
2951
+
2952
+ // Nothing to set up, tear down, or re-register at the vendor — the
2953
+ // grant is the whole integration.
2954
+ cleanup : false,
2955
+ register : false,
2956
+ rehydrate : false,
2957
+
2958
+ // KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
2959
+ // token before every hook run — for Klaviyo that spends the refresh
2960
+ // token, which is the only question that reaches Klaviyo (see
2961
+ // auth.probe) — so this body running at all proves the grant still
2962
+ // rotates, and the daily run keeps it inside Klaviyo's 90-day idle
2963
+ // window. A dead grant never gets here: the shell's mint throws
2964
+ // `grant_refused` and the step runner errors the connection itself.
2965
+ //
2966
+ // The one call below proves the minted token is HONOURED — mint and
2967
+ // acceptance are different facts, and /accounts is already the call
2968
+ // the connect flow makes (auth.connect), so it needs no new scope.
2969
+ health : async ( { connection, token }, { fetcher, read } = {} ) => {
2970
+
2971
+ const request = { connectionId : connection.id };
2972
+
2973
+ try {
2974
+
2975
+ await api$1( '/accounts', { fetcher, token });
2976
+
2977
+ return {
2978
+ message : 'Health check passed — token minted and accepted.',
2979
+ request,
2980
+ response : { pingedAt : new Date() }
2981
+ };
2982
+
2983
+ } catch ( error ) {
2984
+
2985
+ // The token we JUST minted was refused — a revoked or
2986
+ // downgraded grant, never a transient. The write rides out
2987
+ // on the rejection (the shell performs a thrown error's
2988
+ // effects) because the step must still fail.
2989
+ if( [ 401, 403 ].includes( error.status ) && read ){
2990
+
2991
+ const current = await read.get({ collection : 'connection', query : { id : connection.id } });
2992
+
2993
+ if( current ){
2994
+
2995
+ const others = ( current.errors || [] ).filter( ( entry ) => entry.source !== 'oauth' );
2996
+
2997
+ error.writes = [ {
2998
+ collection : 'connection',
2999
+ data : {
3000
+ $set : {
3001
+ errors : [
3002
+ ...others,
3003
+ {
3004
+ message : 'Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.',
3005
+ source : 'oauth'
3006
+ }
3007
+ ],
3008
+ status : 'error'
3009
+ }
3010
+ },
3011
+ operation : 'update',
3012
+ query : { id : connection.id }
3013
+ } ];
3014
+
3015
+ }
3016
+
3017
+ }
3018
+
3019
+ throw error;
3020
+
3021
+ }
3022
+
3023
+ }
3024
+
3025
+ },
2946
3026
 
2947
3027
  resources : {
2948
3028
 
@@ -3049,6 +3129,22 @@ var klaviyo = {
3049
3129
 
3050
3130
  steps : {
3051
3131
 
3132
+ connection : {
3133
+
3134
+ // The daily grant check — same step type and workflow shape as
3135
+ // Shopify's, provisioned as a system workflow per connection.
3136
+ health : {
3137
+ check : () => ({
3138
+ description : 'Keeps the Klaviyo grant working — spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.',
3139
+ hook : 'lifecycle.health',
3140
+ key : 'Klaviyo Connection Health',
3141
+ queue : 'connection',
3142
+ system : true
3143
+ })
3144
+ }
3145
+
3146
+ },
3147
+
3052
3148
  contacts : {
3053
3149
 
3054
3150
  // A DECLARATION, not the work. It names the hook that does the work, and
@@ -741,8 +741,12 @@ const accessToken = async ({
741
741
 
742
742
  // Expired with no way to mint another. Said plainly rather than returning
743
743
  // a token the vendor will refuse — the merchant has to reconnect and the
744
- // error should say so.
745
- throw new Error( 'This connection has expired and cannot be renewed automatically. Reconnect it.' );
744
+ // error should say so. Same `code` as a refused refresh: both mean the
745
+ // grant is dead and only reconnecting revives it.
746
+ throw Object.assign(
747
+ new Error( 'This connection has expired and cannot be renewed automatically. Reconnect it.' ),
748
+ { code : 'grant_refused' }
749
+ );
746
750
 
747
751
  }
748
752
 
@@ -2941,8 +2945,84 @@ var klaviyo = {
2941
2945
  inbound : false,
2942
2946
 
2943
2947
  // Nothing to set up or tear down at the vendor: the grant is the whole
2944
- // integration, and revoking it is auth.disconnect's job.
2945
- lifecycle : false,
2948
+ // integration. What CAN rot is the grant itself, so health is the one
2949
+ // lifecycle hook Klaviyo carries.
2950
+ lifecycle : {
2951
+
2952
+ // Nothing to set up, tear down, or re-register at the vendor — the
2953
+ // grant is the whole integration.
2954
+ cleanup : false,
2955
+ register : false,
2956
+ rehydrate : false,
2957
+
2958
+ // KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
2959
+ // token before every hook run — for Klaviyo that spends the refresh
2960
+ // token, which is the only question that reaches Klaviyo (see
2961
+ // auth.probe) — so this body running at all proves the grant still
2962
+ // rotates, and the daily run keeps it inside Klaviyo's 90-day idle
2963
+ // window. A dead grant never gets here: the shell's mint throws
2964
+ // `grant_refused` and the step runner errors the connection itself.
2965
+ //
2966
+ // The one call below proves the minted token is HONOURED — mint and
2967
+ // acceptance are different facts, and /accounts is already the call
2968
+ // the connect flow makes (auth.connect), so it needs no new scope.
2969
+ health : async ( { connection, token }, { fetcher, read } = {} ) => {
2970
+
2971
+ const request = { connectionId : connection.id };
2972
+
2973
+ try {
2974
+
2975
+ await api$1( '/accounts', { fetcher, token });
2976
+
2977
+ return {
2978
+ message : 'Health check passed — token minted and accepted.',
2979
+ request,
2980
+ response : { pingedAt : new Date() }
2981
+ };
2982
+
2983
+ } catch ( error ) {
2984
+
2985
+ // The token we JUST minted was refused — a revoked or
2986
+ // downgraded grant, never a transient. The write rides out
2987
+ // on the rejection (the shell performs a thrown error's
2988
+ // effects) because the step must still fail.
2989
+ if( [ 401, 403 ].includes( error.status ) && read ){
2990
+
2991
+ const current = await read.get({ collection : 'connection', query : { id : connection.id } });
2992
+
2993
+ if( current ){
2994
+
2995
+ const others = ( current.errors || [] ).filter( ( entry ) => entry.source !== 'oauth' );
2996
+
2997
+ error.writes = [ {
2998
+ collection : 'connection',
2999
+ data : {
3000
+ $set : {
3001
+ errors : [
3002
+ ...others,
3003
+ {
3004
+ message : 'Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.',
3005
+ source : 'oauth'
3006
+ }
3007
+ ],
3008
+ status : 'error'
3009
+ }
3010
+ },
3011
+ operation : 'update',
3012
+ query : { id : connection.id }
3013
+ } ];
3014
+
3015
+ }
3016
+
3017
+ }
3018
+
3019
+ throw error;
3020
+
3021
+ }
3022
+
3023
+ }
3024
+
3025
+ },
2946
3026
 
2947
3027
  resources : {
2948
3028
 
@@ -3049,6 +3129,22 @@ var klaviyo = {
3049
3129
 
3050
3130
  steps : {
3051
3131
 
3132
+ connection : {
3133
+
3134
+ // The daily grant check — same step type and workflow shape as
3135
+ // Shopify's, provisioned as a system workflow per connection.
3136
+ health : {
3137
+ check : () => ({
3138
+ description : 'Keeps the Klaviyo grant working — spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.',
3139
+ hook : 'lifecycle.health',
3140
+ key : 'Klaviyo Connection Health',
3141
+ queue : 'connection',
3142
+ system : true
3143
+ })
3144
+ }
3145
+
3146
+ },
3147
+
3052
3148
  contacts : {
3053
3149
 
3054
3150
  // A DECLARATION, not the work. It names the hook that does the work, and
@@ -390,8 +390,14 @@ var authToken = async ({
390
390
  });
391
391
  const body = await response.json().catch(() => ({}));
392
392
  if (!response.ok) {
393
- throw new Error(
394
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
393
+ throw Object.assign(
394
+ new Error(
395
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
396
+ ),
397
+ {
398
+ status: response.status,
399
+ ...renewing && { code: "grant_refused" }
400
+ }
395
401
  );
396
402
  }
397
403
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
@@ -447,7 +453,10 @@ var accessToken = async ({
447
453
  }
448
454
  if (!force && !isStale(settings, now)) return settings.accessToken;
449
455
  if (!settings.refreshToken) {
450
- throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
456
+ throw Object.assign(
457
+ new Error("This connection has expired and cannot be renewed automatically. Reconnect it."),
458
+ { code: "grant_refused" }
459
+ );
451
460
  }
452
461
  const minted = await manifest.hooks.auth.token({
453
462
  clientId,
@@ -2661,8 +2670,62 @@ var klaviyo_default2 = {
2661
2670
  sms: false,
2662
2671
  inbound: false,
2663
2672
  // Nothing to set up or tear down at the vendor: the grant is the whole
2664
- // integration, and revoking it is auth.disconnect's job.
2665
- lifecycle: false,
2673
+ // integration. What CAN rot is the grant itself, so health is the one
2674
+ // lifecycle hook Klaviyo carries.
2675
+ lifecycle: {
2676
+ // Nothing to set up, tear down, or re-register at the vendor — the
2677
+ // grant is the whole integration.
2678
+ cleanup: false,
2679
+ register: false,
2680
+ rehydrate: false,
2681
+ // KEEP THE GRANT PROVEN AND WARM. The shell mints a fresh access
2682
+ // token before every hook run — for Klaviyo that spends the refresh
2683
+ // token, which is the only question that reaches Klaviyo (see
2684
+ // auth.probe) — so this body running at all proves the grant still
2685
+ // rotates, and the daily run keeps it inside Klaviyo's 90-day idle
2686
+ // window. A dead grant never gets here: the shell's mint throws
2687
+ // `grant_refused` and the step runner errors the connection itself.
2688
+ //
2689
+ // The one call below proves the minted token is HONOURED — mint and
2690
+ // acceptance are different facts, and /accounts is already the call
2691
+ // the connect flow makes (auth.connect), so it needs no new scope.
2692
+ health: async ({ connection: connection2, token }, { fetcher, read } = {}) => {
2693
+ const request2 = { connectionId: connection2.id };
2694
+ try {
2695
+ await api2("/accounts", { fetcher, token });
2696
+ return {
2697
+ message: "Health check passed \u2014 token minted and accepted.",
2698
+ request: request2,
2699
+ response: { pingedAt: /* @__PURE__ */ new Date() }
2700
+ };
2701
+ } catch (error) {
2702
+ if ([401, 403].includes(error.status) && read) {
2703
+ const current = await read.get({ collection: "connection", query: { id: connection2.id } });
2704
+ if (current) {
2705
+ const others = (current.errors || []).filter((entry) => entry.source !== "oauth");
2706
+ error.writes = [{
2707
+ collection: "connection",
2708
+ data: {
2709
+ $set: {
2710
+ errors: [
2711
+ ...others,
2712
+ {
2713
+ message: "Klaviyo no longer accepts this connection. Reconnect Klaviyo to resume syncing.",
2714
+ source: "oauth"
2715
+ }
2716
+ ],
2717
+ status: "error"
2718
+ }
2719
+ },
2720
+ operation: "update",
2721
+ query: { id: connection2.id }
2722
+ }];
2723
+ }
2724
+ }
2725
+ throw error;
2726
+ }
2727
+ }
2728
+ },
2666
2729
  resources: {
2667
2730
  // The lists a merchant can sync into, for the picker on their
2668
2731
  // connection.
@@ -2738,6 +2801,19 @@ var klaviyo_default2 = {
2738
2801
  return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? data2.status : "pending";
2739
2802
  },
2740
2803
  steps: {
2804
+ connection: {
2805
+ // The daily grant check — same step type and workflow shape as
2806
+ // Shopify's, provisioned as a system workflow per connection.
2807
+ health: {
2808
+ check: () => ({
2809
+ description: "Keeps the Klaviyo grant working \u2014 spends the refresh token daily so a revoked or idle grant is reported instead of discovered by a failing sync.",
2810
+ hook: "lifecycle.health",
2811
+ key: "Klaviyo Connection Health",
2812
+ queue: "connection",
2813
+ system: true
2814
+ })
2815
+ }
2816
+ },
2741
2817
  contacts: {
2742
2818
  // A DECLARATION, not the work. It names the hook that does the work, and
2743
2819
  // says where that hook's values belong. Nested like the hooks, and the
@@ -94,8 +94,14 @@ var authToken = async ({
94
94
  });
95
95
  const body = await response.json().catch(() => ({}));
96
96
  if (!response.ok) {
97
- throw new Error(
98
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
97
+ throw Object.assign(
98
+ new Error(
99
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
100
+ ),
101
+ {
102
+ status: response.status,
103
+ ...renewing && { code: "grant_refused" }
104
+ }
99
105
  );
100
106
  }
101
107
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");
@@ -173,9 +173,20 @@ const authToken = async ({
173
173
  // The two paths fail differently for the merchant: a refused exchange is
174
174
  // "try connecting again", a refused refresh is "reconnect this connection",
175
175
  // and collapsing them sends somebody to the wrong screen.
176
- throw new Error( renewing
177
- ? 'The vendor refused the refresh token (' + response.status + ') reconnect the connection'
178
- : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
176
+ //
177
+ // `code` marks the refused REFRESH machine-readably — the health-check
178
+ // shell errors the connection on it rather than retrying a dead grant
179
+ // into the log forever. Only the renewing branch: a refused exchange is
180
+ // a connect-flow failure with a person watching.
181
+ throw Object.assign(
182
+ new Error( renewing
183
+ ? 'The vendor refused the refresh token (' + response.status + ') — reconnect the connection'
184
+ : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
185
+ ),
186
+ {
187
+ status : response.status,
188
+ ...( renewing && { code : 'grant_refused' })
189
+ }
179
190
  );
180
191
 
181
192
  }
@@ -173,9 +173,20 @@ const authToken = async ({
173
173
  // The two paths fail differently for the merchant: a refused exchange is
174
174
  // "try connecting again", a refused refresh is "reconnect this connection",
175
175
  // and collapsing them sends somebody to the wrong screen.
176
- throw new Error( renewing
177
- ? 'The vendor refused the refresh token (' + response.status + ') reconnect the connection'
178
- : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
176
+ //
177
+ // `code` marks the refused REFRESH machine-readably — the health-check
178
+ // shell errors the connection on it rather than retrying a dead grant
179
+ // into the log forever. Only the renewing branch: a refused exchange is
180
+ // a connect-flow failure with a person watching.
181
+ throw Object.assign(
182
+ new Error( renewing
183
+ ? 'The vendor refused the refresh token (' + response.status + ') — reconnect the connection'
184
+ : 'The vendor refused the exchange (' + response.status + ')' + ( body?.error ? ': ' + body.error : '' )
185
+ ),
186
+ {
187
+ status : response.status,
188
+ ...( renewing && { code : 'grant_refused' })
189
+ }
179
190
  );
180
191
 
181
192
  }
@@ -69,8 +69,14 @@ var authToken = async ({
69
69
  });
70
70
  const body = await response.json().catch(() => ({}));
71
71
  if (!response.ok) {
72
- throw new Error(
73
- renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
72
+ throw Object.assign(
73
+ new Error(
74
+ renewing ? "The vendor refused the refresh token (" + response.status + ") \u2014 reconnect the connection" : "The vendor refused the exchange (" + response.status + ")" + ((body == null ? void 0 : body.error) ? ": " + body.error : "")
75
+ ),
76
+ {
77
+ status: response.status,
78
+ ...renewing && { code: "grant_refused" }
79
+ }
74
80
  );
75
81
  }
76
82
  if (!renewing && !body.access_token) throw new Error("The vendor returned no access token");