@zintrust/core 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/boot/Application.d.ts.map +1 -1
- package/src/boot/Application.js +48 -10
- package/src/boot/bootstrap.js +2 -0
- package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
- package/src/cli/commands/MigrateCommand.js +36 -3
- package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
- package/src/cli/d1/D1SqlMigrations.js +6 -1
- package/src/cli/scaffolding/ControllerGenerator.js +4 -4
- package/src/cli/scaffolding/GovernanceScaffolder.js +1 -1
- package/src/cli/scaffolding/MigrationGenerator.js +1 -1
- package/src/cli/scaffolding/ModelGenerator.js +1 -1
- package/src/cli/scaffolding/RouteGenerator.js +1 -1
- package/src/cli/scaffolding/ServiceScaffolder.js +4 -4
- package/src/config/broadcast.d.ts +14 -28
- package/src/config/broadcast.d.ts.map +1 -1
- package/src/config/broadcast.js +69 -35
- package/src/config/cache.d.ts +13 -45
- package/src/config/cache.d.ts.map +1 -1
- package/src/config/cache.js +69 -25
- package/src/config/database.d.ts +22 -64
- package/src/config/database.d.ts.map +1 -1
- package/src/config/database.js +99 -31
- package/src/config/env.d.ts +6 -0
- package/src/config/env.d.ts.map +1 -1
- package/src/config/env.js +7 -0
- package/src/config/index.d.ts +32 -136
- package/src/config/index.d.ts.map +1 -1
- package/src/config/mail.d.ts +19 -55
- package/src/config/mail.d.ts.map +1 -1
- package/src/config/mail.js +63 -21
- package/src/config/middleware.d.ts +24 -0
- package/src/config/middleware.d.ts.map +1 -1
- package/src/config/middleware.js +72 -52
- package/src/config/notification.d.ts +14 -27
- package/src/config/notification.d.ts.map +1 -1
- package/src/config/notification.js +82 -36
- package/src/config/queue.d.ts +21 -51
- package/src/config/queue.d.ts.map +1 -1
- package/src/config/queue.js +72 -27
- package/src/config/storage.d.ts +27 -34
- package/src/config/storage.d.ts.map +1 -1
- package/src/config/storage.js +97 -56
- package/src/config/type.d.ts +12 -1
- package/src/config/type.d.ts.map +1 -1
- package/src/http/parsers/MultipartParser.d.ts.map +1 -1
- package/src/http/parsers/MultipartParser.js +69 -42
- package/src/index.d.ts +9 -5
- package/src/index.d.ts.map +1 -1
- package/src/index.js +1 -0
- package/src/microservices/PostgresAdapter.d.ts.map +1 -1
- package/src/microservices/PostgresAdapter.js +0 -1
- package/src/migrations/MigratorFactory.d.ts.map +1 -1
- package/src/migrations/MigratorFactory.js +18 -2
- package/src/node-singletons/fs.d.ts +1 -1
- package/src/node-singletons/fs.d.ts.map +1 -1
- package/src/node-singletons/fs.js +1 -1
- package/src/orm/Database.d.ts +2 -1
- package/src/orm/Database.d.ts.map +1 -1
- package/src/orm/Database.js +110 -67
- package/src/orm/DatabaseAdapter.d.ts +1 -0
- package/src/orm/DatabaseAdapter.d.ts.map +1 -1
- package/src/orm/DatabaseRuntimeRegistration.d.ts.map +1 -1
- package/src/orm/DatabaseRuntimeRegistration.js +12 -0
- package/src/orm/QueryBuilder.d.ts +1 -1
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +4 -3
- package/src/orm/adapters/SQLiteAdapter.js +1 -1
- package/src/performance/Optimizer.d.ts +6 -6
- package/src/performance/Optimizer.d.ts.map +1 -1
- package/src/performance/Optimizer.js +133 -52
- package/src/routing/doc.d.ts +4 -5
- package/src/routing/doc.d.ts.map +1 -1
- package/src/routing/doc.js +35 -20
- package/src/routing/publicRoot.d.ts +9 -0
- package/src/routing/publicRoot.d.ts.map +1 -1
- package/src/routing/publicRoot.js +63 -2
- package/src/runtime/StartupConfigFileRegistry.d.ts +20 -0
- package/src/runtime/StartupConfigFileRegistry.d.ts.map +1 -0
- package/src/runtime/StartupConfigFileRegistry.js +44 -0
- package/src/runtime/useFileLoader.d.ts +26 -0
- package/src/runtime/useFileLoader.d.ts.map +1 -0
- package/src/runtime/useFileLoader.js +188 -0
- package/src/scripts/TemplateSync.js +4 -4
- package/src/security/XssProtection.d.ts.map +1 -1
- package/src/security/XssProtection.js +62 -14
- package/src/templates/project/basic/config/broadcast.ts.tpl +33 -17
- package/src/templates/project/basic/config/cache.ts.tpl +35 -17
- package/src/templates/project/basic/config/database.ts.tpl +68 -32
- package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +7 -114
- package/src/templates/project/basic/config/mail.ts.tpl +59 -13
- package/src/templates/project/basic/config/notification.ts.tpl +28 -17
- package/src/templates/project/basic/config/queue.ts.tpl +49 -17
- package/src/templates/project/basic/config/storage.ts.tpl +55 -18
- package/src/templates/project/basic/config/type.ts.tpl +0 -1
- package/src/templates/project/basic/src/index.ts.tpl +3 -0
- package/src/templates/project/basic/config/logging/KvLogger.ts.tpl +0 -181
- package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +0 -156
|
@@ -1,121 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP
|
|
3
|
-
* Sends logs to an external HTTP logging service.
|
|
2
|
+
* HTTP Logger (starter override)
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - HTTP_LOG_ENDPOINT_URL
|
|
8
|
-
* - HTTP_LOG_BATCH_SIZE (default: 50)
|
|
9
|
-
* - HTTP_LOG_AUTH_TOKEN (optional)
|
|
4
|
+
* Starter projects should import the framework's HttpLogger from `@zintrust/core`.
|
|
5
|
+
* This keeps templates free of deep/internal imports.
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { ErrorFactory } from '@zintrust/core';
|
|
15
|
-
import { HttpClient } from '@zintrust/core';
|
|
8
|
+
import { HttpLogger } from '@zintrust/core';
|
|
9
|
+
import type { HttpLogEvent } from '@zintrust/core';
|
|
16
10
|
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
20
|
-
message: string;
|
|
21
|
-
category?: string;
|
|
22
|
-
data?: unknown;
|
|
23
|
-
error?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const isEnabled = (): boolean => Env.getBool('HTTP_LOG_ENABLED', false);
|
|
27
|
-
|
|
28
|
-
let buffer: HttpLogEvent[] = [];
|
|
29
|
-
let flushPromise: Promise<void> | undefined;
|
|
30
|
-
|
|
31
|
-
const postBatch = async (events: HttpLogEvent[]): Promise<void> => {
|
|
32
|
-
const endpoint = Env.get('HTTP_LOG_ENDPOINT_URL').trim();
|
|
33
|
-
if (endpoint.length === 0) {
|
|
34
|
-
throw ErrorFactory.createConfigError(
|
|
35
|
-
'HTTP_LOG_ENDPOINT_URL is required when HTTP logging is enabled'
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const token = Env.get('HTTP_LOG_AUTH_TOKEN').trim();
|
|
40
|
-
|
|
41
|
-
const builder = HttpClient.post(endpoint, {
|
|
42
|
-
sentAt: new Date().toISOString(),
|
|
43
|
-
count: events.length,
|
|
44
|
-
events,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (token.length > 0) {
|
|
48
|
-
builder.withAuth(token, 'Bearer');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
await builder.send();
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const flushNow = async (): Promise<void> => {
|
|
55
|
-
const toSend = buffer;
|
|
56
|
-
buffer = [];
|
|
57
|
-
|
|
58
|
-
if (!isEnabled()) return;
|
|
59
|
-
if (toSend.length === 0) return;
|
|
60
|
-
|
|
61
|
-
const maxRetries = 3;
|
|
62
|
-
|
|
63
|
-
const attemptPost = async (attempt: number): Promise<void> => {
|
|
64
|
-
try {
|
|
65
|
-
await postBatch(toSend);
|
|
66
|
-
} catch {
|
|
67
|
-
if (attempt >= maxRetries) return;
|
|
68
|
-
const backoffMs = 100 * 2 ** attempt;
|
|
69
|
-
await delay(backoffMs);
|
|
70
|
-
await attemptPost(attempt + 1);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
await attemptPost(0);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const scheduleFlush = async (): Promise<void> => {
|
|
78
|
-
if (flushPromise !== undefined) return flushPromise;
|
|
79
|
-
|
|
80
|
-
const promise = new Promise<void>((resolve) => {
|
|
81
|
-
const run = async (): Promise<void> => {
|
|
82
|
-
try {
|
|
83
|
-
await flushNow();
|
|
84
|
-
} finally {
|
|
85
|
-
resolve(undefined);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (typeof globalThis.setTimeout !== 'function') {
|
|
90
|
-
void run();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
globalThis.setTimeout(() => {
|
|
95
|
-
void run();
|
|
96
|
-
}, 0);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
flushPromise = promise.finally(() => {
|
|
100
|
-
flushPromise = undefined;
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return flushPromise;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export const HttpLogger = Object.freeze({
|
|
107
|
-
async enqueue(event: HttpLogEvent): Promise<void> {
|
|
108
|
-
if (!isEnabled()) return Promise.resolve(); // NOSONAR
|
|
109
|
-
|
|
110
|
-
buffer.push(event);
|
|
111
|
-
|
|
112
|
-
const batchSize = Math.max(1, Env.getInt('HTTP_LOG_BATCH_SIZE', 50));
|
|
113
|
-
if (buffer.length >= batchSize) {
|
|
114
|
-
return scheduleFlush();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return scheduleFlush();
|
|
118
|
-
},
|
|
119
|
-
});
|
|
11
|
+
export { HttpLogger };
|
|
12
|
+
export type { HttpLogEvent };
|
|
120
13
|
|
|
121
14
|
export default HttpLogger;
|
|
@@ -1,23 +1,69 @@
|
|
|
1
|
+
import { Env, type MailConfigOverrides } from '@zintrust/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Mail Configuration (
|
|
4
|
+
* Mail Configuration (default override)
|
|
3
5
|
*
|
|
4
6
|
* Keep this file declarative:
|
|
5
7
|
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
export default {
|
|
12
|
+
default: Env.get('MAIL_CONNECTION', Env.get('MAIL_DRIVER', 'disabled')).trim().toLowerCase(),
|
|
13
|
+
from: {
|
|
14
|
+
address: Env.get('MAIL_FROM_ADDRESS', ''),
|
|
15
|
+
name: Env.get('MAIL_FROM_NAME', ''),
|
|
16
|
+
},
|
|
17
|
+
drivers: {
|
|
18
|
+
disabled: {
|
|
19
|
+
driver: 'disabled' as const,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
sendgrid: {
|
|
23
|
+
driver: 'sendgrid' as const,
|
|
24
|
+
apiKey: Env.get('SENDGRID_API_KEY', ''),
|
|
25
|
+
},
|
|
10
26
|
|
|
11
|
-
|
|
27
|
+
mailgun: {
|
|
28
|
+
driver: 'mailgun' as const,
|
|
29
|
+
apiKey: Env.get('MAILGUN_API_KEY', ''),
|
|
30
|
+
domain: Env.get('MAILGUN_DOMAIN', ''),
|
|
31
|
+
baseUrl: Env.get('MAILGUN_BASE_URL', 'https://api.mailgun.net').trim(),
|
|
32
|
+
},
|
|
12
33
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
smtp: {
|
|
35
|
+
driver: 'smtp' as const,
|
|
36
|
+
host: Env.get('MAIL_HOST', ''),
|
|
37
|
+
port: Env.getInt('MAIL_PORT', 587),
|
|
38
|
+
username: Env.get('MAIL_USERNAME', ''),
|
|
39
|
+
password: Env.get('MAIL_PASSWORD', ''),
|
|
40
|
+
secure: (() => {
|
|
41
|
+
const raw = Env.get('MAIL_SECURE', '').trim().toLowerCase();
|
|
42
|
+
if (raw === 'starttls') return 'starttls' as const;
|
|
43
|
+
if (raw === 'tls' || raw === 'ssl' || raw === 'smtps' || raw === 'implicit') return true;
|
|
44
|
+
if (raw === 'none' || raw === 'off' || raw === 'false' || raw === '0') return false;
|
|
45
|
+
return Env.getBool('MAIL_SECURE', false);
|
|
46
|
+
})(),
|
|
47
|
+
},
|
|
16
48
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
nodemailer: {
|
|
50
|
+
driver: 'nodemailer' as const,
|
|
51
|
+
host: Env.get('MAIL_HOST', ''),
|
|
52
|
+
port: Env.getInt('MAIL_PORT', 587),
|
|
53
|
+
username: Env.get('MAIL_USERNAME', ''),
|
|
54
|
+
password: Env.get('MAIL_PASSWORD', ''),
|
|
55
|
+
secure: (() => {
|
|
56
|
+
const raw = Env.get('MAIL_SECURE', '').trim().toLowerCase();
|
|
57
|
+
if (raw === 'starttls') return 'starttls' as const;
|
|
58
|
+
if (raw === 'tls' || raw === 'ssl' || raw === 'smtps' || raw === 'implicit') return true;
|
|
59
|
+
if (raw === 'none' || raw === 'off' || raw === 'false' || raw === '0') return false;
|
|
60
|
+
return Env.getBool('MAIL_SECURE', false);
|
|
61
|
+
})(),
|
|
62
|
+
},
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
|
|
64
|
+
ses: {
|
|
65
|
+
driver: 'ses' as const,
|
|
66
|
+
region: Env.get('AWS_REGION', 'us-east-1'),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
} satisfies MailConfigOverrides;
|
|
@@ -1,23 +1,34 @@
|
|
|
1
|
+
import { Env, type NotificationConfigOverrides } from '@zintrust/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Notification Configuration (
|
|
4
|
+
* Notification Configuration (default override)
|
|
3
5
|
*
|
|
4
6
|
* Keep this file declarative:
|
|
5
7
|
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export default {
|
|
12
|
+
default: Env.get('NOTIFICATION_CONNECTION', Env.get('NOTIFICATION_DRIVER', 'console')),
|
|
13
|
+
drivers: {
|
|
14
|
+
console: {
|
|
15
|
+
driver: 'console' as const,
|
|
16
|
+
},
|
|
17
|
+
termii: {
|
|
18
|
+
driver: 'termii' as const,
|
|
19
|
+
apiKey: Env.get('TERMII_API_KEY', ''),
|
|
20
|
+
sender: Env.get('TERMII_SENDER', 'Zintrust'),
|
|
21
|
+
endpoint: Env.get('TERMII_ENDPOINT', 'https://api.termii.com/sms/send'),
|
|
22
|
+
},
|
|
23
|
+
twilio: {
|
|
24
|
+
driver: 'twilio' as const,
|
|
25
|
+
accountSid: Env.get('TWILIO_ACCOUNT_SID', ''),
|
|
26
|
+
authToken: Env.get('TWILIO_AUTH_TOKEN', ''),
|
|
27
|
+
fromNumber: Env.get('TWILIO_FROM_NUMBER', ''),
|
|
28
|
+
},
|
|
29
|
+
slack: {
|
|
30
|
+
driver: 'slack' as const,
|
|
31
|
+
webhookUrl: Env.get('SLACK_WEBHOOK_URL', ''),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
} satisfies NotificationConfigOverrides;
|
|
@@ -1,23 +1,55 @@
|
|
|
1
|
+
import { Env, type QueueConfigOverrides, type QueueDriverName } from '@zintrust/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Queue Configuration (
|
|
4
|
+
* Queue Configuration (default override)
|
|
3
5
|
*
|
|
4
6
|
* Keep this file declarative:
|
|
5
7
|
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export default {
|
|
12
|
+
default: Env.get('QUEUE_DRIVER', 'sync') as QueueDriverName,
|
|
13
|
+
drivers: {
|
|
14
|
+
sync: {
|
|
15
|
+
driver: 'sync' as const,
|
|
16
|
+
},
|
|
17
|
+
database: {
|
|
18
|
+
driver: 'database' as const,
|
|
19
|
+
table: Env.get('QUEUE_TABLE', 'jobs'),
|
|
20
|
+
connection: Env.get('QUEUE_DB_CONNECTION', 'default'),
|
|
21
|
+
},
|
|
22
|
+
redis: {
|
|
23
|
+
driver: 'redis' as const,
|
|
24
|
+
host: Env.get('REDIS_HOST', 'localhost'),
|
|
25
|
+
port: Env.getInt('REDIS_PORT', 6379),
|
|
26
|
+
password: Env.get('REDIS_PASSWORD'),
|
|
27
|
+
database: Env.getInt('REDIS_QUEUE_DB', 1),
|
|
28
|
+
},
|
|
29
|
+
rabbitmq: {
|
|
30
|
+
driver: 'rabbitmq' as const,
|
|
31
|
+
host: Env.get('RABBITMQ_HOST', 'localhost'),
|
|
32
|
+
port: Env.getInt('RABBITMQ_PORT', 5672),
|
|
33
|
+
username: Env.get('RABBITMQ_USER', 'guest'),
|
|
34
|
+
password: Env.get('RABBITMQ_PASSWORD', 'guest'),
|
|
35
|
+
vhost: Env.get('RABBITMQ_VHOST', '/'),
|
|
36
|
+
},
|
|
37
|
+
sqs: {
|
|
38
|
+
driver: 'sqs' as const,
|
|
39
|
+
key: Env.get('AWS_ACCESS_KEY_ID'),
|
|
40
|
+
secret: Env.get('AWS_SECRET_ACCESS_KEY'),
|
|
41
|
+
region: Env.AWS_REGION,
|
|
42
|
+
queueUrl: Env.get('AWS_SQS_QUEUE_URL'),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
failed: {
|
|
46
|
+
database: Env.get('FAILED_JOBS_DB_CONNECTION', 'default'),
|
|
47
|
+
table: Env.get('FAILED_JOBS_TABLE', 'failed_jobs'),
|
|
48
|
+
},
|
|
49
|
+
processing: {
|
|
50
|
+
timeout: Env.getInt('QUEUE_JOB_TIMEOUT', 60),
|
|
51
|
+
retries: Env.getInt('QUEUE_JOB_RETRIES', 3),
|
|
52
|
+
backoff: Env.getInt('QUEUE_JOB_BACKOFF', 0),
|
|
53
|
+
workers: Env.getInt('QUEUE_WORKERS', 1),
|
|
54
|
+
},
|
|
55
|
+
} satisfies QueueConfigOverrides;
|
|
@@ -1,23 +1,60 @@
|
|
|
1
|
+
import { Env, type StorageConfigOverrides } from '@zintrust/core';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Storage Configuration (
|
|
4
|
+
* Storage Configuration (default override)
|
|
3
5
|
*
|
|
4
6
|
* Keep this file declarative:
|
|
5
|
-
* - Core owns env parsing/default logic.
|
|
6
|
-
* - Projects can override
|
|
7
|
+
* - Core owns driver setup and env parsing/default logic.
|
|
8
|
+
* - Projects can override config by editing values below.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
export default {
|
|
12
|
+
default: Env.get('STORAGE_CONNECTION', Env.get('STORAGE_DRIVER', 'local')).trim().toLowerCase(),
|
|
13
|
+
drivers: {
|
|
14
|
+
local: {
|
|
15
|
+
driver: 'local' as const,
|
|
16
|
+
root: Env.get('STORAGE_PATH', 'storage'),
|
|
17
|
+
url: Env.get('STORAGE_URL', '/storage'),
|
|
18
|
+
visibility: Env.get('STORAGE_VISIBILITY', 'private'),
|
|
19
|
+
},
|
|
20
|
+
s3: {
|
|
21
|
+
driver: 's3' as const,
|
|
22
|
+
accessKeyId: Env.get('AWS_ACCESS_KEY_ID', ''),
|
|
23
|
+
secretAccessKey: Env.get('AWS_SECRET_ACCESS_KEY', ''),
|
|
24
|
+
region: Env.get('AWS_REGION', 'us-east-1'),
|
|
25
|
+
bucket: Env.get('AWS_S3_BUCKET', ''),
|
|
26
|
+
url: Env.get('AWS_S3_URL', ''),
|
|
27
|
+
endpoint: Env.get('AWS_S3_ENDPOINT', ''),
|
|
28
|
+
usePathStyleUrl: Env.getBool('AWS_S3_USE_PATH_STYLE_URL', false),
|
|
29
|
+
},
|
|
30
|
+
r2: {
|
|
31
|
+
driver: 'r2' as const,
|
|
32
|
+
accessKeyId: Env.get('R2_ACCESS_KEY_ID', ''),
|
|
33
|
+
secretAccessKey: Env.get('R2_SECRET_ACCESS_KEY', ''),
|
|
34
|
+
region: Env.get('R2_REGION', ''),
|
|
35
|
+
bucket: Env.get('R2_BUCKET', ''),
|
|
36
|
+
endpoint: Env.get('R2_ENDPOINT', ''),
|
|
37
|
+
url: Env.get('R2_URL', ''),
|
|
38
|
+
},
|
|
39
|
+
gcs: {
|
|
40
|
+
driver: 'gcs' as const,
|
|
41
|
+
projectId: Env.get('GCS_PROJECT_ID', ''),
|
|
42
|
+
keyFile: Env.get('GCS_KEY_FILE', ''),
|
|
43
|
+
bucket: Env.get('GCS_BUCKET', ''),
|
|
44
|
+
url: Env.get('GCS_URL', ''),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
temp: {
|
|
48
|
+
path: Env.get('TEMP_PATH', 'storage/temp'),
|
|
49
|
+
maxAge: Env.getInt('TEMP_FILE_MAX_AGE', 86400),
|
|
50
|
+
},
|
|
51
|
+
uploads: {
|
|
52
|
+
maxSize: Env.get('MAX_UPLOAD_SIZE', '100mb'),
|
|
53
|
+
allowedMimes: Env.get('ALLOWED_UPLOAD_MIMES', 'jpg,jpeg,png,pdf,doc,docx'),
|
|
54
|
+
path: Env.get('UPLOADS_PATH', 'storage/uploads'),
|
|
55
|
+
},
|
|
56
|
+
backups: {
|
|
57
|
+
path: Env.get('BACKUPS_PATH', 'storage/backups'),
|
|
58
|
+
driver: Env.get('BACKUP_DRIVER', 's3'),
|
|
59
|
+
},
|
|
60
|
+
} satisfies StorageConfigOverrides;
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* KV Logger
|
|
3
|
-
* Writes batches of log events to a KV namespace (Cloudflare Workers compatible)
|
|
4
|
-
*
|
|
5
|
-
* Enabled via env:
|
|
6
|
-
* - KV_LOG_ENABLED (default: false)
|
|
7
|
-
* - KV_NAMESPACE (binding name; default: 'CACHE')
|
|
8
|
-
* - KV_LOG_RETENTION_DAYS (default: 30)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Cloudflare } from '@zintrust/core';
|
|
12
|
-
import { Env } from '@zintrust/core';
|
|
13
|
-
|
|
14
|
-
export type KvLogEvent = {
|
|
15
|
-
timestamp: string;
|
|
16
|
-
level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
17
|
-
message: string;
|
|
18
|
-
category?: string;
|
|
19
|
-
data?: unknown;
|
|
20
|
-
error?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type KVNamespace = NonNullable<ReturnType<typeof Cloudflare.getKVBinding>>;
|
|
24
|
-
|
|
25
|
-
type PutOptions = { expiration?: number; expirationTtl?: number; metadata?: unknown };
|
|
26
|
-
|
|
27
|
-
const getRetentionTtlSeconds = (): number => {
|
|
28
|
-
const days = Env.getInt('KV_LOG_RETENTION_DAYS', 30);
|
|
29
|
-
const safeDays = Number.isFinite(days) && days > 0 ? days : 30;
|
|
30
|
-
return safeDays * 24 * 60 * 60;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const getKvBindingName = (): string => {
|
|
34
|
-
const name = Env.get('KV_NAMESPACE', 'CACHE').trim();
|
|
35
|
-
return name.length > 0 ? name : 'CACHE';
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const isEnabled = (): boolean => Env.getBool('KV_LOG_ENABLED', false);
|
|
39
|
-
|
|
40
|
-
const safeRandom = (): string => {
|
|
41
|
-
try {
|
|
42
|
-
// Prefer crypto if available
|
|
43
|
-
const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto;
|
|
44
|
-
if (cryptoObj?.getRandomValues) {
|
|
45
|
-
const bytes = new Uint8Array(8);
|
|
46
|
-
cryptoObj.getRandomValues(bytes);
|
|
47
|
-
return Array.from(bytes)
|
|
48
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
49
|
-
.join('');
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
// fall through
|
|
53
|
-
}
|
|
54
|
-
const generate = Math.random().toString(16).slice(2); // NOSONAR
|
|
55
|
-
const fallback = generate + Math.random().toString(16).slice(2); // NOSONAR this is not used for security
|
|
56
|
-
return fallback;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const buildKey = (timestampIso: string): string => {
|
|
60
|
-
const date = timestampIso.slice(0, 10);
|
|
61
|
-
const hour = timestampIso.slice(11, 13);
|
|
62
|
-
return `logs:${date}:${hour}:${safeRandom()}`;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
let buffer: KvLogEvent[] = [];
|
|
66
|
-
let flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
67
|
-
let flushPromise: Promise<void> | undefined;
|
|
68
|
-
|
|
69
|
-
const scheduleFlush = async (): Promise<void> => {
|
|
70
|
-
if (flushPromise !== undefined) return flushPromise;
|
|
71
|
-
|
|
72
|
-
// Fixed small batching window to reduce KV write volume.
|
|
73
|
-
const windowMs = 1000;
|
|
74
|
-
|
|
75
|
-
const promise = new Promise<void>((resolve) => {
|
|
76
|
-
const run = async (): Promise<void> => {
|
|
77
|
-
try {
|
|
78
|
-
await flushNow();
|
|
79
|
-
} finally {
|
|
80
|
-
resolve(undefined);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
if (typeof globalThis.setTimeout !== 'function') {
|
|
85
|
-
// microtask-ish
|
|
86
|
-
void run();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
flushTimer = globalThis.setTimeout(() => {
|
|
91
|
-
flushTimer = undefined;
|
|
92
|
-
void run();
|
|
93
|
-
}, windowMs);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
flushPromise = promise.finally(() => {
|
|
97
|
-
flushPromise = undefined;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
return flushPromise;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const getKv = (): KVNamespace | null => {
|
|
104
|
-
const bindingName = getKvBindingName();
|
|
105
|
-
return Cloudflare.getKVBinding(bindingName);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const putBatch = async (kv: KVNamespace, events: KvLogEvent[]): Promise<void> => {
|
|
109
|
-
if (events.length === 0) return;
|
|
110
|
-
|
|
111
|
-
const timestamp = events.at(-1)?.timestamp ?? new Date().toISOString();
|
|
112
|
-
const key = buildKey(timestamp);
|
|
113
|
-
|
|
114
|
-
const payload = JSON.stringify({
|
|
115
|
-
version: 1,
|
|
116
|
-
createdAt: new Date().toISOString(),
|
|
117
|
-
count: events.length,
|
|
118
|
-
events,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const opts: PutOptions = { expirationTtl: getRetentionTtlSeconds() };
|
|
122
|
-
|
|
123
|
-
await kv.put(key, payload, opts);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const flushNow = async (): Promise<void> => {
|
|
127
|
-
if (!isEnabled()) {
|
|
128
|
-
buffer = [];
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const kv = getKv();
|
|
133
|
-
if (kv === null) {
|
|
134
|
-
buffer = [];
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const toSend = buffer;
|
|
139
|
-
buffer = [];
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
await putBatch(kv, toSend);
|
|
143
|
-
} catch {
|
|
144
|
-
// Best-effort: never throw from logging.
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const flushSoon = async (): Promise<void> => {
|
|
149
|
-
if (flushPromise !== undefined) return flushPromise;
|
|
150
|
-
|
|
151
|
-
flushPromise = Promise.resolve()
|
|
152
|
-
.then(async () => flushNow())
|
|
153
|
-
.finally(() => {
|
|
154
|
-
flushPromise = undefined;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return flushPromise;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
export const KvLogger = Object.freeze({
|
|
161
|
-
async enqueue(event: KvLogEvent): Promise<void> {
|
|
162
|
-
if (!isEnabled()) return Promise.resolve();
|
|
163
|
-
|
|
164
|
-
buffer.push(event);
|
|
165
|
-
|
|
166
|
-
// Basic size guard: flush if it gets too large
|
|
167
|
-
const maxBatch = 100;
|
|
168
|
-
if (buffer.length >= maxBatch) {
|
|
169
|
-
// Cancel scheduled flush and flush immediately
|
|
170
|
-
if (flushTimer !== undefined) {
|
|
171
|
-
globalThis.clearTimeout(flushTimer);
|
|
172
|
-
flushTimer = undefined;
|
|
173
|
-
}
|
|
174
|
-
return flushSoon();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return scheduleFlush();
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
export default KvLogger;
|