@drawbridge/drawbridge-utils 0.0.107 → 0.0.109

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.
@@ -26,14 +26,49 @@ var HOOKS = Object.freeze({
26
26
  "cleanup"
27
27
  ]),
28
28
  // Receiving from the vendor.
29
+ //
30
+ // Split across TWO SERVICES, and the split is the point. `verify` and `event`
31
+ // and `receive` run in drawbridge-webhooks against the vendor's open
32
+ // connection, where the budget is whatever that vendor's timeout is —
33
+ // Shopify's is about five seconds, and missing it means they retry and the
34
+ // delivery is buffered twice. `process` runs in drawbridge-sync off the
35
+ // buffer, where it can take as long as it needs and retry without the vendor
36
+ // ever knowing.
37
+ //
38
+ // One hook spanning that seam would hide it, and the thing it hides is the
39
+ // one most likely to bite: slow work written on the receiving side turns
40
+ // into duplicate deliveries.
29
41
  inbound: Object.freeze([
30
- // Prove the request came from the vendor. Signature schemes differ per
31
- // vendor, which is exactly why this is a hook and not one shared function.
42
+ // Prove the request came from the vendor, AND return the payload it
43
+ // carries. One hook rather than two because for some vendors they are
44
+ // genuinely one operation: a JWT-bodied webhook (Kinde) cannot be decoded
45
+ // into anything trustworthy without verifying it first, and a decode that
46
+ // runs before the signature check is exactly the bug this shape prevents.
47
+ //
48
+ // So the raw bytes stop here. Everything downstream receives the payload
49
+ // this returned, which means nothing downstream can act on unverified
50
+ // data even by mistake.
51
+ //
52
+ // It is also where a request gets refused for any other reason — an event
53
+ // outside the allowlist, a connection in the wrong state. Anything that
54
+ // can reject belongs here, so the route holds no rules of its own.
32
55
  "verify",
33
- // Name the event, from wherever this vendor puts it.
34
- "topic",
35
- // Do the work the event implies.
36
- "handle"
56
+ // Name the event, from wherever this vendor puts it. A header for
57
+ // Shopify, the route itself for Twilio, a claim in the payload for a
58
+ // JWT-bodied vendor.
59
+ "event",
60
+ // What to buffer and what to answer RIGHT NOW, while the vendor waits.
61
+ // Thin by obligation. Most vendors only shape a buffer doc here; one that
62
+ // must reply with content (Twilio answers HELP inline with TwiML, which
63
+ // carriers require) returns that too.
64
+ "receive",
65
+ // Do the work the event implies. Runs in drawbridge-sync, off the buffer.
66
+ //
67
+ // DECLARED here, IMPLEMENTED there — the same split as the step handlers,
68
+ // and for the same reason: this half needs controllers, queues and vendor
69
+ // SDKs, and putting those behind a published package makes every consumer
70
+ // carry them. The declaration is what proves the implementation exists.
71
+ "process"
37
72
  ]),
38
73
  // Vendor data a campaign draws on. Named for what every store platform has,
39
74
  // not for what Shopify calls it: Shopify says discounts, Stripe says coupons
@@ -165,6 +200,56 @@ var refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refr
165
200
  };
166
201
  };
167
202
 
203
+ // lib/connections/token.js
204
+ var SKEW_SECONDS = 120;
205
+ var isStale = (settings, now = Date.now()) => {
206
+ if (!(settings == null ? void 0 : settings.expiresAt)) return false;
207
+ return new Date(settings.expiresAt).getTime() - SKEW_SECONDS * 1e3 <= now;
208
+ };
209
+ var tokenSettings = ({ existing = {}, now = Date.now(), tokens }) => ({
210
+ accessToken: tokens.accessToken,
211
+ // A vendor that does not rotate its refresh token returns none on a refresh
212
+ // (Klaviyo). Keeping the existing one is what makes the NEXT refresh work —
213
+ // dropping it invalidates the grant one call later, nowhere near the cause.
214
+ ...(tokens.refreshToken || existing.refreshToken) && {
215
+ refreshToken: tokens.refreshToken || existing.refreshToken
216
+ },
217
+ // Absent when the vendor issues non-expiring tokens, and absent is meaningful
218
+ // — isStale reads it as "nothing to refresh toward".
219
+ ...tokens.expiresIn && {
220
+ expiresAt: new Date(now + tokens.expiresIn * 1e3).toISOString()
221
+ },
222
+ ...tokens.scope && { scope: tokens.scope }
223
+ });
224
+ var accessToken = async ({
225
+ clientId,
226
+ clientSecret,
227
+ fetcher,
228
+ force = false,
229
+ manifest,
230
+ now = Date.now(),
231
+ save,
232
+ settings
233
+ } = {}) => {
234
+ if (!(settings == null ? void 0 : settings.accessToken) && !(settings == null ? void 0 : settings.refreshToken)) {
235
+ throw new Error("This connection holds no credential, so there is no token to use");
236
+ }
237
+ if (!force && !isStale(settings, now)) return settings.accessToken;
238
+ if (!settings.refreshToken) {
239
+ throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
240
+ }
241
+ const minted = await refresh({
242
+ clientId,
243
+ clientSecret,
244
+ descriptor: manifest.auth.oauth,
245
+ ...fetcher && { fetcher },
246
+ refreshToken: settings.refreshToken
247
+ });
248
+ const next = tokenSettings({ existing: settings, now, tokens: minted });
249
+ if (save) await save(next);
250
+ return next.accessToken;
251
+ };
252
+
168
253
  // lib/connections/icons/klaviyo.js
169
254
  var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
170
255
  <rect width="500" height="500" fill="white"/>
@@ -172,6 +257,26 @@ var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
172
257
  </svg>`;
173
258
 
174
259
  // lib/connections/klaviyo.js
260
+ var REVISION = "2026-07-15";
261
+ var api = async (path, { fetcher = fetch, token }) => {
262
+ const response = await fetcher("https://a.klaviyo.com/api" + path, {
263
+ headers: {
264
+ // Bearer, not Klaviyo-API-Key — that header is for private keys, and
265
+ // sending it with an OAuth token fails in a way that reads like a bad
266
+ // token rather than a bad scheme.
267
+ authorization: "Bearer " + token,
268
+ revision: REVISION
269
+ },
270
+ signal: AbortSignal.timeout(15e3)
271
+ });
272
+ if (!response.ok) {
273
+ throw Object.assign(
274
+ new Error("Klaviyo refused the request (" + response.status + ")"),
275
+ { status: response.status }
276
+ );
277
+ }
278
+ return response.json();
279
+ };
175
280
  var klaviyo_default2 = {
176
281
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
177
282
  // exchange without a code_verifier matching the challenge the consent
@@ -200,6 +305,11 @@ var klaviyo_default2 = {
200
305
  // it is a fact about someone else's records rather than a string this
201
306
  // code computes. Deriving one from a provider key produced
202
307
  // redirect_uri_mismatch on a connection nobody had touched.
308
+ // Klaviyo drops a refresh token after 90 days of NON-USE. The vendor
309
+ // never mentions this at runtime — you discover it when a refresh fails
310
+ // on a connection nobody touched — so it is declared, and it is why
311
+ // auth.probe has to run on a schedule rather than only before a call.
312
+ idleExpiry: 90 * 24 * 60 * 60,
203
313
  redirect: "/api/connection/klaviyo/callback",
204
314
  // Space separated. accounts:read is required by Klaviyo on every app
205
315
  // and must stay in the list; the rest are what a contact sync needs.
@@ -231,7 +341,78 @@ var klaviyo_default2 = {
231
341
  label: "Klaviyo account"
232
342
  }
233
343
  ],
344
+ // The three auth hooks, all pure HTTP against Klaviyo — which is why they
345
+ // live here rather than in sync. A vendor's own protocol belongs beside the
346
+ // vendor.
347
+ hooks: {
348
+ // Turn a fresh grant into settings worth showing. Without this the card
349
+ // renders an empty "Klaviyo account" field, because the merchant is never
350
+ // asked which account they connected — the consent already decided it and
351
+ // asking again would be a question we can answer ourselves.
352
+ "auth.connect": async ({ fetcher, tokens }) => {
353
+ var _a, _b, _c;
354
+ const body = await api("/accounts", { fetcher, token: tokens.accessToken });
355
+ const account = (_a = body == null ? void 0 : body.data) == null ? void 0 : _a[0];
356
+ return {
357
+ account: ((_c = (_b = account == null ? void 0 : account.attributes) == null ? void 0 : _b.contact_information) == null ? void 0 : _c.organization_name) || (account == null ? void 0 : account.id) || null,
358
+ accountId: (account == null ? void 0 : account.id) || null
359
+ };
360
+ },
361
+ // Revoke at KLAVIYO, not just locally. Forgetting our copy leaves the
362
+ // grant live in the merchant's account, so a disconnect that looks
363
+ // complete here still shows Drawbridge with access over there.
364
+ //
365
+ // Basic auth with our client, exactly like the token exchange — the token
366
+ // being revoked is the subject, not the credential.
367
+ "auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
368
+ const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
369
+ if (!token) return { revoked: false };
370
+ const response = await fetcher("https://a.klaviyo.com/oauth/revoke", {
371
+ body: new URLSearchParams({
372
+ token,
373
+ token_type_hint: (settings == null ? void 0 : settings.refreshToken) ? "refresh_token" : "access_token"
374
+ }),
375
+ headers: {
376
+ authorization: "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"),
377
+ "content-type": "application/x-www-form-urlencoded"
378
+ },
379
+ method: "POST",
380
+ signal: AbortSignal.timeout(15e3)
381
+ });
382
+ return { revoked: response.ok };
383
+ },
384
+ // THE MINT IS THE PROBE. Asking "is this token still good" by inspecting
385
+ // what we stored answers the wrong question — a grant revoked inside
386
+ // Klaviyo still looks perfect in our database. Spending the refresh token
387
+ // is the only thing that asks Klaviyo.
388
+ //
389
+ // It also keeps the grant warm: Klaviyo expires a refresh token after 90
390
+ // days of NON-USE, so a connection nobody touches dies silently without
391
+ // this running.
392
+ "auth.probe": async ({ clientId, clientSecret, fetcher, manifest, settings }) => {
393
+ const token = await accessToken({
394
+ clientId,
395
+ clientSecret,
396
+ fetcher,
397
+ // Mint even if the stored token still looks good — a probe that
398
+ // short-circuits never reaches Klaviyo and reports healthy on a
399
+ // grant revoked an hour ago.
400
+ force: true,
401
+ manifest,
402
+ settings
403
+ });
404
+ return { ok: Boolean(token) };
405
+ }
406
+ },
234
407
  icon: klaviyo_default,
408
+ // A grant with no list chosen is authenticated and useless. The list cannot
409
+ // be part of the consent flow — enumerating lists needs the token the consent
410
+ // returns — so it is always a second step, and the card must say so rather
411
+ // than showing Active over nothing.
412
+ incomplete: (data) => {
413
+ var _a;
414
+ return ((_a = data == null ? void 0 : data.settings) == null ? void 0 : _a.list) ? null : "Choose which Klaviyo list your contacts should sync into.";
415
+ },
235
416
  label: "klaviyo",
236
417
  requires: [
237
418
  "KLAVIYO_OAUTH_CLIENT_ID",
@@ -259,13 +440,24 @@ var klaviyo_default2 = {
259
440
  "catalog.prices": false,
260
441
  "catalog.products": false,
261
442
  "catalog.promotions": false,
262
- "inbound.handle": false,
263
- "inbound.topic": false,
443
+ "inbound.event": false,
444
+ "inbound.process": false,
445
+ "inbound.receive": false,
264
446
  "inbound.verify": false,
265
447
  "lifecycle.cleanup": false,
266
448
  "lifecycle.register": false,
267
449
  "lifecycle.rehydrate": false
268
450
  },
451
+ tasks: () => [
452
+ // Mailchimp carries the same warning, deliberately worded the same way. A
453
+ // merchant who connects either one and is told nothing reasonably assumes
454
+ // contacts are flowing, and finds out weeks later that they are not.
455
+ {
456
+ message: "Contact syncing to Klaviyo lists has not shipped yet. Connecting stores your authorization so it is ready, but nothing is being sent to Klaviyo right now.",
457
+ title: "List sync not available yet",
458
+ type: "warning"
459
+ }
460
+ ],
269
461
  title: "Klaviyo"
270
462
  };
271
463
 
@@ -317,6 +509,15 @@ var mailchimp_default2 = {
317
509
  }
318
510
  ],
319
511
  icon: mailchimp_default,
512
+ // A key with no audience chosen is authenticated and inert. Mailchimp also
513
+ // needs its merge fields created on that audience before any Drawbridge total
514
+ // can be written to a member — unlike Klaviyo, its custom fields are not
515
+ // schemaless — so the audience must be picked before lifecycle.register has
516
+ // anything to register against.
517
+ incomplete: (data) => {
518
+ var _a;
519
+ return ((_a = data == null ? void 0 : data.settings) == null ? void 0 : _a.audience) ? null : "Choose which Mailchimp audience your contacts should sync into.";
520
+ },
320
521
  label: "mailchimp",
321
522
  // Uniform surface, honest answers. A key is stored and can be removed; nothing
322
523
  // else is built yet, because audience sync has not shipped. Every false here
@@ -330,8 +531,9 @@ var mailchimp_default2 = {
330
531
  "catalog.prices": false,
331
532
  "catalog.products": false,
332
533
  "catalog.promotions": false,
333
- "inbound.handle": false,
334
- "inbound.topic": false,
534
+ "inbound.event": false,
535
+ "inbound.process": false,
536
+ "inbound.receive": false,
335
537
  "inbound.verify": false,
336
538
  "lifecycle.cleanup": false,
337
539
  "lifecycle.register": false,
@@ -365,7 +567,42 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
365
567
  <path d="M258.993 193.019L249.103 230.052C249.103 230.052 238.079 225.023 225 225.85C205.833 227.059 205.628 239.171 205.824 242.21C206.865 258.756 250.376 262.381 252.821 301.171C254.745 331.688 236.656 352.574 210.592 354.21C179.313 356.19 162.089 337.711 162.089 337.711L168.716 309.472C168.716 309.472 186.052 322.569 199.921 321.686C208.993 321.119 212.228 313.738 211.903 308.514C210.536 286.921 175.102 288.185 172.862 252.695C170.985 222.811 190.57 192.554 233.803 189.821C250.46 188.762 258.993 193.028 258.993 193.028" fill="white"/>
366
568
  </svg>`;
367
569
 
570
+ // lib/connections/inbound.js
571
+ import { createHmac, timingSafeEqual } from "crypto";
572
+ var verifySignature = ({ body, descriptor, headers }) => {
573
+ const provided = headers[descriptor.headers.signature];
574
+ if (!provided) {
575
+ throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
576
+ }
577
+ const digest = createHmac(descriptor.signature.algorithm, process.env[descriptor.signature.secret]).update(body).digest(descriptor.signature.encoding);
578
+ const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
579
+ const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
580
+ if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual(digestBuffer, providedBuffer)) {
581
+ throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
582
+ }
583
+ return JSON.parse(body.toString());
584
+ };
585
+ var readEventHeader = ({ descriptor, headers }) => headers[descriptor.headers.event];
586
+
368
587
  // lib/connections/shopify.js
588
+ var inbound = {
589
+ headers: {
590
+ event: "x-shopify-topic",
591
+ id: "x-shopify-webhook-id",
592
+ shop: "x-shopify-shop-domain",
593
+ signature: "x-shopify-hmac-sha256"
594
+ },
595
+ signature: {
596
+ algorithm: "sha256",
597
+ encoding: "base64",
598
+ secret: "SHOPIFY_API_SECRET"
599
+ }
600
+ };
601
+ var COMPLIANCE_TOPICS = /* @__PURE__ */ new Set([
602
+ "customers/data_request",
603
+ "customers/redact",
604
+ "shop/redact"
605
+ ]);
369
606
  var shopify_default2 = {
370
607
  // INSTALL, not oauth. The grant is an OAuth grant, but the merchant never
371
608
  // sees a consent screen we sent them to -- they start at the App Store, and
@@ -410,30 +647,36 @@ var shopify_default2 = {
410
647
  }
411
648
  ],
412
649
  group: "ecommerce",
413
- // WHERE THIS VENDOR PUTS THINGS ON AN INBOUND REQUEST. Header names are facts
414
- // about someone else's product, so they belong beside the rest of the vendor
415
- // rather than as string literals in a route which is where they were, and
416
- // is why a second inbound vendor meant a second route file.
417
- //
418
- // `signature` describes an HMAC scheme the shared verifier can run: hash the
419
- // raw body with the named secret and compare, constant-time, against the
420
- // header. Vendors whose scheme is not that shape Stripe signs a timestamped
421
- // payload declare no signature block and implement inbound.verify instead.
422
- // That is why verify is a hook and not config.
423
- inbound: {
424
- headers: {
425
- id: "x-shopify-webhook-id",
426
- shop: "x-shopify-shop-domain",
427
- signature: "x-shopify-hmac-sha256",
428
- topic: "x-shopify-topic"
650
+ // The install is the whole configuration Shopify hands back the shop and
651
+ // there is nothing further to choose. `shop` absent means the install did not
652
+ // finish, which is a credential problem rather than a setup one, so the
653
+ // stored status already says so.
654
+ incomplete: () => null,
655
+ // verify and event lean entirely on the shared HMAC helper Shopify's scheme
656
+ // is exactly the shape it covers, so there is nothing vendor-specific to
657
+ // write for either. receive is the one hook that genuinely differs by
658
+ // channel: /events buffers whatever arrives with the shop domain stamped on;
659
+ // /compliance enforces the topic allowlist above, because answering one late
660
+ // is a legal deadline rather than a retry.
661
+ hooks: {
662
+ "inbound.event": (args) => readEventHeader({ ...args, descriptor: inbound }),
663
+ "inbound.receive": ({ channel, event, headers, payload }) => {
664
+ if (channel === "compliance" && !COMPLIANCE_TOPICS.has(event)) {
665
+ throw Object.assign(new Error("Unrecognized compliance topic: " + event), { status: 401 });
666
+ }
667
+ return {
668
+ // Compliance payloads already carry shop_domain in the body — Shopify's
669
+ // own GDPR shape. The app-level event stream does not; that domain
670
+ // lives only in the header, so it is stamped on here rather than left
671
+ // for drawbridge-sync to reach into headers nobody hands it.
672
+ data: channel === "compliance" ? payload : { ...payload, shop_domain: headers[inbound.headers.shop] || null },
673
+ provider: { id: headers[inbound.headers.id] || null }
674
+ };
429
675
  },
430
- signature: {
431
- algorithm: "sha256",
432
- encoding: "base64",
433
- secret: "SHOPIFY_API_SECRET"
434
- }
676
+ "inbound.verify": (args) => verifySignature({ ...args, descriptor: inbound })
435
677
  },
436
678
  icon: shopify_default,
679
+ inbound,
437
680
  label: "shopify",
438
681
  // A pre-launch integration: it only surfaces once the App Store listing
439
682
  // exists and the app is fully configured. Requiring all four means it can
@@ -462,8 +705,9 @@ var shopify_default2 = {
462
705
  "catalog.prices": false,
463
706
  "catalog.products": true,
464
707
  "catalog.promotions": true,
465
- "inbound.handle": true,
466
- "inbound.topic": true,
708
+ "inbound.event": true,
709
+ "inbound.process": true,
710
+ "inbound.receive": true,
467
711
  "inbound.verify": true,
468
712
  "lifecycle.cleanup": true,
469
713
  "lifecycle.register": true,
@@ -607,6 +851,9 @@ var webhook_default = {
607
851
  // It is the one card that reads wrong — our logo among vendor logos — and it
608
852
  // wants a mark of its own when there is one.
609
853
  icon: drawbridge_default,
854
+ // The destination url is supplied per step, not per connection, so there is
855
+ // nothing to finish here — generating the secret IS connecting.
856
+ incomplete: () => null,
610
857
  label: "webhook",
611
858
  // Gated on the encryption secret: without it the signing secret could not be
612
859
  // stored safely, so the connection must not be offered at all.
@@ -623,8 +870,9 @@ var webhook_default = {
623
870
  "catalog.prices": false,
624
871
  "catalog.products": false,
625
872
  "catalog.promotions": false,
626
- "inbound.handle": false,
627
- "inbound.topic": false,
873
+ "inbound.event": false,
874
+ "inbound.process": false,
875
+ "inbound.receive": false,
628
876
  "inbound.verify": false,
629
877
  "lifecycle.cleanup": false,
630
878
  "lifecycle.register": false,
@@ -713,13 +961,21 @@ var build = (manifest) => {
713
961
  throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field);
714
962
  }
715
963
  }
964
+ if (manifest.auth.oauth.redirect !== "/api/connection/" + manifest.slug + "/callback") {
965
+ throw new Error(
966
+ manifest.slug + " declares auth.oauth.redirect " + manifest.auth.oauth.redirect + " but the only callback route is /api/connection/" + manifest.slug + "/callback"
967
+ );
968
+ }
716
969
  }
717
- if (((_d = manifest.supports) == null ? void 0 : _d["inbound.topic"]) && !((_f = (_e = manifest.inbound) == null ? void 0 : _e.headers) == null ? void 0 : _f.topic)) {
718
- throw new Error(manifest.slug + " supports inbound.topic but declares no inbound.headers.topic");
970
+ if (((_d = manifest.supports) == null ? void 0 : _d["inbound.event"]) && !((_f = (_e = manifest.inbound) == null ? void 0 : _e.headers) == null ? void 0 : _f.event)) {
971
+ throw new Error(manifest.slug + " supports inbound.event but declares no inbound.headers.event");
719
972
  }
720
973
  if (((_g = manifest.supports) == null ? void 0 : _g["inbound.verify"]) && !((_i = (_h = manifest.inbound) == null ? void 0 : _h.headers) == null ? void 0 : _i.signature)) {
721
974
  throw new Error(manifest.slug + " supports inbound.verify but declares no inbound.headers.signature");
722
975
  }
976
+ if (typeof (manifest == null ? void 0 : manifest.incomplete) !== "function") {
977
+ throw new Error(manifest.slug + " must declare incomplete( data ) \u2014 return null when the connection is usable, or the reason it is not");
978
+ }
723
979
  if (!Array.isArray(manifest == null ? void 0 : manifest.setup) || !manifest.setup.length) {
724
980
  throw new Error(manifest.slug + " needs a setup guide \u2014 an array of steps for its page");
725
981
  }
@@ -830,7 +1086,7 @@ var runHook = async (slug, name, args = {}) => {
830
1086
  try {
831
1087
  return { outcome: OUTCOMES.answered, result: await hook(args) };
832
1088
  } catch (error) {
833
- return { error: (error == null ? void 0 : error.message) || "failed", outcome: OUTCOMES.failed };
1089
+ return { error: (error == null ? void 0 : error.message) || "failed", outcome: OUTCOMES.failed, status: (error == null ? void 0 : error.status) || null };
834
1090
  }
835
1091
  };
836
1092
  var stepQueues = (env = {}) => Object.fromEntries(
@@ -869,6 +1125,9 @@ var publicConnectionKeys = Object.freeze([
869
1125
  "group",
870
1126
  "id",
871
1127
  "image",
1128
+ // The reason a connected vendor still is not usable — a Klaviyo grant with no
1129
+ // list chosen. Public because the card that shows Pending has to say why.
1130
+ "incomplete",
872
1131
  "label",
873
1132
  "setup",
874
1133
  "settings",
@@ -896,7 +1155,7 @@ var projectConnection = (record) => {
896
1155
  var resolveConnection = (item, data) => {
897
1156
  if (!item) return item;
898
1157
  return Object.fromEntries(
899
- Object.entries(item).filter(([key]) => !["auth", "enabled", "fields", "inbound", "requires", "steps", "supports"].includes(key)).map(([key, value]) => [
1158
+ Object.entries(item).filter(([key]) => !["auth", "enabled", "fields", "hooks", "inbound", "requires", "steps", "supports"].includes(key)).map(([key, value]) => [
900
1159
  key,
901
1160
  typeof value === "function" ? value(data) : value
902
1161
  ])
@@ -910,6 +1169,7 @@ export {
910
1169
  INPUTS,
911
1170
  OAUTH_FIELDS,
912
1171
  OUTCOMES,
1172
+ accessToken,
913
1173
  availableConnections,
914
1174
  build,
915
1175
  connectFields,
@@ -918,6 +1178,7 @@ export {
918
1178
  consentUrl,
919
1179
  exchange,
920
1180
  hookSupport,
1181
+ isStale,
921
1182
  mergeSettings,
922
1183
  pkcePair,
923
1184
  projectConnection,
@@ -928,5 +1189,6 @@ export {
928
1189
  resolveConnection,
929
1190
  runHook,
930
1191
  scopesMessage,
931
- stepQueues
1192
+ stepQueues,
1193
+ tokenSettings
932
1194
  };
package/package.json CHANGED
@@ -200,5 +200,5 @@
200
200
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
201
201
  },
202
202
  "types": "dist/index.d.ts",
203
- "version": "0.0.107"
203
+ "version": "0.0.109"
204
204
  }