@zintrust/core 0.1.11 → 0.1.12
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 +1 -1
- package/src/cache/Cache.d.ts.map +1 -1
- package/src/cache/Cache.js +3 -0
- package/src/cache/drivers/KVRemoteDriver.d.ts +11 -0
- package/src/cache/drivers/KVRemoteDriver.d.ts.map +1 -0
- package/src/cache/drivers/KVRemoteDriver.js +70 -0
- package/src/common/RemoteSignedJson.d.ts +21 -0
- package/src/common/RemoteSignedJson.d.ts.map +1 -0
- package/src/common/RemoteSignedJson.js +86 -0
- package/src/config/cache.d.ts +4 -0
- package/src/config/cache.d.ts.map +1 -1
- package/src/config/cache.js +4 -0
- package/src/config/env.d.ts +10 -0
- package/src/config/env.d.ts.map +1 -1
- package/src/config/env.js +12 -0
- package/src/config/index.d.ts +4 -0
- package/src/config/index.d.ts.map +1 -1
- package/src/config/type.d.ts +6 -1
- package/src/config/type.d.ts.map +1 -1
- package/src/index.d.ts +64 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +18 -1
- package/src/orm/Database.d.ts.map +1 -1
- package/src/orm/Database.js +3 -0
- package/src/orm/DatabaseAdapter.d.ts +1 -1
- package/src/orm/DatabaseAdapter.d.ts.map +1 -1
- package/src/orm/adapters/D1RemoteAdapter.d.ts +11 -0
- package/src/orm/adapters/D1RemoteAdapter.d.ts.map +1 -0
- package/src/orm/adapters/D1RemoteAdapter.js +162 -0
- package/src/runtime/PluginRegistry.d.ts.map +1 -1
- package/src/runtime/PluginRegistry.js +124 -56
- package/src/security/SignedRequest.d.ts +53 -0
- package/src/security/SignedRequest.d.ts.map +1 -0
- package/src/security/SignedRequest.js +172 -0
- package/src/templates/project/basic/config/cache.ts.tpl +4 -0
- package/src/templates/project/basic/config/env.ts.tpl +15 -0
- package/src/templates/project/basic/config/type.ts.tpl +11 -5
- package/src/tools/mail/Mail.d.ts.map +1 -1
- package/src/tools/mail/Mail.js +4 -22
- package/src/tools/storage/StorageDriverRegistry.d.ts +16 -0
- package/src/tools/storage/StorageDriverRegistry.d.ts.map +1 -0
- package/src/tools/storage/StorageDriverRegistry.js +20 -0
- package/src/tools/storage/index.d.ts +1 -5
- package/src/tools/storage/index.d.ts.map +1 -1
- package/src/tools/storage/index.js +22 -17
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"D1RemoteAdapter.d.ts","sourceRoot":"","sources":["../../../../src/orm/adapters/D1RemoteAdapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAe,MAAM,sBAAsB,CAAC;AA+I1F,eAAO,MAAM,eAAe;oBACV,cAAc,GAAG,gBAAgB;EA+EjD,CAAC;AAEH,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 Remote Database Adapter
|
|
3
|
+
*
|
|
4
|
+
* Calls a Zintrust Cloudflare Worker proxy over HTTPS.
|
|
5
|
+
*/
|
|
6
|
+
import { RemoteSignedJson } from '../../common/RemoteSignedJson.js';
|
|
7
|
+
import { Env } from '../../config/env.js';
|
|
8
|
+
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
|
|
9
|
+
import { QueryBuilder } from '../QueryBuilder.js';
|
|
10
|
+
import { SignedRequest } from '../../security/SignedRequest.js';
|
|
11
|
+
const createRemoteConfig = () => {
|
|
12
|
+
const settings = {
|
|
13
|
+
baseUrl: Env.get('D1_REMOTE_URL'),
|
|
14
|
+
keyId: Env.get('D1_REMOTE_KEY_ID'),
|
|
15
|
+
secret: Env.get('D1_REMOTE_SECRET'),
|
|
16
|
+
mode: Env.get('D1_REMOTE_MODE', 'registry') ?? 'registry',
|
|
17
|
+
timeoutMs: Env.getInt('ZT_PROXY_TIMEOUT_MS', Env.REQUEST_TIMEOUT),
|
|
18
|
+
};
|
|
19
|
+
const remote = {
|
|
20
|
+
baseUrl: settings.baseUrl,
|
|
21
|
+
keyId: settings.keyId,
|
|
22
|
+
secret: settings.secret,
|
|
23
|
+
timeoutMs: settings.timeoutMs,
|
|
24
|
+
missingUrlMessage: 'D1 remote proxy URL is missing (D1_REMOTE_URL)',
|
|
25
|
+
missingCredentialsMessage: 'D1 remote signing credentials are missing (D1_REMOTE_KEY_ID / D1_REMOTE_SECRET)',
|
|
26
|
+
messages: {
|
|
27
|
+
unauthorized: 'D1 remote proxy unauthorized',
|
|
28
|
+
forbidden: 'D1 remote proxy forbidden',
|
|
29
|
+
rateLimited: 'D1 remote proxy rate limited',
|
|
30
|
+
rejected: 'D1 remote proxy rejected request',
|
|
31
|
+
error: 'D1 remote proxy error',
|
|
32
|
+
timedOut: 'D1 remote proxy request timed out',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return { mode: settings.mode, remote };
|
|
36
|
+
};
|
|
37
|
+
const isMutatingSql = (sql) => {
|
|
38
|
+
const s = sql.trimStart().toLowerCase();
|
|
39
|
+
return (s.startsWith('insert') ||
|
|
40
|
+
s.startsWith('update') ||
|
|
41
|
+
s.startsWith('delete') ||
|
|
42
|
+
s.startsWith('create') ||
|
|
43
|
+
s.startsWith('drop') ||
|
|
44
|
+
s.startsWith('alter') ||
|
|
45
|
+
s.startsWith('replace'));
|
|
46
|
+
};
|
|
47
|
+
const createStatementPayload = async (sql, parameters) => {
|
|
48
|
+
const statementId = await SignedRequest.sha256Hex(sql);
|
|
49
|
+
return { statementId, params: parameters };
|
|
50
|
+
};
|
|
51
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
52
|
+
const isQueryResponse = (value) => isRecord(value) &&
|
|
53
|
+
Array.isArray(value['rows']) &&
|
|
54
|
+
typeof value['rowCount'] === 'number' &&
|
|
55
|
+
value['rows'].every((r) => isRecord(r));
|
|
56
|
+
const isQueryOneResponse = (value) => isRecord(value) && 'row' in value && (value['row'] === null || isRecord(value['row']));
|
|
57
|
+
const getExecChanges = (value) => {
|
|
58
|
+
if (!isRecord(value) || typeof value['ok'] !== 'boolean')
|
|
59
|
+
return 0;
|
|
60
|
+
const meta = value['meta'];
|
|
61
|
+
if (!isRecord(meta) || typeof meta['changes'] !== 'number')
|
|
62
|
+
return 0;
|
|
63
|
+
return meta['changes'];
|
|
64
|
+
};
|
|
65
|
+
const queryRegistry = async (settings, sql, parameters) => {
|
|
66
|
+
const payload = await createStatementPayload(sql, parameters);
|
|
67
|
+
const out = await RemoteSignedJson.request(settings, '/zin/d1/statement', payload);
|
|
68
|
+
if (isQueryResponse(out)) {
|
|
69
|
+
return { rows: out.rows, rowCount: out.rowCount };
|
|
70
|
+
}
|
|
71
|
+
if (isQueryOneResponse(out)) {
|
|
72
|
+
const row = out.row;
|
|
73
|
+
return { rows: row ? [row] : [], rowCount: row ? 1 : 0 };
|
|
74
|
+
}
|
|
75
|
+
return { rows: [], rowCount: getExecChanges(out) };
|
|
76
|
+
};
|
|
77
|
+
const querySqlMode = async (settings, sql, parameters) => {
|
|
78
|
+
if (isMutatingSql(sql)) {
|
|
79
|
+
const out = await RemoteSignedJson.request(settings, '/zin/d1/exec', {
|
|
80
|
+
sql,
|
|
81
|
+
params: parameters,
|
|
82
|
+
});
|
|
83
|
+
return { rows: [], rowCount: getExecChanges(out) };
|
|
84
|
+
}
|
|
85
|
+
const out = await RemoteSignedJson.request(settings, '/zin/d1/query', {
|
|
86
|
+
sql,
|
|
87
|
+
params: parameters,
|
|
88
|
+
});
|
|
89
|
+
return { rows: out.rows, rowCount: out.rowCount };
|
|
90
|
+
};
|
|
91
|
+
export const D1RemoteAdapter = Object.freeze({
|
|
92
|
+
create(_config) {
|
|
93
|
+
let connected = false;
|
|
94
|
+
const { mode, remote } = createRemoteConfig();
|
|
95
|
+
return {
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
97
|
+
async connect() {
|
|
98
|
+
connected = true;
|
|
99
|
+
},
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
101
|
+
async disconnect() {
|
|
102
|
+
connected = false;
|
|
103
|
+
},
|
|
104
|
+
async query(sql, parameters) {
|
|
105
|
+
if (!connected)
|
|
106
|
+
throw ErrorFactory.createConnectionError('Database not connected');
|
|
107
|
+
if (mode === 'registry') {
|
|
108
|
+
return queryRegistry(remote, sql, parameters);
|
|
109
|
+
}
|
|
110
|
+
return querySqlMode(remote, sql, parameters);
|
|
111
|
+
},
|
|
112
|
+
async queryOne(sql, parameters) {
|
|
113
|
+
if (!connected)
|
|
114
|
+
throw ErrorFactory.createConnectionError('Database not connected');
|
|
115
|
+
if (mode === 'registry') {
|
|
116
|
+
const payload = await createStatementPayload(sql, parameters);
|
|
117
|
+
const out = await RemoteSignedJson.request(remote, '/zin/d1/statement', payload);
|
|
118
|
+
if (isQueryOneResponse(out))
|
|
119
|
+
return out.row;
|
|
120
|
+
if (isQueryResponse(out))
|
|
121
|
+
return out.rows[0] ?? null;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const out = await RemoteSignedJson.request(remote, '/zin/d1/queryOne', {
|
|
125
|
+
sql,
|
|
126
|
+
params: parameters,
|
|
127
|
+
});
|
|
128
|
+
return out.row;
|
|
129
|
+
},
|
|
130
|
+
async ping() {
|
|
131
|
+
if (!connected)
|
|
132
|
+
throw ErrorFactory.createConnectionError('Database not connected');
|
|
133
|
+
const sql = QueryBuilder.create('').select('1').toSQL();
|
|
134
|
+
await this.queryOne(sql, []);
|
|
135
|
+
},
|
|
136
|
+
async transaction(callback) {
|
|
137
|
+
if (!connected)
|
|
138
|
+
throw ErrorFactory.createConnectionError('Database not connected');
|
|
139
|
+
try {
|
|
140
|
+
return await callback(this);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw ErrorFactory.createTryCatchError('Transaction failed', error);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async rawQuery(sql, parameters = []) {
|
|
147
|
+
const out = await this.query(sql, parameters);
|
|
148
|
+
return out.rows;
|
|
149
|
+
},
|
|
150
|
+
getType() {
|
|
151
|
+
return 'd1-remote';
|
|
152
|
+
},
|
|
153
|
+
isConnected() {
|
|
154
|
+
return connected;
|
|
155
|
+
},
|
|
156
|
+
getPlaceholder(_index) {
|
|
157
|
+
return '?';
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
export default D1RemoteAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginRegistry.d.ts","sourceRoot":"","sources":["../../../src/runtime/PluginRegistry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,kBAAkB,GAAG,SAAS,GAAG,QAAQ,CAAC;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;KACrB,EAAE,CAAC;IACJ,WAAW,CAAC,EAAE;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"PluginRegistry.d.ts","sourceRoot":"","sources":["../../../src/runtime/PluginRegistry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,kBAAkB,GAAG,SAAS,GAAG,QAAQ,CAAC;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;KACrB,EAAE,CAAC;IACJ,WAAW,CAAC,EAAE;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AA2DD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAmJ3D,CAAC"}
|
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
* Plugin Registry
|
|
3
3
|
* Defines available plugins, their dependencies, and template paths.
|
|
4
4
|
*/
|
|
5
|
+
const feature = (config) => ({
|
|
6
|
+
name: config.name,
|
|
7
|
+
description: config.description,
|
|
8
|
+
type: 'feature',
|
|
9
|
+
aliases: config.aliases,
|
|
10
|
+
dependencies: config.dependencies ?? [],
|
|
11
|
+
devDependencies: config.devDependencies ?? [],
|
|
12
|
+
autoImports: config.autoImports,
|
|
13
|
+
templates: config.templates,
|
|
14
|
+
postInstall: config.postInstall,
|
|
15
|
+
});
|
|
16
|
+
const driver = (config) => ({
|
|
17
|
+
name: config.name,
|
|
18
|
+
description: config.description,
|
|
19
|
+
type: 'driver',
|
|
20
|
+
aliases: config.aliases,
|
|
21
|
+
dependencies: config.dependencies,
|
|
22
|
+
devDependencies: [],
|
|
23
|
+
autoImports: config.autoImports,
|
|
24
|
+
templates: [],
|
|
25
|
+
});
|
|
26
|
+
const adapter = (config) => ({
|
|
27
|
+
name: config.name,
|
|
28
|
+
description: config.description,
|
|
29
|
+
type: 'database-adapter',
|
|
30
|
+
aliases: config.aliases,
|
|
31
|
+
dependencies: [config.dependency],
|
|
32
|
+
devDependencies: [],
|
|
33
|
+
autoImports: [config.autoImport],
|
|
34
|
+
templates: [],
|
|
35
|
+
});
|
|
5
36
|
export const PluginRegistry = {
|
|
6
37
|
'feature:auth': {
|
|
7
38
|
name: 'Authentication Feature',
|
|
@@ -20,96 +51,133 @@ export const PluginRegistry = {
|
|
|
20
51
|
message: 'Authentication installed! Please add JWT_SECRET to your .env file.',
|
|
21
52
|
},
|
|
22
53
|
},
|
|
23
|
-
'feature:queue': {
|
|
54
|
+
'feature:queue': feature({
|
|
24
55
|
name: 'Queue Feature',
|
|
25
56
|
description: 'Simple job queue interface (In-Memory default)',
|
|
26
|
-
type: 'feature',
|
|
27
57
|
aliases: ['f:queue', 'queue'],
|
|
28
|
-
dependencies: [],
|
|
29
|
-
devDependencies: [],
|
|
30
58
|
templates: [
|
|
31
59
|
{
|
|
32
60
|
source: 'features/Queue.ts.tpl',
|
|
33
61
|
destination: 'src/features/Queue.ts',
|
|
34
62
|
},
|
|
35
63
|
],
|
|
36
|
-
},
|
|
37
|
-
'driver:queue-redis': {
|
|
64
|
+
}),
|
|
65
|
+
'driver:queue-redis': driver({
|
|
38
66
|
name: 'Redis Queue Driver',
|
|
39
|
-
description: 'Redis-backed queue driver (installs redis
|
|
40
|
-
type: 'driver',
|
|
67
|
+
description: 'Redis-backed queue driver (installs @zintrust/queue-redis)',
|
|
41
68
|
aliases: ['queue:redis'],
|
|
42
|
-
dependencies: ['redis'],
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
dependencies: ['@zintrust/queue-redis'],
|
|
70
|
+
autoImports: ['@zintrust/queue-redis/register'],
|
|
71
|
+
}),
|
|
72
|
+
'driver:queue-rabbitmq': driver({
|
|
73
|
+
name: 'RabbitMQ Queue Driver',
|
|
74
|
+
description: 'RabbitMQ-backed queue driver (installs @zintrust/queue-rabbitmq)',
|
|
75
|
+
aliases: ['queue:rabbitmq', 'queue:amqp'],
|
|
76
|
+
dependencies: ['@zintrust/queue-rabbitmq', 'amqplib'],
|
|
77
|
+
autoImports: ['@zintrust/queue-rabbitmq/register'],
|
|
78
|
+
}),
|
|
79
|
+
'driver:queue-sqs': driver({
|
|
80
|
+
name: 'AWS SQS Queue Driver',
|
|
81
|
+
description: 'SQS-backed queue driver (installs @zintrust/queue-sqs)',
|
|
82
|
+
aliases: ['queue:sqs'],
|
|
83
|
+
dependencies: ['@zintrust/queue-sqs', '@aws-sdk/client-sqs'],
|
|
84
|
+
autoImports: ['@zintrust/queue-sqs/register'],
|
|
85
|
+
}),
|
|
86
|
+
'driver:broadcast-redis': driver({
|
|
47
87
|
name: 'Redis Broadcast Driver',
|
|
48
88
|
description: 'Redis-backed broadcast driver (installs redis client dependency)',
|
|
49
|
-
type: 'driver',
|
|
50
89
|
aliases: ['broadcast:redis'],
|
|
51
90
|
dependencies: ['redis'],
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
'driver:cache-redis': {
|
|
91
|
+
}),
|
|
92
|
+
'driver:cache-redis': driver({
|
|
56
93
|
name: 'Redis Cache Driver',
|
|
57
94
|
description: 'Redis-backed cache driver (installs @zintrust/cache-redis)',
|
|
58
|
-
type: 'driver',
|
|
59
95
|
aliases: ['cache:redis'],
|
|
60
96
|
dependencies: ['@zintrust/cache-redis'],
|
|
61
|
-
devDependencies: [],
|
|
62
97
|
autoImports: ['@zintrust/cache-redis/register'],
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
}),
|
|
99
|
+
'driver:cache-mongodb': driver({
|
|
100
|
+
name: 'MongoDB Cache Driver',
|
|
101
|
+
description: 'MongoDB Atlas Data API cache driver (installs @zintrust/cache-mongodb)',
|
|
102
|
+
aliases: ['cache:mongodb', 'cache:mongo'],
|
|
103
|
+
dependencies: ['@zintrust/cache-mongodb'],
|
|
104
|
+
autoImports: ['@zintrust/cache-mongodb/register'],
|
|
105
|
+
}),
|
|
106
|
+
'driver:mail-nodemailer': driver({
|
|
66
107
|
name: 'Nodemailer Mail Driver',
|
|
67
108
|
description: 'Nodemailer-based mail driver (installs @zintrust/mail-nodemailer)',
|
|
68
|
-
type: 'driver',
|
|
69
109
|
aliases: ['mail:nodemailer'],
|
|
70
110
|
dependencies: ['@zintrust/mail-nodemailer'],
|
|
71
|
-
devDependencies: [],
|
|
72
111
|
autoImports: ['@zintrust/mail-nodemailer/register'],
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
}),
|
|
113
|
+
'driver:mail-smtp': driver({
|
|
114
|
+
name: 'SMTP Mail Driver',
|
|
115
|
+
description: 'SMTP mail driver (installs @zintrust/mail-smtp)',
|
|
116
|
+
aliases: ['mail:smtp'],
|
|
117
|
+
dependencies: ['@zintrust/mail-smtp'],
|
|
118
|
+
autoImports: ['@zintrust/mail-smtp/register'],
|
|
119
|
+
}),
|
|
120
|
+
'driver:mail-sendgrid': driver({
|
|
121
|
+
name: 'SendGrid Mail Driver',
|
|
122
|
+
description: 'SendGrid mail driver (installs @zintrust/mail-sendgrid)',
|
|
123
|
+
aliases: ['mail:sendgrid'],
|
|
124
|
+
dependencies: ['@zintrust/mail-sendgrid'],
|
|
125
|
+
autoImports: ['@zintrust/mail-sendgrid/register'],
|
|
126
|
+
}),
|
|
127
|
+
'driver:mail-mailgun': driver({
|
|
128
|
+
name: 'Mailgun Mail Driver',
|
|
129
|
+
description: 'Mailgun mail driver (installs @zintrust/mail-mailgun)',
|
|
130
|
+
aliases: ['mail:mailgun'],
|
|
131
|
+
dependencies: ['@zintrust/mail-mailgun'],
|
|
132
|
+
autoImports: ['@zintrust/mail-mailgun/register'],
|
|
133
|
+
}),
|
|
134
|
+
'driver:storage-s3': driver({
|
|
135
|
+
name: 'S3 Storage Driver',
|
|
136
|
+
description: 'S3 storage driver (installs @zintrust/storage-s3)',
|
|
137
|
+
aliases: ['storage:s3'],
|
|
138
|
+
dependencies: ['@zintrust/storage-s3'],
|
|
139
|
+
autoImports: ['@zintrust/storage-s3/register'],
|
|
140
|
+
}),
|
|
141
|
+
'driver:storage-r2': driver({
|
|
142
|
+
name: 'R2 Storage Driver',
|
|
143
|
+
description: 'Cloudflare R2 storage driver (installs @zintrust/storage-r2)',
|
|
144
|
+
aliases: ['storage:r2'],
|
|
145
|
+
dependencies: ['@zintrust/storage-r2'],
|
|
146
|
+
autoImports: ['@zintrust/storage-r2/register'],
|
|
147
|
+
}),
|
|
148
|
+
'driver:storage-gcs': driver({
|
|
149
|
+
name: 'GCS Storage Driver',
|
|
150
|
+
description: 'Google Cloud Storage driver (installs @zintrust/storage-gcs)',
|
|
151
|
+
aliases: ['storage:gcs'],
|
|
152
|
+
dependencies: ['@zintrust/storage-gcs', '@google-cloud/storage'],
|
|
153
|
+
autoImports: ['@zintrust/storage-gcs/register'],
|
|
154
|
+
}),
|
|
155
|
+
'adapter:postgres': adapter({
|
|
76
156
|
name: 'PostgreSQL Adapter',
|
|
77
157
|
description: 'Production-ready PostgreSQL database adapter using pg',
|
|
78
|
-
type: 'database-adapter',
|
|
79
158
|
aliases: ['a:postgres', 'pg', 'db:postgres', 'postgresql', 'db:postgresql'],
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
'adapter:mysql': {
|
|
159
|
+
dependency: '@zintrust/db-postgres',
|
|
160
|
+
autoImport: '@zintrust/db-postgres/register',
|
|
161
|
+
}),
|
|
162
|
+
'adapter:mysql': adapter({
|
|
86
163
|
name: 'MySQL Adapter',
|
|
87
164
|
description: 'Production-ready MySQL database adapter using mysql2',
|
|
88
|
-
type: 'database-adapter',
|
|
89
165
|
aliases: ['a:mysql', 'mysql', 'db:mysql'],
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
},
|
|
95
|
-
'adapter:mssql': {
|
|
166
|
+
dependency: '@zintrust/db-mysql',
|
|
167
|
+
autoImport: '@zintrust/db-mysql/register',
|
|
168
|
+
}),
|
|
169
|
+
'adapter:mssql': adapter({
|
|
96
170
|
name: 'SQL Server Adapter',
|
|
97
171
|
description: 'Production-ready SQL Server database adapter using mssql',
|
|
98
|
-
type: 'database-adapter',
|
|
99
172
|
aliases: ['a:mssql', 'mssql', 'db:mssql'],
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
'adapter:sqlite': {
|
|
173
|
+
dependency: '@zintrust/db-sqlserver',
|
|
174
|
+
autoImport: '@zintrust/db-sqlserver/register',
|
|
175
|
+
}),
|
|
176
|
+
'adapter:sqlite': adapter({
|
|
106
177
|
name: 'SQLite Adapter',
|
|
107
|
-
description: '
|
|
108
|
-
type: 'database-adapter',
|
|
178
|
+
description: 'SQLite database adapter using better-sqlite3',
|
|
109
179
|
aliases: ['a:sqlite', 'sqlite', 'db:sqlite'],
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
templates: [],
|
|
114
|
-
},
|
|
180
|
+
dependency: '@zintrust/db-sqlite',
|
|
181
|
+
autoImport: '@zintrust/db-sqlite/register',
|
|
182
|
+
}),
|
|
115
183
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type SignedRequestHeaders = {
|
|
2
|
+
'x-zt-key-id': string;
|
|
3
|
+
'x-zt-timestamp': string;
|
|
4
|
+
'x-zt-nonce': string;
|
|
5
|
+
'x-zt-body-sha256': string;
|
|
6
|
+
'x-zt-signature': string;
|
|
7
|
+
};
|
|
8
|
+
export type SignedRequestCreateHeadersParams = {
|
|
9
|
+
method: string;
|
|
10
|
+
url: string | URL;
|
|
11
|
+
body?: string | Uint8Array | null;
|
|
12
|
+
keyId: string;
|
|
13
|
+
secret: string;
|
|
14
|
+
timestampMs?: number;
|
|
15
|
+
nonce?: string;
|
|
16
|
+
};
|
|
17
|
+
export type SignedRequestVerifyParams = {
|
|
18
|
+
method: string;
|
|
19
|
+
url: string | URL;
|
|
20
|
+
body?: string | Uint8Array | null;
|
|
21
|
+
headers: Headers | Record<string, string | undefined>;
|
|
22
|
+
getSecretForKeyId: (keyId: string) => string | undefined | Promise<string | undefined>;
|
|
23
|
+
nowMs?: number;
|
|
24
|
+
windowMs?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Optional replay protection hook.
|
|
27
|
+
* Return true if the nonce is accepted and stored; false if replayed/invalid.
|
|
28
|
+
*/
|
|
29
|
+
verifyNonce?: (keyId: string, nonce: string, ttlMs: number) => Promise<boolean>;
|
|
30
|
+
};
|
|
31
|
+
export type SignedRequestVerifyResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
keyId: string;
|
|
34
|
+
timestampMs: number;
|
|
35
|
+
nonce: string;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
code: 'MISSING_HEADER' | 'INVALID_TIMESTAMP' | 'EXPIRED' | 'INVALID_BODY_SHA' | 'INVALID_SIGNATURE' | 'UNKNOWN_KEY' | 'REPLAYED';
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
export declare const SignedRequest: Readonly<{
|
|
42
|
+
sha256Hex: (data: string | Uint8Array) => Promise<string>;
|
|
43
|
+
canonicalString: (params: {
|
|
44
|
+
method: string;
|
|
45
|
+
url: string | URL;
|
|
46
|
+
timestampMs: number;
|
|
47
|
+
nonce: string;
|
|
48
|
+
bodySha256Hex: string;
|
|
49
|
+
}) => string;
|
|
50
|
+
createHeaders(params: SignedRequestCreateHeadersParams): Promise<SignedRequestHeaders>;
|
|
51
|
+
verify(params: SignedRequestVerifyParams): Promise<SignedRequestVerifyResult>;
|
|
52
|
+
}>;
|
|
53
|
+
//# sourceMappingURL=SignedRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SignedRequest.d.ts","sourceRoot":"","sources":["../../../src/security/SignedRequest.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAIF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACjF,CAAC;AAEF,MAAM,MAAM,yBAAyB,GACjC;IACE,EAAE,EAAE,IAAI,CAAC;IACT,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EACA,gBAAgB,GAChB,mBAAmB,GACnB,SAAS,GACT,kBAAkB,GAClB,mBAAmB,GACnB,aAAa,GACb,UAAU,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAyFN,eAAO,MAAM,aAAa;sBAzBK,MAAM,GAAG,UAAU,KAAG,OAAO,CAAC,MAAM,CAAC;8BAKnC;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;KACvB,KAAG,MAAM;0BAiBoB,gCAAgC,GAAG,OAAO,CAAC,oBAAoB,CAAC;mBAuCvE,yBAAyB,GAAG,OAAO,CAAC,yBAAyB,CAAC;EAuCnF,CAAC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
2
|
+
const getHeader = (headers, name) => {
|
|
3
|
+
if (typeof headers.get === 'function') {
|
|
4
|
+
const value = headers.get(name);
|
|
5
|
+
return value ?? undefined;
|
|
6
|
+
}
|
|
7
|
+
return headers[name];
|
|
8
|
+
};
|
|
9
|
+
const timingSafeEquals = (a, b) => {
|
|
10
|
+
if (a.length !== b.length)
|
|
11
|
+
return false;
|
|
12
|
+
let result = 0;
|
|
13
|
+
for (let i = 0; i < a.length; i++) {
|
|
14
|
+
result |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return result === 0;
|
|
17
|
+
};
|
|
18
|
+
const getCrypto = () => {
|
|
19
|
+
if (typeof crypto === 'undefined' || crypto.subtle === undefined) {
|
|
20
|
+
// Some runtimes (or test environments) may not expose WebCrypto.
|
|
21
|
+
// Keep this as a typed Zintrust error to satisfy lint rules.
|
|
22
|
+
throw ErrorFactory.createSecurityError('WebCrypto is not available in this runtime');
|
|
23
|
+
}
|
|
24
|
+
return crypto;
|
|
25
|
+
};
|
|
26
|
+
const getSubtle = () => getCrypto().subtle;
|
|
27
|
+
const toBytes = (data) => {
|
|
28
|
+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
29
|
+
return new Uint8Array(bytes);
|
|
30
|
+
};
|
|
31
|
+
const toHex = (bytes) => {
|
|
32
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
33
|
+
let out = '';
|
|
34
|
+
for (const element of view) {
|
|
35
|
+
out += element.toString(16).padStart(2, '0');
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
const sha256Hex = async (data) => {
|
|
40
|
+
const digest = await getSubtle().digest('SHA-256', toBytes(data));
|
|
41
|
+
return toHex(digest);
|
|
42
|
+
};
|
|
43
|
+
const canonicalString = (params) => {
|
|
44
|
+
const u = typeof params.url === 'string' ? new URL(params.url) : params.url;
|
|
45
|
+
const method = params.method.toUpperCase();
|
|
46
|
+
// Keep URL pieces aligned with plan: pathname + search (including leading '?' or '').
|
|
47
|
+
return [
|
|
48
|
+
method,
|
|
49
|
+
u.pathname,
|
|
50
|
+
u.search,
|
|
51
|
+
String(params.timestampMs),
|
|
52
|
+
params.nonce,
|
|
53
|
+
params.bodySha256Hex,
|
|
54
|
+
].join('\n');
|
|
55
|
+
};
|
|
56
|
+
export const SignedRequest = Object.freeze({
|
|
57
|
+
sha256Hex,
|
|
58
|
+
canonicalString,
|
|
59
|
+
async createHeaders(params) {
|
|
60
|
+
const timestampMs = params.timestampMs ?? Date.now();
|
|
61
|
+
const webCrypto = getCrypto();
|
|
62
|
+
const nonce = params.nonce ??
|
|
63
|
+
(typeof webCrypto.randomUUID === 'function'
|
|
64
|
+
? webCrypto.randomUUID()
|
|
65
|
+
: toHex(webCrypto.getRandomValues(new Uint8Array(16))));
|
|
66
|
+
const body = params.body ?? '';
|
|
67
|
+
const bodySha = await sha256Hex(body);
|
|
68
|
+
const canonical = canonicalString({
|
|
69
|
+
method: params.method,
|
|
70
|
+
url: params.url,
|
|
71
|
+
timestampMs,
|
|
72
|
+
nonce,
|
|
73
|
+
bodySha256Hex: bodySha,
|
|
74
|
+
});
|
|
75
|
+
const subtle = getSubtle();
|
|
76
|
+
const key = await subtle.importKey('raw', toBytes(params.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
77
|
+
const signatureBytes = await subtle.sign('HMAC', key, toBytes(canonical));
|
|
78
|
+
const signature = toHex(signatureBytes);
|
|
79
|
+
return {
|
|
80
|
+
'x-zt-key-id': params.keyId,
|
|
81
|
+
'x-zt-timestamp': String(timestampMs),
|
|
82
|
+
'x-zt-nonce': nonce,
|
|
83
|
+
'x-zt-body-sha256': bodySha,
|
|
84
|
+
'x-zt-signature': signature,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
async verify(params) {
|
|
88
|
+
const parsed = parseAndValidateHeaders(params.headers);
|
|
89
|
+
if (parsed.ok === false)
|
|
90
|
+
return parsed;
|
|
91
|
+
const { keyId, timestampMs, nonce, bodySha, signature } = parsed;
|
|
92
|
+
const nowMs = params.nowMs ?? Date.now();
|
|
93
|
+
const windowMs = params.windowMs ?? 60_000;
|
|
94
|
+
const windowCheck = validateTimestampWindow({ nowMs, timestampMs, windowMs });
|
|
95
|
+
if (windowCheck.ok === false)
|
|
96
|
+
return windowCheck;
|
|
97
|
+
const bodyCheck = await validateBodyHash({ body: params.body ?? '', bodyShaHeader: bodySha });
|
|
98
|
+
if (bodyCheck.ok === false)
|
|
99
|
+
return bodyCheck;
|
|
100
|
+
const secret = await params.getSecretForKeyId(keyId);
|
|
101
|
+
if (secret === undefined || secret.trim() === '') {
|
|
102
|
+
return { ok: false, code: 'UNKNOWN_KEY', message: 'Unknown key id' };
|
|
103
|
+
}
|
|
104
|
+
const sigCheck = await validateSignature({
|
|
105
|
+
method: params.method,
|
|
106
|
+
url: params.url,
|
|
107
|
+
timestampMs,
|
|
108
|
+
nonce,
|
|
109
|
+
bodySha,
|
|
110
|
+
signature,
|
|
111
|
+
secret,
|
|
112
|
+
});
|
|
113
|
+
if (sigCheck.ok === false)
|
|
114
|
+
return sigCheck;
|
|
115
|
+
if (params.verifyNonce !== undefined) {
|
|
116
|
+
const ok = await params.verifyNonce(keyId, nonce, windowMs);
|
|
117
|
+
if (ok === false) {
|
|
118
|
+
return { ok: false, code: 'REPLAYED', message: 'Nonce replayed or rejected' };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, keyId, timestampMs, nonce };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const parseAndValidateHeaders = (headers) => {
|
|
125
|
+
const keyId = getHeader(headers, 'x-zt-key-id');
|
|
126
|
+
const ts = getHeader(headers, 'x-zt-timestamp');
|
|
127
|
+
const nonce = getHeader(headers, 'x-zt-nonce');
|
|
128
|
+
const bodySha = getHeader(headers, 'x-zt-body-sha256');
|
|
129
|
+
const signature = getHeader(headers, 'x-zt-signature');
|
|
130
|
+
if (keyId === undefined ||
|
|
131
|
+
ts === undefined ||
|
|
132
|
+
nonce === undefined ||
|
|
133
|
+
bodySha === undefined ||
|
|
134
|
+
signature === undefined) {
|
|
135
|
+
return { ok: false, code: 'MISSING_HEADER', message: 'Missing required signing headers' };
|
|
136
|
+
}
|
|
137
|
+
const timestampMs = Number.parseInt(ts, 10);
|
|
138
|
+
if (!Number.isFinite(timestampMs)) {
|
|
139
|
+
return { ok: false, code: 'INVALID_TIMESTAMP', message: 'Invalid x-zt-timestamp' };
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, keyId, timestampMs, nonce, bodySha, signature };
|
|
142
|
+
};
|
|
143
|
+
const validateTimestampWindow = (params) => {
|
|
144
|
+
if (Math.abs(params.nowMs - params.timestampMs) > params.windowMs) {
|
|
145
|
+
return { ok: false, code: 'EXPIRED', message: 'Request timestamp outside allowed window' };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true };
|
|
148
|
+
};
|
|
149
|
+
const validateBodyHash = async (params) => {
|
|
150
|
+
const computedBodySha = await sha256Hex(params.body);
|
|
151
|
+
if (!timingSafeEquals(computedBodySha, params.bodyShaHeader)) {
|
|
152
|
+
return { ok: false, code: 'INVALID_BODY_SHA', message: 'Body hash mismatch' };
|
|
153
|
+
}
|
|
154
|
+
return { ok: true };
|
|
155
|
+
};
|
|
156
|
+
const validateSignature = async (params) => {
|
|
157
|
+
const subtle = getSubtle();
|
|
158
|
+
const canonical = canonicalString({
|
|
159
|
+
method: params.method,
|
|
160
|
+
url: params.url,
|
|
161
|
+
timestampMs: params.timestampMs,
|
|
162
|
+
nonce: params.nonce,
|
|
163
|
+
bodySha256Hex: params.bodySha,
|
|
164
|
+
});
|
|
165
|
+
const key = await subtle.importKey('raw', toBytes(params.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
166
|
+
const expectedBytes = await subtle.sign('HMAC', key, toBytes(canonical));
|
|
167
|
+
const expected = toHex(expectedBytes);
|
|
168
|
+
if (!timingSafeEquals(expected, params.signature)) {
|
|
169
|
+
return { ok: false, code: 'INVALID_SIGNATURE', message: 'Invalid signature' };
|
|
170
|
+
}
|
|
171
|
+
return { ok: true };
|
|
172
|
+
};
|
|
@@ -79,6 +79,21 @@ export const Env = Object.freeze({
|
|
|
79
79
|
D1_DATABASE_ID: get('D1_DATABASE_ID'),
|
|
80
80
|
KV_NAMESPACE_ID: get('KV_NAMESPACE_ID'),
|
|
81
81
|
|
|
82
|
+
// Cloudflare proxy services (D1/KV outside Cloudflare)
|
|
83
|
+
D1_REMOTE_URL: get('D1_REMOTE_URL', ''),
|
|
84
|
+
D1_REMOTE_KEY_ID: get('D1_REMOTE_KEY_ID', ''),
|
|
85
|
+
D1_REMOTE_SECRET: get('D1_REMOTE_SECRET', ''),
|
|
86
|
+
D1_REMOTE_MODE: get('D1_REMOTE_MODE', 'registry'),
|
|
87
|
+
|
|
88
|
+
KV_REMOTE_URL: get('KV_REMOTE_URL', ''),
|
|
89
|
+
KV_REMOTE_KEY_ID: get('KV_REMOTE_KEY_ID', ''),
|
|
90
|
+
KV_REMOTE_SECRET: get('KV_REMOTE_SECRET', ''),
|
|
91
|
+
KV_REMOTE_NAMESPACE: get('KV_REMOTE_NAMESPACE', ''),
|
|
92
|
+
|
|
93
|
+
// Proxy client tuning
|
|
94
|
+
ZT_PROXY_SIGNING_WINDOW_MS: getInt('ZT_PROXY_SIGNING_WINDOW_MS', 60000),
|
|
95
|
+
ZT_PROXY_TIMEOUT_MS: getInt('ZT_PROXY_TIMEOUT_MS', 30000),
|
|
96
|
+
|
|
82
97
|
// Cache
|
|
83
98
|
CACHE_DRIVER: get('CACHE_DRIVER', 'memory'),
|
|
84
99
|
REDIS_HOST: get('REDIS_HOST', 'localhost'),
|