@tbs-fitness/contract 0.2.0 → 0.4.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.
Files changed (48) hide show
  1. package/README.md +21 -0
  2. package/dist/cjs/app-settings/app-settings.dto.d.ts +124 -0
  3. package/dist/cjs/app-settings/app-settings.dto.js +94 -0
  4. package/dist/cjs/app-settings/index.d.ts +1 -0
  5. package/dist/cjs/app-settings/index.js +17 -0
  6. package/dist/cjs/audit/audit-log.dto.js +2 -1
  7. package/dist/cjs/auth/auth.dto.d.ts +36 -0
  8. package/dist/cjs/auth/auth.dto.js +28 -8
  9. package/dist/cjs/coach/coach-auth.dto.d.ts +12 -0
  10. package/dist/cjs/coach/coach-auth.dto.js +18 -6
  11. package/dist/cjs/common/api-response.schema.js +2 -1
  12. package/dist/cjs/common/date-time.schema.d.ts +39 -0
  13. package/dist/cjs/common/date-time.schema.js +112 -0
  14. package/dist/cjs/common/index.d.ts +1 -0
  15. package/dist/cjs/common/index.js +1 -0
  16. package/dist/cjs/communication/index.d.ts +2 -0
  17. package/dist/cjs/communication/index.js +18 -0
  18. package/dist/cjs/communication/message-catalogue.d.ts +26 -0
  19. package/dist/cjs/communication/message-catalogue.js +53 -0
  20. package/dist/cjs/communication/template.dto.d.ts +240 -0
  21. package/dist/cjs/communication/template.dto.js +206 -0
  22. package/dist/cjs/enums/index.d.ts +26 -0
  23. package/dist/cjs/enums/index.js +32 -1
  24. package/dist/cjs/feature-flags/feature-flag.dto.js +10 -9
  25. package/dist/esm/app-settings/app-settings.dto.d.ts +124 -0
  26. package/dist/esm/app-settings/app-settings.dto.js +90 -0
  27. package/dist/esm/app-settings/index.d.ts +1 -0
  28. package/dist/esm/app-settings/index.js +1 -0
  29. package/dist/esm/audit/audit-log.dto.js +2 -1
  30. package/dist/esm/auth/auth.dto.d.ts +36 -0
  31. package/dist/esm/auth/auth.dto.js +28 -8
  32. package/dist/esm/coach/coach-auth.dto.d.ts +12 -0
  33. package/dist/esm/coach/coach-auth.dto.js +19 -7
  34. package/dist/esm/common/api-response.schema.js +2 -1
  35. package/dist/esm/common/date-time.schema.d.ts +39 -0
  36. package/dist/esm/common/date-time.schema.js +107 -0
  37. package/dist/esm/common/index.d.ts +1 -0
  38. package/dist/esm/common/index.js +1 -0
  39. package/dist/esm/communication/index.d.ts +2 -0
  40. package/dist/esm/communication/index.js +2 -0
  41. package/dist/esm/communication/message-catalogue.d.ts +26 -0
  42. package/dist/esm/communication/message-catalogue.js +50 -0
  43. package/dist/esm/communication/template.dto.d.ts +240 -0
  44. package/dist/esm/communication/template.dto.js +200 -0
  45. package/dist/esm/enums/index.d.ts +26 -0
  46. package/dist/esm/enums/index.js +31 -0
  47. package/dist/esm/feature-flags/feature-flag.dto.js +10 -9
  48. package/package.json +21 -1
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dateOnlySchema = exports.instantInputSchema = exports.utcTimestampSchema = exports.detectedTimezoneSchema = exports.timezoneSchema = exports.DEFAULT_TIMEZONE = void 0;
4
+ exports.isValidTimezone = isValidTimezone;
5
+ exports.canonicalTimezone = canonicalTimezone;
6
+ const zod_1 = require("zod");
7
+ /**
8
+ * The platform's date and time rules, shared so the backend and the apps
9
+ * validate them identically:
10
+ *
11
+ * - An instant (something that happened at one moment) travels as ISO 8601.
12
+ * Responses always use UTC with `Z`. Requests may use `Z` or an explicit
13
+ * offset, but never a bare local time, which would be ambiguous.
14
+ * - A date-only value (a birthday, an expiry date) travels as `YYYY-MM-DD` and
15
+ * is never converted between time zones.
16
+ * - A time zone is an IANA identifier such as `Asia/Kolkata`. Abbreviations
17
+ * (`IST`) and fixed offsets (`+05:30`) are refused, because offsets change
18
+ * with daylight saving.
19
+ */
20
+ exports.DEFAULT_TIMEZONE = 'UTC';
21
+ const MAX_TIMEZONE_LENGTH = 64;
22
+ // The IANA areas used for real places. `Etc/*` and the legacy aliases
23
+ // (`US/Eastern`, `EST`) are left out: the first are fixed offsets, and the
24
+ // second have modern names.
25
+ const AREA_LOCATION = /^(?:Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)\/[A-Za-z0-9_+-]+(?:\/[A-Za-z0-9_+-]+)?$/;
26
+ /**
27
+ * True for `UTC` and for an IANA `Area/Location` name the runtime's time zone
28
+ * database knows. Runs anywhere `Intl` does, so the browser and the server
29
+ * agree.
30
+ */
31
+ function isValidTimezone(value) {
32
+ if (typeof value !== 'string' || value.length > MAX_TIMEZONE_LENGTH) {
33
+ return false;
34
+ }
35
+ if (value !== exports.DEFAULT_TIMEZONE && !AREA_LOCATION.test(value)) {
36
+ return false;
37
+ }
38
+ try {
39
+ new Intl.DateTimeFormat('en-US', { timeZone: value });
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ // Browsers and Node report some zones by names IANA has since replaced, because
47
+ // their Unicode data keeps the old ones. Saving the current name means one
48
+ // place is stored, compared and shown one way everywhere.
49
+ const RENAMED_TIMEZONES = {
50
+ 'Asia/Calcutta': 'Asia/Kolkata',
51
+ 'Asia/Saigon': 'Asia/Ho_Chi_Minh',
52
+ 'Asia/Katmandu': 'Asia/Kathmandu',
53
+ 'Asia/Rangoon': 'Asia/Yangon',
54
+ 'Europe/Kiev': 'Europe/Kyiv',
55
+ 'Europe/Uzhgorod': 'Europe/Kyiv',
56
+ 'Europe/Zaporozhye': 'Europe/Kyiv',
57
+ 'Atlantic/Faeroe': 'Atlantic/Faroe',
58
+ 'America/Godthab': 'America/Nuuk',
59
+ 'Pacific/Enderbury': 'Pacific/Kanton',
60
+ 'Pacific/Truk': 'Pacific/Chuuk',
61
+ 'Pacific/Ponape': 'Pacific/Pohnpei',
62
+ 'America/Buenos_Aires': 'America/Argentina/Buenos_Aires',
63
+ 'America/Catamarca': 'America/Argentina/Catamarca',
64
+ 'America/Cordoba': 'America/Argentina/Cordoba',
65
+ 'America/Jujuy': 'America/Argentina/Jujuy',
66
+ 'America/Mendoza': 'America/Argentina/Mendoza',
67
+ 'America/Indianapolis': 'America/Indiana/Indianapolis',
68
+ 'America/Louisville': 'America/Kentucky/Louisville',
69
+ };
70
+ /** The current IANA name for a zone, such as `Asia/Kolkata` for `Asia/Calcutta`. */
71
+ function canonicalTimezone(value) {
72
+ return RENAMED_TIMEZONES[value] ?? value;
73
+ }
74
+ exports.timezoneSchema = zod_1.z
75
+ .string()
76
+ .trim()
77
+ .refine(isValidTimezone, {
78
+ error: 'Choose a time zone such as Asia/Kolkata',
79
+ })
80
+ .transform(canonicalTimezone);
81
+ /**
82
+ * A time zone the client detected and suggests, such as at sign-up. A missing
83
+ * or unusable value becomes `undefined` instead of failing the request, and the
84
+ * server then applies its default.
85
+ */
86
+ exports.detectedTimezoneSchema = zod_1.z
87
+ .unknown()
88
+ .transform((value) => {
89
+ const trimmed = typeof value === 'string' ? value.trim() : value;
90
+ return isValidTimezone(trimmed) ? canonicalTimezone(trimmed) : undefined;
91
+ })
92
+ .optional();
93
+ /** An instant in a response: ISO 8601 in UTC, ending in `Z`. */
94
+ exports.utcTimestampSchema = zod_1.z.iso.datetime();
95
+ /**
96
+ * An instant in a request: ISO 8601 with `Z` or an explicit offset, such as
97
+ * `2026-11-02T15:00:00+05:30`. A local time without either is refused.
98
+ */
99
+ exports.instantInputSchema = zod_1.z.iso.datetime({ offset: true });
100
+ /** A calendar date with no time and no zone, such as `2026-11-02`. */
101
+ exports.dateOnlySchema = zod_1.z
102
+ .string()
103
+ .regex(/^\d{4}-\d{2}-\d{2}$/, { error: 'Use the format YYYY-MM-DD' })
104
+ .refine(isRealCalendarDate, { error: 'Enter a real calendar date' });
105
+ function isRealCalendarDate(value) {
106
+ const [year, month, day] = value.split('-').map(Number);
107
+ // UTC is used only to check the calendar, so no zone can move the date.
108
+ const date = new Date(Date.UTC(year, month - 1, day));
109
+ return (date.getUTCFullYear() === year &&
110
+ date.getUTCMonth() === month - 1 &&
111
+ date.getUTCDate() === day);
112
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './api-response.schema.js';
2
2
  export * from './contact.schema.js';
3
+ export * from './date-time.schema.js';
3
4
  export * from './pagination.schema.js';
4
5
  export * from './safe-text.schema.js';
@@ -16,5 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./api-response.schema.js"), exports);
18
18
  __exportStar(require("./contact.schema.js"), exports);
19
+ __exportStar(require("./date-time.schema.js"), exports);
19
20
  __exportStar(require("./pagination.schema.js"), exports);
20
21
  __exportStar(require("./safe-text.schema.js"), exports);
@@ -0,0 +1,2 @@
1
+ export * from './message-catalogue.js';
2
+ export * from './template.dto.js';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./message-catalogue.js"), exports);
18
+ __exportStar(require("./template.dto.js"), exports);
@@ -0,0 +1,26 @@
1
+ import type { CommunicationChannel, MessageType } from '../enums/index.js';
2
+ /**
3
+ * What the platform knows about each message type: the channels that may carry
4
+ * it, the channels switched on by default, and the variables its sender
5
+ * supplies. A template may only use supplied variables, and must use the
6
+ * required ones, so an admin can never activate a template the system cannot
7
+ * fill.
8
+ */
9
+ export interface MessageTypeDefinition {
10
+ label: string;
11
+ description: string;
12
+ channels: readonly CommunicationChannel[];
13
+ defaultChannels: Readonly<Record<CommunicationChannel, boolean>>;
14
+ /** Every variable the sender passes, with a sample value for previews. */
15
+ variables: Readonly<Record<string, {
16
+ description: string;
17
+ example: string;
18
+ }>>;
19
+ /** Variables every template for this type must use. */
20
+ requiredVariables: readonly string[];
21
+ }
22
+ export declare const MESSAGE_CATALOGUE: Readonly<Record<MessageType, MessageTypeDefinition>>;
23
+ /** The longest SMS body a template may have: four concatenated GSM-7 segments. */
24
+ export declare const MAX_SMS_LENGTH = 612;
25
+ /** One GSM-7 segment. Longer messages are split and cost more. */
26
+ export declare const SMS_SEGMENT_LENGTH = 160;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SMS_SEGMENT_LENGTH = exports.MAX_SMS_LENGTH = exports.MESSAGE_CATALOGUE = void 0;
4
+ const OTP_VARIABLES = {
5
+ code: { description: 'The one-time code', example: '482915' },
6
+ fullName: { description: "The recipient's name", example: 'Biki Singh' },
7
+ expiresInMinutes: {
8
+ description: 'Minutes until the code expires',
9
+ example: '10',
10
+ },
11
+ appName: { description: 'The product name', example: 'TBS' },
12
+ };
13
+ exports.MESSAGE_CATALOGUE = {
14
+ 'auth.otp.signup_verification': {
15
+ label: 'Sign-up verification code',
16
+ description: 'Sent when a coach signs up, to verify their email address.',
17
+ channels: ['EMAIL', 'SMS'],
18
+ defaultChannels: { EMAIL: true, SMS: false },
19
+ variables: OTP_VARIABLES,
20
+ requiredVariables: ['code'],
21
+ },
22
+ 'auth.otp.password_reset': {
23
+ label: 'Password recovery code',
24
+ description: 'Sent when a coach asks to reset a forgotten password.',
25
+ channels: ['EMAIL', 'SMS'],
26
+ defaultChannels: { EMAIL: true, SMS: false },
27
+ variables: OTP_VARIABLES,
28
+ requiredVariables: ['code'],
29
+ },
30
+ 'auth.password_reset_link': {
31
+ label: 'Password reset link',
32
+ description: 'Sent when an administrator account requests a reset link.',
33
+ channels: ['EMAIL'],
34
+ defaultChannels: { EMAIL: true, SMS: false },
35
+ variables: {
36
+ resetUrl: {
37
+ description: 'The single-use reset link',
38
+ example: 'https://app.example.com/reset-password?token=…',
39
+ },
40
+ fullName: { description: "The recipient's name", example: 'Biki Singh' },
41
+ expiresInMinutes: {
42
+ description: 'Minutes until the link expires',
43
+ example: '30',
44
+ },
45
+ appName: { description: 'The product name', example: 'TBS' },
46
+ },
47
+ requiredVariables: ['resetUrl'],
48
+ },
49
+ };
50
+ /** The longest SMS body a template may have: four concatenated GSM-7 segments. */
51
+ exports.MAX_SMS_LENGTH = 612;
52
+ /** One GSM-7 segment. Longer messages are split and cost more. */
53
+ exports.SMS_SEGMENT_LENGTH = 160;
@@ -0,0 +1,240 @@
1
+ import { z } from 'zod';
2
+ import { type CommunicationChannel, type MessageType } from '../enums/index.js';
3
+ /** The variable names used as `{{name}}` in a piece of template text, in order, without repeats. */
4
+ export declare function templatePlaceholders(text: string): string[];
5
+ /** True when the text has a `{{` or `}}` that is not part of a valid placeholder. */
6
+ export declare function hasMalformedPlaceholder(text: string): boolean;
7
+ /** A variable a template declares. Its value is supplied when the message is sent. */
8
+ export declare const templateVariableSchema: z.ZodObject<{
9
+ name: z.ZodString;
10
+ description: z.ZodOptional<z.ZodString>;
11
+ example: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strict>;
13
+ export type TemplateVariable = z.output<typeof templateVariableSchema>;
14
+ interface TemplateShape {
15
+ messageType: MessageType;
16
+ channel: CommunicationChannel;
17
+ subject?: string | null | undefined;
18
+ bodyText: string;
19
+ bodyHtml?: string | null | undefined;
20
+ variables: {
21
+ name: string;
22
+ }[];
23
+ }
24
+ /**
25
+ * The rules every template must meet before it is saved: the declared
26
+ * variables match the placeholders exactly, every one is supplied by the
27
+ * message type, and the type's required variables are used. Shared by the
28
+ * server and the admin editor, so both report the same problems.
29
+ */
30
+ export declare function templateProblems(template: TemplateShape): {
31
+ path: string;
32
+ message: string;
33
+ }[];
34
+ /** A new draft. Saved templates are never edited; a change is a new version. */
35
+ export declare const createTemplateSchema: z.ZodObject<{
36
+ messageType: z.ZodEnum<{
37
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
38
+ "auth.otp.password_reset": "auth.otp.password_reset";
39
+ "auth.password_reset_link": "auth.password_reset_link";
40
+ }>;
41
+ channel: z.ZodEnum<{
42
+ EMAIL: "EMAIL";
43
+ SMS: "SMS";
44
+ }>;
45
+ name: z.ZodPipe<z.ZodString, z.ZodString>;
46
+ subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ bodyText: z.ZodString;
48
+ bodyHtml: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ variables: z.ZodArray<z.ZodObject<{
50
+ name: z.ZodString;
51
+ description: z.ZodOptional<z.ZodString>;
52
+ example: z.ZodOptional<z.ZodString>;
53
+ }, z.core.$strict>>;
54
+ }, z.core.$strict>;
55
+ export type CreateTemplateInput = z.input<typeof createTemplateSchema>;
56
+ export type CreateTemplateBody = z.output<typeof createTemplateSchema>;
57
+ export declare const templateListQuerySchema: z.ZodObject<{
58
+ messageType: z.ZodOptional<z.ZodEnum<{
59
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
60
+ "auth.otp.password_reset": "auth.otp.password_reset";
61
+ "auth.password_reset_link": "auth.password_reset_link";
62
+ }>>;
63
+ channel: z.ZodOptional<z.ZodEnum<{
64
+ EMAIL: "EMAIL";
65
+ SMS: "SMS";
66
+ }>>;
67
+ }, z.core.$strict>;
68
+ export type TemplateListQuery = z.output<typeof templateListQuerySchema>;
69
+ export declare const templateResponseSchema: z.ZodObject<{
70
+ id: z.ZodUUID;
71
+ messageType: z.ZodEnum<{
72
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
73
+ "auth.otp.password_reset": "auth.otp.password_reset";
74
+ "auth.password_reset_link": "auth.password_reset_link";
75
+ }>;
76
+ channel: z.ZodEnum<{
77
+ EMAIL: "EMAIL";
78
+ SMS: "SMS";
79
+ }>;
80
+ version: z.ZodNumber;
81
+ name: z.ZodString;
82
+ subject: z.ZodNullable<z.ZodString>;
83
+ bodyText: z.ZodString;
84
+ bodyHtml: z.ZodNullable<z.ZodString>;
85
+ variables: z.ZodArray<z.ZodObject<{
86
+ name: z.ZodString;
87
+ description: z.ZodOptional<z.ZodString>;
88
+ example: z.ZodOptional<z.ZodString>;
89
+ }, z.core.$strip>>;
90
+ status: z.ZodEnum<{
91
+ active: "active";
92
+ draft: "draft";
93
+ archived: "archived";
94
+ }>;
95
+ createdBy: z.ZodNullable<z.ZodUUID>;
96
+ createdAt: z.ZodISODateTime;
97
+ activatedBy: z.ZodNullable<z.ZodUUID>;
98
+ activatedAt: z.ZodNullable<z.ZodISODateTime>;
99
+ }, z.core.$strip>;
100
+ export type TemplateResponse = z.output<typeof templateResponseSchema>;
101
+ export declare const templateListResponseSchema: z.ZodArray<z.ZodObject<{
102
+ id: z.ZodUUID;
103
+ messageType: z.ZodEnum<{
104
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
105
+ "auth.otp.password_reset": "auth.otp.password_reset";
106
+ "auth.password_reset_link": "auth.password_reset_link";
107
+ }>;
108
+ channel: z.ZodEnum<{
109
+ EMAIL: "EMAIL";
110
+ SMS: "SMS";
111
+ }>;
112
+ version: z.ZodNumber;
113
+ name: z.ZodString;
114
+ subject: z.ZodNullable<z.ZodString>;
115
+ bodyText: z.ZodString;
116
+ bodyHtml: z.ZodNullable<z.ZodString>;
117
+ variables: z.ZodArray<z.ZodObject<{
118
+ name: z.ZodString;
119
+ description: z.ZodOptional<z.ZodString>;
120
+ example: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strip>>;
122
+ status: z.ZodEnum<{
123
+ active: "active";
124
+ draft: "draft";
125
+ archived: "archived";
126
+ }>;
127
+ createdBy: z.ZodNullable<z.ZodUUID>;
128
+ createdAt: z.ZodISODateTime;
129
+ activatedBy: z.ZodNullable<z.ZodUUID>;
130
+ activatedAt: z.ZodNullable<z.ZodISODateTime>;
131
+ }, z.core.$strip>>;
132
+ /** Renders unsaved text, or a saved version, with the catalogue's sample values. */
133
+ export declare const previewTemplateSchema: z.ZodUnion<readonly [z.ZodObject<{
134
+ templateId: z.ZodUUID;
135
+ }, z.core.$strict>, z.ZodObject<{
136
+ messageType: z.ZodEnum<{
137
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
138
+ "auth.otp.password_reset": "auth.otp.password_reset";
139
+ "auth.password_reset_link": "auth.password_reset_link";
140
+ }>;
141
+ channel: z.ZodEnum<{
142
+ EMAIL: "EMAIL";
143
+ SMS: "SMS";
144
+ }>;
145
+ name: z.ZodPipe<z.ZodString, z.ZodString>;
146
+ subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
147
+ bodyText: z.ZodString;
148
+ bodyHtml: z.ZodOptional<z.ZodNullable<z.ZodString>>;
149
+ variables: z.ZodArray<z.ZodObject<{
150
+ name: z.ZodString;
151
+ description: z.ZodOptional<z.ZodString>;
152
+ example: z.ZodOptional<z.ZodString>;
153
+ }, z.core.$strict>>;
154
+ }, z.core.$strict>]>;
155
+ export type PreviewTemplateInput = z.input<typeof previewTemplateSchema>;
156
+ export type PreviewTemplateBody = z.output<typeof previewTemplateSchema>;
157
+ export declare const renderedMessageSchema: z.ZodObject<{
158
+ channel: z.ZodEnum<{
159
+ EMAIL: "EMAIL";
160
+ SMS: "SMS";
161
+ }>;
162
+ subject: z.ZodNullable<z.ZodString>;
163
+ bodyText: z.ZodString;
164
+ bodyHtml: z.ZodNullable<z.ZodString>;
165
+ smsSegments: z.ZodNullable<z.ZodNumber>;
166
+ }, z.core.$strip>;
167
+ export type RenderedMessage = z.output<typeof renderedMessageSchema>;
168
+ export declare const testSendResponseSchema: z.ZodObject<{
169
+ channel: z.ZodEnum<{
170
+ EMAIL: "EMAIL";
171
+ SMS: "SMS";
172
+ }>;
173
+ sentTo: z.ZodString;
174
+ }, z.core.$strip>;
175
+ export type TestSendResponse = z.output<typeof testSendResponseSchema>;
176
+ /** A catalogue entry, with the built-in default template for each channel. */
177
+ export declare const messageTypeResponseSchema: z.ZodObject<{
178
+ type: z.ZodEnum<{
179
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
180
+ "auth.otp.password_reset": "auth.otp.password_reset";
181
+ "auth.password_reset_link": "auth.password_reset_link";
182
+ }>;
183
+ label: z.ZodString;
184
+ description: z.ZodString;
185
+ channels: z.ZodArray<z.ZodEnum<{
186
+ EMAIL: "EMAIL";
187
+ SMS: "SMS";
188
+ }>>;
189
+ variables: z.ZodArray<z.ZodObject<{
190
+ name: z.ZodString;
191
+ description: z.ZodString;
192
+ example: z.ZodString;
193
+ }, z.core.$strip>>;
194
+ requiredVariables: z.ZodArray<z.ZodString>;
195
+ defaults: z.ZodArray<z.ZodObject<{
196
+ channel: z.ZodEnum<{
197
+ EMAIL: "EMAIL";
198
+ SMS: "SMS";
199
+ }>;
200
+ subject: z.ZodNullable<z.ZodString>;
201
+ bodyText: z.ZodString;
202
+ bodyHtml: z.ZodNullable<z.ZodString>;
203
+ variables: z.ZodArray<z.ZodObject<{
204
+ name: z.ZodString;
205
+ }, z.core.$strip>>;
206
+ }, z.core.$strip>>;
207
+ }, z.core.$strip>;
208
+ export type MessageTypeResponse = z.output<typeof messageTypeResponseSchema>;
209
+ export declare const messageTypeListResponseSchema: z.ZodArray<z.ZodObject<{
210
+ type: z.ZodEnum<{
211
+ "auth.otp.signup_verification": "auth.otp.signup_verification";
212
+ "auth.otp.password_reset": "auth.otp.password_reset";
213
+ "auth.password_reset_link": "auth.password_reset_link";
214
+ }>;
215
+ label: z.ZodString;
216
+ description: z.ZodString;
217
+ channels: z.ZodArray<z.ZodEnum<{
218
+ EMAIL: "EMAIL";
219
+ SMS: "SMS";
220
+ }>>;
221
+ variables: z.ZodArray<z.ZodObject<{
222
+ name: z.ZodString;
223
+ description: z.ZodString;
224
+ example: z.ZodString;
225
+ }, z.core.$strip>>;
226
+ requiredVariables: z.ZodArray<z.ZodString>;
227
+ defaults: z.ZodArray<z.ZodObject<{
228
+ channel: z.ZodEnum<{
229
+ EMAIL: "EMAIL";
230
+ SMS: "SMS";
231
+ }>;
232
+ subject: z.ZodNullable<z.ZodString>;
233
+ bodyText: z.ZodString;
234
+ bodyHtml: z.ZodNullable<z.ZodString>;
235
+ variables: z.ZodArray<z.ZodObject<{
236
+ name: z.ZodString;
237
+ }, z.core.$strip>>;
238
+ }, z.core.$strip>>;
239
+ }, z.core.$strip>>;
240
+ export {};
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messageTypeListResponseSchema = exports.messageTypeResponseSchema = exports.testSendResponseSchema = exports.renderedMessageSchema = exports.previewTemplateSchema = exports.templateListResponseSchema = exports.templateResponseSchema = exports.templateListQuerySchema = exports.createTemplateSchema = exports.templateVariableSchema = void 0;
4
+ exports.templatePlaceholders = templatePlaceholders;
5
+ exports.hasMalformedPlaceholder = hasMalformedPlaceholder;
6
+ exports.templateProblems = templateProblems;
7
+ const zod_1 = require("zod");
8
+ const date_time_schema_js_1 = require("../common/date-time.schema.js");
9
+ const safe_text_schema_js_1 = require("../common/safe-text.schema.js");
10
+ const index_js_1 = require("../enums/index.js");
11
+ const message_catalogue_js_1 = require("./message-catalogue.js");
12
+ const PLACEHOLDER = /\{\{\s*([A-Za-z][A-Za-z0-9_]*)\s*\}\}/g;
13
+ /** The variable names used as `{{name}}` in a piece of template text, in order, without repeats. */
14
+ function templatePlaceholders(text) {
15
+ const names = new Set();
16
+ for (const match of text.matchAll(PLACEHOLDER)) {
17
+ names.add(match[1]);
18
+ }
19
+ return [...names];
20
+ }
21
+ /** True when the text has a `{{` or `}}` that is not part of a valid placeholder. */
22
+ function hasMalformedPlaceholder(text) {
23
+ const rest = text.replace(PLACEHOLDER, '');
24
+ return rest.includes('{{') || rest.includes('}}');
25
+ }
26
+ const MAX_SUBJECT_LENGTH = 200;
27
+ const MAX_BODY_LENGTH = 20000;
28
+ /** A variable a template declares. Its value is supplied when the message is sent. */
29
+ exports.templateVariableSchema = zod_1.z.strictObject({
30
+ name: zod_1.z
31
+ .string()
32
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/, {
33
+ error: 'Use letters, digits and underscores, starting with a letter',
34
+ })
35
+ .max(64),
36
+ description: (0, safe_text_schema_js_1.safeTextSchema)(200).optional(),
37
+ example: zod_1.z.string().max(500).optional(),
38
+ });
39
+ const templateFields = {
40
+ messageType: zod_1.z.enum(index_js_1.MESSAGE_TYPES),
41
+ channel: zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS),
42
+ name: (0, safe_text_schema_js_1.safeTextSchema)(100).pipe(zod_1.z.string().min(1, { error: 'Name it' })),
43
+ subject: zod_1.z.string().max(MAX_SUBJECT_LENGTH).nullish(),
44
+ bodyText: zod_1.z
45
+ .string()
46
+ .min(1, { error: 'Write the message' })
47
+ .max(MAX_BODY_LENGTH),
48
+ bodyHtml: zod_1.z.string().max(MAX_BODY_LENGTH).nullish(),
49
+ variables: zod_1.z.array(exports.templateVariableSchema).max(20),
50
+ };
51
+ /**
52
+ * The rules every template must meet before it is saved: the declared
53
+ * variables match the placeholders exactly, every one is supplied by the
54
+ * message type, and the type's required variables are used. Shared by the
55
+ * server and the admin editor, so both report the same problems.
56
+ */
57
+ function templateProblems(template) {
58
+ const problems = [];
59
+ const definition = message_catalogue_js_1.MESSAGE_CATALOGUE[template.messageType];
60
+ const isEmail = template.channel === 'EMAIL';
61
+ if (!definition.channels.includes(template.channel)) {
62
+ problems.push({
63
+ path: 'channel',
64
+ message: `${definition.label} cannot be sent by ${template.channel}`,
65
+ });
66
+ }
67
+ if (isEmail && !template.subject?.trim()) {
68
+ problems.push({ path: 'subject', message: 'An email needs a subject' });
69
+ }
70
+ if (!isEmail && (template.subject || template.bodyHtml)) {
71
+ problems.push({
72
+ path: 'bodyHtml',
73
+ message: 'Only email templates have a subject or HTML',
74
+ });
75
+ }
76
+ if (template.channel === 'SMS' && template.bodyText.length > message_catalogue_js_1.MAX_SMS_LENGTH) {
77
+ problems.push({
78
+ path: 'bodyText',
79
+ message: `An SMS can be at most ${String(message_catalogue_js_1.MAX_SMS_LENGTH)} characters`,
80
+ });
81
+ }
82
+ const texts = [
83
+ template.subject ?? '',
84
+ template.bodyText,
85
+ template.bodyHtml ?? '',
86
+ ];
87
+ if (texts.some(hasMalformedPlaceholder)) {
88
+ problems.push({
89
+ path: 'bodyText',
90
+ message: 'Write placeholders as {{name}}, with matching braces',
91
+ });
92
+ }
93
+ const used = new Set(texts.flatMap(templatePlaceholders));
94
+ const declared = template.variables.map((variable) => variable.name);
95
+ const declaredSet = new Set(declared);
96
+ if (declaredSet.size !== declared.length) {
97
+ problems.push({ path: 'variables', message: 'Declare each variable once' });
98
+ }
99
+ for (const name of used) {
100
+ if (!declaredSet.has(name)) {
101
+ problems.push({
102
+ path: 'variables',
103
+ message: `{{${name}}} is used but not declared`,
104
+ });
105
+ }
106
+ }
107
+ for (const name of declaredSet) {
108
+ if (!used.has(name)) {
109
+ problems.push({
110
+ path: 'variables',
111
+ message: `${name} is declared but not used`,
112
+ });
113
+ }
114
+ if (!(name in definition.variables)) {
115
+ problems.push({
116
+ path: 'variables',
117
+ message: `${definition.label} does not supply ${name}`,
118
+ });
119
+ }
120
+ }
121
+ for (const name of definition.requiredVariables) {
122
+ if (!declaredSet.has(name)) {
123
+ problems.push({
124
+ path: 'variables',
125
+ message: `${definition.label} must include {{${name}}}`,
126
+ });
127
+ }
128
+ }
129
+ return problems;
130
+ }
131
+ /** A new draft. Saved templates are never edited; a change is a new version. */
132
+ exports.createTemplateSchema = zod_1.z
133
+ .strictObject(templateFields)
134
+ .superRefine((template, ctx) => {
135
+ for (const problem of templateProblems(template)) {
136
+ ctx.addIssue({
137
+ code: 'custom',
138
+ path: [problem.path],
139
+ message: problem.message,
140
+ });
141
+ }
142
+ });
143
+ exports.templateListQuerySchema = zod_1.z.strictObject({
144
+ messageType: zod_1.z.enum(index_js_1.MESSAGE_TYPES).optional(),
145
+ channel: zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS).optional(),
146
+ });
147
+ exports.templateResponseSchema = zod_1.z.object({
148
+ id: zod_1.z.uuid(),
149
+ messageType: zod_1.z.enum(index_js_1.MESSAGE_TYPES),
150
+ channel: zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS),
151
+ version: zod_1.z.number().int(),
152
+ name: zod_1.z.string(),
153
+ subject: zod_1.z.string().nullable(),
154
+ bodyText: zod_1.z.string(),
155
+ bodyHtml: zod_1.z.string().nullable(),
156
+ variables: zod_1.z.array(zod_1.z.object({
157
+ name: zod_1.z.string(),
158
+ description: zod_1.z.string().optional(),
159
+ example: zod_1.z.string().optional(),
160
+ })),
161
+ status: zod_1.z.enum(index_js_1.TEMPLATE_STATUSES),
162
+ createdBy: zod_1.z.uuid().nullable(),
163
+ createdAt: date_time_schema_js_1.utcTimestampSchema,
164
+ activatedBy: zod_1.z.uuid().nullable(),
165
+ activatedAt: date_time_schema_js_1.utcTimestampSchema.nullable(),
166
+ });
167
+ exports.templateListResponseSchema = zod_1.z.array(exports.templateResponseSchema);
168
+ /** Renders unsaved text, or a saved version, with the catalogue's sample values. */
169
+ exports.previewTemplateSchema = zod_1.z.union([
170
+ zod_1.z.strictObject({ templateId: zod_1.z.uuid() }),
171
+ zod_1.z.strictObject(templateFields),
172
+ ]);
173
+ exports.renderedMessageSchema = zod_1.z.object({
174
+ channel: zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS),
175
+ subject: zod_1.z.string().nullable(),
176
+ bodyText: zod_1.z.string(),
177
+ bodyHtml: zod_1.z.string().nullable(),
178
+ /** SMS only: how many 160-character segments it takes. */
179
+ smsSegments: zod_1.z.number().int().nullable(),
180
+ });
181
+ exports.testSendResponseSchema = zod_1.z.object({
182
+ channel: zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS),
183
+ /** The signed-in admin's own address, masked, such as `b***@tbs.coach`. */
184
+ sentTo: zod_1.z.string(),
185
+ });
186
+ /** A catalogue entry, with the built-in default template for each channel. */
187
+ exports.messageTypeResponseSchema = zod_1.z.object({
188
+ type: zod_1.z.enum(index_js_1.MESSAGE_TYPES),
189
+ label: zod_1.z.string(),
190
+ description: zod_1.z.string(),
191
+ channels: zod_1.z.array(zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS)),
192
+ variables: zod_1.z.array(zod_1.z.object({
193
+ name: zod_1.z.string(),
194
+ description: zod_1.z.string(),
195
+ example: zod_1.z.string(),
196
+ })),
197
+ requiredVariables: zod_1.z.array(zod_1.z.string()),
198
+ defaults: zod_1.z.array(zod_1.z.object({
199
+ channel: zod_1.z.enum(index_js_1.COMMUNICATION_CHANNELS),
200
+ subject: zod_1.z.string().nullable(),
201
+ bodyText: zod_1.z.string(),
202
+ bodyHtml: zod_1.z.string().nullable(),
203
+ variables: zod_1.z.array(zod_1.z.object({ name: zod_1.z.string() })),
204
+ })),
205
+ });
206
+ exports.messageTypeListResponseSchema = zod_1.z.array(exports.messageTypeResponseSchema);