@betterinternship/core 2.26.2 → 2.27.0

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/README.md CHANGED
@@ -1,26 +1,26 @@
1
- # Package.Core
2
-
3
- Contains dependencies shared between the different services of the site.
4
-
5
- ## How to update
6
-
7
- 1. Build the package:
8
-
9
- ```bash
10
- npm run build
11
- ```
12
-
13
- 2. Commit the build:
14
-
15
- ```bash
16
- git add .
17
- git commit -m "<follow conventional commits, k thanks>"
18
- git push
19
- ```
20
-
21
- 3. Update the version and publish the package to NPM:
22
-
23
- ```bash
24
- npm version <patch|minor|major>
25
- npm publish
26
- ```
1
+ # Package.Core
2
+
3
+ Contains dependencies shared between the different services of the site.
4
+
5
+ ## How to update
6
+
7
+ 1. Build the package:
8
+
9
+ ```bash
10
+ npm run build
11
+ ```
12
+
13
+ 2. Commit the build:
14
+
15
+ ```bash
16
+ git add .
17
+ git commit -m "<follow conventional commits, k thanks>"
18
+ git push
19
+ ```
20
+
21
+ 3. Update the version and publish the package to NPM:
22
+
23
+ ```bash
24
+ npm version <patch|minor|major>
25
+ npm publish
26
+ ```
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './lib/env.js';
2
2
  export * from './lib/email/index.js';
3
3
  export * from './lib/forms/index.js';
4
4
  export * from './lib/partners/forms/index.js';
5
+ export * from './lib/links/index.js';
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export * from './lib/env.js';
2
2
  export * from './lib/email/index.js';
3
3
  export * from './lib/forms/index.js';
4
4
  export * from './lib/partners/forms/index.js';
5
+ export * from './lib/links/index.js';
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC"}
@@ -6,10 +6,9 @@ const escapeHtml = (value) => value
6
6
  .replaceAll("'", '&#39;');
7
7
  export const renderEmailAction = ({ href, label }) => {
8
8
  const escapedHref = escapeHtml(href);
9
- return `<div style="margin:24px 0 0">
10
- <a href="${escapedHref}" style="display:inline-block;padding:12px 22px;background:#155eef;color:#ffffff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:700">${escapeHtml(label)} →</a>
11
- <p style="margin:14px 0 0;font-size:12px;line-height:1.5;color:#64748b">Button not working? Copy and paste this link into your browser:</p>
12
- <p style="margin:5px 0 0;font-size:12px;line-height:1.5;word-break:break-all"><a href="${escapedHref}" style="color:#155eef;text-decoration:underline;word-break:break-all">${escapedHref}</a></p>
9
+ return `<div style="margin:20px 0 0">
10
+ <a href="${escapedHref}" style="display:inline-block;padding:12px 22px;background:#155eef;color:#ffffff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:700">${escapeHtml(label)} →</a>
11
+ <p style="margin:8px 0 0;font-size:11px;line-height:1.45;color:#8a94a8;word-break:break-all">Or copy this link: <a href="${escapedHref}" style="color:#64748b;text-decoration:underline;word-break:break-all">${escapedHref}</a></p>
13
12
  </div>`;
14
13
  };
15
14
  //# sourceMappingURL=action.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","sourceRoot":"","sources":["../../../lib/email/action.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,GAAG,CAAC,KAAa,EAAE,EAAE,CACnC,KAAK;KACF,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;KACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;KACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;KACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;KACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAG9B,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAsB,EAAE,EAAE;IACvE,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IACrC,OAAO;aACI,WAAW,2JAA2J,UAAU,CAAC,KAAK,CAAC;;2FAEzG,WAAW,0EAA0E,WAAW;OACpL,CAAC;AACR,CAAC,CAAC"}
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../../../lib/email/action.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,GAAG,CAAC,KAAa,EAAE,EAAE,CACnC,KAAK;KACF,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;KACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;KACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;KACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;KACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAG9B,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAsB,EAAE,EAAE;IACvE,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IACrC,OAAO;aACI,WAAW,2JAA2J,UAAU,CAAC,KAAK,CAAC;6HACvE,WAAW,0EAA0E,WAAW;OACtN,CAAC;AACR,CAAC,CAAC"}
@@ -26,26 +26,26 @@ export const writeEmailPreviewCatalog = ({ title, previews, outputDirectory, })
26
26
  });
27
27
  const escapedTitle = escapeHtml(title);
28
28
  const catalogPath = join(outputDirectory, 'index.html');
29
- writeFileSync(catalogPath, `<!doctype html>
30
- <html lang="en">
31
- <head>
32
- <meta charset="utf-8" />
33
- <title>${escapedTitle}</title>
34
- <style>
35
- body { margin: 0; padding: 32px; background: #eef2f7; color: #17213d; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
36
- h1 { margin: 0 0 8px; }
37
- p { margin: 0 0 32px; color: #5c6880; }
38
- section { max-width: 680px; margin: 0 auto 36px; }
39
- h2 { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0 0 10px; }
40
- h2 span { padding: 3px 7px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
41
- iframe { width: 100%; height: 760px; border: 1px solid #d7deea; background: #fff; border-radius: 8px; }
42
- </style>
43
- </head>
44
- <body>
45
- <h1>${escapedTitle}</h1>
46
- <p>Generated from the production email factories and their templates.</p>
47
- ${catalogItems.join('\n ')}
48
- </body>
29
+ writeFileSync(catalogPath, `<!doctype html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8" />
33
+ <title>${escapedTitle}</title>
34
+ <style>
35
+ body { margin: 0; padding: 32px; background: #eef2f7; color: #17213d; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
36
+ h1 { margin: 0 0 8px; }
37
+ p { margin: 0 0 32px; color: #5c6880; }
38
+ section { max-width: 680px; margin: 0 auto 36px; }
39
+ h2 { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0 0 10px; }
40
+ h2 span { padding: 3px 7px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
41
+ iframe { width: 100%; height: 760px; border: 1px solid #d7deea; background: #fff; border-radius: 8px; }
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <h1>${escapedTitle}</h1>
46
+ <p>Generated from the production email factories and their templates.</p>
47
+ ${catalogItems.join('\n ')}
48
+ </body>
49
49
  </html>`);
50
50
  return catalogPath;
51
51
  };
@@ -4,27 +4,27 @@ const escapeHtml = (value) => value
4
4
  .replaceAll('>', '&gt;')
5
5
  .replaceAll('"', '&quot;')
6
6
  .replaceAll("'", '&#39;');
7
- export const renderEmailShell = ({ logoUrl, heading, bodyHtml, footerHtml = 'This is an automated message from BetterInternship.', }) => `<!doctype html>
8
- <html lang="en">
9
- <body style="margin:0;background:#ffffff;color:#17213d;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif">
10
- <div style="padding:40px 16px">
11
- <div style="margin:0 auto 20px;text-align:center">
12
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto">
13
- <tr>
14
- <td style="vertical-align:middle"><img src="${escapeHtml(logoUrl)}" alt="BetterInternship" width="30" height="30" style="width:30px;height:30px;display:block;object-fit:contain" /></td>
15
- <td style="padding-left:9px;vertical-align:middle;font-size:19px;font-weight:700;color:#111827">${escapeHtml(heading)}</td>
16
- </tr>
17
- </table>
18
- </div>
19
-
20
- <div style="width:100%;max-width:520px;margin:0 auto;background:#ffffff;border:1px solid #e4e8f0;border-radius:14px;overflow:hidden">
21
- <div style="padding:34px 32px 36px">
22
- ${bodyHtml}
23
- </div>
24
-
25
- <div style="padding:16px 32px;background:#fafbfc;border-top:1px solid #edf0f5;color:#8a94a8;font-size:12px;line-height:1.5">${footerHtml}</div>
26
- </div>
27
- </div>
28
- </body>
7
+ export const renderEmailShell = ({ logoUrl, heading, bodyHtml, footerHtml = 'This is an automated message from BetterInternship.', }) => `<!doctype html>
8
+ <html lang="en">
9
+ <body style="margin:0;background:#ffffff;color:#17213d;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif">
10
+ <div style="padding:40px 16px">
11
+ <div style="margin:0 auto 20px;text-align:center">
12
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 auto">
13
+ <tr>
14
+ <td style="vertical-align:middle"><img src="${escapeHtml(logoUrl)}" alt="BetterInternship" width="30" height="30" style="width:30px;height:30px;display:block;object-fit:contain" /></td>
15
+ <td style="padding-left:9px;vertical-align:middle;font-size:19px;font-weight:700;color:#111827">${escapeHtml(heading)}</td>
16
+ </tr>
17
+ </table>
18
+ </div>
19
+
20
+ <div style="width:100%;max-width:520px;margin:0 auto;background:#ffffff;border:1px solid #e4e8f0;border-radius:14px;overflow:hidden">
21
+ <div style="padding:34px 32px 36px;font-size:15px;line-height:1.7;color:#4b5872">
22
+ ${bodyHtml}
23
+ </div>
24
+
25
+ <div style="padding:16px 32px;background:#fafbfc;border-top:1px solid #edf0f5;color:#8a94a8;font-size:12px;line-height:1.5">${footerHtml}</div>
26
+ </div>
27
+ </div>
28
+ </body>
29
29
  </html>`;
30
30
  //# sourceMappingURL=shell.js.map
@@ -5,14 +5,14 @@ const LONG_TEXT_VALIDATOR = `${TEXT_PREPROCESS}z.string().describe("textarea"))`
5
5
  const NUMBER_VALIDATOR = 'z.number()';
6
6
  const SIGNATURE_VALIDATOR = `${TEXT_PREPROCESS}z.string().nonempty({ message: "This field is required." }))`;
7
7
  const DROPDOWN_VALIDATOR = `z.enum(["Option 1", "Option 2"], {message:"This field is required."})`;
8
- const MULTISELECT_VALIDATOR = `z.array(
9
- z.enum(
10
- [
11
- "Option 1",
12
- "Option 2"
13
- ],
14
- { message: "This field is required." }
15
- )
8
+ const MULTISELECT_VALIDATOR = `z.array(
9
+ z.enum(
10
+ [
11
+ "Option 1",
12
+ "Option 2"
13
+ ],
14
+ { message: "This field is required." }
15
+ )
16
16
  ).describe("multiselect")`;
17
17
  const DATE_VALIDATOR = 'z.coerce.date()';
18
18
  const TIME_VALIDATOR = 'z.string().describe("time")';
@@ -0,0 +1,10 @@
1
+ export declare const LINK_HOSTS: readonly ["career", "hire", "docs", "moa", "uni", "admin", "api"];
2
+ export type LinkHost = (typeof LINK_HOSTS)[number];
3
+ export type LinkHostBases = Readonly<Partial<Record<LinkHost, string>>>;
4
+ export declare const isLinkHost: (value: string) => value is LinkHost;
5
+ export declare const linkHostBase: (host: LinkHost, bases: LinkHostBases) => string;
6
+ export declare const isApprovedLinkHostPair: (shortHost: LinkHost, destinationHost: LinkHost) => boolean;
7
+ export declare const assertApprovedLinkHostPair: (shortHost: LinkHost, destinationHost: LinkHost) => void;
8
+ export declare const configuredLinkHostBases: (env?: Record<string, string | undefined>) => LinkHostBases;
9
+ export declare const assertRootRelativePath: (path: string) => void;
10
+ export declare const generateLinkSlug: (length?: number) => string;
@@ -0,0 +1,57 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { ENV } from '../env.js';
3
+ export const LINK_HOSTS = [
4
+ 'career',
5
+ 'hire',
6
+ 'docs',
7
+ 'moa',
8
+ 'uni',
9
+ 'admin',
10
+ 'api',
11
+ ];
12
+ export const isLinkHost = (value) => LINK_HOSTS.includes(value);
13
+ const configuredBase = (host, bases) => {
14
+ const value = bases[host];
15
+ if (!value)
16
+ throw new Error(`[LINKS] No base URL configured for '${host}'.`);
17
+ return value.replace(/\/+$/, '');
18
+ };
19
+ export const linkHostBase = (host, bases) => configuredBase(host, bases);
20
+ export const isApprovedLinkHostPair = (shortHost, destinationHost) => isLinkHost(shortHost) && isLinkHost(destinationHost);
21
+ export const assertApprovedLinkHostPair = (shortHost, destinationHost) => {
22
+ if (!isApprovedLinkHostPair(shortHost, destinationHost)) {
23
+ throw new Error(`[LINKS] '${shortHost}' short links cannot redirect to '${destinationHost}'.`);
24
+ }
25
+ };
26
+ export const configuredLinkHostBases = (env = ENV) => ({
27
+ career: env.CLIENT_SERVER_URL,
28
+ hire: env.CLIENT_SERVER_HIRE_URL,
29
+ uni: env.CLIENT_DUMMY_UNIV_URL,
30
+ api: env.API_BASE_URL,
31
+ });
32
+ export const assertRootRelativePath = (path) => {
33
+ if (!path.startsWith('/') || path.startsWith('//')) {
34
+ throw new Error('[LINKS] URL must be a root-relative path.');
35
+ }
36
+ try {
37
+ const origin = 'https://betterinternship.invalid';
38
+ if (new URL(path, origin).origin !== origin) {
39
+ throw new Error('[LINKS] URL must be a root-relative path.');
40
+ }
41
+ }
42
+ catch (error) {
43
+ if (error instanceof Error && error.message.startsWith('[LINKS]')) {
44
+ throw error;
45
+ }
46
+ throw new Error('[LINKS] URL must be a valid root-relative path.');
47
+ }
48
+ };
49
+ export const generateLinkSlug = (length = 32) => {
50
+ if (!Number.isSafeInteger(length) || length < 1) {
51
+ throw new Error('[LINKS] Slug length must be a positive integer.');
52
+ }
53
+ return randomBytes(Math.ceil((length * 3) / 4))
54
+ .toString('base64url')
55
+ .slice(0, length);
56
+ };
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/links/index.ts"],"names":[],"mappings":"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"}
@@ -77,13 +77,13 @@ export const ensurePreviewFontsLoaded = () => {
77
77
  if (!document.getElementById(PREVIEW_FONT_STYLE_ID)) {
78
78
  const style = document.createElement("style");
79
79
  style.id = PREVIEW_FONT_STYLE_ID;
80
- style.textContent = `
81
- @font-face {
82
- font-family: "MegastinaPreview";
83
- src: url("/fonts/megastina.regular.ttf") format("truetype");
84
- font-weight: 400;
85
- font-style: normal;
86
- font-display: block;
80
+ style.textContent = `
81
+ @font-face {
82
+ font-family: "MegastinaPreview";
83
+ src: url("/fonts/megastina.regular.ttf") format("truetype");
84
+ font-weight: 400;
85
+ font-style: normal;
86
+ font-display: block;
87
87
  }`;
88
88
  document.head.appendChild(style);
89
89
  }