@zintrust/core 0.1.15 → 0.1.17
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/README.md +2 -2
- package/package.json +1 -1
- package/public/index.html +1 -1
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +6 -0
- package/src/cli/commands/BroadcastWorkCommand.d.ts +10 -0
- package/src/cli/commands/BroadcastWorkCommand.d.ts.map +1 -0
- package/src/cli/commands/BroadcastWorkCommand.js +16 -0
- package/src/cli/commands/NotificationWorkCommand.d.ts +10 -0
- package/src/cli/commands/NotificationWorkCommand.d.ts.map +1 -0
- package/src/cli/commands/NotificationWorkCommand.js +16 -0
- package/src/cli/commands/QueueCommand.d.ts +10 -0
- package/src/cli/commands/QueueCommand.d.ts.map +1 -0
- package/src/cli/commands/QueueCommand.js +63 -0
- package/src/cli/commands/QueueWorkCommandUtils.d.ts +10 -0
- package/src/cli/commands/QueueWorkCommandUtils.d.ts.map +1 -0
- package/src/cli/commands/QueueWorkCommandUtils.js +43 -0
- package/src/cli/commands/createKindWorkCommand.d.ts +9 -0
- package/src/cli/commands/createKindWorkCommand.d.ts.map +1 -0
- package/src/cli/commands/createKindWorkCommand.js +33 -0
- package/src/cli/commands/index.d.ts +3 -0
- package/src/cli/commands/index.d.ts.map +1 -1
- package/src/cli/commands/index.js +3 -0
- package/src/cli/scaffolding/ModelGenerator.d.ts.map +1 -1
- package/src/cli/scaffolding/ModelGenerator.js +1 -0
- package/src/cli/scaffolding/ProjectScaffolder.d.ts.map +1 -1
- package/src/cli/scaffolding/ProjectScaffolder.js +2 -1
- package/src/cli/workers/QueueWorkRunner.d.ts +23 -0
- package/src/cli/workers/QueueWorkRunner.d.ts.map +1 -0
- package/src/cli/workers/QueueWorkRunner.js +142 -0
- package/src/collections/Collection.d.ts +30 -0
- package/src/collections/Collection.d.ts.map +1 -0
- package/src/collections/Collection.js +146 -0
- package/src/collections/index.d.ts +3 -0
- package/src/collections/index.d.ts.map +1 -0
- package/src/collections/index.js +1 -0
- package/src/config/env.d.ts +2 -0
- package/src/config/env.d.ts.map +1 -1
- package/src/config/env.js +4 -0
- package/src/config/index.d.ts +3 -0
- package/src/config/index.d.ts.map +1 -1
- package/src/config/security.d.ts +4 -1
- package/src/config/security.d.ts.map +1 -1
- package/src/config/security.js +9 -1
- package/src/events/EventDispatcher.d.ts +16 -0
- package/src/events/EventDispatcher.d.ts.map +1 -0
- package/src/events/EventDispatcher.js +90 -0
- package/src/events/index.d.ts +3 -0
- package/src/events/index.d.ts.map +1 -0
- package/src/events/index.js +1 -0
- package/src/features/Queue.d.ts +1 -1
- package/src/features/Queue.d.ts.map +1 -1
- package/src/features/Queue.js +2 -2
- package/src/http/Response.d.ts +2 -2
- package/src/http/Response.d.ts.map +1 -1
- package/src/index.d.ts +12 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +12 -0
- package/src/middleware/CsrfMiddleware.d.ts.map +1 -1
- package/src/middleware/CsrfMiddleware.js +20 -25
- package/src/middleware/SessionMiddleware.d.ts +8 -0
- package/src/middleware/SessionMiddleware.d.ts.map +1 -0
- package/src/middleware/SessionMiddleware.js +15 -0
- package/src/node-singletons/crypto.d.ts +1 -1
- package/src/node-singletons/crypto.d.ts.map +1 -1
- package/src/node-singletons/crypto.js +1 -1
- package/src/orm/Model.d.ts +15 -0
- package/src/orm/Model.d.ts.map +1 -1
- package/src/orm/Model.js +57 -8
- package/src/orm/QueryBuilder.d.ts +9 -1
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +54 -2
- package/src/scripts/TemplateSync.js +23 -1
- package/src/security/EncryptedEnvelope.d.ts +77 -0
- package/src/security/EncryptedEnvelope.d.ts.map +1 -0
- package/src/security/EncryptedEnvelope.js +256 -0
- package/src/security/PasswordResetTokenBroker.d.ts +39 -0
- package/src/security/PasswordResetTokenBroker.d.ts.map +1 -0
- package/src/security/PasswordResetTokenBroker.js +131 -0
- package/src/security/StartupSecretValidation.d.ts.map +1 -1
- package/src/security/StartupSecretValidation.js +72 -0
- package/src/session/SessionManager.d.ts +39 -0
- package/src/session/SessionManager.d.ts.map +1 -0
- package/src/session/SessionManager.js +149 -0
- package/src/session/index.d.ts +3 -0
- package/src/session/index.d.ts.map +1 -0
- package/src/session/index.js +1 -0
- package/src/templates/features/Queue.ts.tpl +5 -4
- package/src/templates/project/basic/config/FileLogWriter.ts.tpl +4 -3
- package/src/templates/project/basic/config/SecretsManager.ts.tpl +1 -1
- package/src/templates/project/basic/config/broadcast.ts.tpl +2 -2
- package/src/templates/project/basic/config/cache.ts.tpl +2 -2
- package/src/templates/project/basic/config/database.ts.tpl +2 -2
- package/src/templates/project/basic/config/env.ts.tpl +5 -0
- package/src/templates/project/basic/config/features.ts.tpl +2 -2
- package/src/templates/project/basic/config/logger.ts.tpl +0 -2
- package/src/templates/project/basic/config/logging/HttpLogger.ts.tpl +1 -1
- package/src/templates/project/basic/config/logging/SlackLogger.ts.tpl +1 -1
- package/src/templates/project/basic/config/mail.ts.tpl +2 -2
- package/src/templates/project/basic/config/microservices.ts.tpl +1 -1
- package/src/templates/project/basic/config/middleware.ts.tpl +6 -9
- package/src/templates/project/basic/config/notification.ts.tpl +2 -2
- package/src/templates/project/basic/config/security.ts.tpl +12 -3
- package/src/templates/project/basic/config/storage.ts.tpl +2 -2
- package/src/templates/project/basic/config/type.ts.tpl +2 -2
- package/src/tools/broadcast/Broadcast.d.ts +8 -0
- package/src/tools/broadcast/Broadcast.d.ts.map +1 -1
- package/src/tools/broadcast/Broadcast.js +23 -0
- package/src/tools/notification/Notification.d.ts +10 -0
- package/src/tools/notification/Notification.d.ts.map +1 -1
- package/src/tools/notification/Notification.js +21 -0
- package/src/workers/BroadcastWorker.d.ts +22 -0
- package/src/workers/BroadcastWorker.d.ts.map +1 -0
- package/src/workers/BroadcastWorker.js +24 -0
- package/src/workers/NotificationWorker.d.ts +22 -0
- package/src/workers/NotificationWorker.d.ts.map +1 -0
- package/src/workers/NotificationWorker.js +23 -0
- package/src/workers/createQueueWorker.d.ts +24 -0
- package/src/workers/createQueueWorker.d.ts.map +1 -0
- package/src/workers/createQueueWorker.js +114 -0
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Protects against Cross-Site Request Forgery attacks
|
|
4
4
|
* Uses CsrfTokenManager for token generation and validation
|
|
5
5
|
*/
|
|
6
|
-
import { generateSecureJobId } from '../common/uuid.js';
|
|
7
6
|
import { Logger } from '../config/logger.js';
|
|
8
7
|
import { CsrfTokenManager } from '../security/CsrfTokenManager.js';
|
|
8
|
+
import { SessionManager } from '../session/SessionManager.js';
|
|
9
9
|
const DEFAULT_OPTIONS = {
|
|
10
10
|
cookieName: 'XSRF-TOKEN',
|
|
11
11
|
headerName: 'X-CSRF-Token',
|
|
@@ -19,6 +19,7 @@ export const CsrfMiddleware = Object.freeze({
|
|
|
19
19
|
create(options = {}) {
|
|
20
20
|
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
21
21
|
const manager = CsrfTokenManager.create();
|
|
22
|
+
const sessions = SessionManager.create();
|
|
22
23
|
// Periodic cleanup to prevent memory leaks
|
|
23
24
|
// Run every hour (matching default token TTL)
|
|
24
25
|
const cleanupTimer = setInterval(() => {
|
|
@@ -29,35 +30,17 @@ export const CsrfMiddleware = Object.freeze({
|
|
|
29
30
|
cleanupTimer.unref();
|
|
30
31
|
}
|
|
31
32
|
return async (req, res, next) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
// Here we check for a specific session cookie or header.
|
|
38
|
-
const cookies = parseCookies(req.getHeader('cookie') || '');
|
|
39
|
-
let sessionId = cookies['ZIN_SESSION_ID'] || req.context['sessionId'];
|
|
40
|
-
if (!sessionId) {
|
|
41
|
-
// If no session exists, we can't effectively bind CSRF to a session.
|
|
42
|
-
// However, for the sake of the middleware functioning in a stateless way (Double Submit Cookie pattern),
|
|
43
|
-
// we can generate a pseudo-session-id if one doesn't exist, but it's less secure.
|
|
44
|
-
// Ideally, this middleware should throw if session is missing.
|
|
45
|
-
// For this implementation, we'll skip if no session is found, but log a warning.
|
|
46
|
-
// Logger.warn('CSRF Middleware: No session ID found. Skipping CSRF check.');
|
|
47
|
-
// await next();
|
|
48
|
-
// return;
|
|
49
|
-
// Better approach: Generate a session ID if missing (Double Submit Cookie foundation)
|
|
50
|
-
// IMPORTANT: use a cryptographically secure generator (Sonar S2245).
|
|
51
|
-
sessionId = await generateSecureJobId('CSRF Middleware: secure crypto API not available to generate a session id');
|
|
52
|
-
// We would need to set this session cookie, but we can't easily do that without a SessionManager.
|
|
53
|
-
// We'll assume the SessionMiddleware handles session creation.
|
|
54
|
-
}
|
|
33
|
+
const cookieHeader = req.getHeader('cookie');
|
|
34
|
+
const cookies = parseCookies(typeof cookieHeader === 'string' ? cookieHeader : '');
|
|
35
|
+
// Guarantee a session id exists and a session cookie is set if missing.
|
|
36
|
+
// This allows CSRF tokens to be bound to a stable session identifier.
|
|
37
|
+
const sessionId = await sessions.ensureSessionId(req, res);
|
|
55
38
|
const method = req.getMethod();
|
|
56
39
|
// 1. Token Generation (for safe methods)
|
|
57
40
|
if (config.ignoreMethods?.includes(method) ?? false) {
|
|
58
41
|
const token = manager.generateToken(sessionId);
|
|
59
42
|
// Set cookie for Double Submit Cookie pattern (readable by frontend)
|
|
60
|
-
res
|
|
43
|
+
appendSetCookie(res, `${config.cookieName}=${token}; Path=/; SameSite=Strict`);
|
|
61
44
|
// Also expose in locals for server-side rendering
|
|
62
45
|
res.locals['csrfToken'] = token;
|
|
63
46
|
await next();
|
|
@@ -81,6 +64,18 @@ export const CsrfMiddleware = Object.freeze({
|
|
|
81
64
|
};
|
|
82
65
|
},
|
|
83
66
|
});
|
|
67
|
+
function appendSetCookie(res, cookie) {
|
|
68
|
+
const existing = res.getHeader('Set-Cookie');
|
|
69
|
+
if (existing === undefined) {
|
|
70
|
+
res.setHeader('Set-Cookie', cookie);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(existing)) {
|
|
74
|
+
res.setHeader('Set-Cookie', [...existing, cookie]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
res.setHeader('Set-Cookie', [existing, cookie]);
|
|
78
|
+
}
|
|
84
79
|
/**
|
|
85
80
|
* Helper to parse cookies
|
|
86
81
|
*/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Middleware } from './MiddlewareStack';
|
|
2
|
+
import { type SessionManagerOptions } from '../session/SessionManager';
|
|
3
|
+
export type SessionOptions = SessionManagerOptions;
|
|
4
|
+
export declare const SessionMiddleware: Readonly<{
|
|
5
|
+
create(options?: SessionOptions): Middleware;
|
|
6
|
+
}>;
|
|
7
|
+
export default SessionMiddleware;
|
|
8
|
+
//# sourceMappingURL=SessionMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionMiddleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/SessionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErF,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC;AAEnD,eAAO,MAAM,iBAAiB;qBACZ,cAAc,GAAQ,UAAU;EAehD,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SessionManager } from '../session/SessionManager.js';
|
|
2
|
+
export const SessionMiddleware = Object.freeze({
|
|
3
|
+
create(options = {}) {
|
|
4
|
+
const manager = SessionManager.create(options);
|
|
5
|
+
return async (req, res, next) => {
|
|
6
|
+
const sessionId = await manager.ensureSessionId(req, res);
|
|
7
|
+
// Keep both request state + context aligned.
|
|
8
|
+
req.sessionId = sessionId;
|
|
9
|
+
req.context['sessionId'] = sessionId;
|
|
10
|
+
res.locals['sessionId'] = sessionId;
|
|
11
|
+
await next();
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
export default SessionMiddleware;
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Safe to import in both API and CLI code
|
|
4
4
|
* Exported from node:crypto built-in
|
|
5
5
|
*/
|
|
6
|
-
export { createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, } from 'node:crypto';
|
|
6
|
+
export { createCipheriv, createDecipheriv, createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, timingSafeEqual, } from 'node:crypto';
|
|
7
7
|
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/node-singletons/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,WAAW,EACX,SAAS,
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/node-singletons/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,WAAW,EACX,SAAS,EACT,eAAe,GAChB,MAAM,aAAa,CAAC"}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Safe to import in both API and CLI code
|
|
4
4
|
* Exported from node:crypto built-in
|
|
5
5
|
*/
|
|
6
|
-
export { createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, } from 'node:crypto';
|
|
6
|
+
export { createCipheriv, createDecipheriv, createHash, createHmac, createSign, createVerify, generateKeyPairSync, pbkdf2Sync, randomBytes, randomInt, timingSafeEqual, } from 'node:crypto';
|
package/src/orm/Model.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export interface ModelConfig {
|
|
|
10
10
|
hidden: string[];
|
|
11
11
|
timestamps: boolean;
|
|
12
12
|
casts: Record<string, string>;
|
|
13
|
+
softDeletes?: boolean;
|
|
14
|
+
accessors?: Record<string, (value: unknown, attrs: Record<string, unknown>) => unknown>;
|
|
15
|
+
mutators?: Record<string, (value: unknown, attrs: Record<string, unknown>) => unknown>;
|
|
16
|
+
scopes?: Record<string, (builder: IQueryBuilder, ...args: unknown[]) => IQueryBuilder>;
|
|
17
|
+
observers?: Array<{
|
|
18
|
+
saving?: (model: IModel) => void | Promise<void>;
|
|
19
|
+
saved?: (model: IModel) => void | Promise<void>;
|
|
20
|
+
creating?: (model: IModel) => void | Promise<void>;
|
|
21
|
+
created?: (model: IModel) => void | Promise<void>;
|
|
22
|
+
updating?: (model: IModel) => void | Promise<void>;
|
|
23
|
+
updated?: (model: IModel) => void | Promise<void>;
|
|
24
|
+
deleting?: (model: IModel) => void | Promise<void>;
|
|
25
|
+
deleted?: (model: IModel) => void | Promise<void>;
|
|
26
|
+
}>;
|
|
13
27
|
connection?: string;
|
|
14
28
|
}
|
|
15
29
|
export interface ModelStatic {
|
|
@@ -60,6 +74,7 @@ export type DefinedModel<T extends BoundModelMethods> = {
|
|
|
60
74
|
find: (id: unknown) => Promise<(IModel & T) | null>;
|
|
61
75
|
all: () => Promise<Array<IModel & T>>;
|
|
62
76
|
query: () => IQueryBuilder;
|
|
77
|
+
scope: (name: string, ...args: unknown[]) => IQueryBuilder;
|
|
63
78
|
getTable: () => string;
|
|
64
79
|
db: (connection: string) => DefinedModel<T>;
|
|
65
80
|
};
|
package/src/orm/Model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/orm/Model.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../../src/orm/Model.ts"],"names":[],"mappings":"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"}
|
package/src/orm/Model.js
CHANGED
|
@@ -43,7 +43,27 @@ const castAttribute = (config, key, value) => {
|
|
|
43
43
|
const fillAttributes = (config, attrs, newAttrs) => {
|
|
44
44
|
for (const [key, value] of Object.entries(newAttrs)) {
|
|
45
45
|
if (config.fillable.length === 0 || config.fillable.includes(key)) {
|
|
46
|
-
|
|
46
|
+
const mutator = config.mutators?.[key];
|
|
47
|
+
const nextValue = mutator ? mutator(value, attrs) : value;
|
|
48
|
+
attrs[key] = castAttribute(config, key, nextValue);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const applyAccessor = (config, key, attrs) => {
|
|
53
|
+
const raw = attrs[key];
|
|
54
|
+
const accessor = config.accessors?.[key];
|
|
55
|
+
return accessor ? accessor(raw, attrs) : raw;
|
|
56
|
+
};
|
|
57
|
+
const runObservers = async (config, hook, model) => {
|
|
58
|
+
const observers = config.observers;
|
|
59
|
+
if (observers === undefined || observers.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
for (const observer of observers) {
|
|
62
|
+
const fn = observer[hook];
|
|
63
|
+
if (typeof fn === 'function') {
|
|
64
|
+
// Observers intentionally run sequentially.
|
|
65
|
+
// eslint-disable-next-line no-await-in-loop
|
|
66
|
+
await fn(model);
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
69
|
};
|
|
@@ -85,29 +105,36 @@ export const createModel = (config, attributes = {}) => {
|
|
|
85
105
|
return model;
|
|
86
106
|
},
|
|
87
107
|
setAttribute: (key, value) => {
|
|
88
|
-
|
|
108
|
+
const mutator = config.mutators?.[key];
|
|
109
|
+
const nextValue = mutator ? mutator(value, attrs) : value;
|
|
110
|
+
attrs[key] = castAttribute(config, key, nextValue);
|
|
89
111
|
return model;
|
|
90
112
|
},
|
|
91
|
-
getAttribute: (key) =>
|
|
113
|
+
getAttribute: (key) => applyAccessor(config, key, attrs),
|
|
92
114
|
getAttributes: () => ({ ...attrs }),
|
|
93
115
|
// remove in production - use saveChanges pattern
|
|
94
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
95
116
|
async save() {
|
|
96
117
|
if (db === undefined)
|
|
97
118
|
throw ErrorFactory.createDatabaseError('Database not initialized');
|
|
119
|
+
const isCreate = isExists === false;
|
|
120
|
+
await runObservers(config, 'saving', model);
|
|
121
|
+
await runObservers(config, isCreate ? 'creating' : 'updating', model);
|
|
98
122
|
if (config.timestamps) {
|
|
99
123
|
attrs['created_at'] = attrs['created_at'] ?? new Date().toISOString();
|
|
100
124
|
attrs['updated_at'] = new Date().toISOString();
|
|
101
125
|
}
|
|
102
126
|
isExists = true;
|
|
103
127
|
original = { ...attrs };
|
|
128
|
+
await runObservers(config, isCreate ? 'created' : 'updated', model);
|
|
129
|
+
await runObservers(config, 'saved', model);
|
|
104
130
|
return true;
|
|
105
131
|
},
|
|
106
132
|
// remove in production - use delete pattern
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
108
133
|
async delete() {
|
|
109
134
|
if (!isExists || db === undefined)
|
|
110
135
|
return false;
|
|
136
|
+
await runObservers(config, 'deleting', model);
|
|
137
|
+
await runObservers(config, 'deleted', model);
|
|
111
138
|
return true;
|
|
112
139
|
},
|
|
113
140
|
toJSON: () => createModelJSON(config, attrs),
|
|
@@ -130,11 +157,17 @@ export const query = (table, connection) => {
|
|
|
130
157
|
const db = useDatabase(undefined, connection ?? DEFAULTS.CONNECTION);
|
|
131
158
|
return QueryBuilder.create(table, db);
|
|
132
159
|
};
|
|
160
|
+
const buildSoftDeleteOptions = (config) => {
|
|
161
|
+
if (config.softDeletes !== true)
|
|
162
|
+
return undefined;
|
|
163
|
+
return { softDeleteColumn: 'deleted_at', softDeleteMode: 'exclude' };
|
|
164
|
+
};
|
|
133
165
|
/**
|
|
134
166
|
* Find a model by ID
|
|
135
167
|
*/
|
|
136
168
|
export const find = async (config, id) => {
|
|
137
|
-
const
|
|
169
|
+
const db = useDatabase(undefined, config.connection ?? DEFAULTS.CONNECTION);
|
|
170
|
+
const builder = QueryBuilder.create(config.table, db, buildSoftDeleteOptions(config));
|
|
138
171
|
builder.where('id', '=', String(id)).limit(1);
|
|
139
172
|
const result = await builder.first();
|
|
140
173
|
if (result === null)
|
|
@@ -147,7 +180,8 @@ export const find = async (config, id) => {
|
|
|
147
180
|
* Get all records for a model
|
|
148
181
|
*/
|
|
149
182
|
export const all = async (config) => {
|
|
150
|
-
const
|
|
183
|
+
const db = useDatabase(undefined, config.connection ?? DEFAULTS.CONNECTION);
|
|
184
|
+
const builder = QueryBuilder.create(config.table, db, buildSoftDeleteOptions(config));
|
|
151
185
|
const results = await builder.get();
|
|
152
186
|
return results.map((result) => {
|
|
153
187
|
const model = createModel(config, result);
|
|
@@ -187,7 +221,22 @@ export function define(config, methodsOrPlan) {
|
|
|
187
221
|
const models = await all(cfg);
|
|
188
222
|
return models.map((m) => attach(m));
|
|
189
223
|
},
|
|
190
|
-
query: () =>
|
|
224
|
+
query: () => {
|
|
225
|
+
const db = useDatabase(undefined, cfg.connection ?? DEFAULTS.CONNECTION);
|
|
226
|
+
return QueryBuilder.create(cfg.table, db, buildSoftDeleteOptions(cfg));
|
|
227
|
+
},
|
|
228
|
+
scope: (name, ...args) => {
|
|
229
|
+
const scopes = cfg.scopes;
|
|
230
|
+
const fn = scopes?.[name];
|
|
231
|
+
if (typeof fn !== 'function') {
|
|
232
|
+
throw ErrorFactory.createConfigError(`Unknown query scope: ${name}`);
|
|
233
|
+
}
|
|
234
|
+
const builder = (() => {
|
|
235
|
+
const db = useDatabase(undefined, cfg.connection ?? DEFAULTS.CONNECTION);
|
|
236
|
+
return QueryBuilder.create(cfg.table, db, buildSoftDeleteOptions(cfg));
|
|
237
|
+
})();
|
|
238
|
+
return fn(builder, ...args);
|
|
239
|
+
},
|
|
191
240
|
getTable: () => cfg.table,
|
|
192
241
|
db: (connection) => createDefinedModel({ ...cfg, connection }),
|
|
193
242
|
});
|
|
@@ -8,11 +8,19 @@ export interface WhereClause {
|
|
|
8
8
|
operator: string;
|
|
9
9
|
value: unknown;
|
|
10
10
|
}
|
|
11
|
+
export type SoftDeleteMode = 'exclude' | 'include' | 'only';
|
|
12
|
+
export interface QueryBuilderOptions {
|
|
13
|
+
softDeleteColumn?: string;
|
|
14
|
+
softDeleteMode?: SoftDeleteMode;
|
|
15
|
+
}
|
|
11
16
|
export interface IQueryBuilder {
|
|
12
17
|
select(...columns: string[]): IQueryBuilder;
|
|
13
18
|
where(column: string, operator: string | number | boolean | null, value?: unknown): IQueryBuilder;
|
|
14
19
|
andWhere(column: string, operator: string, value?: unknown): IQueryBuilder;
|
|
15
20
|
orWhere(column: string, operator: string, value?: unknown): IQueryBuilder;
|
|
21
|
+
withTrashed(): IQueryBuilder;
|
|
22
|
+
onlyTrashed(): IQueryBuilder;
|
|
23
|
+
withoutTrashed(): IQueryBuilder;
|
|
16
24
|
join(table: string, on: string): IQueryBuilder;
|
|
17
25
|
leftJoin(table: string, on: string): IQueryBuilder;
|
|
18
26
|
orderBy(column: string, direction?: 'ASC' | 'DESC'): IQueryBuilder;
|
|
@@ -47,7 +55,7 @@ export declare const QueryBuilder: Readonly<{
|
|
|
47
55
|
/**
|
|
48
56
|
* Create a new query builder instance
|
|
49
57
|
*/
|
|
50
|
-
create(tableOrDb: string | IDatabase, db?: IDatabase): IQueryBuilder;
|
|
58
|
+
create(tableOrDb: string | IDatabase, db?: IDatabase, options?: QueryBuilderOptions): IQueryBuilder;
|
|
51
59
|
/**
|
|
52
60
|
* Ping the database connection.
|
|
53
61
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryBuilder.d.ts","sourceRoot":"","sources":["../../../src/orm/QueryBuilder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC;IAC5C,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAClG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAC3E,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAC1E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,aAAa,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,aAAa,CAAC;IACnE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACpC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACrC,eAAe,IAAI,WAAW,EAAE,CAAC;IACjC,gBAAgB,IAAI,MAAM,EAAE,CAAC;IAC7B,QAAQ,IAAI,MAAM,CAAC;IACnB,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAC;IAC/B,SAAS,IAAI,MAAM,GAAG,SAAS,CAAC;IAChC,UAAU,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACxE,QAAQ,IAAI,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,eAAe,IAAI,OAAO,CAAC;IAC3B,KAAK,IAAI,MAAM,CAAC;IAChB,aAAa,IAAI,OAAO,EAAE,CAAC;IAC3B,KAAK,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9B,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"QueryBuilder.d.ts","sourceRoot":"","sources":["../../../src/orm/QueryBuilder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC;IAC5C,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAClG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAC3E,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAC1E,WAAW,IAAI,aAAa,CAAC;IAC7B,WAAW,IAAI,aAAa,CAAC;IAC7B,cAAc,IAAI,aAAa,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,aAAa,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,aAAa,CAAC;IACnE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACpC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACrC,eAAe,IAAI,WAAW,EAAE,CAAC;IACjC,gBAAgB,IAAI,MAAM,EAAE,CAAC;IAC7B,QAAQ,IAAI,MAAM,CAAC;IACnB,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAC;IAC/B,SAAS,IAAI,MAAM,GAAG,SAAS,CAAC;IAChC,UAAU,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACxE,QAAQ,IAAI,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,eAAe,IAAI,OAAO,CAAC;IAC3B,KAAK,IAAI,MAAM,CAAC;IAChB,aAAa,IAAI,OAAO,EAAE,CAAC;IAC3B,KAAK,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9B,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CACxB;AAgWD;;;;;GAKG;AACH,eAAO,MAAM,YAAY;IACvB;;OAEG;sBAEU,MAAM,GAAG,SAAS,OACxB,SAAS,YACL,mBAAmB,GAC3B,aAAa;IAyBhB;;;;;OAKG;aACY,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;EAIxC,CAAC"}
|
package/src/orm/QueryBuilder.js
CHANGED
|
@@ -117,6 +117,25 @@ const compileWhere = (conditions) => {
|
|
|
117
117
|
});
|
|
118
118
|
return { sql: ` WHERE ${clauses.join(' AND ')}`, parameters };
|
|
119
119
|
};
|
|
120
|
+
const buildSoftDeleteWhereClause = (column, mode) => {
|
|
121
|
+
const col = column.trim();
|
|
122
|
+
if (col.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
assertSafeIdentifierPath(col, 'soft delete column');
|
|
125
|
+
if (mode === 'include')
|
|
126
|
+
return null;
|
|
127
|
+
if (mode === 'only')
|
|
128
|
+
return { column: col, operator: 'IS NOT', value: null };
|
|
129
|
+
return { column: col, operator: 'IS', value: null };
|
|
130
|
+
};
|
|
131
|
+
const getEffectiveWhereConditions = (state) => {
|
|
132
|
+
if (state.softDelete === undefined)
|
|
133
|
+
return state.whereConditions;
|
|
134
|
+
const clause = buildSoftDeleteWhereClause(state.softDelete.column, state.softDelete.mode);
|
|
135
|
+
if (clause === null)
|
|
136
|
+
return state.whereConditions;
|
|
137
|
+
return [...state.whereConditions, clause];
|
|
138
|
+
};
|
|
120
139
|
/**
|
|
121
140
|
* Build ORDER BY clause
|
|
122
141
|
*/
|
|
@@ -152,7 +171,7 @@ const buildSelectQuery = (state) => {
|
|
|
152
171
|
}
|
|
153
172
|
const columns = buildSelectClause(state.selectColumns);
|
|
154
173
|
const fromClause = state.tableName.length > 0 ? ` FROM ${escapeIdentifier(state.tableName)}` : '';
|
|
155
|
-
const where = compileWhere(state
|
|
174
|
+
const where = compileWhere(getEffectiveWhereConditions(state));
|
|
156
175
|
const sql = `SELECT ${columns}${fromClause}${where.sql}${buildOrderByClause(state.orderByClause)}${buildLimitOffsetClause(state.limitValue, state.offsetValue)}`;
|
|
157
176
|
return { sql, parameters: where.parameters };
|
|
158
177
|
};
|
|
@@ -224,6 +243,33 @@ function createBuilder(state, db) {
|
|
|
224
243
|
},
|
|
225
244
|
andWhere: (column, operator, value) => builder.where(column, operator, value),
|
|
226
245
|
orWhere: (column, operator, value) => builder.where(column, operator, value),
|
|
246
|
+
withTrashed: () => {
|
|
247
|
+
if (state.softDelete === undefined) {
|
|
248
|
+
state.softDelete = { column: 'deleted_at', mode: 'include' };
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
state.softDelete.mode = 'include';
|
|
252
|
+
}
|
|
253
|
+
return builder;
|
|
254
|
+
},
|
|
255
|
+
onlyTrashed: () => {
|
|
256
|
+
if (state.softDelete === undefined) {
|
|
257
|
+
state.softDelete = { column: 'deleted_at', mode: 'only' };
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
state.softDelete.mode = 'only';
|
|
261
|
+
}
|
|
262
|
+
return builder;
|
|
263
|
+
},
|
|
264
|
+
withoutTrashed: () => {
|
|
265
|
+
if (state.softDelete === undefined) {
|
|
266
|
+
state.softDelete = { column: 'deleted_at', mode: 'exclude' };
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
state.softDelete.mode = 'exclude';
|
|
270
|
+
}
|
|
271
|
+
return builder;
|
|
272
|
+
},
|
|
227
273
|
join: (tableJoin, on) => {
|
|
228
274
|
state.joins.push({ table: tableJoin, on });
|
|
229
275
|
return builder;
|
|
@@ -266,7 +312,7 @@ export const QueryBuilder = Object.freeze({
|
|
|
266
312
|
/**
|
|
267
313
|
* Create a new query builder instance
|
|
268
314
|
*/
|
|
269
|
-
create(tableOrDb, db) {
|
|
315
|
+
create(tableOrDb, db, options = {}) {
|
|
270
316
|
const hasTable = typeof tableOrDb === 'string';
|
|
271
317
|
const tableName = hasTable ? String(tableOrDb).trim() : '';
|
|
272
318
|
const database = hasTable ? db : tableOrDb;
|
|
@@ -279,6 +325,12 @@ export const QueryBuilder = Object.freeze({
|
|
|
279
325
|
selectColumns: ['*'],
|
|
280
326
|
joins: [],
|
|
281
327
|
};
|
|
328
|
+
if (options.softDeleteColumn !== undefined && options.softDeleteColumn.trim().length > 0) {
|
|
329
|
+
state.softDelete = {
|
|
330
|
+
column: options.softDeleteColumn.trim(),
|
|
331
|
+
mode: options.softDeleteMode ?? 'exclude',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
282
334
|
return createBuilder(state, database);
|
|
283
335
|
},
|
|
284
336
|
/**
|
|
@@ -10,6 +10,7 @@ import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
|
10
10
|
import * as crypto from '../node-singletons/crypto.js';
|
|
11
11
|
import fs from '../node-singletons/fs.js';
|
|
12
12
|
import * as path from '../node-singletons/path.js';
|
|
13
|
+
import * as nodePath from 'node:path';
|
|
13
14
|
const __dirname = esmDirname(import.meta.url);
|
|
14
15
|
const ROOT_DIR = path.resolve(__dirname, '../../');
|
|
15
16
|
/**
|
|
@@ -115,9 +116,30 @@ const rewriteStarterTemplateImports = (relPath, content) => {
|
|
|
115
116
|
if (!relPath.endsWith('.ts') && !relPath.endsWith('.tsx') && !relPath.endsWith('.mts')) {
|
|
116
117
|
return content;
|
|
117
118
|
}
|
|
119
|
+
const rewriteConfigAlias = (aliasSuffix) => {
|
|
120
|
+
const currentDir = nodePath.posix.dirname(relPath);
|
|
121
|
+
const from = currentDir === '.' ? '' : currentDir;
|
|
122
|
+
const target = aliasSuffix;
|
|
123
|
+
const relative = nodePath.posix.relative(from, target);
|
|
124
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
125
|
+
};
|
|
118
126
|
// Starter templates should import framework APIs from the public package surface,
|
|
119
127
|
// not from internal path-alias modules that only exist in the framework repo.
|
|
120
128
|
return (content
|
|
129
|
+
// Node-singletons are internal to this repo; starter templates should use Node built-ins.
|
|
130
|
+
.replaceAll("'@node-singletons/fs'", "'node:fs'")
|
|
131
|
+
.replaceAll('"@node-singletons/fs"', '"node:fs"')
|
|
132
|
+
.replaceAll("'@node-singletons/path'", "'node:path'")
|
|
133
|
+
.replaceAll('"@node-singletons/path"', '"node:path"')
|
|
134
|
+
// Starter project config/* should reference sibling config modules via relative imports.
|
|
135
|
+
.replaceAll(/(['"])@config\/([^'"]+)\1/g, (_m, quote, suffix) => {
|
|
136
|
+
const rewritten = rewriteConfigAlias(suffix);
|
|
137
|
+
return `${quote}${rewritten}${quote}`;
|
|
138
|
+
})
|
|
139
|
+
// Middleware imports are framework APIs; they must come from the public package.
|
|
140
|
+
.replaceAll(/(['"])@middleware\/[^'"]+\1/g, (_m, quote) => {
|
|
141
|
+
return `${quote}@zintrust/core${quote}`;
|
|
142
|
+
})
|
|
121
143
|
.replaceAll("'@routing/Router'", "'@zintrust/core'")
|
|
122
144
|
.replaceAll("'@orm/Database'", "'@zintrust/core'")
|
|
123
145
|
.replaceAll("'@orm/QueryBuilder'", "'@zintrust/core'")
|
|
@@ -217,7 +239,7 @@ const syncStarterProjectTemplates = (params) => {
|
|
|
217
239
|
templateDirRel: `${params.projectRoot}/config`,
|
|
218
240
|
description: 'Starter project config/* (from src/config/*)',
|
|
219
241
|
transformContent: rewriteStarterTemplateImports,
|
|
220
|
-
checksumSalt: 'starter-imports-
|
|
242
|
+
checksumSalt: 'starter-imports-v4',
|
|
221
243
|
});
|
|
222
244
|
const s3 = syncProjectTemplateDir({
|
|
223
245
|
checksums: params.checksums,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EncryptedEnvelope
|
|
3
|
+
*
|
|
4
|
+
* Framework-compatible encrypted payload envelope (PHP-style envelope).
|
|
5
|
+
*
|
|
6
|
+
* Format: base64(JSON({ iv, value, mac, tag }))
|
|
7
|
+
* - iv: base64
|
|
8
|
+
* - value: base64 ciphertext
|
|
9
|
+
* - mac: hex string (AES-CBC envelopes)
|
|
10
|
+
* - tag: base64 auth tag (AES-GCM envelopes)
|
|
11
|
+
*/
|
|
12
|
+
export type EncryptedEnvelopeCipher = 'aes-256-cbc' | 'aes-256-gcm';
|
|
13
|
+
export type EncryptedEnvelopeCipherInput = EncryptedEnvelopeCipher | 'AES-256-CBC' | 'AES-256-GCM';
|
|
14
|
+
export type EncryptedEnvelopePayload = {
|
|
15
|
+
iv: string;
|
|
16
|
+
value: string;
|
|
17
|
+
mac?: string;
|
|
18
|
+
tag?: string;
|
|
19
|
+
};
|
|
20
|
+
export type EncryptedEnvelopeSerializer<T> = {
|
|
21
|
+
serialize: (value: T) => string;
|
|
22
|
+
deserialize: (value: string) => T;
|
|
23
|
+
};
|
|
24
|
+
export type EncryptedEnvelopeKeyring = {
|
|
25
|
+
primaryKey: Uint8Array;
|
|
26
|
+
previousKeys: Uint8Array[];
|
|
27
|
+
};
|
|
28
|
+
export type EncryptedEnvelopeEnv = {
|
|
29
|
+
APP_KEY?: string;
|
|
30
|
+
APP_PREVIOUS_KEYS?: string;
|
|
31
|
+
ENCRYPTION_CIPHER?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare const EncryptedEnvelope: Readonly<{
|
|
34
|
+
normalizeCipher: (cipher: EncryptedEnvelopeCipherInput) => EncryptedEnvelopeCipher;
|
|
35
|
+
/**
|
|
36
|
+
* Build a keyring from environment variables.
|
|
37
|
+
* - Uses APP_KEY
|
|
38
|
+
* - Supports APP_PREVIOUS_KEYS (comma-separated or JSON array)
|
|
39
|
+
*/
|
|
40
|
+
keyringFromEnv(env?: EncryptedEnvelopeEnv): EncryptedEnvelopeKeyring;
|
|
41
|
+
/**
|
|
42
|
+
* Encrypt a UTF-8 string and return a framework-compatible base64(JSON) envelope.
|
|
43
|
+
*/
|
|
44
|
+
encryptString(plaintext: string, options: {
|
|
45
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
46
|
+
key: string;
|
|
47
|
+
}): string;
|
|
48
|
+
/**
|
|
49
|
+
* Decrypt a framework-compatible base64(JSON) envelope to a UTF-8 string.
|
|
50
|
+
* Tries the primary key first, then previous keys.
|
|
51
|
+
*/
|
|
52
|
+
decryptString(encrypted: string, options: {
|
|
53
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
54
|
+
key: string;
|
|
55
|
+
previousKeys?: string[];
|
|
56
|
+
}): string;
|
|
57
|
+
/**
|
|
58
|
+
* Encrypt arbitrary values using a caller-provided serializer.
|
|
59
|
+
* This supports encrypted payloads for frameworks that store serialized values.
|
|
60
|
+
*/
|
|
61
|
+
encrypt<T>(value: T, options: {
|
|
62
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
63
|
+
key: string;
|
|
64
|
+
serializer: EncryptedEnvelopeSerializer<T>;
|
|
65
|
+
}): string;
|
|
66
|
+
/**
|
|
67
|
+
* Decrypt into an arbitrary value using a caller-provided serializer.
|
|
68
|
+
*/
|
|
69
|
+
decrypt<T>(encrypted: string, options: {
|
|
70
|
+
cipher: EncryptedEnvelopeCipherInput;
|
|
71
|
+
key: string;
|
|
72
|
+
previousKeys?: string[];
|
|
73
|
+
serializer: EncryptedEnvelopeSerializer<T>;
|
|
74
|
+
}): T;
|
|
75
|
+
}>;
|
|
76
|
+
export default EncryptedEnvelope;
|
|
77
|
+
//# sourceMappingURL=EncryptedEnvelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptedEnvelope.d.ts","sourceRoot":"","sources":["../../../src/security/EncryptedEnvelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH,MAAM,MAAM,uBAAuB,GAAG,aAAa,GAAG,aAAa,CAAC;AACpE,MAAM,MAAM,4BAA4B,GAAG,uBAAuB,GAAG,aAAa,GAAG,aAAa,CAAC;AAEnG,MAAM,MAAM,wBAAwB,GAAG;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAChC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,UAAU,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AA6LF,eAAO,MAAM,iBAAiB;8BA3LG,4BAA4B,KAAG,uBAAuB;IA8LrF;;;;OAIG;yBAEI,oBAAoB,GACxB,wBAAwB;IAe3B;;OAEG;6BAEU,MAAM,WACR;QAAE,MAAM,EAAE,4BAA4B,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAC7D,MAAM;IAgCT;;;OAGG;6BAEU,MAAM,WACR;QAAE,MAAM,EAAE,4BAA4B,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GACtF,MAAM;IA0BT;;;OAGG;YACK,CAAC,SACA,CAAC,WACC;QACP,MAAM,EAAE,4BAA4B,CAAC;QACrC,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,2BAA2B,CAAC,CAAC,CAAC,CAAC;KAC5C,GACA,MAAM;IAQT;;OAEG;YACK,CAAC,aACI,MAAM,WACR;QACP,MAAM,EAAE,4BAA4B,CAAC;QACrC,GAAG,EAAE,MAAM,CAAC;QACZ,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,UAAU,EAAE,2BAA2B,CAAC,CAAC,CAAC,CAAC;KAC5C,GACA,CAAC;EASJ,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
|