@drawbridge/drawbridge-utils 0.0.125 → 0.0.127

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.
@@ -1,9 +1,9 @@
1
1
  import { authToken } from './oauth.js';
2
2
  export { consentUrl, pkcePair } from './oauth.js';
3
3
  import { toE164, detectCountry } from '../phone.js';
4
+ import crypto, { createHmac, timingSafeEqual, createHash, randomUUID } from 'node:crypto';
4
5
  import { request } from '../http.js';
5
6
  import { channels } from '../pricing.js';
6
- import crypto, { createHash, createHmac, timingSafeEqual, randomUUID } from 'node:crypto';
7
7
  import { customAlphabet } from 'nanoid';
8
8
  import { toCanonicalEmail } from '../email.js';
9
9
  import { conversionRate } from '../plans.js';
@@ -282,7 +282,7 @@ const HOOK_EFFECTS = Object.freeze([ 'enqueues', 'events', 'writes' ]);
282
282
  // The controller methods a described write may become. Only the two that a hook
283
283
  // has ever needed — a deletion is a cascade, and cascades are owned by
284
284
  // drawbridge-sync's streams rather than by a step.
285
- const WRITE_OPERATIONS = Object.freeze([ 'create', 'update' ]);
285
+ const WRITE_OPERATIONS = Object.freeze([ 'create', 'delete', 'update' ]);
286
286
 
287
287
  // READ A HOOK'S EFFECTS, or refuse them.
288
288
  //
@@ -414,13 +414,15 @@ const effectsOf = ( answer ) => {
414
414
 
415
415
  }
416
416
 
417
- if( ! write?.data ) throw new Error( 'A described write on ' + write.collection + ' carries no data' );
417
+ // A delete says WHICH, not WHAT its query is its whole statement.
418
+ if( write.operation !== 'delete' && ! write?.data ) throw new Error( 'A described write on ' + write.collection + ' carries no data' );
418
419
 
419
- // AN UPDATE WITH NO QUERY IS EVERY DOCUMENT IN THE COLLECTION. Refused here
420
- // rather than survived, because the controller would happily run it.
421
- if( write.operation === 'update' && ! write.query ){
420
+ // AN UPDATE OR DELETE WITH NO QUERY IS EVERY DOCUMENT IN THE COLLECTION.
421
+ // Refused here rather than survived, because the controller would happily
422
+ // run it.
423
+ if( [ 'delete', 'update' ].includes( write.operation ) && ! write.query ){
422
424
 
423
- throw new Error( 'A described update on ' + write.collection + ' has no query — that is every document in it' );
425
+ throw new Error( 'A described ' + write.operation + ' on ' + write.collection + ' has no query — that is every document in it' );
424
426
 
425
427
  }
426
428
 
@@ -1565,18 +1567,26 @@ const contacts = {
1565
1567
  // lib/ directly. A bare .svg import would need a bundler loader and force the
1566
1568
  // tests onto dist/, which is a worse trade than one line of wrapper.
1567
1569
  var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1568
- <rect width="500" height="500" fill="#BAEC5F"/>
1570
+ <rect width="500" height="500" fill="#0D1314"/>
1569
1571
  <g clip-path="url(#clip0_2115_2832)">
1570
- <path d="M140.224 127.586L174.803 188.73V311.176L140 372.32L176.084 392.031L216.111 321.753V178.278L176.341 108L140.224 127.586Z" fill="#0D1314"/>
1571
- <path d="M360.002 127.523L323.694 108.282L284.949 178.498V321.596L322.924 391.749L359.394 372.79L326.225 311.52V188.73L360.002 127.523Z" fill="#0D1314"/>
1572
+ <path d="M153.198 142.241L183.69 196.065V303.852L153 357.676L184.82 375.028L220.116 313.163V186.865L185.046 125L153.198 142.241Z" fill="#BAEC5F"/>
1573
+ <path d="M347.002 142.186L314.984 125.249L280.818 187.058V313.025L314.306 374.779L346.465 358.091L317.216 304.156V196.066L347.002 142.186Z" fill="#BAEC5F"/>
1572
1574
  </g>
1573
1575
  <defs>
1574
1576
  <clipPath id="clip0_2115_2832">
1575
- <rect width="220" height="284" fill="white" transform="translate(140 108)"/>
1577
+ <rect width="194" height="250" fill="white" transform="translate(153 125)"/>
1576
1578
  </clipPath>
1577
1579
  </defs>
1578
1580
  </svg>`;
1579
1581
 
1582
+ // TWILIO'S OWN KEYWORD LISTS. STOP-family withdraws consent for the number —
1583
+ // platform-wide, because every organization sends from the same platform number,
1584
+ // so a withdrawal cannot be scoped narrower than the number it was sent to.
1585
+ // START-family re-subscribes. Anything else is a real reply and none of our
1586
+ // business.
1587
+ const STOP_KEYWORDS = [ 'STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT' ];
1588
+ const START_KEYWORDS = [ 'START', 'UNSTOP', 'YES' ];
1589
+
1580
1590
  // Drawbridge itself — the PRIVATE connection.
1581
1591
  //
1582
1592
  // Nobody connects this. There is no credential, no consent, and no card on the
@@ -1931,7 +1941,113 @@ var drawbridge = {
1931
1941
  }
1932
1942
 
1933
1943
  },
1934
- inbound : false,
1944
+ // INBOUND SMS to the platform number. Twilio's webhook, verified and
1945
+ // processed HERE — the manifest owns its own inbound, the same as every
1946
+ // other vendor, and the bespoke webhooks route + the handler that lived in
1947
+ // sync's buffer table are gone.
1948
+ inbound : {
1949
+
1950
+ // EVERY POST ON THIS CHANNEL IS AN INBOUND MESSAGE. Twilio sends no
1951
+ // topic header — the registered url IS the topic.
1952
+ event : () => 'message.inbound',
1953
+
1954
+ // STOP AND START, AS DESCRIBED WRITES. A withdrawn number is withdrawn
1955
+ // for everyone — organization : null is the platform floor canSend()
1956
+ // reads on every send path. $setOnInsert + upsert so webhook replays
1957
+ // and repeat STOPs collapse onto the unique { channel, address,
1958
+ // organization } index instead of erroring.
1959
+ process : ({ context }) => {
1960
+
1961
+ const keyword = String( context?.data?.Body || '' ).trim().toUpperCase();
1962
+ const address = toE164( context?.data?.From );
1963
+
1964
+ if( ! address ) return { message : 'Inbound SMS carried no usable sender number.', skipped : true };
1965
+
1966
+ if( STOP_KEYWORDS.includes( keyword ) ){
1967
+
1968
+ return {
1969
+ message : 'STOP recorded — ' + address + ' is suppressed on SMS everywhere.',
1970
+ request : { keyword },
1971
+ writes : [ {
1972
+ collection : 'suppression',
1973
+ data : {
1974
+ $setOnInsert : {
1975
+ address,
1976
+ channel : 'sms',
1977
+ organization : null,
1978
+ reason : keyword,
1979
+ source : 'stop'
1980
+ }
1981
+ },
1982
+ operation : 'update',
1983
+ options : { upsert : true },
1984
+ query : {
1985
+ address,
1986
+ channel : 'sms',
1987
+ organization : null
1988
+ }
1989
+ } ]
1990
+ };
1991
+
1992
+ }
1993
+
1994
+ if( START_KEYWORDS.includes( keyword ) ){
1995
+
1996
+ return {
1997
+ message : 'START recorded — ' + address + ' can receive SMS again.',
1998
+ request : { keyword },
1999
+ writes : [ {
2000
+ collection : 'suppression',
2001
+ operation : 'delete',
2002
+ query : {
2003
+ address,
2004
+ channel : 'sms',
2005
+ organization : null
2006
+ }
2007
+ } ]
2008
+ };
2009
+
2010
+ }
2011
+
2012
+ return { message : 'A real reply, not a keyword — nothing to record.', skipped : true };
2013
+
2014
+ },
2015
+
2016
+ receive : ({ payload }) => ({
2017
+ data : payload,
2018
+ provider : { id : payload?.MessageSid || null }
2019
+ }),
2020
+
2021
+ // TWILIO'S SCHEME, from docs.twilio.com/usage/security: take the full
2022
+ // registered url, sort the POST parameters alphabetically (Unix-style,
2023
+ // case-sensitive), append each name and value with no delimiters, sign
2024
+ // with HMAC-SHA1 keyed by the AuthToken, base64 — compared against
2025
+ // X-Twilio-Signature. It signs the URL rather than the raw body, which
2026
+ // is exactly why the shared body-HMAC verifier cannot cover it and this
2027
+ // hook exists.
2028
+ verify : ({ body, headers, secret, url }) => {
2029
+
2030
+ // OUR misconfiguration, never a forgery — a missing credential must
2031
+ // not be answered 401.
2032
+ if( ! secret ) throw Object.assign( new Error( 'Missing webhook secret: TWILIO_AUTH_TOKEN' ), { status : 500 });
2033
+
2034
+ const params = new URLSearchParams( String( body || '' ) );
2035
+
2036
+ const signed = url + [ ...params.keys() ].sort().map( ( key ) => key + params.get( key ) ).join( '' );
2037
+
2038
+ const expected = createHmac( 'sha1', secret ).update( signed ).digest( 'base64' );
2039
+ const provided = String( headers?.[ 'x-twilio-signature' ] || '' );
2040
+
2041
+ const matches = expected.length === provided.length
2042
+ && timingSafeEqual( Buffer.from( expected ), Buffer.from( provided ) );
2043
+
2044
+ if( ! matches ) throw Object.assign( new Error( 'Invalid Twilio signature' ), { status : 401 });
2045
+
2046
+ return Object.fromEntries( params );
2047
+
2048
+ }
2049
+
2050
+ },
1935
2051
  lifecycle : false,
1936
2052
  resources : {
1937
2053
  audiences : false,
@@ -2200,6 +2316,19 @@ var drawbridge = {
2200
2316
  webhook : false
2201
2317
  },
2202
2318
  icon: icon$3,
2319
+
2320
+ // WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
2321
+ // verifies it. `secret` names the drawbridge provider's smsToken — the route
2322
+ // resolves the name to the stored value and hands it to verify.
2323
+ inbound : {
2324
+ headers : {
2325
+ id : 'i-twilio-idempotency-token',
2326
+ signature : 'x-twilio-signature'
2327
+ },
2328
+ signature : {
2329
+ secret : 'TWILIO_AUTH_TOKEN'
2330
+ }
2331
+ },
2203
2332
  // PRIVATE: never in the catalog, always available to the builder.
2204
2333
  private : true,
2205
2334
  // THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
@@ -5332,7 +5461,17 @@ const build = ( manifest ) => {
5332
5461
  // A vendor that receives from the outside must say where it puts the event
5333
5462
  // name. Without it the receiver has nothing to dispatch on, and the failure
5334
5463
  // is a request accepted and dropped rather than an error.
5335
- if( implemented( manifest.hooks, 'inbound.event' ) && ! manifest.inbound?.headers?.event ){
5464
+ //
5465
+ // A vendor may derive the event from the CHANNEL instead — Twilio sends no
5466
+ // topic header, so drawbridge's inbound.event is a constant and the
5467
+ // registered url is the topic. What is required is that SOMETHING names the
5468
+ // event: a declared header, or an event hook that answers without one. The
5469
+ // build cannot see inside the function, so the hook's existence is the
5470
+ // declaration — and a hook that returns nothing still fails at the route,
5471
+ // where a null event is refused before it is buffered.
5472
+ if( implemented( manifest.hooks, 'inbound.event' )
5473
+ && typeof manifest.hooks?.inbound?.event !== 'function'
5474
+ && ! manifest.inbound?.headers?.event ){
5336
5475
 
5337
5476
  throw new Error( manifest.slug + ' implements inbound.event but declares no inbound.headers.event' );
5338
5477
 
@@ -177,7 +177,7 @@ var HOOKS = Object.freeze({
177
177
  ])
178
178
  });
179
179
  var HOOK_EFFECTS = Object.freeze(["enqueues", "events", "writes"]);
180
- var WRITE_OPERATIONS = Object.freeze(["create", "update"]);
180
+ var WRITE_OPERATIONS = Object.freeze(["create", "delete", "update"]);
181
181
  var HOOK_PROPS = Object.freeze([
182
182
  "channel",
183
183
  "clientId",
@@ -237,9 +237,9 @@ var effectsOf = (answer) => {
237
237
  if (!WRITE_OPERATIONS.includes(write == null ? void 0 : write.operation)) {
238
238
  throw new Error("A described write on " + write.collection + " needs an operation \u2014 one of " + WRITE_OPERATIONS.join(", "));
239
239
  }
240
- if (!(write == null ? void 0 : write.data)) throw new Error("A described write on " + write.collection + " carries no data");
241
- if (write.operation === "update" && !write.query) {
242
- throw new Error("A described update on " + write.collection + " has no query \u2014 that is every document in it");
240
+ if (write.operation !== "delete" && !(write == null ? void 0 : write.data)) throw new Error("A described write on " + write.collection + " carries no data");
241
+ if (["delete", "update"].includes(write.operation) && !write.query) {
242
+ throw new Error("A described " + write.operation + " on " + write.collection + " has no query \u2014 that is every document in it");
243
243
  }
244
244
  if (write.operation === "create" && write.query) {
245
245
  throw new Error("A described create on " + write.collection + " carries a query \u2014 create does not filter");
@@ -835,6 +835,9 @@ var attentive_default2 = {
835
835
  title: "Attentive"
836
836
  };
837
837
 
838
+ // lib/connections/providers/drawbridge.js
839
+ import { createHmac, timingSafeEqual } from "crypto";
840
+
838
841
  // lib/http.js
839
842
  var DEFAULT_TIMEOUT_MS = 15e3;
840
843
  var request = async ({
@@ -1027,14 +1030,14 @@ var contacts = {
1027
1030
 
1028
1031
  // lib/connections/icons/drawbridge.js
1029
1032
  var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1030
- <rect width="500" height="500" fill="#BAEC5F"/>
1033
+ <rect width="500" height="500" fill="#0D1314"/>
1031
1034
  <g clip-path="url(#clip0_2115_2832)">
1032
- <path d="M140.224 127.586L174.803 188.73V311.176L140 372.32L176.084 392.031L216.111 321.753V178.278L176.341 108L140.224 127.586Z" fill="#0D1314"/>
1033
- <path d="M360.002 127.523L323.694 108.282L284.949 178.498V321.596L322.924 391.749L359.394 372.79L326.225 311.52V188.73L360.002 127.523Z" fill="#0D1314"/>
1035
+ <path d="M153.198 142.241L183.69 196.065V303.852L153 357.676L184.82 375.028L220.116 313.163V186.865L185.046 125L153.198 142.241Z" fill="#BAEC5F"/>
1036
+ <path d="M347.002 142.186L314.984 125.249L280.818 187.058V313.025L314.306 374.779L346.465 358.091L317.216 304.156V196.066L347.002 142.186Z" fill="#BAEC5F"/>
1034
1037
  </g>
1035
1038
  <defs>
1036
1039
  <clipPath id="clip0_2115_2832">
1037
- <rect width="220" height="284" fill="white" transform="translate(140 108)"/>
1040
+ <rect width="194" height="250" fill="white" transform="translate(153 125)"/>
1038
1041
  </clipPath>
1039
1042
  </defs>
1040
1043
  </svg>`;
@@ -1661,6 +1664,8 @@ var channels = {
1661
1664
  };
1662
1665
 
1663
1666
  // lib/connections/providers/drawbridge.js
1667
+ var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
1668
+ var START_KEYWORDS = ["START", "UNSTOP", "YES"];
1664
1669
  var interpolate = (template, data2) => {
1665
1670
  if (!template) return template;
1666
1671
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (data2 == null ? void 0 : data2[key]) != null ? String(data2[key]) : "{{" + key + "}}");
@@ -1888,7 +1893,88 @@ var drawbridge_default2 = {
1888
1893
  };
1889
1894
  }
1890
1895
  },
1891
- inbound: false,
1896
+ // INBOUND SMS to the platform number. Twilio's webhook, verified and
1897
+ // processed HERE — the manifest owns its own inbound, the same as every
1898
+ // other vendor, and the bespoke webhooks route + the handler that lived in
1899
+ // sync's buffer table are gone.
1900
+ inbound: {
1901
+ // EVERY POST ON THIS CHANNEL IS AN INBOUND MESSAGE. Twilio sends no
1902
+ // topic header — the registered url IS the topic.
1903
+ event: () => "message.inbound",
1904
+ // STOP AND START, AS DESCRIBED WRITES. A withdrawn number is withdrawn
1905
+ // for everyone — organization : null is the platform floor canSend()
1906
+ // reads on every send path. $setOnInsert + upsert so webhook replays
1907
+ // and repeat STOPs collapse onto the unique { channel, address,
1908
+ // organization } index instead of erroring.
1909
+ process: ({ context }) => {
1910
+ var _a, _b;
1911
+ const keyword = String(((_a = context == null ? void 0 : context.data) == null ? void 0 : _a.Body) || "").trim().toUpperCase();
1912
+ const address = toE164((_b = context == null ? void 0 : context.data) == null ? void 0 : _b.From);
1913
+ if (!address) return { message: "Inbound SMS carried no usable sender number.", skipped: true };
1914
+ if (STOP_KEYWORDS.includes(keyword)) {
1915
+ return {
1916
+ message: "STOP recorded \u2014 " + address + " is suppressed on SMS everywhere.",
1917
+ request: { keyword },
1918
+ writes: [{
1919
+ collection: "suppression",
1920
+ data: {
1921
+ $setOnInsert: {
1922
+ address,
1923
+ channel: "sms",
1924
+ organization: null,
1925
+ reason: keyword,
1926
+ source: "stop"
1927
+ }
1928
+ },
1929
+ operation: "update",
1930
+ options: { upsert: true },
1931
+ query: {
1932
+ address,
1933
+ channel: "sms",
1934
+ organization: null
1935
+ }
1936
+ }]
1937
+ };
1938
+ }
1939
+ if (START_KEYWORDS.includes(keyword)) {
1940
+ return {
1941
+ message: "START recorded \u2014 " + address + " can receive SMS again.",
1942
+ request: { keyword },
1943
+ writes: [{
1944
+ collection: "suppression",
1945
+ operation: "delete",
1946
+ query: {
1947
+ address,
1948
+ channel: "sms",
1949
+ organization: null
1950
+ }
1951
+ }]
1952
+ };
1953
+ }
1954
+ return { message: "A real reply, not a keyword \u2014 nothing to record.", skipped: true };
1955
+ },
1956
+ receive: ({ payload }) => ({
1957
+ data: payload,
1958
+ provider: { id: (payload == null ? void 0 : payload.MessageSid) || null }
1959
+ }),
1960
+ // TWILIO'S SCHEME, from docs.twilio.com/usage/security: take the full
1961
+ // registered url, sort the POST parameters alphabetically (Unix-style,
1962
+ // case-sensitive), append each name and value with no delimiters, sign
1963
+ // with HMAC-SHA1 keyed by the AuthToken, base64 — compared against
1964
+ // X-Twilio-Signature. It signs the URL rather than the raw body, which
1965
+ // is exactly why the shared body-HMAC verifier cannot cover it and this
1966
+ // hook exists.
1967
+ verify: ({ body, headers, secret, url }) => {
1968
+ if (!secret) throw Object.assign(new Error("Missing webhook secret: TWILIO_AUTH_TOKEN"), { status: 500 });
1969
+ const params = new URLSearchParams(String(body || ""));
1970
+ const signed = url + [...params.keys()].sort().map((key) => key + params.get(key)).join("");
1971
+ const expected = createHmac("sha1", secret).update(signed).digest("base64");
1972
+ const provided = String((headers == null ? void 0 : headers["x-twilio-signature"]) || "");
1973
+ const matches = expected.length === provided.length && timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
1974
+ if (!matches) throw Object.assign(new Error("Invalid Twilio signature"), { status: 401 });
1975
+ return Object.fromEntries(params);
1976
+ }
1977
+ },
1892
1978
  lifecycle: false,
1893
1979
  resources: {
1894
1980
  audiences: false,
@@ -2067,6 +2153,18 @@ var drawbridge_default2 = {
2067
2153
  webhook: false
2068
2154
  },
2069
2155
  icon: drawbridge_default,
2156
+ // WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
2157
+ // verifies it. `secret` names the drawbridge provider's smsToken — the route
2158
+ // resolves the name to the stored value and hands it to verify.
2159
+ inbound: {
2160
+ headers: {
2161
+ id: "i-twilio-idempotency-token",
2162
+ signature: "x-twilio-signature"
2163
+ },
2164
+ signature: {
2165
+ secret: "TWILIO_AUTH_TOKEN"
2166
+ }
2167
+ },
2070
2168
  // PRIVATE: never in the catalog, always available to the builder.
2071
2169
  private: true,
2072
2170
  // THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
@@ -3029,7 +3127,7 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
3029
3127
  </svg>`;
3030
3128
 
3031
3129
  // lib/connections/inbound.js
3032
- import { createHmac, timingSafeEqual } from "crypto";
3130
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
3033
3131
  var verifySignature = ({ body, descriptor, headers, secret }) => {
3034
3132
  if (!secret) {
3035
3133
  throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
@@ -3038,10 +3136,10 @@ var verifySignature = ({ body, descriptor, headers, secret }) => {
3038
3136
  if (!provided) {
3039
3137
  throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
3040
3138
  }
3041
- const digest = createHmac(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
3139
+ const digest = createHmac2(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
3042
3140
  const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
3043
3141
  const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
3044
- if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual(digestBuffer, providedBuffer)) {
3142
+ if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual2(digestBuffer, providedBuffer)) {
3045
3143
  throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
3046
3144
  }
3047
3145
  return JSON.parse(body.toString());
@@ -4373,7 +4471,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
4373
4471
  ([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
4374
4472
  );
4375
4473
  var build = (manifest) => {
4376
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
4474
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
4377
4475
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
4378
4476
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
4379
4477
  if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
@@ -4452,16 +4550,16 @@ var build = (manifest) => {
4452
4550
  );
4453
4551
  }
4454
4552
  }
4455
- if (implemented(manifest.hooks, "inbound.event") && !((_l = (_k = manifest.inbound) == null ? void 0 : _k.headers) == null ? void 0 : _l.event)) {
4553
+ if (implemented(manifest.hooks, "inbound.event") && typeof ((_l = (_k = manifest.hooks) == null ? void 0 : _k.inbound) == null ? void 0 : _l.event) !== "function" && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.event)) {
4456
4554
  throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
4457
4555
  }
4458
- if (implemented(manifest.hooks, "inbound.verify") && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.signature)) {
4556
+ if (implemented(manifest.hooks, "inbound.verify") && !((_p = (_o = manifest.inbound) == null ? void 0 : _o.headers) == null ? void 0 : _p.signature)) {
4459
4557
  throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
4460
4558
  }
4461
4559
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
4462
4560
  throw new Error(manifest.slug + " must declare status( data ) \u2014 return null to accept the connection's own status, or { message, status } to override it");
4463
4561
  }
4464
- if (!Array.isArray((_o = manifest == null ? void 0 : manifest.content) == null ? void 0 : _o.guide) || !manifest.content.guide.length) {
4562
+ if (!Array.isArray((_q = manifest == null ? void 0 : manifest.content) == null ? void 0 : _q.guide) || !manifest.content.guide.length) {
4465
4563
  throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
4466
4564
  }
4467
4565
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
@@ -4473,8 +4571,8 @@ var build = (manifest) => {
4473
4571
  }
4474
4572
  for (const [domain, verbs] of Object.entries(HOOKS)) {
4475
4573
  for (const verb of verbs) {
4476
- const hook = (_q = (_p = manifest.hooks) == null ? void 0 : _p[domain]) == null ? void 0 : _q[verb];
4477
- if (((_r = manifest.hooks) == null ? void 0 : _r[domain]) === false) continue;
4574
+ const hook = (_s = (_r = manifest.hooks) == null ? void 0 : _r[domain]) == null ? void 0 : _s[verb];
4575
+ if (((_t = manifest.hooks) == null ? void 0 : _t[domain]) === false) continue;
4478
4576
  if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
4479
4577
  throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
4480
4578
  }