@cratis/chronicle 6.2.0 → 6.3.1
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/confidentiality/EncryptedNotSupportedOnEventSourceId.ts +21 -0
- package/confidentiality/EncryptionScope.ts +34 -0
- package/confidentiality/PIIAndEncryptedCombinedNotSupported.ts +29 -0
- package/confidentiality/SecurityMetadata.ts +22 -0
- package/confidentiality/SecurityMetadataResolver.ts +54 -0
- package/confidentiality/SecurityMetadataType.ts +56 -0
- package/confidentiality/encrypted.spec.ts +105 -0
- package/confidentiality/encrypted.ts +143 -0
- package/confidentiality/index.ts +10 -0
- package/dist/confidentiality/EncryptedNotSupportedOnEventSourceId.d.ts +11 -0
- package/dist/confidentiality/EncryptedNotSupportedOnEventSourceId.d.ts.map +1 -0
- package/dist/confidentiality/EncryptedNotSupportedOnEventSourceId.js +19 -0
- package/dist/confidentiality/EncryptedNotSupportedOnEventSourceId.js.map +1 -0
- package/dist/confidentiality/EncryptionScope.d.ts +30 -0
- package/dist/confidentiality/EncryptionScope.d.ts.map +1 -0
- package/dist/confidentiality/EncryptionScope.js +33 -0
- package/dist/confidentiality/EncryptionScope.js.map +1 -0
- package/dist/confidentiality/PIIAndEncryptedCombinedNotSupported.d.ts +19 -0
- package/dist/confidentiality/PIIAndEncryptedCombinedNotSupported.d.ts.map +1 -0
- package/dist/confidentiality/PIIAndEncryptedCombinedNotSupported.js +27 -0
- package/dist/confidentiality/PIIAndEncryptedCombinedNotSupported.js.map +1 -0
- package/dist/confidentiality/SecurityMetadata.d.ts +18 -0
- package/dist/confidentiality/SecurityMetadata.d.ts.map +1 -0
- package/dist/confidentiality/SecurityMetadata.js +4 -0
- package/dist/confidentiality/SecurityMetadata.js.map +1 -0
- package/dist/confidentiality/SecurityMetadataResolver.d.ts +30 -0
- package/dist/confidentiality/SecurityMetadataResolver.d.ts.map +1 -0
- package/dist/confidentiality/SecurityMetadataResolver.js +48 -0
- package/dist/confidentiality/SecurityMetadataResolver.js.map +1 -0
- package/dist/confidentiality/SecurityMetadataType.d.ts +42 -0
- package/dist/confidentiality/SecurityMetadataType.d.ts.map +1 -0
- package/dist/confidentiality/SecurityMetadataType.js +50 -0
- package/dist/confidentiality/SecurityMetadataType.js.map +1 -0
- package/dist/confidentiality/encrypted.d.ts +72 -0
- package/dist/confidentiality/encrypted.d.ts.map +1 -0
- package/dist/confidentiality/encrypted.js +130 -0
- package/dist/confidentiality/encrypted.js.map +1 -0
- package/dist/confidentiality/index.d.ts +8 -0
- package/dist/confidentiality/index.d.ts.map +1 -0
- package/dist/confidentiality/index.js +9 -0
- package/dist/confidentiality/index.js.map +1 -0
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +6 -1
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/constraints/IUniqueConstraintBuilder.d.ts +2 -1
- package/dist/events/constraints/IUniqueConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueConstraintBuilder.d.ts +3 -0
- package/dist/events/constraints/UniqueConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/UniqueConstraintBuilder.js +7 -2
- package/dist/events/constraints/UniqueConstraintBuilder.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/schemas/JsonSchema.d.ts +18 -0
- package/dist/schemas/JsonSchema.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +41 -0
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +102 -9
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/events/constraints/Constraints.ts +6 -1
- package/events/constraints/IUniqueConstraintBuilder.ts +2 -1
- package/events/constraints/UniqueConstraintBuilder.ts +10 -2
- package/events/constraints/for_Constraints/when_registering_unique_property_constraints.spec.ts +137 -0
- package/index.ts +3 -0
- package/package.json +7 -2
- package/schemas/JsonSchema.ts +20 -0
- package/schemas/JsonSchemaGenerator.confidentiality.spec.ts +194 -0
- package/schemas/JsonSchemaGenerator.ts +115 -10
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when the {@link encrypted} decorator is applied to the `eventSourceId` property.
|
|
6
|
+
*/
|
|
7
|
+
export class EncryptedNotSupportedOnEventSourceId extends Error {
|
|
8
|
+
/**
|
|
9
|
+
* Initializes a new instance of the {@link EncryptedNotSupportedOnEventSourceId} class.
|
|
10
|
+
* @param typeName - The name of the type the decorator was applied to.
|
|
11
|
+
*/
|
|
12
|
+
constructor(typeName: string) {
|
|
13
|
+
super(
|
|
14
|
+
`The @encrypted() decorator cannot be applied to 'eventSourceId' on '${typeName}' because it is the ` +
|
|
15
|
+
'event source identifier. Event source identifiers cannot be encrypted as they are required for event ' +
|
|
16
|
+
'correlation. If the identifier itself is sensitive, use a non-sensitive surrogate value as the event ' +
|
|
17
|
+
'source identifier and store the sensitive value in a separate property marked with @encrypted().'
|
|
18
|
+
);
|
|
19
|
+
this.name = 'EncryptedNotSupportedOnEventSourceId';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines the identity an `@encrypted()` value's encryption key is provisioned under.
|
|
6
|
+
*
|
|
7
|
+
* `@pii()` never gains any scope beyond {@link EncryptionScope.Subject} - GDPR compliance is always
|
|
8
|
+
* resolved against a compliance identity, and widening that would make the compliance subject
|
|
9
|
+
* boundary itself scope-dependent. `EncryptionScope` exists precisely because `@encrypted()` values
|
|
10
|
+
* have no data subject and no erasure obligation, so a wider key boundary is safe for them in a way
|
|
11
|
+
* it is not for PII.
|
|
12
|
+
*/
|
|
13
|
+
export enum EncryptionScope {
|
|
14
|
+
/**
|
|
15
|
+
* The key is provisioned per compliance identity (the same identity - subject or, when none is
|
|
16
|
+
* set, event source id - PII values on the same document resolve to), and is looked up the same
|
|
17
|
+
* way. This is the default.
|
|
18
|
+
*/
|
|
19
|
+
Subject = 0,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The key is provisioned once per event store namespace, independent of any document's subject
|
|
23
|
+
* or compliance identity. Every `@encrypted(EncryptionScope.Namespace)` value in a namespace
|
|
24
|
+
* shares one key.
|
|
25
|
+
*/
|
|
26
|
+
Namespace = 1,
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The key is provisioned once for the whole Chronicle installation, independent of event store,
|
|
30
|
+
* namespace, or compliance identity. Every `@encrypted(EncryptionScope.Global)` value across
|
|
31
|
+
* every event store and namespace shares one key.
|
|
32
|
+
*/
|
|
33
|
+
Global = 2
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when a property, or the type it resolves metadata from, carries both `@pii()` and
|
|
6
|
+
* `@encrypted()`.
|
|
7
|
+
*
|
|
8
|
+
* This is not merely redundant - it corrupts the value. The kernel applies every matching handler
|
|
9
|
+
* for a property in sequence, so a value marked both ways is encrypted first under the PII key and
|
|
10
|
+
* then again under the Encrypted key; releasing it decrypts with the wrong key against ciphertext,
|
|
11
|
+
* which fails loudly (a padding/authentication error) rather than returning a wrong value. A value
|
|
12
|
+
* needs exactly one protection: `@pii()` when it is personal data with a lawful basis for erasure,
|
|
13
|
+
* `@encrypted()` when it is an operational secret with none.
|
|
14
|
+
*/
|
|
15
|
+
export class PIIAndEncryptedCombinedNotSupported extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* Initializes a new instance of the {@link PIIAndEncryptedCombinedNotSupported} class.
|
|
18
|
+
* @param property - The name of the property (or type) carrying both decorators.
|
|
19
|
+
*/
|
|
20
|
+
constructor(property: string) {
|
|
21
|
+
super(
|
|
22
|
+
`'${property}' carries both @pii() and @encrypted(). A value needs exactly one protection - combining ` +
|
|
23
|
+
'them would encrypt it twice, under two different keys, and it cannot be released correctly. Choose ' +
|
|
24
|
+
'@pii() for personal data with a lawful basis for erasure, or @encrypted() for an operational secret ' +
|
|
25
|
+
'with none.'
|
|
26
|
+
);
|
|
27
|
+
this.name = 'PIIAndEncryptedCombinedNotSupported';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { SecurityMetadataType } from './SecurityMetadataType';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents metadata related to security.
|
|
8
|
+
*
|
|
9
|
+
* This is the security counterpart to {@link ComplianceMetadata} - a deliberately separate type
|
|
10
|
+
* rather than a shared one. See {@link SecurityMetadataType} for why.
|
|
11
|
+
*/
|
|
12
|
+
export interface SecurityMetadata {
|
|
13
|
+
/**
|
|
14
|
+
* The type of security metadata.
|
|
15
|
+
*/
|
|
16
|
+
readonly metadataType: SecurityMetadataType;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Any additional details - can be empty.
|
|
20
|
+
*/
|
|
21
|
+
readonly details: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { SecurityMetadata } from './SecurityMetadata';
|
|
5
|
+
import { getEncryptedMetadata, getTypeEncryptedMetadata } from './encrypted';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves security metadata for properties and types.
|
|
9
|
+
*
|
|
10
|
+
* This is the security counterpart to {@link ComplianceMetadataResolver} - a deliberately separate
|
|
11
|
+
* type rather than a shared one. See {@link SecurityMetadataType} for why.
|
|
12
|
+
*/
|
|
13
|
+
export class SecurityMetadataResolver {
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a property has security metadata.
|
|
16
|
+
* @param target - The class prototype.
|
|
17
|
+
* @param propertyKey - The property name.
|
|
18
|
+
* @returns True if the property has security metadata; false otherwise.
|
|
19
|
+
*/
|
|
20
|
+
static hasMetadataFor(target: object, propertyKey: string): boolean {
|
|
21
|
+
return getEncryptedMetadata(target, propertyKey) !== undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets all security metadata for a property.
|
|
26
|
+
* @param target - The class prototype.
|
|
27
|
+
* @param propertyKey - The property name.
|
|
28
|
+
* @returns Array of security metadata.
|
|
29
|
+
*/
|
|
30
|
+
static getMetadataFor(target: object, propertyKey: string): SecurityMetadata[] {
|
|
31
|
+
const metadata: SecurityMetadata[] = [];
|
|
32
|
+
|
|
33
|
+
const encryptedMetadata = getEncryptedMetadata(target, propertyKey);
|
|
34
|
+
if (encryptedMetadata) {
|
|
35
|
+
metadata.push(encryptedMetadata);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return metadata;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets security metadata for a type (e.g., ConceptAs types).
|
|
43
|
+
* @param type - The type constructor.
|
|
44
|
+
* @returns Array of security metadata.
|
|
45
|
+
*/
|
|
46
|
+
static getMetadataForType(type: Function): SecurityMetadata[] {
|
|
47
|
+
const metadata: SecurityMetadata[] = [];
|
|
48
|
+
const encryptedMetadata = getTypeEncryptedMetadata(type);
|
|
49
|
+
if (encryptedMetadata) {
|
|
50
|
+
metadata.push(encryptedMetadata);
|
|
51
|
+
}
|
|
52
|
+
return metadata;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a type of security metadata.
|
|
6
|
+
*
|
|
7
|
+
* This is the security counterpart to {@link ComplianceMetadataType} - a deliberately separate type
|
|
8
|
+
* rather than a shared one. A `@pii()` value and an `@encrypted()` value are never the same kind of
|
|
9
|
+
* thing: one is personal data with a GDPR erasure obligation, the other is an operational secret
|
|
10
|
+
* with none. Keeping the two vocabularies apart keeps that distinction visible at every call site
|
|
11
|
+
* that reads a schema's metadata.
|
|
12
|
+
*/
|
|
13
|
+
export class SecurityMetadataType {
|
|
14
|
+
/**
|
|
15
|
+
* The key is provisioned per compliance identity - the same identity, resolved the same way, a
|
|
16
|
+
* `@pii()` value on the same document would use. This is what `@encrypted()` (with no explicit
|
|
17
|
+
* scope, or `EncryptionScope.Subject`) resolves to.
|
|
18
|
+
*/
|
|
19
|
+
static readonly EncryptedSubject = new SecurityMetadataType('EncryptedSubject');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The key is provisioned once per event store namespace. This is what
|
|
23
|
+
* `@encrypted(EncryptionScope.Namespace)` resolves to.
|
|
24
|
+
*/
|
|
25
|
+
static readonly EncryptedNamespace = new SecurityMetadataType('EncryptedNamespace');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The key is provisioned once for the whole Chronicle installation. This is what
|
|
29
|
+
* `@encrypted(EncryptionScope.Global)` resolves to.
|
|
30
|
+
*/
|
|
31
|
+
static readonly EncryptedGlobal = new SecurityMetadataType('EncryptedGlobal');
|
|
32
|
+
|
|
33
|
+
private readonly _value: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initializes a new instance of SecurityMetadataType.
|
|
37
|
+
* @param value - The string value of the security metadata type.
|
|
38
|
+
*/
|
|
39
|
+
constructor(value: string) {
|
|
40
|
+
this._value = value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the underlying string value.
|
|
45
|
+
*/
|
|
46
|
+
get value(): string {
|
|
47
|
+
return this._value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns the string representation of the security metadata type.
|
|
52
|
+
*/
|
|
53
|
+
toString(): string {
|
|
54
|
+
return this._value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { encrypted, getEncryptedMetadata, getTypeEncryptedMetadata, hasEncryptedMetadata, isEncrypted } from './encrypted';
|
|
7
|
+
import { EncryptionScope } from './EncryptionScope';
|
|
8
|
+
import { EncryptedNotSupportedOnEventSourceId } from './EncryptedNotSupportedOnEventSourceId';
|
|
9
|
+
|
|
10
|
+
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
11
|
+
// fixtures don't depend on the test runner's decorator-syntax support.
|
|
12
|
+
|
|
13
|
+
describe('encrypted', () => {
|
|
14
|
+
describe('when applied to the eventSourceId property', () => {
|
|
15
|
+
class SomeEvent {
|
|
16
|
+
eventSourceId = '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it('should throw EncryptedNotSupportedOnEventSourceId', () => {
|
|
20
|
+
expect(() => encrypted()(SomeEvent.prototype, 'eventSourceId')).toThrow(EncryptedNotSupportedOnEventSourceId);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should describe why the property cannot be encrypted', () => {
|
|
24
|
+
expect(() => encrypted()(SomeEvent.prototype, 'eventSourceId')).toThrow(/event source identifier/);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('when applied to any other property with no explicit scope', () => {
|
|
29
|
+
class SomeEvent {
|
|
30
|
+
eventSourceId = '';
|
|
31
|
+
apiKey = '';
|
|
32
|
+
}
|
|
33
|
+
encrypted()(SomeEvent.prototype, 'apiKey');
|
|
34
|
+
|
|
35
|
+
it('should not throw', () => {
|
|
36
|
+
expect(() => encrypted()(SomeEvent.prototype, 'apiKey')).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should default the scope to Subject', () => {
|
|
40
|
+
expect(getEncryptedMetadata(SomeEvent.prototype, 'apiKey')!.metadataType.value).toBe('EncryptedSubject');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should default details to an empty string', () => {
|
|
44
|
+
expect(getEncryptedMetadata(SomeEvent.prototype, 'apiKey')!.details).toBe('');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should report the property as having encrypted metadata', () => {
|
|
48
|
+
expect(hasEncryptedMetadata(SomeEvent.prototype, 'apiKey')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('when applied with EncryptionScope.Namespace and details', () => {
|
|
53
|
+
class Configuration {
|
|
54
|
+
webhookSecret = '';
|
|
55
|
+
}
|
|
56
|
+
encrypted(EncryptionScope.Namespace, 'Shared per namespace')(Configuration.prototype, 'webhookSecret');
|
|
57
|
+
|
|
58
|
+
it('should resolve to EncryptedNamespace', () => {
|
|
59
|
+
expect(getEncryptedMetadata(Configuration.prototype, 'webhookSecret')!.metadataType.value).toBe('EncryptedNamespace');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should carry the details', () => {
|
|
63
|
+
expect(getEncryptedMetadata(Configuration.prototype, 'webhookSecret')!.details).toBe('Shared per namespace');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('when applied with EncryptionScope.Global', () => {
|
|
68
|
+
class License {
|
|
69
|
+
token = '';
|
|
70
|
+
}
|
|
71
|
+
encrypted(EncryptionScope.Global)(License.prototype, 'token');
|
|
72
|
+
|
|
73
|
+
it('should resolve to EncryptedGlobal', () => {
|
|
74
|
+
expect(getEncryptedMetadata(License.prototype, 'token')!.metadataType.value).toBe('EncryptedGlobal');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('when applied at the class level', () => {
|
|
79
|
+
class PartnerCredentials {
|
|
80
|
+
apiKey = '';
|
|
81
|
+
}
|
|
82
|
+
encrypted(EncryptionScope.Subject, 'Every field is a secret')(PartnerCredentials);
|
|
83
|
+
|
|
84
|
+
it('should report the type as encrypted', () => {
|
|
85
|
+
expect(isEncrypted(PartnerCredentials)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should carry the scope and details on the type metadata', () => {
|
|
89
|
+
const metadata = getTypeEncryptedMetadata(PartnerCredentials)!;
|
|
90
|
+
expect(metadata.metadataType.value).toBe('EncryptedSubject');
|
|
91
|
+
expect(metadata.details).toBe('Every field is a secret');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('when a property has not been decorated', () => {
|
|
96
|
+
class Plain {
|
|
97
|
+
value = '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it('should report no encrypted metadata', () => {
|
|
101
|
+
expect(hasEncryptedMetadata(Plain.prototype, 'value')).toBe(false);
|
|
102
|
+
expect(getEncryptedMetadata(Plain.prototype, 'value')).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import type { SecurityMetadata } from './SecurityMetadata';
|
|
6
|
+
import { SecurityMetadataType } from './SecurityMetadataType';
|
|
7
|
+
import { EncryptionScope } from './EncryptionScope';
|
|
8
|
+
import { EncryptedNotSupportedOnEventSourceId } from './EncryptedNotSupportedOnEventSourceId';
|
|
9
|
+
import { TypeIntrospector } from '../types';
|
|
10
|
+
|
|
11
|
+
/** The property name this client uses everywhere for the event source identifier. */
|
|
12
|
+
const EVENT_SOURCE_ID_PROPERTY = 'eventSourceId';
|
|
13
|
+
|
|
14
|
+
/** Metadata key for the encrypted decorator on properties. */
|
|
15
|
+
const ENCRYPTED_PROPERTY_METADATA_KEY = 'chronicle:security:encrypted:property';
|
|
16
|
+
|
|
17
|
+
/** Metadata key for the encrypted decorator on types. */
|
|
18
|
+
const ENCRYPTED_TYPE_METADATA_KEY = 'chronicle:security:encrypted:type';
|
|
19
|
+
|
|
20
|
+
function metadataTypeFor(scope: EncryptionScope): SecurityMetadataType {
|
|
21
|
+
switch (scope) {
|
|
22
|
+
case EncryptionScope.Subject:
|
|
23
|
+
return SecurityMetadataType.EncryptedSubject;
|
|
24
|
+
case EncryptionScope.Namespace:
|
|
25
|
+
return SecurityMetadataType.EncryptedNamespace;
|
|
26
|
+
case EncryptionScope.Global:
|
|
27
|
+
return SecurityMetadataType.EncryptedGlobal;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decorator that marks a property or type as needing plain-confidentiality encryption at rest - a
|
|
33
|
+
* security measure, not a compliance one.
|
|
34
|
+
*
|
|
35
|
+
* Use `@encrypted()` for an operational secret that has no data subject and no lawful basis for
|
|
36
|
+
* erasure - an API key, a webhook signing secret, a partner credential. Use `@pii()` instead when
|
|
37
|
+
* the value is personal data about a natural person: only `@pii()` encrypts a value *and* enrolls it
|
|
38
|
+
* in GDPR right-to-erasure. Marking a secret `@pii()` would make it erasable on a request that was
|
|
39
|
+
* never about it; marking personal data `@encrypted()` would encrypt it but never erase it. The two
|
|
40
|
+
* are not interchangeable, and this decorator's key is never provisioned under the same identity a
|
|
41
|
+
* `@pii()` value for the same subject uses.
|
|
42
|
+
*
|
|
43
|
+
* All three {@link EncryptionScope} members are honored by the kernel: `EncryptionScope.Subject`
|
|
44
|
+
* (the default) provisions a key per compliance identity, exactly matching how a `@pii()` value on
|
|
45
|
+
* the same document is resolved; `EncryptionScope.Namespace` provisions one key shared by every
|
|
46
|
+
* value marked with it in a given event store namespace; `EncryptionScope.Global` provisions one key
|
|
47
|
+
* shared across the whole installation. Choosing a wider scope is safe for `@encrypted()` in a way
|
|
48
|
+
* it is not for `@pii()`, precisely because an operational secret has no data subject whose erasure
|
|
49
|
+
* request the wider key could ever need to honor separately from another subject's.
|
|
50
|
+
*
|
|
51
|
+
* This decorator can be used in two ways:
|
|
52
|
+
* - As a property decorator: `@encrypted() propertyName: type`
|
|
53
|
+
* - As a class decorator: `@encrypted() class TypeName { }`
|
|
54
|
+
*
|
|
55
|
+
* @param scope - The {@link EncryptionScope} the key is provisioned under - defaults to `EncryptionScope.Subject`.
|
|
56
|
+
* @param details - Optional details explaining why or to what purpose/extent the property/type needs encryption.
|
|
57
|
+
* @returns A decorator function that can be applied to either properties or classes (but not both on the same target).
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* Property usage:
|
|
61
|
+
* ```typescript
|
|
62
|
+
* @readModel()
|
|
63
|
+
* class PartnerIntegration {
|
|
64
|
+
* @encrypted()
|
|
65
|
+
* apiKey: string = '';
|
|
66
|
+
*
|
|
67
|
+
* @encrypted(EncryptionScope.Namespace)
|
|
68
|
+
* webhookSigningSecret: string = '';
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function encrypted(scope: EncryptionScope = EncryptionScope.Subject, details?: string): PropertyDecorator & ClassDecorator {
|
|
73
|
+
return (target: object | Function, propertyKey?: string | symbol) => {
|
|
74
|
+
// Class decorator usage (for types like ConceptAs)
|
|
75
|
+
if (typeof target === 'function' && propertyKey === undefined) {
|
|
76
|
+
const metadata: SecurityMetadata = {
|
|
77
|
+
metadataType: metadataTypeFor(scope),
|
|
78
|
+
details: details ?? ''
|
|
79
|
+
};
|
|
80
|
+
Reflect.defineMetadata(ENCRYPTED_TYPE_METADATA_KEY, metadata, target);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Property decorator usage
|
|
85
|
+
if (propertyKey !== undefined) {
|
|
86
|
+
const key = propertyKey.toString();
|
|
87
|
+
const declaringType = (target as { constructor: Function }).constructor;
|
|
88
|
+
|
|
89
|
+
// Encrypting the event source identifier would make its own decryption key
|
|
90
|
+
// unfindable - the identifier is required, in the clear, to correlate events and
|
|
91
|
+
// look up the key that protects everything else. Mirrors @pii()'s
|
|
92
|
+
// PIINotSupportedOnEventSourceId guard, for the same reason.
|
|
93
|
+
if (key === EVENT_SOURCE_ID_PROPERTY) {
|
|
94
|
+
throw new EncryptedNotSupportedOnEventSourceId(declaringType.name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
TypeIntrospector.trackProperty(declaringType, key);
|
|
98
|
+
const metadata: SecurityMetadata = {
|
|
99
|
+
metadataType: metadataTypeFor(scope),
|
|
100
|
+
details: details ?? ''
|
|
101
|
+
};
|
|
102
|
+
Reflect.defineMetadata(ENCRYPTED_PROPERTY_METADATA_KEY, metadata, target, key);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets the encrypted security metadata for a property.
|
|
109
|
+
* @param target - The class prototype.
|
|
110
|
+
* @param propertyKey - The property name.
|
|
111
|
+
* @returns The security metadata, or undefined if not decorated with @encrypted.
|
|
112
|
+
*/
|
|
113
|
+
export function getEncryptedMetadata(target: object, propertyKey: string): SecurityMetadata | undefined {
|
|
114
|
+
return Reflect.getMetadata(ENCRYPTED_PROPERTY_METADATA_KEY, target, propertyKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Checks whether a property has been decorated with @encrypted.
|
|
119
|
+
* @param target - The class prototype.
|
|
120
|
+
* @param propertyKey - The property name.
|
|
121
|
+
* @returns True if the property has @encrypted decorator; false otherwise.
|
|
122
|
+
*/
|
|
123
|
+
export function hasEncryptedMetadata(target: object, propertyKey: string): boolean {
|
|
124
|
+
return Reflect.hasMetadata(ENCRYPTED_PROPERTY_METADATA_KEY, target, propertyKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Gets the encrypted security metadata for a type.
|
|
129
|
+
* @param type - The type constructor.
|
|
130
|
+
* @returns The security metadata, or undefined if not decorated with @encrypted.
|
|
131
|
+
*/
|
|
132
|
+
export function getTypeEncryptedMetadata(type: Function): SecurityMetadata | undefined {
|
|
133
|
+
return Reflect.getMetadata(ENCRYPTED_TYPE_METADATA_KEY, type);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Checks whether a type has been decorated with @encrypted.
|
|
138
|
+
* @param type - The type constructor.
|
|
139
|
+
* @returns True if the type has @encrypted decorator; false otherwise.
|
|
140
|
+
*/
|
|
141
|
+
export function isEncrypted(type: Function): boolean {
|
|
142
|
+
return Reflect.hasMetadata(ENCRYPTED_TYPE_METADATA_KEY, type);
|
|
143
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
export { SecurityMetadataType } from './SecurityMetadataType';
|
|
5
|
+
export type { SecurityMetadata } from './SecurityMetadata';
|
|
6
|
+
export { EncryptionScope } from './EncryptionScope';
|
|
7
|
+
export { encrypted, getEncryptedMetadata, hasEncryptedMetadata, getTypeEncryptedMetadata, isEncrypted } from './encrypted';
|
|
8
|
+
export { EncryptedNotSupportedOnEventSourceId } from './EncryptedNotSupportedOnEventSourceId';
|
|
9
|
+
export { PIIAndEncryptedCombinedNotSupported } from './PIIAndEncryptedCombinedNotSupported';
|
|
10
|
+
export { SecurityMetadataResolver } from './SecurityMetadataResolver';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when the {@link encrypted} decorator is applied to the `eventSourceId` property.
|
|
3
|
+
*/
|
|
4
|
+
export declare class EncryptedNotSupportedOnEventSourceId extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* Initializes a new instance of the {@link EncryptedNotSupportedOnEventSourceId} class.
|
|
7
|
+
* @param typeName - The name of the type the decorator was applied to.
|
|
8
|
+
*/
|
|
9
|
+
constructor(typeName: string);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=EncryptedNotSupportedOnEventSourceId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptedNotSupportedOnEventSourceId.d.ts","sourceRoot":"","sources":["../../confidentiality/EncryptedNotSupportedOnEventSourceId.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,qBAAa,oCAAqC,SAAQ,KAAK;IAC3D;;;OAGG;gBACS,QAAQ,EAAE,MAAM;CAS/B"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when the {@link encrypted} decorator is applied to the `eventSourceId` property.
|
|
5
|
+
*/
|
|
6
|
+
export class EncryptedNotSupportedOnEventSourceId extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* Initializes a new instance of the {@link EncryptedNotSupportedOnEventSourceId} class.
|
|
9
|
+
* @param typeName - The name of the type the decorator was applied to.
|
|
10
|
+
*/
|
|
11
|
+
constructor(typeName) {
|
|
12
|
+
super(`The @encrypted() decorator cannot be applied to 'eventSourceId' on '${typeName}' because it is the ` +
|
|
13
|
+
'event source identifier. Event source identifiers cannot be encrypted as they are required for event ' +
|
|
14
|
+
'correlation. If the identifier itself is sensitive, use a non-sensitive surrogate value as the event ' +
|
|
15
|
+
'source identifier and store the sensitive value in a separate property marked with @encrypted().');
|
|
16
|
+
this.name = 'EncryptedNotSupportedOnEventSourceId';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=EncryptedNotSupportedOnEventSourceId.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptedNotSupportedOnEventSourceId.js","sourceRoot":"","sources":["../../confidentiality/EncryptedNotSupportedOnEventSourceId.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG;;GAEG;AACH,MAAM,OAAO,oCAAqC,SAAQ,KAAK;IAC3D;;;OAGG;IACH,YAAY,QAAgB;QACxB,KAAK,CACD,uEAAuE,QAAQ,sBAAsB;YACrG,uGAAuG;YACvG,uGAAuG;YACvG,kGAAkG,CACrG,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sCAAsC,CAAC;IACvD,CAAC;CACJ"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the identity an `@encrypted()` value's encryption key is provisioned under.
|
|
3
|
+
*
|
|
4
|
+
* `@pii()` never gains any scope beyond {@link EncryptionScope.Subject} - GDPR compliance is always
|
|
5
|
+
* resolved against a compliance identity, and widening that would make the compliance subject
|
|
6
|
+
* boundary itself scope-dependent. `EncryptionScope` exists precisely because `@encrypted()` values
|
|
7
|
+
* have no data subject and no erasure obligation, so a wider key boundary is safe for them in a way
|
|
8
|
+
* it is not for PII.
|
|
9
|
+
*/
|
|
10
|
+
export declare enum EncryptionScope {
|
|
11
|
+
/**
|
|
12
|
+
* The key is provisioned per compliance identity (the same identity - subject or, when none is
|
|
13
|
+
* set, event source id - PII values on the same document resolve to), and is looked up the same
|
|
14
|
+
* way. This is the default.
|
|
15
|
+
*/
|
|
16
|
+
Subject = 0,
|
|
17
|
+
/**
|
|
18
|
+
* The key is provisioned once per event store namespace, independent of any document's subject
|
|
19
|
+
* or compliance identity. Every `@encrypted(EncryptionScope.Namespace)` value in a namespace
|
|
20
|
+
* shares one key.
|
|
21
|
+
*/
|
|
22
|
+
Namespace = 1,
|
|
23
|
+
/**
|
|
24
|
+
* The key is provisioned once for the whole Chronicle installation, independent of event store,
|
|
25
|
+
* namespace, or compliance identity. Every `@encrypted(EncryptionScope.Global)` value across
|
|
26
|
+
* every event store and namespace shares one key.
|
|
27
|
+
*/
|
|
28
|
+
Global = 2
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=EncryptionScope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptionScope.d.ts","sourceRoot":"","sources":["../../confidentiality/EncryptionScope.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,oBAAY,eAAe;IACvB;;;;OAIG;IACH,OAAO,IAAI;IAEX;;;;OAIG;IACH,SAAS,IAAI;IAEb;;;;OAIG;IACH,MAAM,IAAI;CACb"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
/**
|
|
4
|
+
* Defines the identity an `@encrypted()` value's encryption key is provisioned under.
|
|
5
|
+
*
|
|
6
|
+
* `@pii()` never gains any scope beyond {@link EncryptionScope.Subject} - GDPR compliance is always
|
|
7
|
+
* resolved against a compliance identity, and widening that would make the compliance subject
|
|
8
|
+
* boundary itself scope-dependent. `EncryptionScope` exists precisely because `@encrypted()` values
|
|
9
|
+
* have no data subject and no erasure obligation, so a wider key boundary is safe for them in a way
|
|
10
|
+
* it is not for PII.
|
|
11
|
+
*/
|
|
12
|
+
export var EncryptionScope;
|
|
13
|
+
(function (EncryptionScope) {
|
|
14
|
+
/**
|
|
15
|
+
* The key is provisioned per compliance identity (the same identity - subject or, when none is
|
|
16
|
+
* set, event source id - PII values on the same document resolve to), and is looked up the same
|
|
17
|
+
* way. This is the default.
|
|
18
|
+
*/
|
|
19
|
+
EncryptionScope[EncryptionScope["Subject"] = 0] = "Subject";
|
|
20
|
+
/**
|
|
21
|
+
* The key is provisioned once per event store namespace, independent of any document's subject
|
|
22
|
+
* or compliance identity. Every `@encrypted(EncryptionScope.Namespace)` value in a namespace
|
|
23
|
+
* shares one key.
|
|
24
|
+
*/
|
|
25
|
+
EncryptionScope[EncryptionScope["Namespace"] = 1] = "Namespace";
|
|
26
|
+
/**
|
|
27
|
+
* The key is provisioned once for the whole Chronicle installation, independent of event store,
|
|
28
|
+
* namespace, or compliance identity. Every `@encrypted(EncryptionScope.Global)` value across
|
|
29
|
+
* every event store and namespace shares one key.
|
|
30
|
+
*/
|
|
31
|
+
EncryptionScope[EncryptionScope["Global"] = 2] = "Global";
|
|
32
|
+
})(EncryptionScope || (EncryptionScope = {}));
|
|
33
|
+
//# sourceMappingURL=EncryptionScope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptionScope.js","sourceRoot":"","sources":["../../confidentiality/EncryptionScope.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG;;;;;;;;GAQG;AACH,MAAM,CAAN,IAAY,eAqBX;AArBD,WAAY,eAAe;IACvB;;;;OAIG;IACH,2DAAW,CAAA;IAEX;;;;OAIG;IACH,+DAAa,CAAA;IAEb;;;;OAIG;IACH,yDAAU,CAAA;AACd,CAAC,EArBW,eAAe,KAAf,eAAe,QAqB1B"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a property, or the type it resolves metadata from, carries both `@pii()` and
|
|
3
|
+
* `@encrypted()`.
|
|
4
|
+
*
|
|
5
|
+
* This is not merely redundant - it corrupts the value. The kernel applies every matching handler
|
|
6
|
+
* for a property in sequence, so a value marked both ways is encrypted first under the PII key and
|
|
7
|
+
* then again under the Encrypted key; releasing it decrypts with the wrong key against ciphertext,
|
|
8
|
+
* which fails loudly (a padding/authentication error) rather than returning a wrong value. A value
|
|
9
|
+
* needs exactly one protection: `@pii()` when it is personal data with a lawful basis for erasure,
|
|
10
|
+
* `@encrypted()` when it is an operational secret with none.
|
|
11
|
+
*/
|
|
12
|
+
export declare class PIIAndEncryptedCombinedNotSupported extends Error {
|
|
13
|
+
/**
|
|
14
|
+
* Initializes a new instance of the {@link PIIAndEncryptedCombinedNotSupported} class.
|
|
15
|
+
* @param property - The name of the property (or type) carrying both decorators.
|
|
16
|
+
*/
|
|
17
|
+
constructor(property: string);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=PIIAndEncryptedCombinedNotSupported.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PIIAndEncryptedCombinedNotSupported.d.ts","sourceRoot":"","sources":["../../confidentiality/PIIAndEncryptedCombinedNotSupported.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,qBAAa,mCAAoC,SAAQ,KAAK;IAC1D;;;OAGG;gBACS,QAAQ,EAAE,MAAM;CAS/B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when a property, or the type it resolves metadata from, carries both `@pii()` and
|
|
5
|
+
* `@encrypted()`.
|
|
6
|
+
*
|
|
7
|
+
* This is not merely redundant - it corrupts the value. The kernel applies every matching handler
|
|
8
|
+
* for a property in sequence, so a value marked both ways is encrypted first under the PII key and
|
|
9
|
+
* then again under the Encrypted key; releasing it decrypts with the wrong key against ciphertext,
|
|
10
|
+
* which fails loudly (a padding/authentication error) rather than returning a wrong value. A value
|
|
11
|
+
* needs exactly one protection: `@pii()` when it is personal data with a lawful basis for erasure,
|
|
12
|
+
* `@encrypted()` when it is an operational secret with none.
|
|
13
|
+
*/
|
|
14
|
+
export class PIIAndEncryptedCombinedNotSupported extends Error {
|
|
15
|
+
/**
|
|
16
|
+
* Initializes a new instance of the {@link PIIAndEncryptedCombinedNotSupported} class.
|
|
17
|
+
* @param property - The name of the property (or type) carrying both decorators.
|
|
18
|
+
*/
|
|
19
|
+
constructor(property) {
|
|
20
|
+
super(`'${property}' carries both @pii() and @encrypted(). A value needs exactly one protection - combining ` +
|
|
21
|
+
'them would encrypt it twice, under two different keys, and it cannot be released correctly. Choose ' +
|
|
22
|
+
'@pii() for personal data with a lawful basis for erasure, or @encrypted() for an operational secret ' +
|
|
23
|
+
'with none.');
|
|
24
|
+
this.name = 'PIIAndEncryptedCombinedNotSupported';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=PIIAndEncryptedCombinedNotSupported.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PIIAndEncryptedCombinedNotSupported.js","sourceRoot":"","sources":["../../confidentiality/PIIAndEncryptedCombinedNotSupported.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG;;;;;;;;;;GAUG;AACH,MAAM,OAAO,mCAAoC,SAAQ,KAAK;IAC1D;;;OAGG;IACH,YAAY,QAAgB;QACxB,KAAK,CACD,IAAI,QAAQ,2FAA2F;YACvG,qGAAqG;YACrG,sGAAsG;YACtG,YAAY,CACf,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qCAAqC,CAAC;IACtD,CAAC;CACJ"}
|