@rx-ted/packages-honest-plugins 1.0.1
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/CHANGELOG.md +13 -0
- package/README.md +45 -0
- package/api-doc/.turbo/turbo-build.log +1 -0
- package/api-doc/.turbo/turbo-test.log +11 -0
- package/api-doc/.turbo/turbo-typecheck.log +1 -0
- package/api-doc/CHANGELOG.md +22 -0
- package/api-doc/README.md +80 -0
- package/api-doc/dist/api-doc.plugin.d.ts +53 -0
- package/api-doc/dist/api-doc.plugin.js +452 -0
- package/api-doc/dist/index.d.ts +5 -0
- package/api-doc/dist/index.js +3 -0
- package/api-doc/dist/interfaces/renderer.interface.d.ts +7 -0
- package/api-doc/dist/interfaces/renderer.interface.js +1 -0
- package/api-doc/dist/renderers/scalar.renderer.d.ts +10 -0
- package/api-doc/dist/renderers/scalar.renderer.js +76 -0
- package/api-doc/dist/renderers/swagger.renderer.d.ts +10 -0
- package/api-doc/dist/renderers/swagger.renderer.js +83 -0
- package/api-doc/node_modules/.bin/esbuild +22 -0
- package/api-doc/node_modules/.bin/jiti +22 -0
- package/api-doc/node_modules/.bin/sass +22 -0
- package/api-doc/node_modules/.bin/tsx +22 -0
- package/api-doc/node_modules/.bin/vite +22 -0
- package/api-doc/node_modules/.bin/vitest +22 -0
- package/api-doc/node_modules/.bin/yaml +22 -0
- package/api-doc/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/api-doc/package.json +48 -0
- package/api-doc/src/api-doc.plugin.ts +554 -0
- package/api-doc/src/index.ts +5 -0
- package/api-doc/src/interfaces/renderer.interface.ts +5 -0
- package/api-doc/src/plugin.test.ts +740 -0
- package/api-doc/src/renderers/scalar.renderer.ts +82 -0
- package/api-doc/src/renderers/swagger.renderer.ts +89 -0
- package/api-doc/tsconfig.json +21 -0
- package/api-doc/vitest.config.ts +13 -0
- package/cache/.turbo/turbo-build.log +1 -0
- package/cache/.turbo/turbo-test.log +16 -0
- package/cache/CHANGELOG.md +23 -0
- package/cache/README.md +79 -0
- package/cache/dist/cache-service.d.ts +8 -0
- package/cache/dist/cache-service.js +21 -0
- package/cache/dist/cacheable.d.ts +2 -0
- package/cache/dist/cacheable.js +10 -0
- package/cache/dist/cloudflare/driver.d.ts +23 -0
- package/cache/dist/cloudflare/driver.js +102 -0
- package/cache/dist/cloudflare/index.d.ts +3 -0
- package/cache/dist/cloudflare/index.js +2 -0
- package/cache/dist/cloudflare/plugin.d.ts +19 -0
- package/cache/dist/cloudflare/plugin.js +58 -0
- package/cache/dist/index.d.ts +11 -0
- package/cache/dist/index.js +14 -0
- package/cache/dist/local/driver.d.ts +2 -0
- package/cache/dist/local/driver.js +98 -0
- package/cache/dist/local/index.d.ts +2 -0
- package/cache/dist/local/index.js +2 -0
- package/cache/dist/local/plugin.d.ts +14 -0
- package/cache/dist/local/plugin.js +24 -0
- package/cache/dist/redis/driver.d.ts +10 -0
- package/cache/dist/redis/driver.js +102 -0
- package/cache/dist/redis/index.d.ts +4 -0
- package/cache/dist/redis/index.js +2 -0
- package/cache/dist/redis/plugin.d.ts +25 -0
- package/cache/dist/redis/plugin.js +61 -0
- package/cache/dist/resolve.d.ts +24 -0
- package/cache/dist/resolve.js +114 -0
- package/cache/dist/types.d.ts +18 -0
- package/cache/dist/types.js +1 -0
- package/cache/node_modules/.bin/esbuild +22 -0
- package/cache/node_modules/.bin/jiti +22 -0
- package/cache/node_modules/.bin/sass +22 -0
- package/cache/node_modules/.bin/tsx +22 -0
- package/cache/node_modules/.bin/vite +22 -0
- package/cache/node_modules/.bin/vitest +22 -0
- package/cache/node_modules/.bin/yaml +22 -0
- package/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/cache/package.json +46 -0
- package/cache/src/cache-service.test.ts +16 -0
- package/cache/src/cache-service.ts +15 -0
- package/cache/src/cacheable.test.ts +78 -0
- package/cache/src/cacheable.ts +16 -0
- package/cache/src/cloudflare/driver.test.ts +188 -0
- package/cache/src/cloudflare/driver.ts +118 -0
- package/cache/src/cloudflare/index.ts +3 -0
- package/cache/src/cloudflare/plugin.ts +71 -0
- package/cache/src/index.test.ts +68 -0
- package/cache/src/index.ts +20 -0
- package/cache/src/local/driver.ts +117 -0
- package/cache/src/local/index.ts +2 -0
- package/cache/src/local/plugin.ts +34 -0
- package/cache/src/redis/driver.test.ts +71 -0
- package/cache/src/redis/driver.ts +124 -0
- package/cache/src/redis/index.ts +4 -0
- package/cache/src/redis/plugin.ts +84 -0
- package/cache/src/resolve.test.ts +257 -0
- package/cache/src/resolve.ts +140 -0
- package/cache/src/types.ts +14 -0
- package/cache/tsconfig.json +21 -0
- package/cache/vitest.config.ts +13 -0
- package/db/.turbo/turbo-build.log +1 -0
- package/db/.turbo/turbo-test.log +21 -0
- package/db/CHANGELOG.md +26 -0
- package/db/README.md +99 -0
- package/db/dist/constants.d.ts +1 -0
- package/db/dist/constants.js +1 -0
- package/db/dist/d1/index.d.ts +3 -0
- package/db/dist/d1/index.js +2 -0
- package/db/dist/d1/plugin.d.ts +18 -0
- package/db/dist/d1/plugin.js +104 -0
- package/db/dist/db-service.d.ts +7 -0
- package/db/dist/db-service.js +21 -0
- package/db/dist/index.d.ts +15 -0
- package/db/dist/index.js +20 -0
- package/db/dist/mysql/client.d.ts +36 -0
- package/db/dist/mysql/client.js +84 -0
- package/db/dist/mysql/driver.d.ts +18 -0
- package/db/dist/mysql/driver.js +107 -0
- package/db/dist/mysql/index.d.ts +11 -0
- package/db/dist/mysql/index.js +11 -0
- package/db/dist/mysql/plugin.d.ts +51 -0
- package/db/dist/mysql/plugin.js +94 -0
- package/db/dist/mysql/types.d.ts +38 -0
- package/db/dist/mysql/types.js +1 -0
- package/db/dist/postgres/index.d.ts +2 -0
- package/db/dist/postgres/index.js +3 -0
- package/db/dist/repository.d.ts +14 -0
- package/db/dist/repository.js +79 -0
- package/db/dist/resolve.d.ts +6 -0
- package/db/dist/resolve.js +81 -0
- package/db/dist/schema-builder/column-builder.d.ts +26 -0
- package/db/dist/schema-builder/column-builder.js +53 -0
- package/db/dist/schema-builder/compile-d1.d.ts +5 -0
- package/db/dist/schema-builder/compile-d1.js +67 -0
- package/db/dist/schema-builder/compile-mysql.d.ts +12 -0
- package/db/dist/schema-builder/compile-mysql.js +78 -0
- package/db/dist/schema-builder/index.d.ts +8 -0
- package/db/dist/schema-builder/index.js +22 -0
- package/db/dist/schema-builder/table-builder.d.ts +16 -0
- package/db/dist/schema-builder/table-builder.js +38 -0
- package/db/dist/schema-builder/types.d.ts +26 -0
- package/db/dist/schema-builder/types.js +1 -0
- package/db/dist/schema-builder/zod-bridge.d.ts +24 -0
- package/db/dist/schema-builder/zod-bridge.js +53 -0
- package/db/dist/sqlite/index.d.ts +4 -0
- package/db/dist/sqlite/index.js +2 -0
- package/db/dist/sqlite/plugin.d.ts +17 -0
- package/db/dist/sqlite/plugin.js +33 -0
- package/db/dist/types.d.ts +42 -0
- package/db/dist/types.js +1 -0
- package/db/node_modules/.bin/esbuild +22 -0
- package/db/node_modules/.bin/jiti +22 -0
- package/db/node_modules/.bin/sass +22 -0
- package/db/node_modules/.bin/tsx +22 -0
- package/db/node_modules/.bin/vite +22 -0
- package/db/node_modules/.bin/vitest +22 -0
- package/db/node_modules/.bin/yaml +22 -0
- package/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/db/package.json +59 -0
- package/db/src/constants.test.ts +8 -0
- package/db/src/constants.ts +1 -0
- package/db/src/d1/index.ts +13 -0
- package/db/src/d1/plugin.ts +135 -0
- package/db/src/db-service.ts +14 -0
- package/db/src/index.test.ts +63 -0
- package/db/src/index.ts +57 -0
- package/db/src/mysql/client.ts +112 -0
- package/db/src/mysql/driver.test.ts +190 -0
- package/db/src/mysql/driver.ts +116 -0
- package/db/src/mysql/index.ts +51 -0
- package/db/src/mysql/plugin.test.ts +228 -0
- package/db/src/mysql/plugin.ts +128 -0
- package/db/src/mysql/types.ts +32 -0
- package/db/src/postgres/index.ts +15 -0
- package/db/src/repository.test.ts +195 -0
- package/db/src/repository.ts +95 -0
- package/db/src/resolve.ts +105 -0
- package/db/src/schema-builder/column-builder.test.ts +55 -0
- package/db/src/schema-builder/column-builder.ts +87 -0
- package/db/src/schema-builder/compile-d1.test.ts +60 -0
- package/db/src/schema-builder/compile-d1.ts +70 -0
- package/db/src/schema-builder/compile-mysql.test.ts +64 -0
- package/db/src/schema-builder/compile-mysql.ts +102 -0
- package/db/src/schema-builder/index.test.ts +36 -0
- package/db/src/schema-builder/index.ts +41 -0
- package/db/src/schema-builder/table-builder.test.ts +55 -0
- package/db/src/schema-builder/table-builder.ts +50 -0
- package/db/src/schema-builder/types.test.ts +68 -0
- package/db/src/schema-builder/types.ts +30 -0
- package/db/src/schema-builder/zod-bridge.ts +89 -0
- package/db/src/sqlite/index.ts +14 -0
- package/db/src/sqlite/plugin.ts +46 -0
- package/db/src/types.ts +45 -0
- package/db/tsconfig.json +21 -0
- package/db/vitest.config.ts +13 -0
- package/mail/.turbo/turbo-build.log +1 -0
- package/mail/.turbo/turbo-test.log +12 -0
- package/mail/.turbo/turbo-typecheck.log +1 -0
- package/mail/CHANGELOG.md +25 -0
- package/mail/README.md +214 -0
- package/mail/dist/index.d.ts +4 -0
- package/mail/dist/index.js +2 -0
- package/mail/dist/plugin.d.ts +64 -0
- package/mail/dist/plugin.js +232 -0
- package/mail/dist/providers/brevo.provider.d.ts +15 -0
- package/mail/dist/providers/brevo.provider.js +75 -0
- package/mail/dist/providers/custom.provider.d.ts +9 -0
- package/mail/dist/providers/custom.provider.js +22 -0
- package/mail/dist/providers/resend.provider.d.ts +16 -0
- package/mail/dist/providers/resend.provider.js +77 -0
- package/mail/dist/providers/smtp.provider.d.ts +21 -0
- package/mail/dist/providers/smtp.provider.js +86 -0
- package/mail/dist/rate-limiter.d.ts +16 -0
- package/mail/dist/rate-limiter.js +49 -0
- package/mail/dist/types.d.ts +68 -0
- package/mail/dist/types.js +6 -0
- package/mail/node_modules/.bin/esbuild +22 -0
- package/mail/node_modules/.bin/jiti +22 -0
- package/mail/node_modules/.bin/sass +22 -0
- package/mail/node_modules/.bin/tsx +22 -0
- package/mail/node_modules/.bin/vite +22 -0
- package/mail/node_modules/.bin/vitest +22 -0
- package/mail/node_modules/.bin/yaml +22 -0
- package/mail/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/mail/package.json +52 -0
- package/mail/src/index.ts +4 -0
- package/mail/src/plugin.test.ts +219 -0
- package/mail/src/plugin.ts +313 -0
- package/mail/src/providers/brevo.provider.ts +78 -0
- package/mail/src/providers/custom.provider.ts +29 -0
- package/mail/src/providers/resend.provider.ts +74 -0
- package/mail/src/providers/smtp.provider.ts +85 -0
- package/mail/src/rate-limiter.test.ts +81 -0
- package/mail/src/rate-limiter.ts +54 -0
- package/mail/src/types.ts +81 -0
- package/mail/tsconfig.json +21 -0
- package/mail/vitest.config.ts +13 -0
- package/package.json +36 -0
- package/s3/.turbo/turbo-build.log +1 -0
- package/s3/.turbo/turbo-test.log +11 -0
- package/s3/CHANGELOG.md +20 -0
- package/s3/README.md +90 -0
- package/s3/dist/index.d.ts +6 -0
- package/s3/dist/index.js +4 -0
- package/s3/dist/s3-service.d.ts +7 -0
- package/s3/dist/s3-service.js +21 -0
- package/s3/dist/s3.driver.d.ts +6 -0
- package/s3/dist/s3.driver.js +221 -0
- package/s3/dist/s3.plugin.d.ts +24 -0
- package/s3/dist/s3.plugin.js +58 -0
- package/s3/dist/types.d.ts +30 -0
- package/s3/dist/types.js +1 -0
- package/s3/node_modules/.bin/esbuild +22 -0
- package/s3/node_modules/.bin/jiti +22 -0
- package/s3/node_modules/.bin/sass +22 -0
- package/s3/node_modules/.bin/tsx +22 -0
- package/s3/node_modules/.bin/vite +22 -0
- package/s3/node_modules/.bin/vitest +22 -0
- package/s3/node_modules/.bin/yaml +22 -0
- package/s3/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/s3/package.json +47 -0
- package/s3/src/index.test.ts +20 -0
- package/s3/src/index.ts +13 -0
- package/s3/src/s3-service.ts +14 -0
- package/s3/src/s3.driver.ts +259 -0
- package/s3/src/s3.plugin.ts +80 -0
- package/s3/src/types.ts +37 -0
- package/s3/tsconfig.json +21 -0
- package/s3/vitest.config.ts +13 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
|
|
2
|
+
|
|
3
|
+
export class ResendMailProvider implements MailProvider {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
private client: any = null;
|
|
6
|
+
private initPromise: Promise<void> | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
name: string,
|
|
10
|
+
private config: { apiKey: string },
|
|
11
|
+
private fromEmail: string,
|
|
12
|
+
private fromName?: string,
|
|
13
|
+
) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private async ensureInit(): Promise<void> {
|
|
18
|
+
if (this.client) return;
|
|
19
|
+
if (this.initPromise) return this.initPromise;
|
|
20
|
+
this.initPromise = (async () => {
|
|
21
|
+
const { Resend } = await import('resend');
|
|
22
|
+
this.client = new Resend(this.config.apiKey);
|
|
23
|
+
})();
|
|
24
|
+
return this.initPromise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async send(options: SendMailOptions, _providerName?: string): Promise<SendMailResult> {
|
|
28
|
+
await this.ensureInit();
|
|
29
|
+
const payload: Record<string, unknown> = {
|
|
30
|
+
from: this.fromName
|
|
31
|
+
? `${this.fromName} <${options.from || this.fromEmail}>`
|
|
32
|
+
: options.from || this.fromEmail,
|
|
33
|
+
to: Array.isArray(options.to) ? options.to : [options.to],
|
|
34
|
+
subject: options.subject,
|
|
35
|
+
};
|
|
36
|
+
if (options.cc) payload.cc = Array.isArray(options.cc) ? options.cc : [options.cc];
|
|
37
|
+
if (options.bcc) payload.bcc = Array.isArray(options.bcc) ? options.bcc : [options.bcc];
|
|
38
|
+
if (options.html) payload.html = options.html;
|
|
39
|
+
if (options.text) payload.text = options.text;
|
|
40
|
+
if (options.attachments?.length) {
|
|
41
|
+
payload.attachments = options.attachments.map((a) => ({
|
|
42
|
+
filename: a.filename,
|
|
43
|
+
content: a.content,
|
|
44
|
+
path: a.path,
|
|
45
|
+
content_type: a.contentType,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
if (options.headers) payload.headers = options.headers;
|
|
49
|
+
if (options.tags)
|
|
50
|
+
payload.tags = Object.entries(options.tags).map(([key, value]) => ({ name: key, value }));
|
|
51
|
+
const { data, error } = await this.client.emails.send(payload);
|
|
52
|
+
if (error) throw new Error(`Resend send failed: ${error.message}`);
|
|
53
|
+
return { id: data?.id ?? 'unknown', provider: this.name };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async healthCheck(): Promise<boolean> {
|
|
57
|
+
try {
|
|
58
|
+
await this.ensureInit();
|
|
59
|
+
await this.client.emails.send({
|
|
60
|
+
from: this.fromName ? `${this.fromName} <${this.fromEmail}>` : this.fromEmail,
|
|
61
|
+
to: [this.fromEmail],
|
|
62
|
+
subject: 'Health check',
|
|
63
|
+
text: 'This is a health check email from Resend provider.',
|
|
64
|
+
});
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async close(): Promise<void> {
|
|
72
|
+
this.client = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { MailProvider, SendMailOptions, SendMailResult } from '../types';
|
|
2
|
+
|
|
3
|
+
export class SmtpMailProvider implements MailProvider {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
private mailer: any = null;
|
|
6
|
+
private initPromise: Promise<void> | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
name: string,
|
|
10
|
+
private config: { host: string; port: number; user: string; pass: string; secure?: boolean },
|
|
11
|
+
private fromEmail: string,
|
|
12
|
+
private fromName?: string,
|
|
13
|
+
) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private async ensureInit(): Promise<void> {
|
|
18
|
+
if (this.mailer) return;
|
|
19
|
+
if (this.initPromise) return this.initPromise;
|
|
20
|
+
this.initPromise = (async () => {
|
|
21
|
+
const { createSMTPMailer } = await import('sently/smtp');
|
|
22
|
+
this.mailer = await createSMTPMailer({
|
|
23
|
+
host: this.config.host,
|
|
24
|
+
port: this.config.port,
|
|
25
|
+
secure: this.config.secure,
|
|
26
|
+
auth: {
|
|
27
|
+
user: this.config.user,
|
|
28
|
+
pass: this.config.pass,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
})();
|
|
32
|
+
return this.initPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async send(options: SendMailOptions, _providerName?: string): Promise<SendMailResult> {
|
|
36
|
+
await this.ensureInit();
|
|
37
|
+
const email = options.from || this.fromEmail;
|
|
38
|
+
const payload: Record<string, unknown> = {
|
|
39
|
+
from: this.fromName ? { name: this.fromName, address: email } : email,
|
|
40
|
+
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
|
41
|
+
subject: options.subject,
|
|
42
|
+
};
|
|
43
|
+
if (options.html) payload.html = options.html;
|
|
44
|
+
if (options.text) payload.text = options.text;
|
|
45
|
+
if (options.cc) payload.cc = Array.isArray(options.cc) ? options.cc.join(', ') : options.cc;
|
|
46
|
+
if (options.bcc)
|
|
47
|
+
payload.bcc = Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc;
|
|
48
|
+
if (options.replyTo) payload.replyTo = options.replyTo;
|
|
49
|
+
if (options.attachments?.length) {
|
|
50
|
+
payload.attachments = options.attachments.map((a) => ({
|
|
51
|
+
filename: a.filename,
|
|
52
|
+
content: a.content,
|
|
53
|
+
path: a.path,
|
|
54
|
+
contentType: a.contentType,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
const info = await this.mailer.send(payload);
|
|
58
|
+
return { id: info.messageId ?? 'unknown', provider: this.name };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async warmUp(): Promise<void> {
|
|
62
|
+
await this.ensureInit();
|
|
63
|
+
await this.mailer.verify();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async healthCheck(): Promise<boolean> {
|
|
67
|
+
try {
|
|
68
|
+
await this.warmUp();
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async close(): Promise<void> {
|
|
76
|
+
if (this.mailer) {
|
|
77
|
+
try {
|
|
78
|
+
await this.mailer.close();
|
|
79
|
+
} catch {
|
|
80
|
+
/* ignore */
|
|
81
|
+
}
|
|
82
|
+
this.mailer = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { RateLimiter, type MailCacheDriver } from './rate-limiter';
|
|
3
|
+
|
|
4
|
+
describe('RateLimiter', () => {
|
|
5
|
+
let cache: MailCacheDriver & { store: Map<string, number> };
|
|
6
|
+
let limiter: RateLimiter;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
cache = {
|
|
10
|
+
store: new Map(),
|
|
11
|
+
async incr(key: string) {
|
|
12
|
+
const next = (this.store.get(key) ?? 0) + 1;
|
|
13
|
+
this.store.set(key, next);
|
|
14
|
+
return next;
|
|
15
|
+
},
|
|
16
|
+
async expire(_key: string, _ttl: number) {
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
async get(key: string) {
|
|
20
|
+
return this.store.get(key) ?? null;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('with cache', () => {
|
|
26
|
+
it('passes check when quota is not exceeded', async () => {
|
|
27
|
+
limiter = new RateLimiter(5, 'test', cache);
|
|
28
|
+
await expect(limiter.checkAndIncrement()).resolves.toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('throws when quota is exceeded', async () => {
|
|
32
|
+
limiter = new RateLimiter(2, 'test', cache);
|
|
33
|
+
await limiter.checkAndIncrement();
|
|
34
|
+
await limiter.checkAndIncrement();
|
|
35
|
+
await expect(limiter.checkAndIncrement()).rejects.toThrow(/quota exceeded/i);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('increments the cache key on each check', async () => {
|
|
39
|
+
limiter = new RateLimiter(10, 'test', cache);
|
|
40
|
+
await limiter.checkAndIncrement();
|
|
41
|
+
await limiter.checkAndIncrement();
|
|
42
|
+
await limiter.checkAndIncrement();
|
|
43
|
+
const key = `mail:quota:test:${new Date().toISOString().slice(0, 10)}`;
|
|
44
|
+
expect(cache.store.get(key)).toBe(3);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('without cache', () => {
|
|
49
|
+
it('always passes when no cache is provided', async () => {
|
|
50
|
+
limiter = new RateLimiter(1, 'test', null);
|
|
51
|
+
await limiter.checkAndIncrement();
|
|
52
|
+
await limiter.checkAndIncrement();
|
|
53
|
+
await limiter.checkAndIncrement();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('health check tracking', () => {
|
|
58
|
+
it('markHealthCheckDone sets a cache key', async () => {
|
|
59
|
+
limiter = new RateLimiter(100, 'test', cache);
|
|
60
|
+
await limiter.markHealthCheckDone();
|
|
61
|
+
const key = `mail:healthcheck:test:${new Date().toISOString().slice(0, 10)}`;
|
|
62
|
+
expect(cache.store.get(key)).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('wasHealthCheckDoneToday returns true after marking done', async () => {
|
|
66
|
+
limiter = new RateLimiter(100, 'test', cache);
|
|
67
|
+
await limiter.markHealthCheckDone();
|
|
68
|
+
await expect(limiter.wasHealthCheckDoneToday()).resolves.toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('wasHealthCheckDoneToday returns false before any check', async () => {
|
|
72
|
+
limiter = new RateLimiter(100, 'test', cache);
|
|
73
|
+
await expect(limiter.wasHealthCheckDoneToday()).resolves.toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns false when no cache', async () => {
|
|
77
|
+
limiter = new RateLimiter(100, 'test', null);
|
|
78
|
+
await expect(limiter.wasHealthCheckDoneToday()).resolves.toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ILogger } from '@rx-ted/packages-core';
|
|
2
|
+
|
|
3
|
+
export interface MailCacheDriver {
|
|
4
|
+
incr(key: string): Promise<number>;
|
|
5
|
+
expire(key: string, ttl: number): Promise<boolean>;
|
|
6
|
+
get?(key: string): Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class RateLimiter {
|
|
10
|
+
constructor(
|
|
11
|
+
private quota: number,
|
|
12
|
+
private providerName: string,
|
|
13
|
+
private cache?: MailCacheDriver | null,
|
|
14
|
+
private logger?: ILogger,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async checkAndIncrement(): Promise<void> {
|
|
18
|
+
if (!this.cache) return;
|
|
19
|
+
const key = `mail:quota:${this.providerName}:${today()}`;
|
|
20
|
+
const count = await this.cache.incr(key);
|
|
21
|
+
if (count === 1) {
|
|
22
|
+
await this.cache.expire(key, 86400);
|
|
23
|
+
}
|
|
24
|
+
if (count > this.quota) {
|
|
25
|
+
this.logger?.warn?.('Daily quota exceeded', {
|
|
26
|
+
provider: this.providerName,
|
|
27
|
+
quota: this.quota,
|
|
28
|
+
});
|
|
29
|
+
const { MailQuotaExceededError } = await import('./types');
|
|
30
|
+
throw new MailQuotaExceededError(this.providerName, this.quota);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async markHealthCheckDone(): Promise<void> {
|
|
35
|
+
if (!this.cache) return;
|
|
36
|
+
const key = `mail:healthcheck:${this.providerName}:${today()}`;
|
|
37
|
+
await this.cache.incr(key);
|
|
38
|
+
await this.cache.expire(key, 86400);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async wasHealthCheckDoneToday(): Promise<boolean> {
|
|
42
|
+
if (!this.cache) return false;
|
|
43
|
+
const key = `mail:healthcheck:${this.providerName}:${today()}`;
|
|
44
|
+
if (typeof this.cache.get === 'function') {
|
|
45
|
+
const val = await this.cache.get(key);
|
|
46
|
+
return val !== null && val !== undefined;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function today(): string {
|
|
53
|
+
return new Date().toISOString().slice(0, 10);
|
|
54
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface Attachment {
|
|
2
|
+
filename: string;
|
|
3
|
+
content?: string | Uint8Array;
|
|
4
|
+
path?: string;
|
|
5
|
+
contentType?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SendMailOptions {
|
|
9
|
+
from?: string;
|
|
10
|
+
to: string | string[];
|
|
11
|
+
cc?: string | string[];
|
|
12
|
+
bcc?: string | string[];
|
|
13
|
+
subject: string;
|
|
14
|
+
text?: string;
|
|
15
|
+
html?: string;
|
|
16
|
+
replyTo?: string;
|
|
17
|
+
attachments?: Attachment[];
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
tags?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SendMailResult {
|
|
23
|
+
id: string;
|
|
24
|
+
provider: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MailProvider {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
send(options: SendMailOptions, providerName?: string): Promise<SendMailResult>;
|
|
30
|
+
healthCheck(): Promise<boolean>;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
warmUp?(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResendProviderConfig {
|
|
36
|
+
apiKey: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BrevoProviderConfig {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SmtpProviderConfig {
|
|
44
|
+
host: string;
|
|
45
|
+
port: number;
|
|
46
|
+
user: string;
|
|
47
|
+
pass: string;
|
|
48
|
+
secure?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ProviderType = 'resend' | 'brevo' | 'smtp' | 'custom';
|
|
52
|
+
|
|
53
|
+
export interface MailProviderConfigEntry {
|
|
54
|
+
name: string;
|
|
55
|
+
provider: ProviderType;
|
|
56
|
+
fromEmail: string;
|
|
57
|
+
fromName?: string;
|
|
58
|
+
limits?: {
|
|
59
|
+
dailyQuota?: number;
|
|
60
|
+
};
|
|
61
|
+
resend?: ResendProviderConfig;
|
|
62
|
+
brevo?: BrevoProviderConfig;
|
|
63
|
+
smtp?: SmtpProviderConfig;
|
|
64
|
+
custom?: MailProvider;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface MailPluginOptions {
|
|
68
|
+
providers: MailProviderConfigEntry[];
|
|
69
|
+
healthCheck?: {
|
|
70
|
+
intervalMs?: number;
|
|
71
|
+
maxRetries?: number;
|
|
72
|
+
retryDelayMs?: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class MailQuotaExceededError extends Error {
|
|
77
|
+
constructor(providerName: string, quota: number) {
|
|
78
|
+
super(`Mail quota exceeded for provider "${providerName}": ${quota}/day`);
|
|
79
|
+
this.name = 'MailQuotaExceededError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"sourceMap": false,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"emitDecoratorMetadata": true,
|
|
11
|
+
"types": ["node"],
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"rootDir": "./src",
|
|
17
|
+
"outDir": "./dist"
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"],
|
|
20
|
+
"exclude": ["**/*.test.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { getWorkspaceAliases } from '../../../vitest.workspace-aliases';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: getWorkspaceAliases(),
|
|
7
|
+
},
|
|
8
|
+
test: {
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: 'node',
|
|
11
|
+
exclude: ['**/node_modules/**'],
|
|
12
|
+
},
|
|
13
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rx-ted/packages-honest-plugins",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "HonestJS plugins — db, cache, mail, s3, api-doc",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./db": {
|
|
9
|
+
"types": "./db/dist/index.d.ts",
|
|
10
|
+
"default": "./db/dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./cache": {
|
|
13
|
+
"types": "./cache/dist/index.d.ts",
|
|
14
|
+
"default": "./cache/dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./mail": {
|
|
17
|
+
"types": "./mail/dist/index.d.ts",
|
|
18
|
+
"default": "./mail/dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./s3": {
|
|
21
|
+
"types": "./s3/dist/index.d.ts",
|
|
22
|
+
"default": "./s3/dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./api-doc": {
|
|
25
|
+
"types": "./api-doc/dist/index.d.ts",
|
|
26
|
+
"default": "./api-doc/dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@rx-ted/packages-honest-plugins-db": "workspace:^",
|
|
31
|
+
"@rx-ted/packages-honest-plugins-cache": "workspace:^",
|
|
32
|
+
"@rx-ted/packages-honest-plugins-mail": "workspace:^",
|
|
33
|
+
"@rx-ted/packages-honest-plugins-s3": "workspace:^",
|
|
34
|
+
"@rx-ted/packages-honest-plugins-api-doc": "workspace:^"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ tsc -p tsconfig.json
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
$ vitest run
|
|
2
|
+
|
|
3
|
+
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.8 [39m[90m/Users/ben/projects/app/packages/honest-plugins/s3[39m
|
|
4
|
+
|
|
5
|
+
[32m✓[39m src/index.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 1[2mms[22m[39m
|
|
6
|
+
|
|
7
|
+
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
8
|
+
[2m Tests [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
9
|
+
[2m Start at [22m 14:17:13
|
|
10
|
+
[2m Duration [22m 194ms[2m (transform 103ms, setup 0ms, import 134ms, tests 1ms, environment 0ms)[22m
|
|
11
|
+
|
package/s3/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins-s3
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11bbae7: Remove defunct packages (web-admin, auth, http-client, search) and introduce barrel packages for consolidated exports. Migrate all internal imports from scoped plugin names (`@rx-ted/packages-honest-plugins-db`) to path-based barrel imports (`@rx-ted/packages-honest-plugins/db`). Reset all package versions to 1.0.0 with consolidated changelogs. Clean up obsolete documentation across the monorepo.
|
|
8
|
+
- Updated dependencies [11bbae7]
|
|
9
|
+
- @rx-ted/packages-core@1.0.1
|
|
10
|
+
- @rx-ted/packages-honest@1.0.1
|
|
11
|
+
|
|
12
|
+
## 1.0.0
|
|
13
|
+
|
|
14
|
+
Initial consolidated release.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- S3 plugin for @rx-ted/packages-honest
|
|
19
|
+
- AWS S3 client integration (`@aws-sdk/client-s3`)
|
|
20
|
+
- S3 request presigner for temporary URLs
|
package/s3/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @rx-ted/packages-honest-plugins-s3
|
|
2
|
+
|
|
3
|
+
S3 object storage plugin for [@rx-ted/packages-honest](https://github.com/rx-ted/honest). Supports AWS S3 and S3-compatible services.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **AWS S3**: full S3 API support via `@aws-sdk/client-s3`
|
|
8
|
+
- **Presigned URLs**: generate presigned upload/download URLs
|
|
9
|
+
- **File metadata**: track file type, size, and custom metadata
|
|
10
|
+
- **Runtime-agnostic**: works on Node.js, Bun, Deno
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @rx-ted/packages-honest-plugins-s3
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Peer dependencies: `@rx-ted/packages-honest`, `hono`
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Register as a plugin
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { S3Plugin } from '@rx-ted/packages-honest-plugins/s3';
|
|
26
|
+
|
|
27
|
+
const plugin = new S3Plugin({
|
|
28
|
+
bucket: 'my-bucket',
|
|
29
|
+
region: 'us-east-1',
|
|
30
|
+
credentials: {
|
|
31
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
32
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const { hono } = await Application.create(AppModule, {
|
|
37
|
+
plugins: [plugin],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Access S3 service
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { ComponentManager } from '@rx-ted/packages-honest';
|
|
45
|
+
import { S3Service } from '@rx-ted/packages-honest-plugins/s3';
|
|
46
|
+
|
|
47
|
+
const s3 = ComponentManager.getPlugin<S3Service>('s3');
|
|
48
|
+
|
|
49
|
+
// Upload
|
|
50
|
+
await s3.upload(file, { key: 'uploads/image.png', contentType: 'image/png' });
|
|
51
|
+
|
|
52
|
+
// Get presigned URL
|
|
53
|
+
const url = await s3.getPresignedUrl('uploads/image.png', { expiresIn: 3600 });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Driver options
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createS3Driver } from '@rx-ted/packages-honest-plugins/s3';
|
|
60
|
+
|
|
61
|
+
const driver = createS3Driver({
|
|
62
|
+
endpoint: 'https://s3.amazonaws.com',
|
|
63
|
+
bucket: 'my-bucket',
|
|
64
|
+
region: 'us-east-1',
|
|
65
|
+
credentials: {
|
|
66
|
+
accessKeyId: '...',
|
|
67
|
+
secretAccessKey: '...',
|
|
68
|
+
},
|
|
69
|
+
forcePathStyle: false, // true for MinIO
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Types
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
interface FileDriver {
|
|
77
|
+
upload(key: string, body: Buffer, options?: FileUploadOptions): Promise<FileMetadata>;
|
|
78
|
+
download(key: string): Promise<Buffer>;
|
|
79
|
+
delete(key: string): Promise<void>;
|
|
80
|
+
getPresignedUrl(key: string, options?: S3Options): Promise<string>;
|
|
81
|
+
list(prefix: string): Promise<FileMetadata[]>;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Scripts
|
|
86
|
+
|
|
87
|
+
| Script | Description |
|
|
88
|
+
|--------|-------------|
|
|
89
|
+
| `pnpm test` | Run tests |
|
|
90
|
+
| `pnpm build` | Build |
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { S3Plugin, S3_CONTEXT_KEY, S3_GLOBAL_KEY } from './s3.plugin';
|
|
2
|
+
export type { S3PluginOptions } from './s3.plugin';
|
|
3
|
+
export { createS3Driver } from './s3.driver';
|
|
4
|
+
export type { S3DriverOptions } from './s3.driver';
|
|
5
|
+
export type { FileDriver, FileMetadata, FileUploadOptions, S3Options, } from './types';
|
|
6
|
+
export { S3Service } from './s3-service';
|
package/s3/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Service, ComponentManager } from '@rx-ted/packages-honest';
|
|
11
|
+
import { S3_GLOBAL_KEY } from './s3.plugin';
|
|
12
|
+
let S3Service = class S3Service {
|
|
13
|
+
constructor() {
|
|
14
|
+
return ComponentManager.getPlugin(S3_GLOBAL_KEY);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
S3Service = __decorate([
|
|
18
|
+
Service(),
|
|
19
|
+
__metadata("design:paramtypes", [])
|
|
20
|
+
], S3Service);
|
|
21
|
+
export { S3Service };
|