@learncard/email-templates 1.0.1

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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/dist/branding.d.ts +36 -0
  4. package/dist/branding.d.ts.map +1 -0
  5. package/dist/branding.js +28 -0
  6. package/dist/branding.js.map +1 -0
  7. package/dist/components/CodeBlock.d.ts +16 -0
  8. package/dist/components/CodeBlock.d.ts.map +1 -0
  9. package/dist/components/CodeBlock.js +42 -0
  10. package/dist/components/CodeBlock.js.map +1 -0
  11. package/dist/components/EmailButton.d.ts +15 -0
  12. package/dist/components/EmailButton.d.ts.map +1 -0
  13. package/dist/components/EmailButton.js +20 -0
  14. package/dist/components/EmailButton.js.map +1 -0
  15. package/dist/components/IssuerLogo.d.ts +14 -0
  16. package/dist/components/IssuerLogo.d.ts.map +1 -0
  17. package/dist/components/IssuerLogo.js +23 -0
  18. package/dist/components/IssuerLogo.js.map +1 -0
  19. package/dist/components/Layout.d.ts +19 -0
  20. package/dist/components/Layout.d.ts.map +1 -0
  21. package/dist/components/Layout.js +95 -0
  22. package/dist/components/Layout.js.map +1 -0
  23. package/dist/components/LinkFallback.d.ts +12 -0
  24. package/dist/components/LinkFallback.d.ts.map +1 -0
  25. package/dist/components/LinkFallback.js +29 -0
  26. package/dist/components/LinkFallback.js.map +1 -0
  27. package/dist/index.d.ts +24 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +28 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/previews/_fixtures.d.ts +12 -0
  32. package/dist/previews/_fixtures.d.ts.map +1 -0
  33. package/dist/previews/_fixtures.js +34 -0
  34. package/dist/previews/_fixtures.js.map +1 -0
  35. package/dist/previews/account-approved-scenarios.d.ts +5 -0
  36. package/dist/previews/account-approved-scenarios.d.ts.map +1 -0
  37. package/dist/previews/account-approved-scenarios.js +7 -0
  38. package/dist/previews/account-approved-scenarios.js.map +1 -0
  39. package/dist/previews/endorsement-request-scenarios.d.ts +5 -0
  40. package/dist/previews/endorsement-request-scenarios.d.ts.map +1 -0
  41. package/dist/previews/endorsement-request-scenarios.js +7 -0
  42. package/dist/previews/endorsement-request-scenarios.js.map +1 -0
  43. package/dist/previews/guardian-approval-scenarios.d.ts +5 -0
  44. package/dist/previews/guardian-approval-scenarios.d.ts.map +1 -0
  45. package/dist/previews/guardian-approval-scenarios.js +7 -0
  46. package/dist/previews/guardian-approval-scenarios.js.map +1 -0
  47. package/dist/previews/guardian-credential-scenarios.d.ts +5 -0
  48. package/dist/previews/guardian-credential-scenarios.d.ts.map +1 -0
  49. package/dist/previews/guardian-credential-scenarios.js +11 -0
  50. package/dist/previews/guardian-credential-scenarios.js.map +1 -0
  51. package/dist/previews/inbox-claim-scenarios.d.ts +8 -0
  52. package/dist/previews/inbox-claim-scenarios.d.ts.map +1 -0
  53. package/dist/previews/inbox-claim-scenarios.js +7 -0
  54. package/dist/previews/inbox-claim-scenarios.js.map +1 -0
  55. package/dist/previews/recovery-key-scenarios.d.ts +5 -0
  56. package/dist/previews/recovery-key-scenarios.d.ts.map +1 -0
  57. package/dist/previews/recovery-key-scenarios.js +7 -0
  58. package/dist/previews/recovery-key-scenarios.js.map +1 -0
  59. package/dist/previews/verification-code-scenarios.d.ts +5 -0
  60. package/dist/previews/verification-code-scenarios.d.ts.map +1 -0
  61. package/dist/previews/verification-code-scenarios.js +7 -0
  62. package/dist/previews/verification-code-scenarios.js.map +1 -0
  63. package/dist/render.d.ts +185 -0
  64. package/dist/render.d.ts.map +1 -0
  65. package/dist/render.js +153 -0
  66. package/dist/render.js.map +1 -0
  67. package/dist/sms.d.ts +34 -0
  68. package/dist/sms.d.ts.map +1 -0
  69. package/dist/sms.js +34 -0
  70. package/dist/sms.js.map +1 -0
  71. package/dist/templates/account-approved.d.ts +17 -0
  72. package/dist/templates/account-approved.d.ts.map +1 -0
  73. package/dist/templates/account-approved.js +46 -0
  74. package/dist/templates/account-approved.js.map +1 -0
  75. package/dist/templates/credential-awaiting-guardian.d.ts +25 -0
  76. package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -0
  77. package/dist/templates/credential-awaiting-guardian.js +65 -0
  78. package/dist/templates/credential-awaiting-guardian.js.map +1 -0
  79. package/dist/templates/email-verification.d.ts +21 -0
  80. package/dist/templates/email-verification.d.ts.map +1 -0
  81. package/dist/templates/email-verification.js +51 -0
  82. package/dist/templates/email-verification.js.map +1 -0
  83. package/dist/templates/endorsement-request.d.ts +28 -0
  84. package/dist/templates/endorsement-request.d.ts.map +1 -0
  85. package/dist/templates/endorsement-request.js +67 -0
  86. package/dist/templates/endorsement-request.js.map +1 -0
  87. package/dist/templates/guardian-approval.d.ts +23 -0
  88. package/dist/templates/guardian-approval.d.ts.map +1 -0
  89. package/dist/templates/guardian-approval.js +50 -0
  90. package/dist/templates/guardian-approval.js.map +1 -0
  91. package/dist/templates/guardian-approved-claim.d.ts +22 -0
  92. package/dist/templates/guardian-approved-claim.d.ts.map +1 -0
  93. package/dist/templates/guardian-approved-claim.js +49 -0
  94. package/dist/templates/guardian-approved-claim.js.map +1 -0
  95. package/dist/templates/guardian-credential-approval.d.ts +27 -0
  96. package/dist/templates/guardian-credential-approval.d.ts.map +1 -0
  97. package/dist/templates/guardian-credential-approval.js +51 -0
  98. package/dist/templates/guardian-credential-approval.js.map +1 -0
  99. package/dist/templates/guardian-email-otp.d.ts +16 -0
  100. package/dist/templates/guardian-email-otp.d.ts.map +1 -0
  101. package/dist/templates/guardian-email-otp.js +49 -0
  102. package/dist/templates/guardian-email-otp.js.map +1 -0
  103. package/dist/templates/guardian-rejected-credential.d.ts +24 -0
  104. package/dist/templates/guardian-rejected-credential.d.ts.map +1 -0
  105. package/dist/templates/guardian-rejected-credential.js +44 -0
  106. package/dist/templates/guardian-rejected-credential.js.map +1 -0
  107. package/dist/templates/inbox-claim.d.ts +28 -0
  108. package/dist/templates/inbox-claim.d.ts.map +1 -0
  109. package/dist/templates/inbox-claim.js +58 -0
  110. package/dist/templates/inbox-claim.js.map +1 -0
  111. package/dist/templates/index.d.ts +25 -0
  112. package/dist/templates/index.d.ts.map +1 -0
  113. package/dist/templates/index.js +13 -0
  114. package/dist/templates/index.js.map +1 -0
  115. package/dist/templates/recovery-key.d.ts +15 -0
  116. package/dist/templates/recovery-key.d.ts.map +1 -0
  117. package/dist/templates/recovery-key.js +46 -0
  118. package/dist/templates/recovery-key.js.map +1 -0
  119. package/dist/templates/verification-code.d.ts +24 -0
  120. package/dist/templates/verification-code.d.ts.map +1 -0
  121. package/dist/templates/verification-code.js +87 -0
  122. package/dist/templates/verification-code.js.map +1 -0
  123. package/dist/tenant-registry.d.ts +48 -0
  124. package/dist/tenant-registry.d.ts.map +1 -0
  125. package/dist/tenant-registry.js +146 -0
  126. package/dist/tenant-registry.js.map +1 -0
  127. package/package.json +40 -0
@@ -0,0 +1,46 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * RecoveryKey — email containing a user's account recovery key.
4
+ *
5
+ * Used by: lca-api keys.ts (templateAlias: RECOVERY_KEY_TEMPLATE_ALIAS)
6
+ */
7
+ import { Text } from '@react-email/components';
8
+ import { DEFAULT_BRANDING } from '../branding';
9
+ import { Layout } from '../components/Layout';
10
+ import { CodeBlock } from '../components/CodeBlock';
11
+ export const RecoveryKey = ({ branding, recoveryKey, }) => (_jsxs(Layout, { branding: branding, preview: `Your ${branding.brandName} recovery key — keep this safe`, children: [_jsxs(Text, { style: heading, children: ["Your ", branding.brandName, " Recovery Key"] }), _jsx(Text, { style: paragraph, children: "Hello," }), _jsxs(Text, { style: paragraph, children: ["Keep this email safe. You can use the recovery key below to regain access to your ", branding.brandName, " account if you lose your device."] }), _jsx(CodeBlock, { code: recoveryKey, label: "Recovery Key \u2014 do NOT share this with anyone", variant: "key" }), _jsxs(Text, { style: paragraph, children: ["To recover your account, choose ", _jsx("strong", { children: "\u201CRecover via Email\u201D" }), " in the app and paste the recovery key above when prompted."] }), _jsx(Text, { style: muted, children: "If you did not request this, you can safely ignore this email." }), _jsxs(Text, { style: paragraph, children: [branding.brandName, " is your private, digital passport for learning and work. It lets you securely collect and share your verified skills and achievements online."] }), _jsxs(Text, { style: signOff, children: ["Sincerely,", _jsx("br", {}), "The ", branding.brandName, " Team"] })] }));
12
+ export const getRecoveryKeySubject = (branding) => `Your ${branding.brandName} recovery key`;
13
+ // ---------------------------------------------------------------------------
14
+ // Styles
15
+ // ---------------------------------------------------------------------------
16
+ const heading = {
17
+ fontSize: 24,
18
+ fontWeight: 600,
19
+ color: '#111827',
20
+ margin: '0 0 24px',
21
+ };
22
+ const paragraph = {
23
+ fontSize: 16,
24
+ color: '#374151',
25
+ lineHeight: '24px',
26
+ margin: '0 0 24px',
27
+ };
28
+ const muted = {
29
+ fontSize: 14,
30
+ color: '#6b7280',
31
+ lineHeight: '20px',
32
+ margin: '0 0 24px',
33
+ };
34
+ const signOff = {
35
+ fontSize: 14,
36
+ color: '#374151',
37
+ lineHeight: '20px',
38
+ margin: '24px 0 0',
39
+ };
40
+ // ---------------------------------------------------------------------------
41
+ // Preview
42
+ // ---------------------------------------------------------------------------
43
+ export default function Preview() {
44
+ return (_jsx(RecoveryKey, { branding: DEFAULT_BRANDING, recoveryKey: "mango-delta-fox-echo-bravo-seven-lima-niner" }));
45
+ }
46
+ //# sourceMappingURL=recovery-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-key.js","sourceRoot":"","sources":["../../src/templates/recovery-key.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAI/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAOpD,MAAM,CAAC,MAAM,WAAW,GAA+B,CAAC,EACpD,QAAQ,EACR,WAAW,GACd,EAAE,EAAE,CAAC,CACF,MAAC,MAAM,IAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,QAAQ,CAAC,SAAS,gCAAgC,aAC3F,MAAC,IAAI,IAAC,KAAK,EAAE,OAAO,sBAAQ,QAAQ,CAAC,SAAS,qBAAqB,EAEnE,KAAC,IAAI,IAAC,KAAK,EAAE,SAAS,uBAAe,EAErC,MAAC,IAAI,IAAC,KAAK,EAAE,SAAS,mGAET,QAAQ,CAAC,SAAS,yCACxB,EAEP,KAAC,SAAS,IAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAC,mDAA8C,EAAC,OAAO,EAAC,KAAK,GAAG,EAEnG,MAAC,IAAI,IAAC,KAAK,EAAE,SAAS,iDACc,6DAAgD,mEAE7E,EAEP,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,+EAEX,EAEP,MAAC,IAAI,IAAC,KAAK,EAAE,SAAS,aACjB,QAAQ,CAAC,SAAS,sJAEhB,EAEP,MAAC,IAAI,IAAC,KAAK,EAAE,OAAO,2BACN,cAAM,UAAK,QAAQ,CAAC,SAAS,aACpC,IACF,CACZ,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,QAAwB,EAAU,EAAE,CACtE,QAAQ,QAAQ,CAAC,SAAS,eAAe,CAAC;AAE9C,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E,MAAM,OAAO,GAAwB;IACjC,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,GAAG;IACf,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,UAAU;CACrB,CAAC;AAEF,MAAM,SAAS,GAAwB;IACnC,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,SAAS;IAChB,UAAU,EAAE,MAAM;IAClB,MAAM,EAAE,UAAU;CACrB,CAAC;AAEF,MAAM,KAAK,GAAwB;IAC/B,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,SAAS;IAChB,UAAU,EAAE,MAAM;IAClB,MAAM,EAAE,UAAU;CACrB,CAAC;AAEF,MAAM,OAAO,GAAwB;IACjC,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,SAAS;IAChB,UAAU,EAAE,MAAM;IAClB,MAAM,EAAE,UAAU;CACrB,CAAC;AAEF,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,MAAM,CAAC,OAAO,UAAU,OAAO;IAC3B,OAAO,CACH,KAAC,WAAW,IACR,QAAQ,EAAE,gBAAgB,EAC1B,WAAW,EAAC,6CAA6C,GAC3D,CACL,CAAC;AACN,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * VerificationCode — shared template for all code-based verification emails.
3
+ *
4
+ * Used by:
5
+ * - login-verification-code (lca-api firebase.ts)
6
+ * - recovery-email-code (lca-api keys.ts)
7
+ * - embed-email-verification (brain-service contact-methods.ts)
8
+ * - contact-method-verification (brain-service contact-methods.ts)
9
+ *
10
+ * The `variant` prop controls the subject line and descriptive text.
11
+ */
12
+ import * as React from 'react';
13
+ import type { TenantBranding } from '../branding';
14
+ export type VerificationCodeVariant = 'login' | 'recovery-email' | 'embed-verification' | 'contact-method';
15
+ export interface VerificationCodeProps {
16
+ branding: TenantBranding;
17
+ verificationCode: string;
18
+ verificationEmail?: string;
19
+ variant: VerificationCodeVariant;
20
+ }
21
+ export declare const VerificationCode: React.FC<VerificationCodeProps>;
22
+ export declare const getVerificationCodeSubject: (branding: TenantBranding, variant: VerificationCodeVariant) => string;
23
+ export default function Preview(): import("react/jsx-runtime").JSX.Element;
24
+ //# sourceMappingURL=verification-code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-code.d.ts","sourceRoot":"","sources":["../../src/templates/verification-code.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,MAAM,MAAM,uBAAuB,GAC7B,OAAO,GACP,gBAAgB,GAChB,oBAAoB,GACpB,gBAAgB,CAAC;AAEvB,MAAM,WAAW,qBAAqB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,uBAAuB,CAAC;CACpC;AAwCD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA8B5D,CAAC;AAEF,eAAO,MAAM,0BAA0B,aACzB,cAAc,WACf,uBAAuB,KACjC,MAA6D,CAAC;AAsCjE,MAAM,CAAC,OAAO,UAAU,OAAO,4CAS9B"}
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * VerificationCode — shared template for all code-based verification emails.
4
+ *
5
+ * Used by:
6
+ * - login-verification-code (lca-api firebase.ts)
7
+ * - recovery-email-code (lca-api keys.ts)
8
+ * - embed-email-verification (brain-service contact-methods.ts)
9
+ * - contact-method-verification (brain-service contact-methods.ts)
10
+ *
11
+ * The `variant` prop controls the subject line and descriptive text.
12
+ */
13
+ import { Text } from '@react-email/components';
14
+ import { DEFAULT_BRANDING } from '../branding';
15
+ import { Layout } from '../components/Layout';
16
+ import { CodeBlock } from '../components/CodeBlock';
17
+ const VARIANT_CONFIG = {
18
+ login: {
19
+ subject: (b) => `Your ${b} login code`,
20
+ heading: (b) => `Your ${b} Login Code`,
21
+ description: (email, b) => email
22
+ ? `Here's your secure 6-digit code to log into ${b}`
23
+ : `Here's your secure 6-digit code to log into ${b}`,
24
+ expiry: 'This code will expire in 5 minutes. If you didn\u2019t request this, you can safely ignore this email.',
25
+ },
26
+ 'recovery-email': {
27
+ subject: () => 'Verify your recovery email',
28
+ heading: (b) => `Your ${b} Verification Code`,
29
+ description: (email, b) => email
30
+ ? `Here's your secure 6-digit code to add ${email} as a recovery method for ${b}`
31
+ : `Here's your secure 6-digit code to add a recovery method for ${b}`,
32
+ expiry: 'This code will expire in 5 minutes. If you didn\u2019t request this, you can safely ignore this email.',
33
+ },
34
+ 'embed-verification': {
35
+ subject: (b) => `Your ${b} verification code`,
36
+ heading: (b) => `Your ${b} Verification Code`,
37
+ description: (email) => email
38
+ ? `Enter this code to verify ${email} and claim your credential:`
39
+ : 'Enter this code to verify your email address and claim your credential:',
40
+ expiry: 'This code will expire in 10 minutes. If you didn\u2019t request this, you can safely ignore this email.',
41
+ },
42
+ 'contact-method': {
43
+ subject: (b) => `Your ${b} verification code`,
44
+ heading: (b) => `Your ${b} Verification Code`,
45
+ description: () => 'Enter this code in the app to verify your contact information.',
46
+ expiry: 'This code will expire in 24 hours. If you didn\u2019t request this, you can safely ignore this email.',
47
+ },
48
+ };
49
+ export const VerificationCode = ({ branding, verificationCode, verificationEmail, variant, }) => {
50
+ const config = VARIANT_CONFIG[variant];
51
+ return (_jsxs(Layout, { branding: branding, preview: `Your code: ${verificationCode}`, children: [_jsx(Text, { style: headingStyle, children: config.heading(branding.brandName) }), _jsx(Text, { style: paragraph, children: "Hello," }), _jsx(Text, { style: paragraph, children: config.description(verificationEmail, branding.brandName) }), _jsx(CodeBlock, { code: verificationCode, label: "Verification code" }), _jsx(Text, { style: muted, children: config.expiry }), _jsxs(Text, { style: paragraph, children: [branding.brandName, " is your private, digital passport for learning and work. It lets you securely collect and share your verified skills and achievements online."] }), _jsxs(Text, { style: signOff, children: ["Sincerely,", _jsx("br", {}), "The ", branding.brandName, " Team"] })] }));
52
+ };
53
+ export const getVerificationCodeSubject = (branding, variant) => VARIANT_CONFIG[variant].subject(branding.brandName);
54
+ // ---------------------------------------------------------------------------
55
+ // Styles
56
+ // ---------------------------------------------------------------------------
57
+ const headingStyle = {
58
+ fontSize: 24,
59
+ fontWeight: 600,
60
+ color: '#111827',
61
+ margin: '0 0 24px',
62
+ };
63
+ const paragraph = {
64
+ fontSize: 16,
65
+ color: '#374151',
66
+ lineHeight: '24px',
67
+ margin: '0 0 24px',
68
+ };
69
+ const muted = {
70
+ fontSize: 14,
71
+ color: '#6b7280',
72
+ lineHeight: '20px',
73
+ margin: '0 0 24px',
74
+ };
75
+ const signOff = {
76
+ fontSize: 14,
77
+ color: '#374151',
78
+ lineHeight: '20px',
79
+ margin: '24px 0 0',
80
+ };
81
+ // ---------------------------------------------------------------------------
82
+ // Preview (used by `pnpm dev` / react-email dev server)
83
+ // ---------------------------------------------------------------------------
84
+ export default function Preview() {
85
+ return (_jsx(VerificationCode, { branding: DEFAULT_BRANDING, verificationCode: "847293", verificationEmail: "jane@example.com", variant: "login" }));
86
+ }
87
+ //# sourceMappingURL=verification-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-code.js","sourceRoot":"","sources":["../../src/templates/verification-code.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Server-side tenant resolution for brain-service and lca-api.
3
+ *
4
+ * Resolution order (first match wins):
5
+ * 1. Explicit `X-Tenant-Id` header — native apps, any client that knows its tenant
6
+ * 2. `Origin` / `Referer` header hostname — web apps (e.g. alpha.vetpass.app → vetpass)
7
+ * 3. `DEFAULT_TENANT_ID` env var — per-tenant deploys, cron jobs, server-to-server
8
+ * 4. Fallback to 'learncard'
9
+ *
10
+ * The registry maps tenant IDs to email branding overrides. Missing fields
11
+ * fall back to LearnCard defaults via `resolveBranding()`.
12
+ */
13
+ import type { TenantBranding } from './branding';
14
+ export interface ResolvedTenant {
15
+ /** Canonical tenant identifier, e.g. 'vetpass', 'learncard'. */
16
+ id: string;
17
+ /** Partial email branding — pass to `resolveBranding()` for full defaults. */
18
+ emailBranding: Partial<TenantBranding>;
19
+ /** How the tenant was resolved — useful for logging / debugging. */
20
+ resolvedVia: 'header' | 'origin' | 'env' | 'default';
21
+ }
22
+ export interface RequestHeaders {
23
+ /** Value of the X-Tenant-Id header. */
24
+ 'x-tenant-id'?: string | undefined;
25
+ /** Origin header (e.g. "https://alpha.vetpass.app"). */
26
+ origin?: string | undefined;
27
+ /** Referer header (fallback for Origin). */
28
+ referer?: string | undefined;
29
+ [key: string]: string | string[] | undefined;
30
+ }
31
+ /**
32
+ * Resolve the tenant from HTTP request headers.
33
+ *
34
+ * @param headers — Request headers object. Accepts plain objects, Node IncomingHttpHeaders, etc.
35
+ * @param envTenantId — Override for `process.env.DEFAULT_TENANT_ID` (useful for testing).
36
+ */
37
+ export declare const resolveTenantFromRequest: (headers: RequestHeaders, envTenantId?: string) => ResolvedTenant;
38
+ /**
39
+ * Register additional origin → tenant mappings at startup.
40
+ * Useful for dev/staging domains configured via env vars.
41
+ */
42
+ export declare const registerOriginMapping: (hostname: string, tenantId: string) => void;
43
+ /**
44
+ * Register or update email branding for a tenant at startup.
45
+ * Merges with any existing branding (does not replace).
46
+ */
47
+ export declare const registerTenantBranding: (tenantId: string, branding: Partial<TenantBranding>) => void;
48
+ //# sourceMappingURL=tenant-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-registry.d.ts","sourceRoot":"","sources":["../src/tenant-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAMjD,MAAM,WAAW,cAAc;IAC3B,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;IAEX,8EAA8E;IAC9E,aAAa,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAEvC,oEAAoE;IACpE,WAAW,EAAE,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;CACxD;AAED,MAAM,WAAW,cAAc;IAC3B,uCAAuC;IACvC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEnC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE7B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;CAChD;AAkFD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,YACxB,cAAc,gBACT,MAAM,KACrB,cAgCF,CAAC;AAsBF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,aAAc,MAAM,YAAY,MAAM,KAAG,IAE1E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,aAAc,MAAM,YAAY,OAAO,CAAC,cAAc,CAAC,KAAG,IAK5F,CAAC"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Server-side tenant resolution for brain-service and lca-api.
3
+ *
4
+ * Resolution order (first match wins):
5
+ * 1. Explicit `X-Tenant-Id` header — native apps, any client that knows its tenant
6
+ * 2. `Origin` / `Referer` header hostname — web apps (e.g. alpha.vetpass.app → vetpass)
7
+ * 3. `DEFAULT_TENANT_ID` env var — per-tenant deploys, cron jobs, server-to-server
8
+ * 4. Fallback to 'learncard'
9
+ *
10
+ * The registry maps tenant IDs to email branding overrides. Missing fields
11
+ * fall back to LearnCard defaults via `resolveBranding()`.
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // Origin → tenant ID mapping
15
+ //
16
+ // Add new entries when a tenant gets a web deployment.
17
+ // Supports exact hostname match and wildcard subdomains.
18
+ // ---------------------------------------------------------------------------
19
+ const ORIGIN_MAP = {
20
+ // VetPass
21
+ 'vetpass.app': 'vetpass',
22
+ 'alpha.vetpass.app': 'vetpass',
23
+ 'staging.vetpass.app': 'vetpass',
24
+ // LearnCard
25
+ 'learncard.app': 'learncard',
26
+ 'staging.learncard.app': 'learncard',
27
+ 'app.learncard.com': 'learncard',
28
+ // Local dev
29
+ 'localhost': 'learncard',
30
+ };
31
+ // ---------------------------------------------------------------------------
32
+ // Tenant email branding registry
33
+ //
34
+ // Only override fields that differ from LearnCard defaults.
35
+ // Missing fields → resolveBranding() fills in DEFAULT_BRANDING.
36
+ // ---------------------------------------------------------------------------
37
+ const TENANT_EMAIL_BRANDING = {
38
+ learncard: {},
39
+ vetpass: {
40
+ brandName: 'VetPass',
41
+ logoUrl: 'https://vetpass.app/assets/icon/icon.png',
42
+ logoAlt: 'VetPass',
43
+ primaryColor: '#1B5E20',
44
+ primaryTextColor: '#ffffff',
45
+ supportEmail: 'support@vetpass.app',
46
+ websiteUrl: 'https://www.vetpass.app',
47
+ appUrl: 'https://vetpass.app',
48
+ fromDomain: 'vetpass.app',
49
+ copyrightHolder: 'VetPass',
50
+ },
51
+ };
52
+ // ---------------------------------------------------------------------------
53
+ // Resolution
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Extract a hostname from a URL string. Returns undefined if parsing fails.
57
+ */
58
+ const extractHostname = (urlString) => {
59
+ try {
60
+ return new URL(urlString).hostname;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ };
66
+ /**
67
+ * Look up tenant ID from a hostname, checking exact match first,
68
+ * then stripping subdomains progressively (e.g. "foo.bar.vetpass.app" → "vetpass.app").
69
+ */
70
+ const tenantIdFromHostname = (hostname) => {
71
+ if (ORIGIN_MAP[hostname])
72
+ return ORIGIN_MAP[hostname];
73
+ // Strip subdomains: "alpha.vetpass.app" → "vetpass.app"
74
+ const parts = hostname.split('.');
75
+ for (let i = 1; i < parts.length - 1; i++) {
76
+ const parent = parts.slice(i).join('.');
77
+ if (ORIGIN_MAP[parent])
78
+ return ORIGIN_MAP[parent];
79
+ }
80
+ return undefined;
81
+ };
82
+ /**
83
+ * Resolve the tenant from HTTP request headers.
84
+ *
85
+ * @param headers — Request headers object. Accepts plain objects, Node IncomingHttpHeaders, etc.
86
+ * @param envTenantId — Override for `process.env.DEFAULT_TENANT_ID` (useful for testing).
87
+ */
88
+ export const resolveTenantFromRequest = (headers, envTenantId) => {
89
+ // 1. Explicit X-Tenant-Id header (native apps, any informed client)
90
+ const explicit = normalizeHeader(headers['x-tenant-id']);
91
+ if (explicit) {
92
+ return buildTenant(explicit, 'header');
93
+ }
94
+ // 2. Origin / Referer header (web apps)
95
+ const origin = normalizeHeader(headers.origin) || normalizeHeader(headers.referer);
96
+ if (origin) {
97
+ const hostname = extractHostname(origin);
98
+ if (hostname) {
99
+ const matched = tenantIdFromHostname(hostname);
100
+ if (matched) {
101
+ return buildTenant(matched, 'origin');
102
+ }
103
+ }
104
+ }
105
+ // 3. Deploy-level env var
106
+ const envDefault = envTenantId ?? process.env.DEFAULT_TENANT_ID;
107
+ if (envDefault) {
108
+ return buildTenant(envDefault, 'env');
109
+ }
110
+ // 4. Safe default
111
+ return buildTenant('learncard', 'default');
112
+ };
113
+ // ---------------------------------------------------------------------------
114
+ // Helpers
115
+ // ---------------------------------------------------------------------------
116
+ const normalizeHeader = (value) => {
117
+ if (Array.isArray(value))
118
+ return value[0];
119
+ return value || undefined;
120
+ };
121
+ const buildTenant = (id, resolvedVia) => ({
122
+ id,
123
+ emailBranding: TENANT_EMAIL_BRANDING[id] ?? {},
124
+ resolvedVia,
125
+ });
126
+ // ---------------------------------------------------------------------------
127
+ // Admin helpers (for extending the registry at runtime via env vars)
128
+ // ---------------------------------------------------------------------------
129
+ /**
130
+ * Register additional origin → tenant mappings at startup.
131
+ * Useful for dev/staging domains configured via env vars.
132
+ */
133
+ export const registerOriginMapping = (hostname, tenantId) => {
134
+ ORIGIN_MAP[hostname] = tenantId;
135
+ };
136
+ /**
137
+ * Register or update email branding for a tenant at startup.
138
+ * Merges with any existing branding (does not replace).
139
+ */
140
+ export const registerTenantBranding = (tenantId, branding) => {
141
+ TENANT_EMAIL_BRANDING[tenantId] = {
142
+ ...TENANT_EMAIL_BRANDING[tenantId],
143
+ ...branding,
144
+ };
145
+ };
146
+ //# sourceMappingURL=tenant-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-registry.js","sourceRoot":"","sources":["../src/tenant-registry.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@learncard/email-templates",
3
+ "version": "1.0.1",
4
+ "description": "Git-managed, tenant-branded email and SMS templates for LearnCard services",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "dependencies": {
11
+ "@react-email/components": "^0.0.36",
12
+ "@react-email/render": "^1.0.5",
13
+ "react": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^18.3.0",
17
+ "react-email": "^3.0.0",
18
+ "typescript": "^5.0.0",
19
+ "vitest": "^1.0.0"
20
+ },
21
+ "author": "Learning Economy Foundation (www.learningeconomy.io)",
22
+ "license": "MIT",
23
+ "homepage": "https://github.com/learningeconomy/LearnCard/tree/main/packages/email-templates/README.md",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/learningeconomy/LearnCard"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/learningeconomy/LearnCard/issues"
30
+ },
31
+ "sideEffects": false,
32
+ "scripts": {
33
+ "dev": "email dev --dir src --port 3333",
34
+ "preview": "email dev --dir src --port 3333",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "typecheck": "tsc --noEmit",
38
+ "build": "tsc"
39
+ }
40
+ }