@drawbridge/drawbridge-utils 0.0.155 → 0.0.156

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.
@@ -1520,10 +1520,15 @@ const contacts = {
1520
1520
  remove : async ( { email, id, token }, { fetcher } = {} ) => {
1521
1521
 
1522
1522
  // NO TOKEN IS A NO-OP HERE, unlike sendgrid and twilio, and the
1523
- // difference is what the credential is declared to be: hubspotToken
1524
- // is the one provider field that is not `required`, because this is
1525
- // internal CRM tooling no merchant sees. A deployment with no portal
1526
- // is a supported state, not a missing credential.
1523
+ // difference is what the credential is FOR: this is internal CRM
1524
+ // tooling no merchant sees, so a deployment with no portal is a
1525
+ // supported state rather than a missing credential.
1526
+ //
1527
+ // The manifest does mark hubspotToken `required`, and that is not a
1528
+ // contradiction — the flag feeds `isLive` and nothing else, so it
1529
+ // decides whether the admin card reads live. Not live is the honest
1530
+ // badge when nothing will sync. It gates no behaviour, and
1531
+ // saveProviderSettings never consults it, so the row still writes.
1527
1532
  if( ! token ) return;
1528
1533
 
1529
1534
  const contact = id || await lookup({ email, fetcher, token });
@@ -1520,10 +1520,15 @@ const contacts = {
1520
1520
  remove : async ( { email, id, token }, { fetcher } = {} ) => {
1521
1521
 
1522
1522
  // NO TOKEN IS A NO-OP HERE, unlike sendgrid and twilio, and the
1523
- // difference is what the credential is declared to be: hubspotToken
1524
- // is the one provider field that is not `required`, because this is
1525
- // internal CRM tooling no merchant sees. A deployment with no portal
1526
- // is a supported state, not a missing credential.
1523
+ // difference is what the credential is FOR: this is internal CRM
1524
+ // tooling no merchant sees, so a deployment with no portal is a
1525
+ // supported state rather than a missing credential.
1526
+ //
1527
+ // The manifest does mark hubspotToken `required`, and that is not a
1528
+ // contradiction — the flag feeds `isLive` and nothing else, so it
1529
+ // decides whether the admin card reads live. Not live is the honest
1530
+ // badge when nothing will sync. It gates no behaviour, and
1531
+ // saveProviderSettings never consults it, so the row still writes.
1527
1532
  if( ! token ) return;
1528
1533
 
1529
1534
  const contact = id || await lookup({ email, fetcher, token });
package/dist/email.cjs CHANGED
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // lib/email.js
20
20
  var email_exports = {};
21
21
  __export(email_exports, {
22
+ subAddress: () => subAddress,
22
23
  toCanonicalEmail: () => toCanonicalEmail
23
24
  });
24
25
  module.exports = __toCommonJS(email_exports);
@@ -36,7 +37,20 @@ var toCanonicalEmail = (value) => {
36
37
  if (!local) return null;
37
38
  return local + "@" + domain;
38
39
  };
40
+ var LOCAL_PART_LIMIT = 64;
41
+ var subAddress = (email, tag) => {
42
+ if (!email || typeof email !== "string") return email;
43
+ if (!tag || typeof tag !== "string") return email;
44
+ if (!/^[A-Za-z0-9]+$/.test(tag)) return email;
45
+ const at = email.lastIndexOf("@");
46
+ if (at < 1 || at === email.length - 1) return email;
47
+ const local = email.slice(0, at);
48
+ if (local.includes("+")) return email;
49
+ if (local.length + 1 + tag.length > LOCAL_PART_LIMIT) return email;
50
+ return local + "+" + tag + "@" + email.slice(at + 1);
51
+ };
39
52
  // Annotate the CommonJS export names for ESM import in node:
40
53
  0 && (module.exports = {
54
+ subAddress,
41
55
  toCanonicalEmail
42
56
  });
package/dist/email.d.cts CHANGED
@@ -33,4 +33,56 @@ const toCanonicalEmail = ( value ) => {
33
33
 
34
34
  };
35
35
 
36
- export { toCanonicalEmail };
36
+ // RFC 5322 caps the local part at 64 octets. Everything here is ASCII, so
37
+ // length is octets.
38
+ const LOCAL_PART_LIMIT = 64;
39
+
40
+ // The inverse of toCanonicalEmail: put a tag INTO the local part rather than
41
+ // strip one out. `subAddress( 'org@send.drwbrdg.com', '507f…' )` gives
42
+ // `org+507f…@send.drwbrdg.com` — RFC 5233 sub-addressing, which Google delivers
43
+ // to the untagged mailbox natively.
44
+ //
45
+ // This is how the platform's lead-facing sender carries the ORGANIZATION.
46
+ // Shopify does the same thing with `store+<shopId>@shopifyemail.com`: one
47
+ // mailbox, one MX record, one authenticated domain, and the tenant rides in the
48
+ // address instead of in infrastructure. There is no per-organization address to
49
+ // store, so there is nothing to migrate or backfill.
50
+ //
51
+ // It is derived at send time, never persisted. Worth settling BEFORE the first
52
+ // send rather than after: the From line is frozen into every message already
53
+ // delivered, so a shared address cannot be split apart retroactively.
54
+ //
55
+ // Returns the address UNCHANGED whenever it cannot be tagged safely — a flat
56
+ // from-address is a working email, a malformed one is a send the provider
57
+ // rejects outright. Callers rely on that: the sender resolvers fall through to
58
+ // their own defaults on a falsy return, and never on a broken string.
59
+ const subAddress = ( email, tag ) => {
60
+
61
+ if( ! email || typeof email !== 'string' ) return email;
62
+ if( ! tag || typeof tag !== 'string' ) return email;
63
+
64
+ // Organization ids are 24-char ObjectId hex, and the short-id generator is
65
+ // base36 — both alphanumeric, both safely atext. Checked rather than assumed
66
+ // because the api schema types `id` as a bare string with no pattern, and
67
+ // `formats.documents.insert` spreads caller data OVER the generated id, so a
68
+ // hand-written id would win.
69
+ if( ! /^[A-Za-z0-9]+$/.test( tag ) ) return email;
70
+
71
+ const at = email.lastIndexOf( '@' );
72
+
73
+ if( at < 1 || at === email.length - 1 ) return email;
74
+
75
+ const local = email.slice( 0, at );
76
+
77
+ // Already tagged. Re-tagging would nest (`org+a+b@`), and the first tag is
78
+ // the one a receiving mailbox would route on — so the second would be a
79
+ // silent lie about which tenant the mail belongs to.
80
+ if( local.includes( '+' ) ) return email;
81
+
82
+ if( local.length + 1 + tag.length > LOCAL_PART_LIMIT ) return email;
83
+
84
+ return local + '+' + tag + '@' + email.slice( at + 1 );
85
+
86
+ };
87
+
88
+ export { subAddress, toCanonicalEmail };
package/dist/email.d.ts CHANGED
@@ -33,4 +33,56 @@ const toCanonicalEmail = ( value ) => {
33
33
 
34
34
  };
35
35
 
36
- export { toCanonicalEmail };
36
+ // RFC 5322 caps the local part at 64 octets. Everything here is ASCII, so
37
+ // length is octets.
38
+ const LOCAL_PART_LIMIT = 64;
39
+
40
+ // The inverse of toCanonicalEmail: put a tag INTO the local part rather than
41
+ // strip one out. `subAddress( 'org@send.drwbrdg.com', '507f…' )` gives
42
+ // `org+507f…@send.drwbrdg.com` — RFC 5233 sub-addressing, which Google delivers
43
+ // to the untagged mailbox natively.
44
+ //
45
+ // This is how the platform's lead-facing sender carries the ORGANIZATION.
46
+ // Shopify does the same thing with `store+<shopId>@shopifyemail.com`: one
47
+ // mailbox, one MX record, one authenticated domain, and the tenant rides in the
48
+ // address instead of in infrastructure. There is no per-organization address to
49
+ // store, so there is nothing to migrate or backfill.
50
+ //
51
+ // It is derived at send time, never persisted. Worth settling BEFORE the first
52
+ // send rather than after: the From line is frozen into every message already
53
+ // delivered, so a shared address cannot be split apart retroactively.
54
+ //
55
+ // Returns the address UNCHANGED whenever it cannot be tagged safely — a flat
56
+ // from-address is a working email, a malformed one is a send the provider
57
+ // rejects outright. Callers rely on that: the sender resolvers fall through to
58
+ // their own defaults on a falsy return, and never on a broken string.
59
+ const subAddress = ( email, tag ) => {
60
+
61
+ if( ! email || typeof email !== 'string' ) return email;
62
+ if( ! tag || typeof tag !== 'string' ) return email;
63
+
64
+ // Organization ids are 24-char ObjectId hex, and the short-id generator is
65
+ // base36 — both alphanumeric, both safely atext. Checked rather than assumed
66
+ // because the api schema types `id` as a bare string with no pattern, and
67
+ // `formats.documents.insert` spreads caller data OVER the generated id, so a
68
+ // hand-written id would win.
69
+ if( ! /^[A-Za-z0-9]+$/.test( tag ) ) return email;
70
+
71
+ const at = email.lastIndexOf( '@' );
72
+
73
+ if( at < 1 || at === email.length - 1 ) return email;
74
+
75
+ const local = email.slice( 0, at );
76
+
77
+ // Already tagged. Re-tagging would nest (`org+a+b@`), and the first tag is
78
+ // the one a receiving mailbox would route on — so the second would be a
79
+ // silent lie about which tenant the mail belongs to.
80
+ if( local.includes( '+' ) ) return email;
81
+
82
+ if( local.length + 1 + tag.length > LOCAL_PART_LIMIT ) return email;
83
+
84
+ return local + '+' + tag + '@' + email.slice( at + 1 );
85
+
86
+ };
87
+
88
+ export { subAddress, toCanonicalEmail };
package/dist/email.js CHANGED
@@ -13,6 +13,19 @@ var toCanonicalEmail = (value) => {
13
13
  if (!local) return null;
14
14
  return local + "@" + domain;
15
15
  };
16
+ var LOCAL_PART_LIMIT = 64;
17
+ var subAddress = (email, tag) => {
18
+ if (!email || typeof email !== "string") return email;
19
+ if (!tag || typeof tag !== "string") return email;
20
+ if (!/^[A-Za-z0-9]+$/.test(tag)) return email;
21
+ const at = email.lastIndexOf("@");
22
+ if (at < 1 || at === email.length - 1) return email;
23
+ const local = email.slice(0, at);
24
+ if (local.includes("+")) return email;
25
+ if (local.length + 1 + tag.length > LOCAL_PART_LIMIT) return email;
26
+ return local + "+" + tag + "@" + email.slice(at + 1);
27
+ };
16
28
  export {
29
+ subAddress,
17
30
  toCanonicalEmail
18
31
  };
package/package.json CHANGED
@@ -216,5 +216,5 @@
216
216
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
217
217
  },
218
218
  "types": "dist/index.d.ts",
219
- "version": "0.0.155"
219
+ "version": "0.0.156"
220
220
  }