@company-semantics/contracts 0.42.0 → 0.43.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": "@company-semantics/contracts",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,6 +32,10 @@
32
32
  "types": "./src/execution/index.ts",
33
33
  "default": "./src/execution/index.ts"
34
34
  },
35
+ "./email": {
36
+ "types": "./src/email/index.ts",
37
+ "default": "./src/email/index.ts"
38
+ },
35
39
  "./schemas/guard-result.schema.json": "./schemas/guard-result.schema.json"
36
40
  },
37
41
  "types": "./src/index.ts",
@@ -0,0 +1,35 @@
1
+ # email/
2
+
3
+ ## Purpose
4
+
5
+ TypeScript types and registry for transactional email kinds and their metadata.
6
+
7
+ ## Invariants
8
+
9
+ - Types only; minimal runtime code (pure functions for registry lookup)
10
+ - No external dependencies (contracts policy)
11
+ - `EmailKind` union MUST match keys in `EMAIL_KINDS` registry
12
+ - `EMAIL_KINDS` is the single source of truth for subjects and rendering rules
13
+ - OTP values MUST NEVER appear in contracts (security: logged if leaked)
14
+ - Subjects are owned by the registry, never duplicated in templates
15
+ - Unknown email kinds MUST be rejected at API boundaries via `isValidEmailKind()`
16
+
17
+ ## Public API
18
+
19
+ ### Types
20
+
21
+ - `EmailKind` — Union of valid email kind identifiers
22
+ - `EmailPayloads` — Type-safe payload mapping for each kind
23
+ - `SendEmailInput<K>` — Type-safe input for sending emails
24
+ - `EmailKindDefinition` — Full definition schema for a kind
25
+
26
+ ### Registry & Functions
27
+
28
+ - `EMAIL_KINDS` — Registry of all email kinds
29
+ - `getEmailKindDefinition(kind)` — Type-safe registry lookup
30
+ - `isValidEmailKind(kind)` — Validate string is EmailKind
31
+
32
+ ## Dependencies
33
+
34
+ **Imports from:** (none — leaf module)
35
+ **Imported by:** company-semantics-backend (email service, templates)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Email Domain Barrel
3
+ *
4
+ * Re-exports email kind vocabulary types and registry.
5
+ * Import from '@company-semantics/contracts/email'.
6
+ *
7
+ * @see ADR-CONT-034 for design rationale
8
+ */
9
+
10
+ // =============================================================================
11
+ // Kind Types
12
+ // =============================================================================
13
+
14
+ export type { EmailKind, EmailPayloads, SendEmailInput } from './types'
15
+
16
+ // =============================================================================
17
+ // Definition Types
18
+ // =============================================================================
19
+
20
+ export type { EmailKindDefinition } from './registry'
21
+
22
+ // =============================================================================
23
+ // Registry
24
+ // =============================================================================
25
+
26
+ export {
27
+ EMAIL_KINDS,
28
+ getEmailKindDefinition,
29
+ isValidEmailKind,
30
+ } from './registry'
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Email Kind Registry
3
+ *
4
+ * Central registry of all email kinds and their definitions.
5
+ * This is the single source of truth for email metadata.
6
+ *
7
+ * Invariants:
8
+ * - Every EmailKind MUST have an entry in EMAIL_KINDS
9
+ * - Registry keys MUST match definition.kind
10
+ * - Registry is exhaustive (satisfies Record<EmailKind, ...>)
11
+ * - Subjects are owned here, not duplicated in templates
12
+ *
13
+ * @see ADR-CONT-034 for design rationale
14
+ */
15
+
16
+ import type { EmailKind } from './types'
17
+
18
+ // =============================================================================
19
+ // Email Kind Definition
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Complete definition for an email kind.
24
+ *
25
+ * This interface is the schema for entries in EMAIL_KINDS registry.
26
+ * It captures subject, rendering requirements, and domain metadata.
27
+ *
28
+ * Invariants:
29
+ * - kind field MUST match the registry key
30
+ * - subject is the authoritative source (templates import from here)
31
+ */
32
+ export interface EmailKindDefinition {
33
+ /** The email kind this definition describes */
34
+ kind: EmailKind
35
+ /** Email subject line (single source of truth) */
36
+ subject: string
37
+ /** Whether plain text body is required */
38
+ plainTextRequired: boolean
39
+ /** Whether HTML body is supported */
40
+ htmlSupported: boolean
41
+ }
42
+
43
+ // =============================================================================
44
+ // Registry
45
+ // =============================================================================
46
+
47
+ /**
48
+ * EMAIL_KINDS is the authoritative registry.
49
+ *
50
+ * All subjects, rendering rules, and domain metadata are derived from here.
51
+ * Backend email templates MUST use this registry for subjects
52
+ * rather than hardcoding values.
53
+ *
54
+ * To add a new kind:
55
+ * 1. Add to EmailKind union in types.ts
56
+ * 2. Add entry to this registry
57
+ * 3. Add payload to EmailPayloads if needed
58
+ * 4. Implement template in backend
59
+ */
60
+ export const EMAIL_KINDS = {
61
+ 'auth.otp': {
62
+ kind: 'auth.otp',
63
+ subject: 'Your login code',
64
+ plainTextRequired: true,
65
+ htmlSupported: false,
66
+ },
67
+ 'auth.magic_link': {
68
+ kind: 'auth.magic_link',
69
+ subject: 'Your login link',
70
+ plainTextRequired: true,
71
+ htmlSupported: false,
72
+ },
73
+ 'org.invite': {
74
+ kind: 'org.invite',
75
+ subject: 'You have been invited to join a workspace',
76
+ plainTextRequired: true,
77
+ htmlSupported: true,
78
+ },
79
+ 'security.alert': {
80
+ kind: 'security.alert',
81
+ subject: 'Security alert for your account',
82
+ plainTextRequired: true,
83
+ htmlSupported: false,
84
+ },
85
+ } as const satisfies Record<EmailKind, EmailKindDefinition>
86
+
87
+ // =============================================================================
88
+ // Registry Helpers
89
+ // =============================================================================
90
+
91
+ /**
92
+ * Type-safe registry lookup.
93
+ * Returns the definition for a given email kind.
94
+ */
95
+ export function getEmailKindDefinition(kind: EmailKind): EmailKindDefinition {
96
+ return EMAIL_KINDS[kind]
97
+ }
98
+
99
+ /**
100
+ * Check if a string is a valid EmailKind.
101
+ * Use at API boundaries to reject unknown kinds.
102
+ */
103
+ export function isValidEmailKind(kind: string): kind is EmailKind {
104
+ return kind in EMAIL_KINDS
105
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Email Domain Types
3
+ *
4
+ * Shared types for transactional email across Company Semantics codebases.
5
+ * Types only - no runtime code, no business logic.
6
+ *
7
+ * @see ADR-CONT-034 for design rationale
8
+ */
9
+
10
+ // =============================================================================
11
+ // EmailKind Union
12
+ // =============================================================================
13
+
14
+ /**
15
+ * EmailKind identifies the type of transactional email.
16
+ *
17
+ * Naming convention: `{domain}.{type}`
18
+ * - domain: auth, org, security
19
+ * - type: specific email variant
20
+ *
21
+ * New kinds MUST be added to:
22
+ * 1. This union type
23
+ * 2. EMAIL_KINDS registry in registry.ts
24
+ * 3. EmailPayloads interface (if kind has payload)
25
+ */
26
+ export type EmailKind =
27
+ | 'auth.otp'
28
+ | 'auth.magic_link' // future
29
+ | 'org.invite' // future
30
+ | 'security.alert' // future
31
+
32
+ // =============================================================================
33
+ // Email Payloads
34
+ // =============================================================================
35
+
36
+ /**
37
+ * Type-safe payload mapping for each email kind.
38
+ *
39
+ * Each key is an EmailKind, and the value is the required payload shape.
40
+ * Kinds without entries here have empty payloads.
41
+ */
42
+ export interface EmailPayloads {
43
+ 'auth.otp': {
44
+ /** The 6-digit OTP code */
45
+ otp: string
46
+ /** How long until the code expires */
47
+ expiresInMinutes: number
48
+ /** IP address of the request (for security context) */
49
+ requestIp?: string
50
+ /** User agent of the request (for security context) */
51
+ userAgent?: string
52
+ }
53
+ }
54
+
55
+ // =============================================================================
56
+ // Send Email Input
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Type-safe input for sending emails.
61
+ *
62
+ * The payload type is inferred from the kind.
63
+ * If the kind is in EmailPayloads, that payload is required.
64
+ */
65
+ export interface SendEmailInput<K extends EmailKind> {
66
+ /** The type of email to send */
67
+ kind: K
68
+ /** Recipient email address (will be normalized) */
69
+ to: string
70
+ /** Type-safe payload for this email kind */
71
+ payload: K extends keyof EmailPayloads ? EmailPayloads[K] : never
72
+ /** Idempotency key to prevent duplicate sends */
73
+ idempotencyKey: string
74
+ }
package/src/index.ts CHANGED
@@ -112,6 +112,21 @@ export { extractFirstWord, resolveDisplayName, deriveFullName } from './identity
112
112
  // Auth domain types
113
113
  export { OTPErrorCode } from './auth/index'
114
114
 
115
+ // Email domain types
116
+ // @see ADR-CONT-034 for design rationale
117
+ export type {
118
+ EmailKind,
119
+ EmailPayloads,
120
+ SendEmailInput,
121
+ EmailKindDefinition,
122
+ } from './email/index'
123
+
124
+ export {
125
+ EMAIL_KINDS,
126
+ getEmailKindDefinition,
127
+ isValidEmailKind,
128
+ } from './email/index'
129
+
115
130
  // Organization domain types
116
131
  // @see ADR-BE-XXX for design rationale (Personal vs Shared Organization Model)
117
132
  export type {