@object-ui/data-objectstack 2.0.0 → 3.0.0
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/dist/index.cjs +429 -6
- package/dist/index.d.cts +496 -1
- package/dist/index.d.ts +496 -1
- package/dist/index.js +420 -5
- package/package.json +6 -6
- package/src/cache/MetadataCache.ts +22 -0
- package/src/cloud.ts +109 -0
- package/src/contracts.ts +115 -0
- package/src/index.ts +73 -5
- package/src/integration.ts +192 -0
- package/src/security.ts +230 -0
- package/src/studio.ts +152 -0
- package/src/v3-compat.test.ts +240 -0
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Contracts module integration for @objectstack/spec v3.0.0
|
|
11
|
+
* Provides plugin contract validation and marketplace publishing utilities.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PluginContract {
|
|
15
|
+
/** Plugin name */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Plugin version */
|
|
18
|
+
version: string;
|
|
19
|
+
/** Required peer dependencies */
|
|
20
|
+
peerDependencies?: Record<string, string>;
|
|
21
|
+
/** Exported component types */
|
|
22
|
+
exports: PluginExport[];
|
|
23
|
+
/** Required permissions */
|
|
24
|
+
permissions?: string[];
|
|
25
|
+
/** API surface contract */
|
|
26
|
+
api?: PluginAPIContract;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PluginExport {
|
|
30
|
+
/** Export name */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Export type */
|
|
33
|
+
type: 'component' | 'hook' | 'utility' | 'provider';
|
|
34
|
+
/** Description */
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PluginAPIContract {
|
|
39
|
+
/** Consumed data sources */
|
|
40
|
+
dataSources?: string[];
|
|
41
|
+
/** Required object schemas */
|
|
42
|
+
requiredSchemas?: string[];
|
|
43
|
+
/** Event subscriptions */
|
|
44
|
+
events?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ContractValidationResult {
|
|
48
|
+
valid: boolean;
|
|
49
|
+
errors: ContractValidationError[];
|
|
50
|
+
warnings: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ContractValidationError {
|
|
54
|
+
field: string;
|
|
55
|
+
message: string;
|
|
56
|
+
code: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate a plugin contract against the ObjectStack spec.
|
|
61
|
+
*/
|
|
62
|
+
export function validatePluginContract(contract: PluginContract): ContractValidationResult {
|
|
63
|
+
const errors: ContractValidationError[] = [];
|
|
64
|
+
const warnings: string[] = [];
|
|
65
|
+
|
|
66
|
+
if (!contract.name || contract.name.trim().length === 0) {
|
|
67
|
+
errors.push({ field: 'name', message: 'Plugin name is required', code: 'MISSING_NAME' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!contract.version || !/^\d+\.\d+\.\d+/.test(contract.version)) {
|
|
71
|
+
errors.push({ field: 'version', message: 'Valid semver version is required', code: 'INVALID_VERSION' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!contract.exports || contract.exports.length === 0) {
|
|
75
|
+
errors.push({ field: 'exports', message: 'At least one export is required', code: 'NO_EXPORTS' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (contract.exports) {
|
|
79
|
+
const validTypes = ['component', 'hook', 'utility', 'provider'];
|
|
80
|
+
for (const exp of contract.exports) {
|
|
81
|
+
if (!exp.name) {
|
|
82
|
+
errors.push({ field: 'exports.name', message: 'Export name is required', code: 'MISSING_EXPORT_NAME' });
|
|
83
|
+
}
|
|
84
|
+
if (!validTypes.includes(exp.type)) {
|
|
85
|
+
errors.push({ field: 'exports.type', message: `Invalid export type: ${exp.type}`, code: 'INVALID_EXPORT_TYPE' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!contract.permissions || contract.permissions.length === 0) {
|
|
91
|
+
warnings.push('No permissions declared — plugin will have minimal access');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate a plugin contract manifest for marketplace publishing.
|
|
99
|
+
*/
|
|
100
|
+
export function generateContractManifest(contract: PluginContract): Record<string, unknown> {
|
|
101
|
+
return {
|
|
102
|
+
$schema: 'https://objectui.org/schemas/plugin-contract-v1.json',
|
|
103
|
+
name: contract.name,
|
|
104
|
+
version: contract.version,
|
|
105
|
+
peerDependencies: contract.peerDependencies ?? {},
|
|
106
|
+
exports: contract.exports.map(exp => ({
|
|
107
|
+
name: exp.name,
|
|
108
|
+
type: exp.type,
|
|
109
|
+
description: exp.description ?? '',
|
|
110
|
+
})),
|
|
111
|
+
permissions: contract.permissions ?? [],
|
|
112
|
+
api: contract.api ?? {},
|
|
113
|
+
generatedAt: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -268,14 +268,16 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
const resultObj = result as { value?: T[]; count?: number };
|
|
271
|
+
const resultObj = result as { records?: T[]; total?: number; value?: T[]; count?: number };
|
|
272
|
+
const records = resultObj.records || resultObj.value || [];
|
|
273
|
+
const total = resultObj.total ?? resultObj.count ?? records.length;
|
|
272
274
|
return {
|
|
273
|
-
data:
|
|
274
|
-
total
|
|
275
|
+
data: records,
|
|
276
|
+
total,
|
|
275
277
|
// Calculate page number safely
|
|
276
278
|
page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
|
|
277
279
|
pageSize: params?.$top,
|
|
278
|
-
hasMore: params?.$top ?
|
|
280
|
+
hasMore: params?.$top ? records.length === params.$top : false,
|
|
279
281
|
};
|
|
280
282
|
}
|
|
281
283
|
|
|
@@ -557,7 +559,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
557
559
|
try {
|
|
558
560
|
// Use cache with automatic fetching
|
|
559
561
|
const schema = await this.metadataCache.get(objectName, async () => {
|
|
560
|
-
const result: any = await this.client.meta.
|
|
562
|
+
const result: any = await this.client.meta.getItem('object', objectName);
|
|
561
563
|
|
|
562
564
|
// Unwrap 'item' property if present (common API response wrapper)
|
|
563
565
|
if (result && result.item) {
|
|
@@ -667,6 +669,56 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
667
669
|
}
|
|
668
670
|
}
|
|
669
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Get a page definition from ObjectStack.
|
|
674
|
+
* Uses the metadata API to fetch page layouts.
|
|
675
|
+
* Returns null if the server doesn't support page metadata.
|
|
676
|
+
*/
|
|
677
|
+
async getPage(pageId: string): Promise<unknown | null> {
|
|
678
|
+
await this.connect();
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
const cacheKey = `page:${pageId}`;
|
|
682
|
+
return await this.metadataCache.get(cacheKey, async () => {
|
|
683
|
+
const result: any = await this.client.meta.getItem('pages', pageId);
|
|
684
|
+
if (result && result.item) return result.item;
|
|
685
|
+
return result ?? null;
|
|
686
|
+
});
|
|
687
|
+
} catch {
|
|
688
|
+
// Server doesn't support page metadata — return null to fall back to static config
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get multiple metadata items from ObjectStack.
|
|
695
|
+
* Uses v3.0.0 metadata API pattern: getItems for batch retrieval.
|
|
696
|
+
*/
|
|
697
|
+
async getItems(category: string, names: string[]): Promise<unknown[]> {
|
|
698
|
+
await this.connect();
|
|
699
|
+
|
|
700
|
+
const results = await Promise.all(
|
|
701
|
+
names.map(async (name) => {
|
|
702
|
+
const cacheKey = `${category}:${name}`;
|
|
703
|
+
return this.metadataCache.get(cacheKey, async () => {
|
|
704
|
+
const result: any = await this.client.meta.getItem(category, name);
|
|
705
|
+
if (result && result.item) return result.item;
|
|
706
|
+
return result;
|
|
707
|
+
});
|
|
708
|
+
})
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
return results;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Get cached metadata if available, without triggering a fetch.
|
|
716
|
+
* Uses v3.0.0 metadata API pattern: getCached for synchronous cache access.
|
|
717
|
+
*/
|
|
718
|
+
getCached(key: string): unknown | undefined {
|
|
719
|
+
return this.metadataCache.getCachedSync(key);
|
|
720
|
+
}
|
|
721
|
+
|
|
670
722
|
/**
|
|
671
723
|
* Get cache statistics for monitoring performance.
|
|
672
724
|
*/
|
|
@@ -860,3 +912,19 @@ export {
|
|
|
860
912
|
|
|
861
913
|
// Export cache types
|
|
862
914
|
export type { CacheStats } from './cache/MetadataCache';
|
|
915
|
+
|
|
916
|
+
// v3.0.0 Deep Integration modules
|
|
917
|
+
export { CloudOperations } from './cloud';
|
|
918
|
+
export type { CloudDeploymentConfig, CloudHostingConfig, CloudMarketplaceEntry } from './cloud';
|
|
919
|
+
|
|
920
|
+
export { validatePluginContract, generateContractManifest } from './contracts';
|
|
921
|
+
export type { PluginContract, PluginExport, PluginAPIContract, ContractValidationResult, ContractValidationError } from './contracts';
|
|
922
|
+
|
|
923
|
+
export { IntegrationManager } from './integration';
|
|
924
|
+
export type { IntegrationConfig, IntegrationTrigger, IntegrationProvider, SlackIntegrationConfig, EmailIntegrationConfig, WebhookIntegrationConfig } from './integration';
|
|
925
|
+
|
|
926
|
+
export { SecurityManager } from './security';
|
|
927
|
+
export type { SecurityPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
|
|
928
|
+
|
|
929
|
+
export { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
|
|
930
|
+
export type { StudioCanvasConfig, StudioPropertyEditor, StudioThemeBuilderConfig, StudioColorPalette, StudioTypographyPreset, StudioShadowPreset } from './studio';
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Integration module for @objectstack/spec v3.0.0
|
|
11
|
+
* Provides third-party service connectors for Slack, email, webhooks.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
|
|
15
|
+
|
|
16
|
+
export interface IntegrationConfig {
|
|
17
|
+
/** Integration provider type */
|
|
18
|
+
provider: IntegrationProvider;
|
|
19
|
+
/** Whether this integration is enabled */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Provider-specific configuration */
|
|
22
|
+
config: Record<string, unknown>;
|
|
23
|
+
/** Event triggers */
|
|
24
|
+
triggers?: IntegrationTrigger[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IntegrationTrigger {
|
|
28
|
+
/** Event name (e.g. 'record.created', 'record.updated') */
|
|
29
|
+
event: string;
|
|
30
|
+
/** Filter condition */
|
|
31
|
+
filter?: string;
|
|
32
|
+
/** Template for the message/payload */
|
|
33
|
+
template?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SlackIntegrationConfig extends IntegrationConfig {
|
|
37
|
+
provider: 'slack';
|
|
38
|
+
config: {
|
|
39
|
+
webhookUrl: string;
|
|
40
|
+
channel?: string;
|
|
41
|
+
username?: string;
|
|
42
|
+
iconEmoji?: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EmailIntegrationConfig extends IntegrationConfig {
|
|
47
|
+
provider: 'email';
|
|
48
|
+
config: {
|
|
49
|
+
smtpHost: string;
|
|
50
|
+
smtpPort: number;
|
|
51
|
+
secure: boolean;
|
|
52
|
+
from: string;
|
|
53
|
+
to: string[];
|
|
54
|
+
subject?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WebhookIntegrationConfig extends IntegrationConfig {
|
|
59
|
+
provider: 'webhook';
|
|
60
|
+
config: {
|
|
61
|
+
url: string;
|
|
62
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH';
|
|
63
|
+
headers?: Record<string, string>;
|
|
64
|
+
retryCount?: number;
|
|
65
|
+
retryDelay?: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Integration service manager.
|
|
71
|
+
* Manages third-party service connections and event dispatching.
|
|
72
|
+
*/
|
|
73
|
+
export class IntegrationManager {
|
|
74
|
+
private integrations: Map<string, IntegrationConfig> = new Map();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Register a new integration.
|
|
78
|
+
*/
|
|
79
|
+
register(id: string, config: IntegrationConfig): void {
|
|
80
|
+
this.integrations.set(id, config);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove an integration.
|
|
85
|
+
*/
|
|
86
|
+
unregister(id: string): void {
|
|
87
|
+
this.integrations.delete(id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all registered integrations.
|
|
92
|
+
*/
|
|
93
|
+
getAll(): Map<string, IntegrationConfig> {
|
|
94
|
+
return new Map(this.integrations);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get integrations that match a specific event.
|
|
99
|
+
*/
|
|
100
|
+
getForEvent(event: string): IntegrationConfig[] {
|
|
101
|
+
return Array.from(this.integrations.values()).filter(
|
|
102
|
+
(integration) =>
|
|
103
|
+
integration.enabled &&
|
|
104
|
+
integration.triggers?.some((t) => t.event === event)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Dispatch an event to all matching integrations.
|
|
110
|
+
* Returns results for each integration.
|
|
111
|
+
*/
|
|
112
|
+
async dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{ id: string; success: boolean; error?: string }>> {
|
|
113
|
+
const matching = this.getForEvent(event);
|
|
114
|
+
const results: Array<{ id: string; success: boolean; error?: string }> = [];
|
|
115
|
+
|
|
116
|
+
for (const [id, integration] of this.integrations) {
|
|
117
|
+
if (!matching.includes(integration)) continue;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await this.send(integration, payload);
|
|
121
|
+
results.push({ id, success: true });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
results.push({ id, success: false, error: (err as Error).message });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Send payload to a specific integration.
|
|
132
|
+
*/
|
|
133
|
+
private async send(integration: IntegrationConfig, payload: Record<string, unknown>): Promise<void> {
|
|
134
|
+
switch (integration.provider) {
|
|
135
|
+
case 'webhook': {
|
|
136
|
+
const cfg = integration.config as WebhookIntegrationConfig['config'];
|
|
137
|
+
const url = cfg.url;
|
|
138
|
+
// Validate URL - only allow http and https protocols
|
|
139
|
+
try {
|
|
140
|
+
const parsed = new URL(url);
|
|
141
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
142
|
+
throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (e instanceof TypeError) {
|
|
146
|
+
throw new Error(`Invalid webhook URL: ${url}`);
|
|
147
|
+
}
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
await fetch(url, {
|
|
151
|
+
method: cfg.method,
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
...cfg.headers,
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(payload),
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'slack': {
|
|
161
|
+
const cfg = integration.config as SlackIntegrationConfig['config'];
|
|
162
|
+
const url = cfg.webhookUrl;
|
|
163
|
+
// Validate URL - only allow https protocol for Slack webhooks
|
|
164
|
+
try {
|
|
165
|
+
const parsed = new URL(url);
|
|
166
|
+
if (parsed.protocol !== 'https:') {
|
|
167
|
+
throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (e instanceof TypeError) {
|
|
171
|
+
throw new Error(`Invalid Slack webhook URL: ${url}`);
|
|
172
|
+
}
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
await fetch(url, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
channel: cfg.channel,
|
|
180
|
+
username: cfg.username,
|
|
181
|
+
icon_emoji: cfg.iconEmoji,
|
|
182
|
+
text: JSON.stringify(payload),
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
// Email and other providers would require server-side implementation
|
|
188
|
+
default:
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/security.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Security module integration for @objectstack/spec v3.0.0
|
|
11
|
+
* Provides advanced security policies: CSP config, audit logging, data masking.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface SecurityPolicy {
|
|
15
|
+
/** Content Security Policy configuration */
|
|
16
|
+
csp?: CSPConfig;
|
|
17
|
+
/** Audit logging configuration */
|
|
18
|
+
auditLog?: AuditLogConfig;
|
|
19
|
+
/** Data masking rules */
|
|
20
|
+
dataMasking?: DataMaskingConfig;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CSPConfig {
|
|
24
|
+
/** Script sources */
|
|
25
|
+
scriptSrc: string[];
|
|
26
|
+
/** Style sources */
|
|
27
|
+
styleSrc: string[];
|
|
28
|
+
/** Image sources */
|
|
29
|
+
imgSrc: string[];
|
|
30
|
+
/** Connect sources (APIs, WebSockets) */
|
|
31
|
+
connectSrc: string[];
|
|
32
|
+
/** Font sources */
|
|
33
|
+
fontSrc: string[];
|
|
34
|
+
/** Frame sources */
|
|
35
|
+
frameSrc?: string[];
|
|
36
|
+
/** Report URI for violations */
|
|
37
|
+
reportUri?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AuditLogConfig {
|
|
41
|
+
/** Whether audit logging is enabled */
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
/** Events to audit */
|
|
44
|
+
events: AuditEventType[];
|
|
45
|
+
/** Log retention days */
|
|
46
|
+
retentionDays?: number;
|
|
47
|
+
/** External log destination */
|
|
48
|
+
destination?: 'console' | 'server' | 'both';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AuditEventType =
|
|
52
|
+
| 'auth.login'
|
|
53
|
+
| 'auth.logout'
|
|
54
|
+
| 'auth.failed'
|
|
55
|
+
| 'data.create'
|
|
56
|
+
| 'data.read'
|
|
57
|
+
| 'data.update'
|
|
58
|
+
| 'data.delete'
|
|
59
|
+
| 'admin.config'
|
|
60
|
+
| 'admin.user'
|
|
61
|
+
| 'admin.role';
|
|
62
|
+
|
|
63
|
+
export interface DataMaskingConfig {
|
|
64
|
+
/** Fields to mask */
|
|
65
|
+
rules: DataMaskingRule[];
|
|
66
|
+
/** Default masking character */
|
|
67
|
+
maskChar?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DataMaskingRule {
|
|
71
|
+
/** Field name or pattern */
|
|
72
|
+
field: string;
|
|
73
|
+
/** Masking strategy */
|
|
74
|
+
strategy: 'full' | 'partial' | 'hash' | 'redact';
|
|
75
|
+
/** Number of visible characters (for partial masking) */
|
|
76
|
+
visibleChars?: number;
|
|
77
|
+
/** Roles that can see unmasked data */
|
|
78
|
+
exemptRoles?: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AuditLogEntry {
|
|
82
|
+
/** Event timestamp */
|
|
83
|
+
timestamp: string;
|
|
84
|
+
/** Event type */
|
|
85
|
+
event: AuditEventType;
|
|
86
|
+
/** User who triggered the event */
|
|
87
|
+
userId: string;
|
|
88
|
+
/** Target resource */
|
|
89
|
+
resource?: string;
|
|
90
|
+
/** Record ID */
|
|
91
|
+
recordId?: string;
|
|
92
|
+
/** Additional details */
|
|
93
|
+
details?: Record<string, unknown>;
|
|
94
|
+
/** IP address */
|
|
95
|
+
ipAddress?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Security policy manager.
|
|
100
|
+
* Handles CSP generation, audit logging, and data masking.
|
|
101
|
+
*/
|
|
102
|
+
export class SecurityManager {
|
|
103
|
+
private policy: SecurityPolicy;
|
|
104
|
+
private auditLog: AuditLogEntry[] = [];
|
|
105
|
+
|
|
106
|
+
constructor(policy: SecurityPolicy = {}) {
|
|
107
|
+
this.policy = policy;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate a CSP header string from the configuration.
|
|
112
|
+
*/
|
|
113
|
+
generateCSPHeader(): string {
|
|
114
|
+
const csp = this.policy.csp;
|
|
115
|
+
if (!csp) return '';
|
|
116
|
+
|
|
117
|
+
const directives: string[] = [];
|
|
118
|
+
|
|
119
|
+
if (csp.scriptSrc?.length) directives.push(`script-src ${csp.scriptSrc.join(' ')}`);
|
|
120
|
+
if (csp.styleSrc?.length) directives.push(`style-src ${csp.styleSrc.join(' ')}`);
|
|
121
|
+
if (csp.imgSrc?.length) directives.push(`img-src ${csp.imgSrc.join(' ')}`);
|
|
122
|
+
if (csp.connectSrc?.length) directives.push(`connect-src ${csp.connectSrc.join(' ')}`);
|
|
123
|
+
if (csp.fontSrc?.length) directives.push(`font-src ${csp.fontSrc.join(' ')}`);
|
|
124
|
+
if (csp.frameSrc?.length) directives.push(`frame-src ${csp.frameSrc.join(' ')}`);
|
|
125
|
+
if (csp.reportUri) directives.push(`report-uri ${csp.reportUri}`);
|
|
126
|
+
|
|
127
|
+
return directives.join('; ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Record an audit log entry.
|
|
132
|
+
*/
|
|
133
|
+
recordAudit(entry: Omit<AuditLogEntry, 'timestamp'>): void {
|
|
134
|
+
if (!this.policy.auditLog?.enabled) return;
|
|
135
|
+
if (this.policy.auditLog.events && !this.policy.auditLog.events.includes(entry.event)) return;
|
|
136
|
+
|
|
137
|
+
const fullEntry: AuditLogEntry = {
|
|
138
|
+
...entry,
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
this.auditLog.push(fullEntry);
|
|
143
|
+
|
|
144
|
+
const dest = this.policy.auditLog.destination ?? 'console';
|
|
145
|
+
if (dest === 'console' || dest === 'both') {
|
|
146
|
+
console.info('[AUDIT]', fullEntry.event, fullEntry.userId, fullEntry.resource ?? '');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get audit log entries.
|
|
152
|
+
*/
|
|
153
|
+
getAuditLog(filter?: { event?: AuditEventType; userId?: string; since?: string }): AuditLogEntry[] {
|
|
154
|
+
let entries = [...this.auditLog];
|
|
155
|
+
if (filter?.event) entries = entries.filter(e => e.event === filter.event);
|
|
156
|
+
if (filter?.userId) entries = entries.filter(e => e.userId === filter.userId);
|
|
157
|
+
if (filter?.since) entries = entries.filter(e => e.timestamp >= filter.since!);
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Apply data masking to a record.
|
|
163
|
+
*/
|
|
164
|
+
maskRecord(record: Record<string, unknown>, userRoles: string[] = []): Record<string, unknown> {
|
|
165
|
+
if (!this.policy.dataMasking?.rules?.length) return record;
|
|
166
|
+
|
|
167
|
+
const masked = { ...record };
|
|
168
|
+
const maskChar = this.policy.dataMasking.maskChar ?? '*';
|
|
169
|
+
|
|
170
|
+
for (const rule of this.policy.dataMasking.rules) {
|
|
171
|
+
if (!(rule.field in masked) || masked[rule.field] == null) continue;
|
|
172
|
+
|
|
173
|
+
// Check role exemptions
|
|
174
|
+
if (rule.exemptRoles?.some(role => userRoles.includes(role))) continue;
|
|
175
|
+
|
|
176
|
+
const value = String(masked[rule.field]);
|
|
177
|
+
|
|
178
|
+
switch (rule.strategy) {
|
|
179
|
+
case 'full':
|
|
180
|
+
masked[rule.field] = maskChar.repeat(value.length);
|
|
181
|
+
break;
|
|
182
|
+
case 'partial': {
|
|
183
|
+
const visible = rule.visibleChars ?? 4;
|
|
184
|
+
// Values shorter than visibleChars are fully masked for security
|
|
185
|
+
if (value.length <= visible) {
|
|
186
|
+
masked[rule.field] = maskChar.repeat(value.length);
|
|
187
|
+
} else {
|
|
188
|
+
masked[rule.field] = value.slice(0, visible) + maskChar.repeat(value.length - visible);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'hash':
|
|
193
|
+
masked[rule.field] = `[HASHED:${simpleHash(value)}]`;
|
|
194
|
+
break;
|
|
195
|
+
case 'redact':
|
|
196
|
+
masked[rule.field] = '[REDACTED]';
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return masked;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Update the security policy.
|
|
206
|
+
*/
|
|
207
|
+
updatePolicy(policy: Partial<SecurityPolicy>): void {
|
|
208
|
+
this.policy = { ...this.policy, ...policy };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get current security policy.
|
|
213
|
+
*/
|
|
214
|
+
getPolicy(): SecurityPolicy {
|
|
215
|
+
return { ...this.policy };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Simple hash function for data masking (not cryptographic).
|
|
221
|
+
*/
|
|
222
|
+
function simpleHash(str: string): string {
|
|
223
|
+
let hash = 0;
|
|
224
|
+
for (let i = 0; i < str.length; i++) {
|
|
225
|
+
const char = str.charCodeAt(i);
|
|
226
|
+
hash = ((hash << 5) - hash) + char;
|
|
227
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
228
|
+
}
|
|
229
|
+
return Math.abs(hash).toString(36);
|
|
230
|
+
}
|