vintasend-api 0.0.0-stage → 1.0.0-alpha6

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -2
  3. package/dist/app.d.ts +33 -0
  4. package/dist/app.js +33 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/config.d.ts +13 -0
  7. package/dist/config.js +39 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/contract/types.d.ts +196 -0
  10. package/dist/contract/types.js +12 -0
  11. package/dist/contract/types.js.map +1 -0
  12. package/dist/domain/capabilities.d.ts +13 -0
  13. package/dist/domain/capabilities.js +19 -0
  14. package/dist/domain/capabilities.js.map +1 -0
  15. package/dist/domain/filters.d.ts +16 -0
  16. package/dist/domain/filters.js +82 -0
  17. package/dist/domain/filters.js.map +1 -0
  18. package/dist/domain/pagination.d.ts +20 -0
  19. package/dist/domain/pagination.js +24 -0
  20. package/dist/domain/pagination.js.map +1 -0
  21. package/dist/domain/schemas.d.ts +90 -0
  22. package/dist/domain/schemas.js +52 -0
  23. package/dist/domain/schemas.js.map +1 -0
  24. package/dist/domain/serialize.d.ts +19 -0
  25. package/dist/domain/serialize.js +101 -0
  26. package/dist/domain/serialize.js.map +1 -0
  27. package/dist/errors.d.ts +45 -0
  28. package/dist/errors.js +94 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/exports.d.ts +14 -0
  31. package/dist/exports.js +14 -0
  32. package/dist/exports.js.map +1 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.js +34 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/middleware/authenticate.d.ts +34 -0
  37. package/dist/middleware/authenticate.js +68 -0
  38. package/dist/middleware/authenticate.js.map +1 -0
  39. package/dist/middleware/error-handler.d.ts +35 -0
  40. package/dist/middleware/error-handler.js +89 -0
  41. package/dist/middleware/error-handler.js.map +1 -0
  42. package/dist/routes/notifications.d.ts +14 -0
  43. package/dist/routes/notifications.js +119 -0
  44. package/dist/routes/notifications.js.map +1 -0
  45. package/dist/routes/validation.d.ts +14 -0
  46. package/dist/routes/validation.js +44 -0
  47. package/dist/routes/validation.js.map +1 -0
  48. package/dist/services/github-template-client.d.ts +26 -0
  49. package/dist/services/github-template-client.js +148 -0
  50. package/dist/services/github-template-client.js.map +1 -0
  51. package/dist/services/github-template-preview-config.d.ts +8 -0
  52. package/dist/services/github-template-preview-config.js +51 -0
  53. package/dist/services/github-template-preview-config.js.map +1 -0
  54. package/dist/services/notification-preview.d.ts +22 -0
  55. package/dist/services/notification-preview.js +66 -0
  56. package/dist/services/notification-preview.js.map +1 -0
  57. package/dist/services/notification-service-port.d.ts +40 -0
  58. package/dist/services/notification-service-port.js +19 -0
  59. package/dist/services/notification-service-port.js.map +1 -0
  60. package/dist/services/paged-notification-reader.d.ts +22 -0
  61. package/dist/services/paged-notification-reader.js +36 -0
  62. package/dist/services/paged-notification-reader.js.map +1 -0
  63. package/dist/services/service-loader.d.ts +15 -0
  64. package/dist/services/service-loader.js +56 -0
  65. package/dist/services/service-loader.js.map +1 -0
  66. package/dist/services/template-path-resolver.d.ts +4 -0
  67. package/dist/services/template-path-resolver.js +17 -0
  68. package/dist/services/template-path-resolver.js.map +1 -0
  69. package/openapi.yaml +666 -0
  70. package/package.json +66 -4
@@ -0,0 +1,148 @@
1
+ import { getGitHubTemplatePreviewConfigFromEnv, } from './github-template-preview-config.js';
2
+ import { resolveTemplatePath } from './template-path-resolver.js';
3
+ /**
4
+ * Decode GitHub's base64 file content as UTF-8, with Web APIs only, so the client works wherever
5
+ * `fetch` does and not just in Node.
6
+ */
7
+ function decodeBase64Utf8(base64) {
8
+ const binary = atob(base64);
9
+ return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
10
+ }
11
+ export class GitHubTemplateClient {
12
+ config;
13
+ fetcher;
14
+ cacheMaxEntries;
15
+ cache = new Map();
16
+ constructor(config, fetcher = fetch, options = {}) {
17
+ this.config = config;
18
+ this.fetcher = fetcher;
19
+ this.cacheMaxEntries = options.cacheMaxEntries ?? 100;
20
+ }
21
+ async getTemplateContentByCommit(request) {
22
+ const resolvedPath = resolveTemplatePath(request.templatePath, {
23
+ templatesBasePath: this.config.templatesBasePath,
24
+ });
25
+ const cacheKey = this.buildCacheKey(resolvedPath, request.gitCommitSha);
26
+ const fromCache = this.cache.get(cacheKey);
27
+ if (fromCache !== undefined) {
28
+ return fromCache;
29
+ }
30
+ const apiUrl = this.buildApiUrl(resolvedPath, request.gitCommitSha);
31
+ const response = await this.fetcher(apiUrl, {
32
+ method: 'GET',
33
+ headers: this.buildHeaders(),
34
+ });
35
+ if (!response.ok) {
36
+ throw await this.buildHttpError(response);
37
+ }
38
+ const payload = (await response.json());
39
+ if (!payload.content || payload.encoding !== 'base64') {
40
+ throw new Error('GitHub template response is invalid or unsupported.');
41
+ }
42
+ const decoded = decodeBase64Utf8(payload.content.replace(/\n/g, ''));
43
+ this.setCache(cacheKey, decoded);
44
+ return decoded;
45
+ }
46
+ async getLatestMainCommitSha() {
47
+ const apiUrl = `${this.config.apiBaseUrl}/repos/${this.config.repo}/commits/main`;
48
+ const response = await this.fetcher(apiUrl, {
49
+ method: 'GET',
50
+ headers: this.buildHeaders(),
51
+ });
52
+ if (!response.ok) {
53
+ throw await this.buildCommitLookupHttpError(response);
54
+ }
55
+ const payload = (await response.json());
56
+ if (!payload.sha) {
57
+ throw new Error('GitHub main branch commit lookup returned an invalid response.');
58
+ }
59
+ return payload.sha;
60
+ }
61
+ buildApiUrl(path, gitCommitSha) {
62
+ const encodedPath = path
63
+ .split('/')
64
+ .map((segment) => encodeURIComponent(segment))
65
+ .join('/');
66
+ const refParam = encodeURIComponent(gitCommitSha);
67
+ return `${this.config.apiBaseUrl}/repos/${this.config.repo}/contents/${encodedPath}?ref=${refParam}`;
68
+ }
69
+ buildCacheKey(path, gitCommitSha) {
70
+ return `${this.config.repo}:${path}:${gitCommitSha}`;
71
+ }
72
+ buildHeaders() {
73
+ return {
74
+ Accept: 'application/vnd.github+json',
75
+ Authorization: `Bearer ${this.config.apiKey}`,
76
+ 'X-GitHub-Api-Version': '2022-11-28',
77
+ };
78
+ }
79
+ setCache(key, value) {
80
+ if (this.cache.has(key)) {
81
+ this.cache.delete(key);
82
+ }
83
+ this.cache.set(key, value);
84
+ while (this.cache.size > this.cacheMaxEntries) {
85
+ const firstKey = this.cache.keys().next().value;
86
+ if (!firstKey) {
87
+ break;
88
+ }
89
+ this.cache.delete(firstKey);
90
+ }
91
+ }
92
+ async buildHttpError(response) {
93
+ if (response.status === 404) {
94
+ return new Error('Template file was not found in GitHub for the requested commit.');
95
+ }
96
+ if (response.status === 403) {
97
+ const remaining = response.headers.get('x-ratelimit-remaining');
98
+ if (remaining === '0') {
99
+ return new Error('GitHub API rate limit exceeded while fetching template preview.');
100
+ }
101
+ return new Error('GitHub API request was forbidden. Check repository access and token permissions.');
102
+ }
103
+ if (response.status === 429) {
104
+ return new Error('GitHub API rate limit exceeded while fetching template preview.');
105
+ }
106
+ let messageSuffix = '';
107
+ try {
108
+ const body = (await response.json());
109
+ if (body.message) {
110
+ messageSuffix = `: ${body.message}`;
111
+ }
112
+ }
113
+ catch {
114
+ // noop
115
+ }
116
+ return new Error(`GitHub template fetch failed with status ${response.status}${messageSuffix}`);
117
+ }
118
+ async buildCommitLookupHttpError(response) {
119
+ if (response.status === 404) {
120
+ return new Error('Unable to resolve latest commit SHA from the main branch.');
121
+ }
122
+ if (response.status === 403) {
123
+ const remaining = response.headers.get('x-ratelimit-remaining');
124
+ if (remaining === '0') {
125
+ return new Error('GitHub API rate limit exceeded while fetching template preview.');
126
+ }
127
+ return new Error('GitHub API request was forbidden. Check repository access and token permissions.');
128
+ }
129
+ if (response.status === 429) {
130
+ return new Error('GitHub API rate limit exceeded while fetching template preview.');
131
+ }
132
+ let messageSuffix = '';
133
+ try {
134
+ const body = (await response.json());
135
+ if (body.message) {
136
+ messageSuffix = `: ${body.message}`;
137
+ }
138
+ }
139
+ catch {
140
+ // noop
141
+ }
142
+ return new Error(`GitHub main branch commit lookup failed with status ${response.status}${messageSuffix}`);
143
+ }
144
+ }
145
+ export function createGitHubTemplateClientFromEnv(options = {}) {
146
+ return new GitHubTemplateClient(getGitHubTemplatePreviewConfigFromEnv(), fetch, options);
147
+ }
148
+ //# sourceMappingURL=github-template-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-template-client.js","sourceRoot":"","sources":["../../src/services/github-template-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export declare const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
2
+ export type GitHubTemplatePreviewConfig = {
3
+ repo: string;
4
+ apiKey: string;
5
+ apiBaseUrl: string;
6
+ templatesBasePath?: string;
7
+ };
8
+ export declare function getGitHubTemplatePreviewConfigFromEnv(): GitHubTemplatePreviewConfig;
@@ -0,0 +1,51 @@
1
+ export const DEFAULT_GITHUB_API_BASE_URL = 'https://api.github.com';
2
+ function normalizeBaseUrl(url) {
3
+ return url.replace(/\/+$/, '');
4
+ }
5
+ function normalizeRepo(repo) {
6
+ const trimmedRepo = repo.trim().replace(/\.git$/, '');
7
+ if (/^https?:\/\//i.test(trimmedRepo)) {
8
+ let parsedUrl;
9
+ try {
10
+ parsedUrl = new URL(trimmedRepo);
11
+ }
12
+ catch {
13
+ throw new Error('GITHUB_REPO must be a valid GitHub repository URL or owner/repo.');
14
+ }
15
+ const segments = parsedUrl.pathname.split('/').filter(Boolean);
16
+ if (segments.length < 2) {
17
+ throw new Error('GITHUB_REPO URL must include owner and repository name.');
18
+ }
19
+ return `${segments[0]}/${segments[1]}`;
20
+ }
21
+ const sshMatch = trimmedRepo.match(/^git@[^:]+:([^/\s]+)\/([^/\s]+)$/);
22
+ if (sshMatch) {
23
+ const owner = sshMatch[1];
24
+ const repoName = sshMatch[2]?.replace(/\.git$/, '');
25
+ return `${owner}/${repoName}`;
26
+ }
27
+ const directMatch = trimmedRepo.match(/^([^/\s]+)\/([^/\s]+)$/);
28
+ if (directMatch) {
29
+ return `${directMatch[1]}/${directMatch[2]}`;
30
+ }
31
+ throw new Error('GITHUB_REPO is required in owner/repo format or as a GitHub repository URL.');
32
+ }
33
+ export function getGitHubTemplatePreviewConfigFromEnv() {
34
+ const repo = process.env.GITHUB_REPO?.trim();
35
+ const apiKey = process.env.GITHUB_API_KEY?.trim();
36
+ const apiBaseUrl = (process.env.GITHUB_API_BASE_URL || DEFAULT_GITHUB_API_BASE_URL).trim();
37
+ const templatesBasePath = process.env.GITHUB_TEMPLATES_BASE_PATH?.trim();
38
+ if (!repo) {
39
+ throw new Error('GITHUB_REPO is required (owner/repo).');
40
+ }
41
+ if (!apiKey) {
42
+ throw new Error('GITHUB_API_KEY is required for template preview.');
43
+ }
44
+ return {
45
+ repo: normalizeRepo(repo),
46
+ apiKey,
47
+ apiBaseUrl: normalizeBaseUrl(apiBaseUrl),
48
+ templatesBasePath,
49
+ };
50
+ }
51
+ //# sourceMappingURL=github-template-preview-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-template-preview-config.js","sourceRoot":"","sources":["../../src/services/github-template-preview-config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASpE,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAEtD,IAAI,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QACtC,IAAI,SAAc,CAAC;QAEnB,IAAI,CAAC;YACH,SAAS,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACtF,CAAC;QAED,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7E,CAAC;QAED,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACvE,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC1B,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACpD,OAAO,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC;IAChC,CAAC;IAED,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAChE,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,qCAAqC;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC;IAClD,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,2BAA2B,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3F,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,IAAI,EAAE,CAAC;IAEzE,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,OAAO;QACL,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC;QACzB,MAAM;QACN,UAAU,EAAE,gBAAgB,CAAC,UAAU,CAAC;QACxC,iBAAiB;KAClB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Builds a rendered preview of a notification's templates.
3
+ *
4
+ * Templates are fetched from GitHub at the commit SHA persisted with the
5
+ * notification, so a preview always shows the template as it was when the
6
+ * notification was created. Notifications that are still pending fall back to
7
+ * the current `main` commit, since they have not been rendered yet.
8
+ */
9
+ import type { NotificationPreview } from '../contract/types.js';
10
+ import type { ApiAnyDatabaseNotification, NotificationServicePort } from './notification-service-port.js';
11
+ export interface TemplateSourceClient {
12
+ getTemplateContentByCommit(request: {
13
+ templatePath: string;
14
+ gitCommitSha: string;
15
+ }): Promise<string>;
16
+ getLatestMainCommitSha(): Promise<string>;
17
+ }
18
+ export declare function buildNotificationPreview({ service, templateClient, notification, }: {
19
+ service: NotificationServicePort;
20
+ templateClient: TemplateSourceClient;
21
+ notification: ApiAnyDatabaseNotification;
22
+ }): Promise<NotificationPreview>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Builds a rendered preview of a notification's templates.
3
+ *
4
+ * Templates are fetched from GitHub at the commit SHA persisted with the
5
+ * notification, so a preview always shows the template as it was when the
6
+ * notification was created. Notifications that are still pending fall back to
7
+ * the current `main` commit, since they have not been rendered yet.
8
+ */
9
+ import { ApiError, errorMessage } from '../errors.js';
10
+ /**
11
+ * Runs a template-source call, reporting its failures as UPSTREAM_ERROR (502).
12
+ *
13
+ * A template lookup depends on a third party (GitHub, by default): being unable
14
+ * to reach it, or being refused by it, is not a fault of this API and should not
15
+ * read as one. The client's messages are deterministic and carry no credentials,
16
+ * so they are safe to pass through to the caller.
17
+ */
18
+ async function fromTemplateSource(operation) {
19
+ try {
20
+ return await operation();
21
+ }
22
+ catch (error) {
23
+ if (error instanceof ApiError) {
24
+ throw error;
25
+ }
26
+ throw new ApiError('UPSTREAM_ERROR', errorMessage(error));
27
+ }
28
+ }
29
+ export async function buildNotificationPreview({ service, templateClient, notification, }) {
30
+ let gitCommitSha = notification.gitCommitSha;
31
+ if (!gitCommitSha && notification.status === 'PENDING_SEND') {
32
+ gitCommitSha = await fromTemplateSource(() => templateClient.getLatestMainCommitSha());
33
+ }
34
+ if (!gitCommitSha) {
35
+ throw new ApiError('PREVIEW_UNAVAILABLE', 'This notification does not have a tracked git commit SHA and is not pending send, so preview is unavailable.');
36
+ }
37
+ const bodyTemplateContent = await fromTemplateSource(() => templateClient.getTemplateContentByCommit({
38
+ templatePath: notification.bodyTemplate,
39
+ gitCommitSha,
40
+ }));
41
+ let subjectTemplateContent = null;
42
+ if (notification.subjectTemplate) {
43
+ const subjectTemplate = notification.subjectTemplate;
44
+ subjectTemplateContent = await fromTemplateSource(() => templateClient.getTemplateContentByCommit({
45
+ templatePath: subjectTemplate,
46
+ gitCommitSha,
47
+ }));
48
+ }
49
+ // Context generators may be async, so a stored context is typed as possibly a
50
+ // promise; awaiting resolves that without changing already-materialised data.
51
+ const storedContext = notification.contextUsed ? await notification.contextUsed : null;
52
+ const renderedTemplate = await service.renderEmailTemplateFromContent(notification, { body: bodyTemplateContent, subject: subjectTemplateContent }, storedContext
53
+ ? { context: storedContext }
54
+ : {
55
+ contextName: notification.contextName,
56
+ contextParameters: notification.contextParameters,
57
+ });
58
+ return {
59
+ gitCommitSha,
60
+ bodyTemplatePath: notification.bodyTemplate,
61
+ subjectTemplatePath: notification.subjectTemplate,
62
+ renderedBodyHtml: renderedTemplate.body,
63
+ renderedSubjectHtml: renderedTemplate.subject,
64
+ };
65
+ }
66
+ //# sourceMappingURL=notification-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-preview.js","sourceRoot":"","sources":["../../src/services/notification-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMtD;;;;;;;GAOG;AACH,KAAK,UAAU,kBAAkB,CAAI,SAA2B;IAC9D,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;YAC9B,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,IAAI,QAAQ,CAAC,gBAAgB,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAUD,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,EAC7C,OAAO,EACP,cAAc,EACd,YAAY,GAKb;IACC,IAAI,YAAY,GAAG,YAAY,CAAC,YAAY,CAAC;IAE7C,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QAC5D,YAAY,GAAG,MAAM,kBAAkB,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,QAAQ,CAChB,qBAAqB,EACrB,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IAED,MAAM,mBAAmB,GAAG,MAAM,kBAAkB,CAAC,GAAG,EAAE,CACxD,cAAc,CAAC,0BAA0B,CAAC;QACxC,YAAY,EAAE,YAAY,CAAC,YAAY;QACvC,YAAY;KACb,CAAC,CACH,CAAC;IAEF,IAAI,sBAAsB,GAAkB,IAAI,CAAC;IACjD,IAAI,YAAY,CAAC,eAAe,EAAE,CAAC;QACjC,MAAM,eAAe,GAAG,YAAY,CAAC,eAAe,CAAC;QACrD,sBAAsB,GAAG,MAAM,kBAAkB,CAAC,GAAG,EAAE,CACrD,cAAc,CAAC,0BAA0B,CAAC;YACxC,YAAY,EAAE,eAAe;YAC7B,YAAY;SACb,CAAC,CACH,CAAC;IACJ,CAAC;IAED,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,aAAa,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;IAEvF,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,8BAA8B,CACnE,YAAY,EACZ,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,sBAAsB,EAAE,EAC9D,aAAa;QACX,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE;QAC5B,CAAC,CAAC;YACE,WAAW,EAAE,YAAY,CAAC,WAAW;YACrC,iBAAiB,EAAE,YAAY,CAAC,iBAAiB;SAClD,CACN,CAAC;IAEF,OAAO;QACL,YAAY;QACZ,gBAAgB,EAAE,YAAY,CAAC,YAAY;QAC3C,mBAAmB,EAAE,YAAY,CAAC,eAAe;QACjD,gBAAgB,EAAE,gBAAgB,CAAC,IAAI;QACvC,mBAAmB,EAAE,gBAAgB,CAAC,OAAO;KAC9C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The slice of a VintaSend service this API depends on.
3
+ *
4
+ * The API is generic over notification config: it only reads fields that exist
5
+ * on every notification, so it pins the config to `BaseNotificationTypeConfig`.
6
+ * Operator-provided services are configured with their own concrete config and
7
+ * are adapted to this port when loaded (see `service-loader.ts`).
8
+ */
9
+ import type { AnyDatabaseNotification, BaseNotificationTypeConfig, DatabaseNotification, DatabaseOneOffNotification, EmailTemplate, EmailTemplateContent, JsonObject, NotificationFilterCapabilities, NotificationFilterFields, NotificationOrderBy } from 'vintasend';
10
+ export type ApiNotificationConfig = BaseNotificationTypeConfig;
11
+ export type ApiDatabaseNotification = DatabaseNotification<ApiNotificationConfig>;
12
+ export type ApiDatabaseOneOffNotification = DatabaseOneOffNotification<ApiNotificationConfig>;
13
+ export type ApiAnyDatabaseNotification = AnyDatabaseNotification<ApiNotificationConfig>;
14
+ export type ApiNotificationFilterFields = NotificationFilterFields<ApiNotificationConfig>;
15
+ export type RenderContextInput = {
16
+ context: JsonObject;
17
+ } | {
18
+ contextName: string;
19
+ contextParameters: JsonObject;
20
+ };
21
+ export interface NotificationServicePort {
22
+ getBackendSupportedFilterCapabilities(backendIdentifier?: string): Promise<NotificationFilterCapabilities>;
23
+ filterNotifications(filter: ApiNotificationFilterFields, page: number, pageSize: number, orderBy?: NotificationOrderBy, backendIdentifier?: string): Promise<ApiAnyDatabaseNotification[]>;
24
+ getPendingNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<ApiAnyDatabaseNotification[]>;
25
+ getFutureNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<ApiAnyDatabaseNotification[]>;
26
+ getOneOffNotifications(page: number, pageSize: number, backendIdentifier?: string): Promise<ApiDatabaseOneOffNotification[]>;
27
+ getNotification(notificationId: string, forUpdate?: boolean, backendIdentifier?: string): Promise<ApiDatabaseNotification | null>;
28
+ getOneOffNotification(notificationId: string, forUpdate?: boolean, backendIdentifier?: string): Promise<ApiDatabaseOneOffNotification | null>;
29
+ resendNotification(notificationId: string, useStoredContextIfAvailable?: boolean): Promise<ApiDatabaseNotification | undefined>;
30
+ cancelNotification(notificationId: string): Promise<void>;
31
+ renderEmailTemplateFromContent(notification: ApiAnyDatabaseNotification, templateContent: EmailTemplateContent, contextInput: RenderContextInput): Promise<EmailTemplate>;
32
+ }
33
+ /**
34
+ * Resolves a VintaSend service (or anything structurally compatible) to the port.
35
+ *
36
+ * VintaSend services are generic over the operator's notification config, so a
37
+ * concrete service is not assignable to the base-config port even though every
38
+ * member the API touches is compatible. The cast is confined to this function.
39
+ */
40
+ export declare function asNotificationServicePort(service: unknown): NotificationServicePort;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The slice of a VintaSend service this API depends on.
3
+ *
4
+ * The API is generic over notification config: it only reads fields that exist
5
+ * on every notification, so it pins the config to `BaseNotificationTypeConfig`.
6
+ * Operator-provided services are configured with their own concrete config and
7
+ * are adapted to this port when loaded (see `service-loader.ts`).
8
+ */
9
+ /**
10
+ * Resolves a VintaSend service (or anything structurally compatible) to the port.
11
+ *
12
+ * VintaSend services are generic over the operator's notification config, so a
13
+ * concrete service is not assignable to the base-config port even though every
14
+ * member the API touches is compatible. The cast is confined to this function.
15
+ */
16
+ export function asNotificationServicePort(service) {
17
+ return service;
18
+ }
19
+ //# sourceMappingURL=notification-service-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-service-port.js","sourceRoot":"","sources":["../../src/services/notification-service-port.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmFH;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAgB;IACxD,OAAO,OAAkC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The one place page numbers are translated.
3
+ *
4
+ * Routes deal exclusively in the contract's 1-indexed pages; this wrapper takes
5
+ * those, reads the backend's pagination convention once per request, and calls
6
+ * the service with whatever numbering it expects. Keeping the arithmetic here
7
+ * rather than in each route means a route cannot forget it — which is how
8
+ * `/pending`, `/future` and `/one-off` previously ended up paging by assumption.
9
+ */
10
+ import type { NotificationFilterCapabilities, NotificationOrderBy } from 'vintasend';
11
+ import type { ApiAnyDatabaseNotification, ApiDatabaseOneOffNotification, ApiNotificationFilterFields, NotificationServicePort } from './notification-service-port.js';
12
+ export interface PagedNotificationReader {
13
+ /**
14
+ * The backend's capability report, fetched at most once per reader.
15
+ */
16
+ capabilities(): Promise<NotificationFilterCapabilities>;
17
+ filterNotifications(filter: ApiNotificationFilterFields, page: number, pageSize: number, orderBy?: NotificationOrderBy): Promise<ApiAnyDatabaseNotification[]>;
18
+ getPendingNotifications(page: number, pageSize: number): Promise<ApiAnyDatabaseNotification[]>;
19
+ getFutureNotifications(page: number, pageSize: number): Promise<ApiAnyDatabaseNotification[]>;
20
+ getOneOffNotifications(page: number, pageSize: number): Promise<ApiDatabaseOneOffNotification[]>;
21
+ }
22
+ export declare function createPagedNotificationReader(service: NotificationServicePort, backendIdentifier?: string | undefined): PagedNotificationReader;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The one place page numbers are translated.
3
+ *
4
+ * Routes deal exclusively in the contract's 1-indexed pages; this wrapper takes
5
+ * those, reads the backend's pagination convention once per request, and calls
6
+ * the service with whatever numbering it expects. Keeping the arithmetic here
7
+ * rather than in each route means a route cannot forget it — which is how
8
+ * `/pending`, `/future` and `/one-off` previously ended up paging by assumption.
9
+ */
10
+ import { toBackendPage } from '../domain/pagination.js';
11
+ export function createPagedNotificationReader(service, backendIdentifier) {
12
+ let cached;
13
+ const capabilities = () => {
14
+ if (!cached) {
15
+ cached = service.getBackendSupportedFilterCapabilities(backendIdentifier);
16
+ }
17
+ return cached;
18
+ };
19
+ const backendPage = async (page) => toBackendPage(page, await capabilities());
20
+ return {
21
+ capabilities,
22
+ async filterNotifications(filter, page, pageSize, orderBy) {
23
+ return service.filterNotifications(filter, await backendPage(page), pageSize, orderBy, backendIdentifier);
24
+ },
25
+ async getPendingNotifications(page, pageSize) {
26
+ return service.getPendingNotifications(await backendPage(page), pageSize, backendIdentifier);
27
+ },
28
+ async getFutureNotifications(page, pageSize) {
29
+ return service.getFutureNotifications(await backendPage(page), pageSize, backendIdentifier);
30
+ },
31
+ async getOneOffNotifications(page, pageSize) {
32
+ return service.getOneOffNotifications(await backendPage(page), pageSize, backendIdentifier);
33
+ },
34
+ };
35
+ }
36
+ //# sourceMappingURL=paged-notification-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paged-notification-reader.js","sourceRoot":"","sources":["../../src/services/paged-notification-reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AA4BxD,MAAM,UAAU,6BAA6B,CAC3C,OAAgC,EAChC,iBAAsC;IAEtC,IAAI,MAA2D,CAAC;IAEhE,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,OAAO,CAAC,qCAAqC,CAAC,iBAAiB,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,YAAY,EAAE,CAAC,CAAC;IAEtF,OAAO;QACL,YAAY;QAEZ,KAAK,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO;YACvD,OAAO,OAAO,CAAC,mBAAmB,CAChC,MAAM,EACN,MAAM,WAAW,CAAC,IAAI,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,iBAAiB,CAClB,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,uBAAuB,CAAC,IAAI,EAAE,QAAQ;YAC1C,OAAO,OAAO,CAAC,uBAAuB,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC;QAC/F,CAAC;QAED,KAAK,CAAC,sBAAsB,CAAC,IAAI,EAAE,QAAQ;YACzC,OAAO,OAAO,CAAC,sBAAsB,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC;QAC9F,CAAC;QAED,KAAK,CAAC,sBAAsB,CAAC,IAAI,EAAE,QAAQ;YACzC,OAAO,OAAO,CAAC,sBAAsB,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC;QAC9F,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Loads the operator-provided VintaSend service.
3
+ *
4
+ * The API ships no backend of its own: which database, adapters and template
5
+ * renderers to use is a deployment decision. `VINTASEND_SERVICE_MODULE` points
6
+ * at a module whose default export builds a configured VintaSend service; see
7
+ * `src/vintasend.config.example.ts` and the README.
8
+ */
9
+ import { type NotificationServicePort } from './notification-service-port.js';
10
+ export type VintaSendServiceFactory = () => Promise<unknown> | unknown;
11
+ export declare function loadNotificationService(modulePath: string): Promise<NotificationServicePort>;
12
+ /**
13
+ * Builds the service once and reuses it for every request.
14
+ */
15
+ export declare function createServiceProvider(modulePath: string): () => Promise<NotificationServicePort>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Loads the operator-provided VintaSend service.
3
+ *
4
+ * The API ships no backend of its own: which database, adapters and template
5
+ * renderers to use is a deployment decision. `VINTASEND_SERVICE_MODULE` points
6
+ * at a module whose default export builds a configured VintaSend service; see
7
+ * `src/vintasend.config.example.ts` and the README.
8
+ */
9
+ import { isAbsolute, resolve } from 'node:path';
10
+ import { pathToFileURL } from 'node:url';
11
+ import { asNotificationServicePort, } from './notification-service-port.js';
12
+ function resolveModuleSpecifier(modulePath) {
13
+ if (modulePath.startsWith('.') || isAbsolute(modulePath)) {
14
+ return pathToFileURL(resolve(process.cwd(), modulePath)).href;
15
+ }
16
+ // Bare specifier: let Node resolve it from node_modules.
17
+ return modulePath;
18
+ }
19
+ export async function loadNotificationService(modulePath) {
20
+ let loaded;
21
+ try {
22
+ loaded = (await import(resolveModuleSpecifier(modulePath)));
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Could not load the VintaSend service module "${modulePath}". ` +
26
+ 'Set VINTASEND_SERVICE_MODULE to a module that default-exports a service factory ' +
27
+ `(see vintasend.config.example.ts). Cause: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ const factory = loaded.default ?? loaded.createVintaSendService;
30
+ if (typeof factory !== 'function') {
31
+ throw new Error(`The VintaSend service module "${modulePath}" must export a default function ` +
32
+ 'that returns a configured VintaSend service.');
33
+ }
34
+ const service = await factory();
35
+ if (!service || typeof service !== 'object') {
36
+ throw new Error(`The VintaSend service factory in "${modulePath}" did not return a service instance.`);
37
+ }
38
+ return asNotificationServicePort(service);
39
+ }
40
+ /**
41
+ * Builds the service once and reuses it for every request.
42
+ */
43
+ export function createServiceProvider(modulePath) {
44
+ let cached;
45
+ return () => {
46
+ if (!cached) {
47
+ cached = loadNotificationService(modulePath).catch((error) => {
48
+ // Do not cache failures: a transient misconfiguration should be retried.
49
+ cached = undefined;
50
+ throw error;
51
+ });
52
+ }
53
+ return cached;
54
+ };
55
+ }
56
+ //# sourceMappingURL=service-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-loader.js","sourceRoot":"","sources":["../../src/services/service-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EACL,yBAAyB,GAE1B,MAAM,gCAAgC,CAAC;AASxC,SAAS,sBAAsB,CAAC,UAAkB;IAChD,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACzD,OAAO,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IAChE,CAAC;IAED,yDAAyD;IACzD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,UAAkB;IAElB,IAAI,MAAqB,CAAC;IAE1B,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,sBAAsB,CAAC,UAAU,CAAC,CAAC,CAAkB,CAAC;IAC/E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,gDAAgD,UAAU,KAAK;YAC7D,kFAAkF;YAClF,6CACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACL,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,sBAAsB,CAAC;IAEhE,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,iCAAiC,UAAU,mCAAmC;YAC5E,8CAA8C,CACjD,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,OAAO,EAAE,CAAC;IAEhC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,qCAAqC,UAAU,sCAAsC,CACtF,CAAC;IACJ,CAAC;IAED,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAAkB;IACtD,IAAI,MAAoD,CAAC;IAEzD,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC3D,yEAAyE;gBACzE,MAAM,GAAG,SAAS,CAAC;gBACnB,MAAM,KAAK,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ export type TemplatePathResolverOptions = {
2
+ templatesBasePath?: string;
3
+ };
4
+ export declare function resolveTemplatePath(templatePath: string, options?: TemplatePathResolverOptions): string;
@@ -0,0 +1,17 @@
1
+ function trimPathEdges(path) {
2
+ return path.replace(/^\/+|\/+$/g, '');
3
+ }
4
+ export function resolveTemplatePath(templatePath, options = {}) {
5
+ const cleanedTemplatePath = trimPathEdges(templatePath.trim());
6
+ if (!cleanedTemplatePath) {
7
+ throw new Error('Template path is required for preview rendering.');
8
+ }
9
+ const cleanedBasePath = options.templatesBasePath
10
+ ? trimPathEdges(options.templatesBasePath.trim())
11
+ : '';
12
+ if (!cleanedBasePath) {
13
+ return cleanedTemplatePath;
14
+ }
15
+ return `${cleanedBasePath}/${cleanedTemplatePath}`;
16
+ }
17
+ //# sourceMappingURL=template-path-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-path-resolver.js","sourceRoot":"","sources":["../../src/services/template-path-resolver.ts"],"names":[],"mappings":"AAIA,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,YAAoB,EACpB,UAAuC,EAAE;IAEzC,MAAM,mBAAmB,GAAG,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IAE/D,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,eAAe,GAAG,OAAO,CAAC,iBAAiB;QAC/C,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;QACjD,CAAC,CAAC,EAAE,CAAC;IAEP,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IAED,OAAO,GAAG,eAAe,IAAI,mBAAmB,EAAE,CAAC;AACrD,CAAC"}