@meith/settings 0.29.0 → 0.30.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/settings",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "zod": "^4.4.3",
23
- "@meith/core": "0.29.0",
24
- "@meith/i18n": "0.29.0"
23
+ "@meith/core": "0.30.0",
24
+ "@meith/i18n": "0.30.0"
25
25
  }
26
26
  }
package/src/branding.ts CHANGED
@@ -4,6 +4,8 @@ export const LOGO_SCHEMES: readonly LogoScheme[] = ['light', 'dark']
4
4
 
5
5
  const LOGO_KEY = /^board\/logo-(light|dark)-[a-f0-9-]{36}\.(png|jpg|webp|svg)$/
6
6
 
7
+ const FAVICON_KEY = /^board\/favicon-[a-f0-9-]{36}\.(png|jpg|webp|svg)$/
8
+
7
9
  export function isLogoScheme(value: unknown): value is LogoScheme {
8
10
  return value === 'light' || value === 'dark'
9
11
  }
@@ -12,10 +14,22 @@ export function isLogoKey(value: string): boolean {
12
14
  return LOGO_KEY.test(value)
13
15
  }
14
16
 
17
+ export function isFaviconKey(value: string): boolean {
18
+ return FAVICON_KEY.test(value)
19
+ }
20
+
15
21
  export function logoFormat(key: string): string | null {
16
22
  return LOGO_KEY.exec(key)?.[2] ?? null
17
23
  }
18
24
 
25
+ export function faviconFormat(key: string): string | null {
26
+ return FAVICON_KEY.exec(key)?.[1] ?? null
27
+ }
28
+
29
+ export function faviconPath(key: string): string {
30
+ return `/brand/favicon?v=${key.slice(-16, -4)}`
31
+ }
32
+
19
33
  export function logoPath(scheme: LogoScheme, key: string): string {
20
34
  return `/logo/${scheme}?v=${key.slice(-16, -4)}`
21
35
  }
@@ -3,6 +3,7 @@ import { z } from 'zod'
3
3
  import { normaliseLocale, SOURCE_LOCALE } from '@meith/i18n'
4
4
 
5
5
  import { DEFAULT_PRIVACY_POLICY, DEFAULT_TERMS_OF_SERVICE } from './legal'
6
+ import { mailEndpointProblem } from './mail'
6
7
  import { isUsableFeedUrl, isUsableIssuer, isUsableOrigin } from './origin'
7
8
 
8
9
  export type SettingGroup =
@@ -301,6 +302,19 @@ export const SETTING_DEFINITIONS = [
301
302
  invalidates: ['settings', 'layout'],
302
303
  ui: { managed: true },
303
304
  }),
305
+ define({
306
+ key: 'board.favicon',
307
+ group: 'board',
308
+ label: 'Favicon',
309
+ description:
310
+ 'The icon browser tabs, bookmarks and an installed shortcut show for this board. ' +
311
+ 'Uploaded, not typed. Left empty, an icon is generated from the board name and its ' +
312
+ 'theme colours.',
313
+ schema: z.string().max(300),
314
+ default: '',
315
+ invalidates: ['settings', 'layout'],
316
+ ui: { managed: true },
317
+ }),
304
318
  define({
305
319
  key: 'board.logo_alt',
306
320
  group: 'board',
@@ -417,14 +431,16 @@ export const SETTING_DEFINITIONS = [
417
431
  'Only for the provider-API transport. The board posts Resend’s exact ' +
418
432
  'field names with a Bearer token, so this works for Resend and for ' +
419
433
  'anything that copies it — and not for Postmark or Mailgun, whose SMTP ' +
420
- 'hosts are the way in.',
434
+ 'hosts are the way in. It must be an https:// address with no embedded ' +
435
+ 'credentials, and the board refuses to reach one that resolves to a ' +
436
+ 'private or internal address.',
421
437
  schema: z
422
438
  .string()
423
439
  .trim()
424
- .refine(
425
- (value) => value === '' || z.string().url().safeParse(value).success,
426
- 'That is not a URL.',
427
- ),
440
+ .superRefine((value, ctx) => {
441
+ const problem = mailEndpointProblem(value)
442
+ if (problem !== null) ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem })
443
+ }),
428
444
  default: '',
429
445
  }),
430
446
  define({
package/src/index.ts CHANGED
@@ -7,6 +7,9 @@ export {
7
7
  resolveBoardUrl,
8
8
  } from './board-url'
9
9
  export {
10
+ faviconFormat,
11
+ faviconPath,
12
+ isFaviconKey,
10
13
  isLogoKey,
11
14
  isLogoScheme,
12
15
  LOGO_SCHEMES,
@@ -48,6 +51,7 @@ export {
48
51
  mailConfigFromEnvironment,
49
52
  mailConfigFromSettings,
50
53
  mailConfigProblems,
54
+ mailEndpointProblem,
51
55
  NO_MAIL,
52
56
  resolveMailConfig,
53
57
  type SmtpMailConfig,
package/src/mail.ts CHANGED
@@ -175,6 +175,24 @@ function hostOf(endpoint: string): string {
175
175
  }
176
176
  }
177
177
 
178
+ export function mailEndpointProblem(value: string): string | null {
179
+ const trimmed = value.trim()
180
+ if (trimmed === '') return null
181
+
182
+ let url: URL
183
+ try {
184
+ url = new URL(trimmed)
185
+ } catch {
186
+ return 'That is not a URL.'
187
+ }
188
+
189
+ if (url.protocol !== 'https:') return 'The endpoint must be an https:// URL.'
190
+ if (url.username !== '' || url.password !== '') {
191
+ return 'The endpoint must not carry a username or password.'
192
+ }
193
+ return null
194
+ }
195
+
178
196
  export interface MailPreset {
179
197
  readonly id: string
180
198
  readonly label: string