@aglyn/shared-util-email 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +104 -0
- package/package.json +37 -0
- package/src/index.d.ts +37 -0
- package/src/index.js +46 -0
- package/src/index.js.map +1 -0
- package/src/lib/email-delivery-events.d.ts +198 -0
- package/src/lib/email-delivery-events.js +310 -0
- package/src/lib/email-delivery-events.js.map +1 -0
- package/src/lib/email-health.d.ts +154 -0
- package/src/lib/email-health.js +264 -0
- package/src/lib/email-health.js.map +1 -0
- package/src/lib/email-media-src.d.ts +33 -0
- package/src/lib/email-media-src.js +113 -0
- package/src/lib/email-media-src.js.map +1 -0
- package/src/lib/email-merge.d.ts +31 -0
- package/src/lib/email-merge.js +36 -0
- package/src/lib/email-merge.js.map +1 -0
- package/src/lib/email-render.d.ts +114 -0
- package/src/lib/email-render.js +293 -0
- package/src/lib/email-render.js.map +1 -0
- package/src/lib/email-revenue-window.d.ts +55 -0
- package/src/lib/email-revenue-window.js +58 -0
- package/src/lib/email-revenue-window.js.map +1 -0
- package/src/lib/host-email-render.d.ts +99 -0
- package/src/lib/host-email-render.js +126 -0
- package/src/lib/host-email-render.js.map +1 -0
- package/src/lib/host-sender.d.ts +86 -0
- package/src/lib/host-sender.js +131 -0
- package/src/lib/host-sender.js.map +1 -0
- package/src/lib/marketing-send.d.ts +466 -0
- package/src/lib/marketing-send.js +459 -0
- package/src/lib/marketing-send.js.map +1 -0
- package/src/lib/platform-sending-domain.d.ts +362 -0
- package/src/lib/platform-sending-domain.js +697 -0
- package/src/lib/platform-sending-domain.js.map +1 -0
- package/src/lib/received-email.d.ts +86 -0
- package/src/lib/received-email.js +124 -0
- package/src/lib/received-email.js.map +1 -0
- package/src/lib/send-ceilings.d.ts +394 -0
- package/src/lib/send-ceilings.js +341 -0
- package/src/lib/send-ceilings.js.map +1 -0
- package/src/lib/send-email.d.ts +385 -0
- package/src/lib/send-email.js +586 -0
- package/src/lib/send-email.js.map +1 -0
- package/src/lib/send-rate.d.ts +298 -0
- package/src/lib/send-rate.js +310 -0
- package/src/lib/send-rate.js.map +1 -0
- package/src/lib/sender-reputation.d.ts +357 -0
- package/src/lib/sender-reputation.js +392 -0
- package/src/lib/sender-reputation.js.map +1 -0
- package/src/lib/sending-domain.d.ts +744 -0
- package/src/lib/sending-domain.js +798 -0
- package/src/lib/sending-domain.js.map +1 -0
- package/src/lib/sending-mailbox.d.ts +132 -0
- package/src/lib/sending-mailbox.js +192 -0
- package/src/lib/sending-mailbox.js.map +1 -0
- package/src/lib/stored-email-nodes.d.ts +38 -0
- package/src/lib/stored-email-nodes.js +78 -0
- package/src/lib/stored-email-nodes.js.map +1 -0
- package/src/lib/svix-signature.d.ts +38 -0
- package/src/lib/svix-signature.js +55 -0
- package/src/lib/svix-signature.js.map +1 -0
- package/src/lib/system-email-catalog.d.ts +170 -0
- package/src/lib/system-email-catalog.js +828 -0
- package/src/lib/system-email-catalog.js.map +1 -0
- package/src/lib/tenant-email-catalog.d.ts +78 -0
- package/src/lib/tenant-email-catalog.js +452 -0
- package/src/lib/tenant-email-catalog.js.map +1 -0
- package/src/lib/text-email-html.d.ts +32 -0
- package/src/lib/text-email-html.js +119 -0
- package/src/lib/text-email-html.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/stored-email-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { decode } from '@msgpack/msgpack'\n\n/**\n * A COPY of `decodeStoredNodes` from `@aglyn/aglyn` (AGL-1223), for the same\n * reason `email-media-src.ts` carries a copy of `resolveMediaSrc`:\n * `shared-util-email` is tagged `scope:shared`, the module-boundary rule makes\n * shared libs leaves, and the arrow points the other way so every send site\n * can pull the email renderer without the framework.\n *\n * `email-nodes-drift.spec.ts` in the console — which may import both — runs\n * the two implementations over one table of inputs, so a divergence fails a\n * build rather than silently changing what a recipient receives.\n *\n * ## Why an email loader needs this at all\n *\n * An email version's `nodes` is stored in the same two live forms every other\n * besigner document uses: a plain Firestore map, and msgpack bytes. Reading\n * the field raw is not a loud failure — a `Buffer` walks, `Object.keys` over\n * one returns BYTE INDICES rather than nothing, so an emptiness guard passes\n * and the send renders an empty email instead of falling back to its built-in\n * copy.\n */\nexport function decodeEmailNodes<T = Record<string, unknown>>(\n raw: unknown,\n): T | null {\n if (raw === null || raw === undefined) return null\n const bytes = ArrayBuffer.isView(raw)\n ? // Offset and length, ALWAYS. firebase-admin hands back POOLED Buffers,\n // so a small field is typically a view into a shared 8 KB allocation\n // and decoding the whole pool throws on the trailing bytes.\n new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)\n : byteSourceBytes(raw) ?? bufferEnvelopeBytes(raw)\n if (bytes) {\n try {\n return decode(bytes) as T\n } catch (error) {\n // Undecodable nodes must never read as \"this template is empty\" — that\n // is indistinguishable from a template nobody has designed yet.\n console.error('could not decode stored email nodes', error)\n return null\n }\n }\n return raw as T\n}\n\n/**\n * The client SDK's `Bytes`, structurally. Matched on the method rather than\n * the class so no Firestore package is imported here. A node map cannot\n * collide: its values are node OBJECTS, never functions.\n */\nfunction byteSourceBytes(raw: unknown): Uint8Array | null {\n if (typeof raw !== 'object') return null\n const source = raw as { toUint8Array?: unknown }\n return typeof source.toUint8Array === 'function'\n ? (source.toUint8Array as () => Uint8Array)()\n : null\n}\n\n/**\n * A JSON-serialized Node `Buffer` — what `JSON.stringify` makes of one\n * (AGL-1391). The test is deliberately exact, because the alternative reading\n * is a node map with nodes called `type` and `data`; that map cannot exist,\n * since `type` would have to hold the literal string `'Buffer'` and `data` an\n * array, in the same document.\n */\nfunction bufferEnvelopeBytes(raw: unknown): Uint8Array | null {\n if (typeof raw !== 'object' || Array.isArray(raw)) return null\n const value = raw as { type?: unknown; data?: unknown }\n if (value.type !== 'Buffer' || !Array.isArray(value.data)) return null\n if (Object.keys(value).length !== 2) return null\n return Uint8Array.from(value.data as number[])\n}\n\nexport default decodeEmailNodes\n"],"names":["decode","decodeEmailNodes","raw","byteSourceBytes","undefined","bytes","ArrayBuffer","isView","Uint8Array","buffer","byteOffset","byteLength","bufferEnvelopeBytes","error","console","source","toUint8Array","Array","isArray","value","type","data","Object","keys","length","from"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,MAAM,QAAQ,mBAAkB;AAEzC;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,iBACdC,GAAY;QAQRC;IANJ,IAAID,QAAQ,QAAQA,QAAQE,WAAW,OAAO;IAC9C,MAAMC,QAAQC,YAAYC,MAAM,CAACL,OAE7B,qEAAqE;IACrE,4DAA4D;IAC5D,IAAIM,WAAWN,IAAIO,MAAM,EAAEP,IAAIQ,UAAU,EAAER,IAAIS,UAAU,KACzDR,mBAAAA,gBAAgBD,gBAAhBC,mBAAwBS,oBAAoBV;IAChD,IAAIG,OAAO;QACT,IAAI;YACF,OAAOL,OAAOK;QAChB,EAAE,OAAOQ,OAAO;YACd,uEAAuE;YACvE,gEAAgE;YAChEC,QAAQD,KAAK,CAAC,uCAAuCA;YACrD,OAAO;QACT;IACF;IACA,OAAOX;AACT;AAEA;;;;CAIC,GACD,SAASC,gBAAgBD,GAAY;IACnC,IAAI,OAAOA,QAAQ,UAAU,OAAO;IACpC,MAAMa,SAASb;IACf,OAAO,OAAOa,OAAOC,YAAY,KAAK,aAClC,AAACD,OAAOC,YAAY,KACpB;AACN;AAEA;;;;;;CAMC,GACD,SAASJ,oBAAoBV,GAAY;IACvC,IAAI,OAAOA,QAAQ,YAAYe,MAAMC,OAAO,CAAChB,MAAM,OAAO;IAC1D,MAAMiB,QAAQjB;IACd,IAAIiB,MAAMC,IAAI,KAAK,YAAY,CAACH,MAAMC,OAAO,CAACC,MAAME,IAAI,GAAG,OAAO;IAClE,IAAIC,OAAOC,IAAI,CAACJ,OAAOK,MAAM,KAAK,GAAG,OAAO;IAC5C,OAAOhB,WAAWiB,IAAI,CAACN,MAAME,IAAI;AACnC;AAEA,eAAepB,iBAAgB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* THE ONE SVIX SIGNATURE CHECK.
|
|
19
|
+
*
|
|
20
|
+
* Resend signs every webhook the Svix way: HMAC-SHA256 over
|
|
21
|
+
* `{svix-id}.{svix-timestamp}.{raw body}` with the base64 secret after
|
|
22
|
+
* `whsec_`, and a `svix-signature` header carrying space-delimited
|
|
23
|
+
* `v1,<base64>` entries, any one of which may match during a secret
|
|
24
|
+
* rotation. The delivery-events webhook has verified this since AGL-268;
|
|
25
|
+
* the CRM's capture webhook (AGL-2657) verifies it too, and a second
|
|
26
|
+
* implementation would be a second place for the comparison to stop being
|
|
27
|
+
* constant-time. So the check lives here, and both import it.
|
|
28
|
+
*
|
|
29
|
+
* Signs the RAW body — the exact request text, never a re-serialization —
|
|
30
|
+
* which is why the callers hand this a `Buffer` of `req.rawBody`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function verifySvixSignature(secret: string, id: string, timestamp: string, payload: Buffer, signatureHeader: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The `svix-signature` header value a test fixture or a replay tool signs a
|
|
35
|
+
* payload with — the inverse of {@link verifySvixSignature}, so a spec never
|
|
36
|
+
* spells the HMAC recipe a second time.
|
|
37
|
+
*/
|
|
38
|
+
export declare function signSvixPayload(secret: string, id: string, timestamp: string, payload: Buffer | string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { createHmac, timingSafeEqual } from "crypto";
|
|
17
|
+
/**
|
|
18
|
+
* THE ONE SVIX SIGNATURE CHECK.
|
|
19
|
+
*
|
|
20
|
+
* Resend signs every webhook the Svix way: HMAC-SHA256 over
|
|
21
|
+
* `{svix-id}.{svix-timestamp}.{raw body}` with the base64 secret after
|
|
22
|
+
* `whsec_`, and a `svix-signature` header carrying space-delimited
|
|
23
|
+
* `v1,<base64>` entries, any one of which may match during a secret
|
|
24
|
+
* rotation. The delivery-events webhook has verified this since AGL-268;
|
|
25
|
+
* the CRM's capture webhook (AGL-2657) verifies it too, and a second
|
|
26
|
+
* implementation would be a second place for the comparison to stop being
|
|
27
|
+
* constant-time. So the check lives here, and both import it.
|
|
28
|
+
*
|
|
29
|
+
* Signs the RAW body — the exact request text, never a re-serialization —
|
|
30
|
+
* which is why the callers hand this a `Buffer` of `req.rawBody`.
|
|
31
|
+
*/ export function verifySvixSignature(secret, id, timestamp, payload, signatureHeader) {
|
|
32
|
+
try {
|
|
33
|
+
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
|
|
34
|
+
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(payload).digest();
|
|
35
|
+
return signatureHeader.split(' ').some((entry)=>{
|
|
36
|
+
const [, signature] = entry.split(',');
|
|
37
|
+
if (!signature) return false;
|
|
38
|
+
const candidate = Buffer.from(signature, 'base64');
|
|
39
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
40
|
+
});
|
|
41
|
+
} catch (unused) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The `svix-signature` header value a test fixture or a replay tool signs a
|
|
47
|
+
* payload with — the inverse of {@link verifySvixSignature}, so a spec never
|
|
48
|
+
* spells the HMAC recipe a second time.
|
|
49
|
+
*/ export function signSvixPayload(secret, id, timestamp, payload) {
|
|
50
|
+
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
|
|
51
|
+
const signature = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload).digest('base64');
|
|
52
|
+
return `v1,${signature}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=svix-signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/svix-signature.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto'\n\n/**\n * THE ONE SVIX SIGNATURE CHECK.\n *\n * Resend signs every webhook the Svix way: HMAC-SHA256 over\n * `{svix-id}.{svix-timestamp}.{raw body}` with the base64 secret after\n * `whsec_`, and a `svix-signature` header carrying space-delimited\n * `v1,<base64>` entries, any one of which may match during a secret\n * rotation. The delivery-events webhook has verified this since AGL-268;\n * the CRM's capture webhook (AGL-2657) verifies it too, and a second\n * implementation would be a second place for the comparison to stop being\n * constant-time. So the check lives here, and both import it.\n *\n * Signs the RAW body — the exact request text, never a re-serialization —\n * which is why the callers hand this a `Buffer` of `req.rawBody`.\n */\nexport function verifySvixSignature(\n secret: string,\n id: string,\n timestamp: string,\n payload: Buffer,\n signatureHeader: string,\n): boolean {\n try {\n const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')\n const expected = createHmac('sha256', key)\n .update(`${id}.${timestamp}.`)\n .update(payload)\n .digest()\n return signatureHeader.split(' ').some((entry) => {\n const [, signature] = entry.split(',')\n if (!signature) return false\n const candidate = Buffer.from(signature, 'base64')\n return (\n candidate.length === expected.length &&\n timingSafeEqual(candidate, expected)\n )\n })\n } catch {\n return false\n }\n}\n\n/**\n * The `svix-signature` header value a test fixture or a replay tool signs a\n * payload with — the inverse of {@link verifySvixSignature}, so a spec never\n * spells the HMAC recipe a second time.\n */\nexport function signSvixPayload(\n secret: string,\n id: string,\n timestamp: string,\n payload: Buffer | string,\n): string {\n const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')\n const signature = createHmac('sha256', key)\n .update(`${id}.${timestamp}.`)\n .update(typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload)\n .digest('base64')\n return `v1,${signature}`\n}\n"],"names":["createHmac","timingSafeEqual","verifySvixSignature","secret","id","timestamp","payload","signatureHeader","key","Buffer","from","replace","expected","update","digest","split","some","entry","signature","candidate","length","signSvixPayload"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,eAAe,QAAQ,SAAQ;AAEpD;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,oBACdC,MAAc,EACdC,EAAU,EACVC,SAAiB,EACjBC,OAAe,EACfC,eAAuB;IAEvB,IAAI;QACF,MAAMC,MAAMC,OAAOC,IAAI,CAACP,OAAOQ,OAAO,CAAC,WAAW,KAAK;QACvD,MAAMC,WAAWZ,WAAW,UAAUQ,KACnCK,MAAM,CAAC,GAAGT,GAAG,CAAC,EAAEC,UAAU,CAAC,CAAC,EAC5BQ,MAAM,CAACP,SACPQ,MAAM;QACT,OAAOP,gBAAgBQ,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC;YACtC,MAAM,GAAGC,UAAU,GAAGD,MAAMF,KAAK,CAAC;YAClC,IAAI,CAACG,WAAW,OAAO;YACvB,MAAMC,YAAYV,OAAOC,IAAI,CAACQ,WAAW;YACzC,OACEC,UAAUC,MAAM,KAAKR,SAASQ,MAAM,IACpCnB,gBAAgBkB,WAAWP;QAE/B;IACF,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASS,gBACdlB,MAAc,EACdC,EAAU,EACVC,SAAiB,EACjBC,OAAwB;IAExB,MAAME,MAAMC,OAAOC,IAAI,CAACP,OAAOQ,OAAO,CAAC,WAAW,KAAK;IACvD,MAAMO,YAAYlB,WAAW,UAAUQ,KACpCK,MAAM,CAAC,GAAGT,GAAG,CAAC,EAAEC,UAAU,CAAC,CAAC,EAC5BQ,MAAM,CAAC,OAAOP,YAAY,WAAWG,OAAOC,IAAI,CAACJ,SAAS,UAAUA,SACpEQ,MAAM,CAAC;IACV,OAAO,CAAC,GAAG,EAAEI,WAAW;AAC1B"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Who actually puts the message on the wire.
|
|
19
|
+
*
|
|
20
|
+
* `resend` — Aglyn composes and sends it, so a besigner template can change
|
|
21
|
+
* what the recipient sees.
|
|
22
|
+
*
|
|
23
|
+
* `firebase` — Firebase Auth sends it from its own templates, configured in
|
|
24
|
+
* the Firebase console. **A besigner template cannot affect these at all**
|
|
25
|
+
* because the sender owns the copy (AGL-767).
|
|
26
|
+
*
|
|
27
|
+
* `stripe` — Stripe sends it on Aglyn's behalf from the Dashboard's
|
|
28
|
+
* Customer-emails settings (billing receipts, dunning, refunds, expiring-card
|
|
29
|
+
* notices). Aglyn never composes these, so like `firebase` they cannot be
|
|
30
|
+
* designed here (AGL-767).
|
|
31
|
+
*
|
|
32
|
+
* The non-`resend` entries are listed so staff can see the full set of mail
|
|
33
|
+
* the product sends and where each one is controlled; the UI must present
|
|
34
|
+
* them as non-editable rather than offering an editor that silently does
|
|
35
|
+
* nothing.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Who actually puts the message on the wire.
|
|
39
|
+
*
|
|
40
|
+
* `'firebase'` was removed by AGL-1112, once nothing was left that Firebase
|
|
41
|
+
* Auth composed. Deliberately not kept "just in case": with the variant gone,
|
|
42
|
+
* reintroducing a Firebase-sent email is a compile error rather than a row in
|
|
43
|
+
* a table that quietly stops being editable. The staff editor is wired to
|
|
44
|
+
* `'resend'`, so any other value silently produces a template that renders
|
|
45
|
+
* nothing — which is exactly how the `[aglyn.io]` subject survived so long.
|
|
46
|
+
*/
|
|
47
|
+
export type SystemEmailDeliveredBy = 'resend' | 'stripe';
|
|
48
|
+
export interface SystemEmailMergeToken {
|
|
49
|
+
/** Token as written in the template, without braces. */
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
/** Stand-in used by the staff preview. */
|
|
53
|
+
sample: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One block of a template's starting content (AGL-764). Declarative on
|
|
57
|
+
* purpose: the catalog stays a dependency-free data module, and the console
|
|
58
|
+
* turns these into email-plugin nodes (`emailSection` → `emailText`/
|
|
59
|
+
* `emailButton`) when it seeds the first version. Mirror the copy the send
|
|
60
|
+
* site uses today, with the same `{{tokens}}`, so a staffer who opens the
|
|
61
|
+
* editor starts from what recipients actually get rather than a blank canvas.
|
|
62
|
+
*/
|
|
63
|
+
export type SystemEmailDefaultBlock = {
|
|
64
|
+
block: 'text';
|
|
65
|
+
text: string;
|
|
66
|
+
variant?: 'heading' | 'subheading' | 'body' | 'caption';
|
|
67
|
+
} | {
|
|
68
|
+
block: 'button';
|
|
69
|
+
label: string;
|
|
70
|
+
href: string;
|
|
71
|
+
};
|
|
72
|
+
export interface SystemEmailTemplateDefinition {
|
|
73
|
+
/** Stable storage key — the Firestore document id. Never rename. */
|
|
74
|
+
key: string;
|
|
75
|
+
name: string;
|
|
76
|
+
description: string;
|
|
77
|
+
deliveredBy: SystemEmailDeliveredBy;
|
|
78
|
+
/** Subject used when no template has been published. */
|
|
79
|
+
defaultSubject: string;
|
|
80
|
+
mergeTokens: SystemEmailMergeToken[];
|
|
81
|
+
/**
|
|
82
|
+
* Starting content for the first version staff design (AGL-764). Every
|
|
83
|
+
* `resend` template should carry one; a `firebase` template has no editor
|
|
84
|
+
* so it needs none. Absent → the editor seeds a minimal placeholder.
|
|
85
|
+
*/
|
|
86
|
+
defaultBody?: readonly SystemEmailDefaultBlock[];
|
|
87
|
+
/**
|
|
88
|
+
* Where the fallback copy lives, for staff wondering what recipients get
|
|
89
|
+
* today. Informational — nothing reads it at runtime.
|
|
90
|
+
*/
|
|
91
|
+
source: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every system email Aglyn sends.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately **code-defined and fixed**: staff edit the system emails that
|
|
97
|
+
* exist, they do not create or delete them. Adding one is a code change,
|
|
98
|
+
* which is what keeps this list from drifting into advertising an email the
|
|
99
|
+
* product never actually sends — the failure mode a Firestore-backed list
|
|
100
|
+
* would have.
|
|
101
|
+
*
|
|
102
|
+
* The staff page renders from this registry, not from Firestore, so an email
|
|
103
|
+
* with no template designed yet still appears (as "Using default").
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Brand tokens EVERY system email resolves (AGL-2139).
|
|
107
|
+
*
|
|
108
|
+
* White-label used to invert precisely when staff published a template: the
|
|
109
|
+
* designed template wins over each sender's branded fallback, and the copy
|
|
110
|
+
* here hard-coded "Aglyn". Nor was there anything to design against — the
|
|
111
|
+
* merge maps carried `org.name`, `invite.role`, `signInUrl` and friends and
|
|
112
|
+
* nothing about the brand, while `blankUnresolvedTokens` DELETES any token
|
|
113
|
+
* the caller did not supply, so a designer who typed `{{brand.productName}}`
|
|
114
|
+
* would have shipped an email with a hole in the sentence.
|
|
115
|
+
*
|
|
116
|
+
* Declared once and appended to every template's own list rather than copied
|
|
117
|
+
* into each: 13 hand-maintained copies is how the token list falls behind the
|
|
118
|
+
* copy, and the editor renders this list as the palette a designer picks
|
|
119
|
+
* from — a token missing here is a token nobody knows exists.
|
|
120
|
+
*
|
|
121
|
+
* The samples are the DEPLOYMENT's own values (`BRAND.ORG_NAME`, AGL-2319),
|
|
122
|
+
* because that is what an unbranded org genuinely renders — on a self-host
|
|
123
|
+
* install a hardcoded "Aglyn" would preview a brand that appears nowhere in
|
|
124
|
+
* the mail the designer is editing.
|
|
125
|
+
*/
|
|
126
|
+
export declare const BRAND_MERGE_TOKENS: readonly SystemEmailMergeToken[];
|
|
127
|
+
/**
|
|
128
|
+
* Every system email Aglyn sends, each with the universal brand tokens
|
|
129
|
+
* appended to its own (AGL-2139).
|
|
130
|
+
*
|
|
131
|
+
* The template's own tokens come FIRST so the editor's palette still leads
|
|
132
|
+
* with what is specific to that email; the brand block reads as the shared
|
|
133
|
+
* footer it is.
|
|
134
|
+
*/
|
|
135
|
+
export declare const SYSTEM_EMAIL_TEMPLATES: readonly SystemEmailTemplateDefinition[];
|
|
136
|
+
/** Firestore collection holding the designed templates. */
|
|
137
|
+
export declare const SYSTEM_EMAIL_COLLECTION = "systemEmailTemplates";
|
|
138
|
+
export declare function getSystemEmailTemplate(key: string): SystemEmailTemplateDefinition | undefined;
|
|
139
|
+
/** True when a besigner template can actually change what is delivered. */
|
|
140
|
+
export declare function isSystemEmailEditable(definition: SystemEmailTemplateDefinition): boolean;
|
|
141
|
+
/** The stored template document, when staff have designed one. */
|
|
142
|
+
export interface SystemEmailTemplateDoc {
|
|
143
|
+
subject?: string;
|
|
144
|
+
preheader?: string;
|
|
145
|
+
versionId?: string;
|
|
146
|
+
updatedAt?: unknown;
|
|
147
|
+
updatedByEmail?: string;
|
|
148
|
+
}
|
|
149
|
+
/** A besigner node in the flat, denormalized map a version stores. */
|
|
150
|
+
export interface SystemEmailNode {
|
|
151
|
+
$id: string;
|
|
152
|
+
componentId: string;
|
|
153
|
+
pluginId?: string;
|
|
154
|
+
parentId?: string;
|
|
155
|
+
props?: Record<string, unknown>;
|
|
156
|
+
nodes?: string[];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Turns a template's `defaultBody` into the besigner node map the editor
|
|
160
|
+
* seeds and the send-time default renders (AGL-764/766).
|
|
161
|
+
*
|
|
162
|
+
* One place builds this so the two never diverge — the version a staffer
|
|
163
|
+
* opens is byte-for-byte what a test/send renders when nothing is published.
|
|
164
|
+
* Rooted at {@link EMAIL_NODE_ROOT_ID} so `renderEmailHtml` finds it (AGL-765),
|
|
165
|
+
* with deterministic ids: a version doc only needs ids unique within itself,
|
|
166
|
+
* and determinism keeps the map stable and diffable.
|
|
167
|
+
*/
|
|
168
|
+
export declare function buildDefaultEmailNodeMap(definition: {
|
|
169
|
+
defaultBody?: readonly SystemEmailDefaultBlock[];
|
|
170
|
+
}): Record<string, SystemEmailNode>;
|