@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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/cache/Cache.d.ts.map +1 -1
  3. package/src/cache/Cache.js +3 -0
  4. package/src/cache/drivers/KVRemoteDriver.d.ts +11 -0
  5. package/src/cache/drivers/KVRemoteDriver.d.ts.map +1 -0
  6. package/src/cache/drivers/KVRemoteDriver.js +70 -0
  7. package/src/common/RemoteSignedJson.d.ts +21 -0
  8. package/src/common/RemoteSignedJson.d.ts.map +1 -0
  9. package/src/common/RemoteSignedJson.js +86 -0
  10. package/src/config/cache.d.ts +4 -0
  11. package/src/config/cache.d.ts.map +1 -1
  12. package/src/config/cache.js +4 -0
  13. package/src/config/env.d.ts +10 -0
  14. package/src/config/env.d.ts.map +1 -1
  15. package/src/config/env.js +12 -0
  16. package/src/config/index.d.ts +4 -0
  17. package/src/config/index.d.ts.map +1 -1
  18. package/src/config/type.d.ts +6 -1
  19. package/src/config/type.d.ts.map +1 -1
  20. package/src/index.d.ts +64 -1
  21. package/src/index.d.ts.map +1 -1
  22. package/src/index.js +18 -1
  23. package/src/orm/Database.d.ts.map +1 -1
  24. package/src/orm/Database.js +3 -0
  25. package/src/orm/DatabaseAdapter.d.ts +1 -1
  26. package/src/orm/DatabaseAdapter.d.ts.map +1 -1
  27. package/src/orm/adapters/D1RemoteAdapter.d.ts +11 -0
  28. package/src/orm/adapters/D1RemoteAdapter.d.ts.map +1 -0
  29. package/src/orm/adapters/D1RemoteAdapter.js +162 -0
  30. package/src/runtime/PluginRegistry.d.ts.map +1 -1
  31. package/src/runtime/PluginRegistry.js +124 -56
  32. package/src/security/SignedRequest.d.ts +53 -0
  33. package/src/security/SignedRequest.d.ts.map +1 -0
  34. package/src/security/SignedRequest.js +172 -0
  35. package/src/templates/project/basic/config/cache.ts.tpl +4 -0
  36. package/src/templates/project/basic/config/env.ts.tpl +15 -0
  37. package/src/templates/project/basic/config/type.ts.tpl +11 -5
  38. package/src/tools/mail/Mail.d.ts.map +1 -1
  39. package/src/tools/mail/Mail.js +4 -22
  40. package/src/tools/storage/StorageDriverRegistry.d.ts +16 -0
  41. package/src/tools/storage/StorageDriverRegistry.d.ts.map +1 -0
  42. package/src/tools/storage/StorageDriverRegistry.js +20 -0
  43. package/src/tools/storage/index.d.ts +1 -5
  44. package/src/tools/storage/index.d.ts.map +1 -1
  45. 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;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA8G3D,CAAC"}
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 client dependency)',
40
- type: 'driver',
67
+ description: 'Redis-backed queue driver (installs @zintrust/queue-redis)',
41
68
  aliases: ['queue:redis'],
42
- dependencies: ['redis'],
43
- devDependencies: [],
44
- templates: [],
45
- },
46
- 'driver:broadcast-redis': {
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
- devDependencies: [],
53
- templates: [],
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
- templates: [],
64
- },
65
- 'driver:mail-nodemailer': {
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
- templates: [],
74
- },
75
- 'adapter:postgres': {
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
- dependencies: ['@zintrust/db-postgres'],
81
- devDependencies: [],
82
- autoImports: ['@zintrust/db-postgres/register'],
83
- templates: [],
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
- dependencies: ['@zintrust/db-mysql'],
91
- devDependencies: [],
92
- autoImports: ['@zintrust/db-mysql/register'],
93
- templates: [],
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
- dependencies: ['@zintrust/db-sqlserver'],
101
- devDependencies: [],
102
- autoImports: ['@zintrust/db-sqlserver/register'],
103
- templates: [],
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: 'Production-ready SQLite database adapter using better-sqlite3',
108
- type: 'database-adapter',
178
+ description: 'SQLite database adapter using better-sqlite3',
109
179
  aliases: ['a:sqlite', 'sqlite', 'db:sqlite'],
110
- dependencies: ['@zintrust/db-sqlite'],
111
- devDependencies: [],
112
- autoImports: ['@zintrust/db-sqlite/register'],
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
+ };
@@ -48,6 +48,10 @@ const cacheConfigObj = {
48
48
  driver: 'kv' as const,
49
49
  ttl: Env.getInt('CACHE_KV_TTL', 3600),
50
50
  },
51
+ 'kv-remote': {
52
+ driver: 'kv-remote' as const,
53
+ ttl: Env.getInt('CACHE_KV_TTL', 3600),
54
+ },
51
55
  },
52
56
 
53
57
  /**
@@ -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'),