@rvoh/dream 2.8.0 → 2.9.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/cjs/src/bin/index.js +17 -2
- package/dist/cjs/src/cli/index.js +88 -12
- package/dist/cjs/src/db/helpers/syncDbTypesFiles.js +17 -12
- package/dist/cjs/src/decorators/class/ReplicaSafe.js +3 -4
- package/dist/cjs/src/decorators/class/STI.js +1 -2
- package/dist/cjs/src/decorators/class/SoftDelete.js +4 -5
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +21 -10
- package/dist/cjs/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +19 -6
- package/dist/cjs/src/encrypt/index.js +80 -11
- package/dist/cjs/src/errors/SspawnRequiresDevelopmentOrTest.js +26 -0
- package/dist/cjs/src/errors/encrypt/DecryptionError.js +5 -0
- package/dist/cjs/src/errors/encrypt/DecryptionParseError.js +5 -0
- package/dist/cjs/src/errors/encrypt/DecryptionWithRotationError.js +12 -0
- package/dist/cjs/src/ops/index.js +28 -1
- package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +29 -10
- package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +20 -4
- package/dist/esm/src/bin/index.js +17 -2
- package/dist/esm/src/cli/index.js +88 -12
- package/dist/esm/src/db/helpers/syncDbTypesFiles.js +17 -12
- package/dist/esm/src/decorators/class/ReplicaSafe.js +3 -4
- package/dist/esm/src/decorators/class/STI.js +1 -2
- package/dist/esm/src/decorators/class/SoftDelete.js +4 -5
- package/dist/esm/src/dream/QueryDriver/Kysely.js +21 -10
- package/dist/esm/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +19 -6
- package/dist/esm/src/encrypt/index.js +80 -11
- package/dist/esm/src/errors/SspawnRequiresDevelopmentOrTest.js +26 -0
- package/dist/esm/src/errors/encrypt/DecryptionError.js +5 -0
- package/dist/esm/src/errors/encrypt/DecryptionParseError.js +5 -0
- package/dist/esm/src/errors/encrypt/DecryptionWithRotationError.js +12 -0
- package/dist/esm/src/ops/index.js +28 -1
- package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +29 -10
- package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +20 -4
- package/dist/types/src/cli/index.d.ts +57 -4
- package/dist/types/src/decorators/class/ReplicaSafe.d.ts +2 -1
- package/dist/types/src/decorators/class/STI.d.ts +1 -1
- package/dist/types/src/decorators/class/SoftDelete.d.ts +2 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +17 -0
- package/dist/types/src/dream-app/index.d.ts +25 -1
- package/dist/types/src/encrypt/algorithms/aes-gcm/decryptAESGCM.d.ts +1 -1
- package/dist/types/src/encrypt/index.d.ts +59 -0
- package/dist/types/src/errors/SspawnRequiresDevelopmentOrTest.d.ts +5 -0
- package/dist/types/src/errors/encrypt/DecryptionError.d.ts +3 -0
- package/dist/types/src/errors/encrypt/DecryptionParseError.d.ts +3 -0
- package/dist/types/src/errors/encrypt/DecryptionWithRotationError.d.ts +7 -0
- package/dist/types/src/ops/index.d.ts +28 -1
- package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +40 -12
- package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +23 -5
- package/docs/classes/db.DreamMigrationHelpers.html +9 -9
- package/docs/classes/db.KyselyQueryDriver.html +32 -32
- package/docs/classes/db.PostgresQueryDriver.html +33 -33
- package/docs/classes/db.QueryDriverBase.html +31 -31
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +118 -118
- package/docs/classes/index.DreamApp.html +5 -5
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +56 -56
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +41 -6
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +45 -23
- package/docs/classes/system.ObjectSerializerBuilder.html +32 -13
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +51 -2
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +2 -2
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +26 -4
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/dist/cjs/src/helpers/sspawn.js +0 -44
- package/dist/esm/src/helpers/sspawn.js +0 -44
- package/dist/types/src/helpers/sspawn.d.ts +0 -7
|
@@ -20,14 +20,27 @@ export default class DreamSerializerBuilder {
|
|
|
20
20
|
/**
|
|
21
21
|
* Includes an attribute from a nested object in the serialized output.
|
|
22
22
|
*
|
|
23
|
-
* Accesses `targetName.name` on the data object.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Accesses `targetName.name` on the data object. When the target object or the
|
|
24
|
+
* delegated attribute is null/undefined, the resolution order is:
|
|
25
|
+
* 1. `default` if provided → renders the default value
|
|
26
|
+
* 2. `required: false` → omits the key entirely from the rendered output
|
|
27
|
+
* 3. otherwise → renders `null`
|
|
26
28
|
*
|
|
27
29
|
* When the target is a Dream model, OpenAPI types may be automatically inferred
|
|
28
30
|
* for standard database columns. For json/jsonb columns or non-Dream targets,
|
|
29
31
|
* the `openapi` option is required.
|
|
30
32
|
*
|
|
33
|
+
* `optional` and `required` are not aliases — they encode different things and can
|
|
34
|
+
* be used together:
|
|
35
|
+
* - `optional: true` is an OpenAPI-only marker (no runtime effect). It declares
|
|
36
|
+
* that the rendered value may be `null` (e.g. when delegating through a HasOne
|
|
37
|
+
* that may be missing). Use this when you want the key to always be present
|
|
38
|
+
* but the value to be nullable in the schema.
|
|
39
|
+
* - `required: false` affects both runtime and OpenAPI. At runtime, when the
|
|
40
|
+
* resolved value is `undefined` (which includes the case of a missing delegated
|
|
41
|
+
* association with no `default`), the key is omitted from the rendered output.
|
|
42
|
+
* In OpenAPI, the field is marked as not required.
|
|
43
|
+
*
|
|
31
44
|
* @param targetName - The property name containing the target object (e.g., an association name)
|
|
32
45
|
* @param name - The attribute name within the target object
|
|
33
46
|
* @param options - Configuration options:
|
|
@@ -35,16 +48,22 @@ export default class DreamSerializerBuilder {
|
|
|
35
48
|
* (e.g., delegating `'user', 'email'` with `as: 'userEmail'` outputs the value
|
|
36
49
|
* under `userEmail`)
|
|
37
50
|
* - `default` - Value to use when the target object or its attribute is null/undefined
|
|
51
|
+
* (not available when delegating to a `'type'` STI discriminator column: substituting
|
|
52
|
+
* a discriminator string when the association is actually missing produces a response
|
|
53
|
+
* indistinguishable from "association present with that type," which is misleading)
|
|
38
54
|
* - `openapi` - OpenAPI schema definition; required for non-Dream targets and json/jsonb
|
|
39
55
|
* columns, optional for standard Dream columns (where types are inferred)
|
|
40
|
-
* - `optional` - Set to `true` to
|
|
41
|
-
* (wraps the type in `anyOf: [schema, { type: 'null' }]`).
|
|
42
|
-
*
|
|
43
|
-
*
|
|
56
|
+
* - `optional` - Set to `true` to mark the value as nullable in the OpenAPI schema
|
|
57
|
+
* (wraps the type in `anyOf: [schema, { type: 'null' }]`). OpenAPI-only — the
|
|
58
|
+
* key is still rendered (as `null`). For Dream models, this is auto-inferred
|
|
59
|
+
* from optional BelongsTo associations. Use this when delegating through a
|
|
60
|
+
* HasOne or other nullable association.
|
|
44
61
|
* - `precision` - Round decimal values to the specified number of decimal places (0–9)
|
|
45
|
-
* during rendering; does not affect the OpenAPI shape
|
|
46
|
-
*
|
|
47
|
-
*
|
|
62
|
+
* during rendering; does not affect the OpenAPI shape (not available when delegating
|
|
63
|
+
* to a `'type'` STI discriminator column, which is always a string enum)
|
|
64
|
+
* - `required` - Set to `false` to omit the key from the rendered output when the
|
|
65
|
+
* resolved value is `undefined` (including a missing delegated association with
|
|
66
|
+
* no `default`), and to mark the field as not required in the OpenAPI schema
|
|
48
67
|
* @returns The serializer builder for method chaining
|
|
49
68
|
*
|
|
50
69
|
* @example
|
|
@@ -69,8 +69,20 @@ export default class ObjectSerializerBuilder {
|
|
|
69
69
|
* Includes an attribute from a nested object in the serialized output.
|
|
70
70
|
*
|
|
71
71
|
* Accesses `targetName.name` on the data object. The `openapi` option is always
|
|
72
|
-
* required.
|
|
73
|
-
* the
|
|
72
|
+
* required. When the target object or the delegated attribute is null/undefined,
|
|
73
|
+
* the resolution order is:
|
|
74
|
+
* 1. `default` if provided → renders the default value
|
|
75
|
+
* 2. `required: false` → omits the key entirely from the rendered output
|
|
76
|
+
* 3. otherwise → renders `null`
|
|
77
|
+
*
|
|
78
|
+
* `optional` and `required` are not aliases — they encode different things and can
|
|
79
|
+
* be used together:
|
|
80
|
+
* - `optional: true` is an OpenAPI-only marker (no runtime effect). It declares
|
|
81
|
+
* that the rendered value may be `null`. Use this when you want the key to always
|
|
82
|
+
* be present but the value to be nullable in the schema.
|
|
83
|
+
* - `required: false` affects both runtime and OpenAPI. At runtime, when the
|
|
84
|
+
* resolved value is `undefined`, the key is omitted from the rendered output.
|
|
85
|
+
* In OpenAPI, the field is marked as not required.
|
|
74
86
|
*
|
|
75
87
|
* @param targetName - The property name containing the target object
|
|
76
88
|
* @param name - The attribute name within the target object
|
|
@@ -80,10 +92,14 @@ export default class ObjectSerializerBuilder {
|
|
|
80
92
|
* (e.g., delegating `'profile', 'avatarUrl'` with `as: 'avatar'` outputs the value
|
|
81
93
|
* under `avatar`)
|
|
82
94
|
* - `default` - Value to use when the target object or its attribute is null/undefined
|
|
95
|
+
* - `optional` - Set to `true` to mark the value as nullable in the OpenAPI schema
|
|
96
|
+
* (wraps the type in `anyOf: [schema, { type: 'null' }]`). OpenAPI-only — the
|
|
97
|
+
* key is still rendered (as `null`)
|
|
83
98
|
* - `precision` - Round decimal values to the specified number of decimal places (0–9)
|
|
84
99
|
* during rendering; does not affect the OpenAPI shape
|
|
85
|
-
* - `required` - Set to `false` to
|
|
86
|
-
*
|
|
100
|
+
* - `required` - Set to `false` to omit the key from the rendered output when the
|
|
101
|
+
* resolved value is `undefined`, and to mark the field as not required in the
|
|
102
|
+
* OpenAPI schema
|
|
87
103
|
* @returns The serializer builder for method chaining
|
|
88
104
|
*
|
|
89
105
|
* @example
|
|
@@ -3,9 +3,13 @@ import DreamApp from '../dream-app/index.js';
|
|
|
3
3
|
import Query from '../dream/Query.js';
|
|
4
4
|
import DBClassDeprecation from '../helpers/cli/DBClassDeprecation.js';
|
|
5
5
|
import generateDream from '../helpers/cli/generateDream.js';
|
|
6
|
-
import
|
|
6
|
+
import EnvInternal from '../helpers/EnvInternal.js';
|
|
7
7
|
export default class DreamBin {
|
|
8
8
|
static async sync(onSync, options) {
|
|
9
|
+
if (!EnvInternal.isTest) {
|
|
10
|
+
DreamCLI.logger.log(`skipping sync: auto-generated type/schema files are only built when NODE_ENV=test (current NODE_ENV: ${process.env.NODE_ENV ?? 'unset'}). Run with NODE_ENV=test to regenerate.`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
9
13
|
const dreamApp = DreamApp.getOrFail();
|
|
10
14
|
for (const connectionName of Object.keys(dreamApp.dbCredentials)) {
|
|
11
15
|
await Query.dbDriverClass(connectionName).sync(connectionName, onSync, options);
|
|
@@ -76,7 +80,18 @@ export default class DreamBin {
|
|
|
76
80
|
// to use it to generate docs for their apps.
|
|
77
81
|
static async buildDocs() {
|
|
78
82
|
DreamCLI.logger.logStartProgress('generating docs...');
|
|
79
|
-
|
|
83
|
+
// safe (R-015): all argv elements are constant literals; typedoc itself
|
|
84
|
+
// expands the glob so we don't need shell-form invocation.
|
|
85
|
+
await DreamCLI.spawn('pnpm', {
|
|
86
|
+
args: [
|
|
87
|
+
'typedoc',
|
|
88
|
+
'src/package-exports/*.ts',
|
|
89
|
+
'--tsconfig',
|
|
90
|
+
'./tsconfig.esm.build.json',
|
|
91
|
+
'--out',
|
|
92
|
+
'docs',
|
|
93
|
+
],
|
|
94
|
+
});
|
|
80
95
|
DreamCLI.logger.logEndProgress();
|
|
81
96
|
}
|
|
82
97
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
1
2
|
import { InvalidArgumentError, Option } from 'commander';
|
|
2
3
|
import DreamBin from '../bin/index.js';
|
|
3
4
|
import DreamApp from '../dream-app/index.js';
|
|
4
5
|
import Encrypt from '../encrypt/index.js';
|
|
6
|
+
import SspawnRequiresDevelopmentOrTest from '../errors/SspawnRequiresDevelopmentOrTest.js';
|
|
5
7
|
import generateDream from '../helpers/cli/generateDream.js';
|
|
6
8
|
import EnvInternal from '../helpers/EnvInternal.js';
|
|
7
9
|
import loadRepl from '../helpers/loadRepl.js';
|
|
8
|
-
import sspawn from '../helpers/sspawn.js';
|
|
9
10
|
import DreamCliLogger from './logger/DreamCliLogger.js';
|
|
10
11
|
import colorize from './logger/loggable/colorize.js';
|
|
11
12
|
export const CLI_INDENT = ' ';
|
|
@@ -208,6 +209,8 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
|
|
|
208
209
|
.alias('g:sti-child')
|
|
209
210
|
.description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Generates a child model decorated with @STI(Parent), child serializers extending the parent's base serializers, a migration that ALTERs the parent table (not a new table), check constraints, a factory, and spec skeleton.
|
|
210
211
|
${INDENT}
|
|
212
|
+
${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
|
|
213
|
+
${INDENT}
|
|
211
214
|
${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
|
|
212
215
|
${INDENT}
|
|
213
216
|
${INDENT}Examples:
|
|
@@ -288,7 +291,7 @@ ${INDENT} pnpm psy g:encryption-key --algorithm=aes-128-gcm`)
|
|
|
288
291
|
});
|
|
289
292
|
program
|
|
290
293
|
.command('db:migrate')
|
|
291
|
-
.description(`Runs all pending database migrations in order, then automatically syncs types (
|
|
294
|
+
.description(`Runs all pending database migrations in order, then automatically syncs types (only when NODE_ENV=test, to avoid clobbering generated types from a stale dev database). This is the primary command for applying schema changes after generating or editing a migration.
|
|
292
295
|
${INDENT}
|
|
293
296
|
${INDENT}Example workflow:
|
|
294
297
|
${INDENT} pnpm psy g:migration add-phone-to-users phone:string:optional
|
|
@@ -298,14 +301,14 @@ ${INDENT} pnpm psy db:migrate`)
|
|
|
298
301
|
.action(async ({ skipSync }) => {
|
|
299
302
|
await initializeDreamApp({ bypassDreamIntegrityChecks: true });
|
|
300
303
|
await DreamBin.dbMigrate();
|
|
301
|
-
if (EnvInternal.
|
|
304
|
+
if (EnvInternal.isTest && !skipSync) {
|
|
302
305
|
await DreamBin.sync(onSync);
|
|
303
306
|
}
|
|
304
307
|
process.exit();
|
|
305
308
|
});
|
|
306
309
|
program
|
|
307
310
|
.command('db:rollback')
|
|
308
|
-
.description(`Rolls back the most recent migration(s), then automatically syncs types (
|
|
311
|
+
.description(`Rolls back the most recent migration(s), then automatically syncs types (only when NODE_ENV=test, to avoid clobbering generated types from a stale dev database). Use this to undo a migration so you can edit and re-run it.
|
|
309
312
|
${INDENT}
|
|
310
313
|
${INDENT}Examples:
|
|
311
314
|
${INDENT} pnpm psy db:rollback # rolls back the last migration
|
|
@@ -315,7 +318,7 @@ ${INDENT} pnpm psy db:rollback --steps=3 # rolls back the last 3 migrations`
|
|
|
315
318
|
.action(async ({ steps, skipSync }) => {
|
|
316
319
|
await initializeDreamApp({ bypassDreamIntegrityChecks: true });
|
|
317
320
|
await DreamBin.dbRollback({ steps });
|
|
318
|
-
if (EnvInternal.
|
|
321
|
+
if (EnvInternal.isTest && !skipSync) {
|
|
319
322
|
await DreamBin.sync(onSync);
|
|
320
323
|
}
|
|
321
324
|
process.exit();
|
|
@@ -409,15 +412,84 @@ ${INDENT}Examples: User, Place, Room/Bedroom, Settings/CommunicationPreferences`
|
|
|
409
412
|
process.exit();
|
|
410
413
|
});
|
|
411
414
|
}
|
|
412
|
-
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
415
|
+
/**
|
|
416
|
+
* Run a developer-authored CLI command. Always runs in argv form (the
|
|
417
|
+
* underlying child_process `spawn` is called with `shell: false`):
|
|
418
|
+
* `command` is exec'd literally and `opts.args` are passed as separate
|
|
419
|
+
* argv elements. Shell-form invocation is intentionally not supported —
|
|
420
|
+
* there is no caller that needs `&&`-chaining, globs, or other shell
|
|
421
|
+
* features that can't be expressed as argv.
|
|
422
|
+
*
|
|
423
|
+
* For backward compatibility, `command` may contain implicit args
|
|
424
|
+
* separated by whitespace (e.g. `'pnpm psy sync'`); the leading token
|
|
425
|
+
* becomes the program and the rest are split out and prepended to any
|
|
426
|
+
* `opts.args` so the original argument order is preserved:
|
|
427
|
+
*
|
|
428
|
+
* DreamCLI.spawn('pnpm psy sync')
|
|
429
|
+
* → spawn('pnpm', ['psy', 'sync'])
|
|
430
|
+
*
|
|
431
|
+
* DreamCLI.spawn('pnpm psy', { args: ['sync', '--flag'] })
|
|
432
|
+
* → spawn('pnpm', ['psy', 'sync', '--flag'])
|
|
433
|
+
*
|
|
434
|
+
* ## Threat model (R-015)
|
|
435
|
+
*
|
|
436
|
+
* For dev-time CLI glue only (scaffolding, doc generation, type sync).
|
|
437
|
+
* **No runtime HTTP request input ever reaches this function.** Inputs
|
|
438
|
+
* are constant literals or composed from developer-supplied config
|
|
439
|
+
* (package.json scripts, CLI argv, scaffold templates) — never from
|
|
440
|
+
* runtime request input or any other untrusted external source.
|
|
441
|
+
*
|
|
442
|
+
* Argv-form is the safe choice for any caller that interpolates a
|
|
443
|
+
* config value, path, or credential: a database password containing
|
|
444
|
+
* `$` or backticks is passed literally to the child rather than
|
|
445
|
+
* interpreted by a shell.
|
|
446
|
+
*
|
|
447
|
+
* ## Layered defense
|
|
448
|
+
*
|
|
449
|
+
* Primary gate: every caller restricts spawn use to dev/test code paths
|
|
450
|
+
* (CLI commands, the dev watcher, scaffold-time code generators,
|
|
451
|
+
* generated `cli:sync` initializers wrapped in
|
|
452
|
+
* `if (AppEnv.isDevelopmentOrTest)`).
|
|
453
|
+
*
|
|
454
|
+
* Backstop: throws `SspawnRequiresDevelopmentOrTest` when `NODE_ENV` is
|
|
455
|
+
* anything other than `development` or `test`. Checking
|
|
456
|
+
* `!isDevelopmentOrTest` (rather than `isProduction`) means staging-style
|
|
457
|
+
* envs and any unforeseen NODE_ENV value also fail closed.
|
|
418
458
|
*/
|
|
419
459
|
static async spawn(command, opts) {
|
|
420
|
-
|
|
460
|
+
const tokens = command.trim().split(/\s+/).filter(Boolean);
|
|
461
|
+
const [program = '', ...implicitArgs] = tokens;
|
|
462
|
+
const { args: callerArgs = [], onStdout, ...spawnOpts } = opts ?? {};
|
|
463
|
+
const args = [...implicitArgs, ...callerArgs];
|
|
464
|
+
if (!EnvInternal.isDevelopmentOrTest) {
|
|
465
|
+
throw new SspawnRequiresDevelopmentOrTest([program, ...args].join(' '));
|
|
466
|
+
}
|
|
467
|
+
return new Promise((accept, reject) => {
|
|
468
|
+
const proc = spawn(program, args, { shell: false, ...spawnOpts });
|
|
469
|
+
// NOTE: stdout spy so this CLI utility can hijack the stdout from the
|
|
470
|
+
// child command and route it through a caller-provided sink.
|
|
471
|
+
proc.stdout?.on('data', chunk => {
|
|
472
|
+
const txt = chunk?.toString()?.trim();
|
|
473
|
+
if (typeof txt !== 'string' || !txt)
|
|
474
|
+
return;
|
|
475
|
+
if (onStdout) {
|
|
476
|
+
onStdout(txt);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// eslint-disable-next-line no-console
|
|
480
|
+
console.log(txt);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
proc.stdout?.on('error', handleSpawnError);
|
|
484
|
+
proc.stderr?.on('error', handleSpawnError);
|
|
485
|
+
proc.stderr?.on('data', handleSpawnError);
|
|
486
|
+
proc.on('error', handleSpawnError);
|
|
487
|
+
proc.on('close', code => {
|
|
488
|
+
if (code !== 0)
|
|
489
|
+
reject(code);
|
|
490
|
+
accept();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
421
493
|
}
|
|
422
494
|
static get logger() {
|
|
423
495
|
this._logger ||= new DreamCliLogger();
|
|
@@ -432,3 +504,7 @@ function myParseInt(value) {
|
|
|
432
504
|
}
|
|
433
505
|
return parsedValue;
|
|
434
506
|
}
|
|
507
|
+
function handleSpawnError(err) {
|
|
508
|
+
// eslint-disable-next-line no-console
|
|
509
|
+
console.error(err?.toString());
|
|
510
|
+
}
|
|
@@ -5,7 +5,6 @@ import colorize from '../../cli/logger/loggable/colorize.js';
|
|
|
5
5
|
import DreamApp from '../../dream-app/index.js';
|
|
6
6
|
import ASTKyselyCodegenEnhancer from '../../helpers/cli/ASTKyselyCodegenEnhancer.js';
|
|
7
7
|
import dreamPath from '../../helpers/path/dreamPath.js';
|
|
8
|
-
import sspawn from '../../helpers/sspawn.js';
|
|
9
8
|
import dbTypesFilenameForConnection from './dbTypesFilenameForConnection.js';
|
|
10
9
|
export default async function syncDbTypesFiles(connectionName) {
|
|
11
10
|
const dreamApp = DreamApp.getOrFail();
|
|
@@ -16,17 +15,23 @@ export default async function syncDbTypesFiles(connectionName) {
|
|
|
16
15
|
const absoluteDbSyncPath = path.join(dreamApp.projectRoot, dbSyncFilePath);
|
|
17
16
|
await CliFileWriter.cache(absoluteDbSyncPath);
|
|
18
17
|
const lowLevelDbOpts = dreamApp.dbCredentialsFor(connectionName);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
// Argv form (R-015): the connection URL embeds the DB password, and passwords
|
|
19
|
+
// may legitimately contain shell meta-characters (`$`, backticks, spaces, etc.).
|
|
20
|
+
// DreamCLI.spawn always uses shell:false, so each arg is passed literally to
|
|
21
|
+
// the child rather than parsed by a shell — credentials are preserved and any
|
|
22
|
+
// future mutable source can't open a command-injection surface.
|
|
23
|
+
const userinfo = `${dbConf.user}${dbConf.password ? `:${dbConf.password}` : ''}`;
|
|
24
|
+
const url = `${driverClass.syncDialect}://${userinfo}@${dbConf.host}:${dbConf.port}/${dbConf.name}`;
|
|
25
|
+
const args = [`--dialect=${driverClass.syncDialect}`, `--url=${url}`];
|
|
26
|
+
if (lowLevelDbOpts?.tableIncludePattern) {
|
|
27
|
+
args.push(`--include-pattern=${lowLevelDbOpts.tableIncludePattern}`);
|
|
28
|
+
}
|
|
29
|
+
if (lowLevelDbOpts?.tableExcludePattern) {
|
|
30
|
+
args.push(`--exclude-pattern=${lowLevelDbOpts.tableExcludePattern}`);
|
|
31
|
+
}
|
|
32
|
+
args.push(`--out-file=${absoluteDbSyncPath}`);
|
|
33
|
+
await DreamCLI.spawn('kysely-codegen', {
|
|
34
|
+
args,
|
|
30
35
|
onStdout: message => {
|
|
31
36
|
DreamCLI.logger.logContinueProgress(colorize(`[db]`, { color: 'greenBright' }) + ' ' + message, {
|
|
32
37
|
logPrefixColor: 'greenBright',
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import StiChildIncompatibleWithReplicaSafeDecorator from '../../errors/sti/StiChildIncompatibleWithReplicaSafeDecorator.js';
|
|
2
2
|
export default function ReplicaSafe() {
|
|
3
3
|
return function (target) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
dreamClass['replicaSafe'] = true;
|
|
4
|
+
if (target['isSTIChild'])
|
|
5
|
+
throw new StiChildIncompatibleWithReplicaSafeDecorator(target);
|
|
6
|
+
target['replicaSafe'] = true;
|
|
8
7
|
};
|
|
9
8
|
}
|
|
@@ -4,8 +4,7 @@ import StiChildIncompatibleWithSoftDeleteDecorator from '../../errors/sti/StiChi
|
|
|
4
4
|
import { scopeImplementation } from '../static-method/Scope.js';
|
|
5
5
|
export const STI_SCOPE_NAME = 'dream:STI';
|
|
6
6
|
export default function STI(dreamClass) {
|
|
7
|
-
return function (
|
|
8
|
-
const stiChildClass = target;
|
|
7
|
+
return function (stiChildClass) {
|
|
9
8
|
const baseClass = dreamClass['sti'].baseClass || dreamClass;
|
|
10
9
|
if (Object.getOwnPropertyDescriptor(stiChildClass, 'associationMetadataByType'))
|
|
11
10
|
throw new StiChildCannotDefineNewAssociations(baseClass, stiChildClass);
|
|
@@ -38,12 +38,11 @@ export const SOFT_DELETE_SCOPE_NAME = 'dream:SoftDelete';
|
|
|
38
38
|
*/
|
|
39
39
|
export default function SoftDelete() {
|
|
40
40
|
return function (target) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
dreamClass['softDelete'] = true;
|
|
41
|
+
if (target['isSTIChild'])
|
|
42
|
+
throw new StiChildIncompatibleWithSoftDeleteDecorator(target);
|
|
43
|
+
target['softDelete'] = true;
|
|
45
44
|
target[SOFT_DELETE_SCOPE_NAME] = function (query) {
|
|
46
|
-
return query.where({ [
|
|
45
|
+
return query.where({ [target.prototype['_deletedAtField']]: null });
|
|
47
46
|
};
|
|
48
47
|
scopeImplementation(target, SOFT_DELETE_SCOPE_NAME, { default: true });
|
|
49
48
|
};
|
|
@@ -99,7 +99,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
99
99
|
database: DreamApp.getOrFail().dbName(connectionName, dbConnectionType),
|
|
100
100
|
host: connectionConf.host || 'localhost',
|
|
101
101
|
port: connectionConf.port || 5432,
|
|
102
|
-
ssl:
|
|
102
|
+
ssl: resolvePostgresSsl(connectionConf),
|
|
103
103
|
}),
|
|
104
104
|
});
|
|
105
105
|
}
|
|
@@ -2043,15 +2043,26 @@ const associationStringToAssociationAndMaybeAlias = function ({ dreamClass, asso
|
|
|
2043
2043
|
alias,
|
|
2044
2044
|
};
|
|
2045
2045
|
};
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2046
|
+
/**
|
|
2047
|
+
* Resolve the value passed to `pg.Pool`'s `ssl` field for a given credential.
|
|
2048
|
+
*
|
|
2049
|
+
* Precedence:
|
|
2050
|
+
* 1. If `connectionConf.ssl` is set (boolean or `tls.ConnectionOptions`),
|
|
2051
|
+
* pass it straight through to `pg`. This is the verified-TLS path —
|
|
2052
|
+
* apps configure `{ rejectUnauthorized: true, ca: <bundle> }` for
|
|
2053
|
+
* authenticated TLS against a private PKI, or `true` to use Node's
|
|
2054
|
+
* default verification against the system CA store.
|
|
2055
|
+
* 2. Else if `connectionConf.useSsl` is `true`, fall back to
|
|
2056
|
+
* `{ rejectUnauthorized: false }` — encrypted but **not** authenticated.
|
|
2057
|
+
* Preserved for back-compat; new code should set `ssl` explicitly.
|
|
2058
|
+
* 3. Else disable TLS.
|
|
2059
|
+
*/
|
|
2060
|
+
export function resolvePostgresSsl(connectionConf) {
|
|
2061
|
+
if (connectionConf.ssl !== undefined)
|
|
2062
|
+
return connectionConf.ssl;
|
|
2063
|
+
if (connectionConf.useSsl)
|
|
2064
|
+
return { rejectUnauthorized: false };
|
|
2065
|
+
return false;
|
|
2055
2066
|
}
|
|
2056
2067
|
function shouldSoftDelete(dream, reallyDestroy) {
|
|
2057
2068
|
const dreamClass = dream.constructor;
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
|
+
import DecryptionError from '../../../errors/encrypt/DecryptionError.js';
|
|
3
|
+
import DecryptionParseError from '../../../errors/encrypt/DecryptionParseError.js';
|
|
2
4
|
export default function decryptAESGCM(algorithm, encrypted, key) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
let plaintext;
|
|
6
|
+
try {
|
|
7
|
+
const { ciphertext, tag, iv } = unpackPayloadOrFail(encrypted);
|
|
8
|
+
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'));
|
|
9
|
+
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
|
10
|
+
plaintext = decipher.update(ciphertext, 'base64', 'utf8');
|
|
11
|
+
plaintext += decipher.final('utf8');
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
throw Object.assign(new DecryptionError(), { cause });
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(plaintext);
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
throw Object.assign(new DecryptionParseError(), { cause });
|
|
21
|
+
}
|
|
9
22
|
}
|
|
10
23
|
function unpackPayloadOrFail(payload) {
|
|
11
24
|
const unpackedPayload = (typeof payload === 'string' ? JSON.parse(Buffer.from(payload, 'base64').toString()) : payload);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import DecryptionError from '../errors/encrypt/DecryptionError.js';
|
|
2
|
+
import DecryptionWithRotationError from '../errors/encrypt/DecryptionWithRotationError.js';
|
|
1
3
|
import MissingEncryptionKey from '../errors/encrypt/MissingEncryptionKey.js';
|
|
2
4
|
import decryptAESGCM from './algorithms/aes-gcm/decryptAESGCM.js';
|
|
3
5
|
import encryptAESGCM from './algorithms/aes-gcm/encryptAESGCM.js';
|
|
@@ -20,6 +22,30 @@ export default class Encrypt {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Decrypts a value previously produced by {@link Encrypt.encrypt}.
|
|
27
|
+
*
|
|
28
|
+
* Behavior depends on whether `legacyOpts` is provided:
|
|
29
|
+
*
|
|
30
|
+
* **Two-arg form** (no rotation):
|
|
31
|
+
* - `null`/`undefined` input returns `null`.
|
|
32
|
+
* - Cipher op / auth tag / payload-shape failure throws `DecryptionError`.
|
|
33
|
+
* - Successful decrypt with non-JSON plaintext throws `DecryptionParseError`.
|
|
34
|
+
*
|
|
35
|
+
* **Three-arg form** (rotation): tries the current key first; on
|
|
36
|
+
* `DecryptionError` falls back to the legacy key. If both fail, throws
|
|
37
|
+
* `DecryptionWithRotationError` carrying both per-key errors. A
|
|
38
|
+
* `DecryptionParseError` from the current key is **not** retried — the
|
|
39
|
+
* cipher already matched, so a parse failure means the encrypted format
|
|
40
|
+
* is wrong (an app bug), not a wrong key.
|
|
41
|
+
*
|
|
42
|
+
* `MissingEncryptionKey` propagates from either form when a key is missing.
|
|
43
|
+
*
|
|
44
|
+
* @throws MissingEncryptionKey
|
|
45
|
+
* @throws DecryptionError
|
|
46
|
+
* @throws DecryptionParseError
|
|
47
|
+
* @throws DecryptionWithRotationError
|
|
48
|
+
*/
|
|
23
49
|
static decrypt(encrypted, { algorithm, key }, legacyOpts) {
|
|
24
50
|
if (legacyOpts)
|
|
25
51
|
return this.attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts);
|
|
@@ -31,12 +57,7 @@ export default class Encrypt {
|
|
|
31
57
|
case 'aes-256-gcm':
|
|
32
58
|
case 'aes-192-gcm':
|
|
33
59
|
case 'aes-128-gcm':
|
|
34
|
-
|
|
35
|
-
return decryptAESGCM(algorithm, encrypted, key);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
60
|
+
return decryptAESGCM(algorithm, encrypted, key);
|
|
40
61
|
default: {
|
|
41
62
|
// protection so that if a new EncryptAlgorithm is ever added, this will throw a type error at build time
|
|
42
63
|
const _never = algorithm;
|
|
@@ -44,12 +65,60 @@ export default class Encrypt {
|
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
|
-
static attemptDecryptionWithLegacyKeys(encrypted,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
|
|
68
|
+
static attemptDecryptionWithLegacyKeys(encrypted, currentOpts, legacyOpts) {
|
|
69
|
+
let currentKeyError;
|
|
70
|
+
try {
|
|
71
|
+
return this.decrypt(encrypted, currentOpts);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (!(err instanceof DecryptionError))
|
|
75
|
+
throw err;
|
|
76
|
+
currentKeyError = err;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return this.decrypt(encrypted, legacyOpts);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (!(err instanceof DecryptionError))
|
|
83
|
+
throw err;
|
|
84
|
+
throw new DecryptionWithRotationError(currentKeyError, err);
|
|
85
|
+
}
|
|
52
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Generates a base64-encoded random key suitable for the given algorithm.
|
|
89
|
+
*
|
|
90
|
+
* ## Rotation workflow
|
|
91
|
+
*
|
|
92
|
+
* 1. Generate a new key: `const newKey = Encrypt.generateKey('aes-256-gcm')`.
|
|
93
|
+
* 2. Configure rotation by setting both `current` and `legacy`. For
|
|
94
|
+
* encrypted columns: `dreamApp.set('encryption', { columns: { current:
|
|
95
|
+
* { algorithm: 'aes-256-gcm', key: newKey }, legacy: { algorithm:
|
|
96
|
+
* 'aes-256-gcm', key: oldKey } } })`. For cookies, use the equivalent
|
|
97
|
+
* shape under `psychicApp.set('encryption', { cookies: { current,
|
|
98
|
+
* legacy } })`.
|
|
99
|
+
* 3. Deploy. New encryptions use `current`; existing ciphertext continues
|
|
100
|
+
* to decrypt via `legacy` fallback.
|
|
101
|
+
* 4. For cookies, wait at least the cookie `maxAge` so all in-flight
|
|
102
|
+
* cookies have either expired or been re-issued under the new key. For
|
|
103
|
+
* `@Encrypted` columns, re-encrypt every existing row under the new
|
|
104
|
+
* key (read each row and write it back; the setter re-encrypts with
|
|
105
|
+
* `current`).
|
|
106
|
+
* 5. Drop `legacy` from config and deploy again.
|
|
107
|
+
*
|
|
108
|
+
* ## When to rotate
|
|
109
|
+
*
|
|
110
|
+
* - On a scheduled cadence (90–180 days is a reasonable policy default).
|
|
111
|
+
* - Incident response: leaked env file, departing employee with key
|
|
112
|
+
* access, or any suspected key compromise.
|
|
113
|
+
*
|
|
114
|
+
* ## How long to keep `legacy`
|
|
115
|
+
*
|
|
116
|
+
* - For cookies: at least the cookie `maxAge`, so in-flight sessions are
|
|
117
|
+
* not forced to re-authenticate.
|
|
118
|
+
* - For `@Encrypted` columns: until every existing row has been
|
|
119
|
+
* re-encrypted under the new key. Dropping `legacy` early will cause
|
|
120
|
+
* `DecryptionWithRotationError` on any not-yet-rewritten row.
|
|
121
|
+
*/
|
|
53
122
|
static generateKey(algorithm) {
|
|
54
123
|
switch (algorithm) {
|
|
55
124
|
case 'aes-256-gcm':
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default class SspawnRequiresDevelopmentOrTest extends Error {
|
|
2
|
+
command;
|
|
3
|
+
constructor(command) {
|
|
4
|
+
super();
|
|
5
|
+
this.command = command;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
DreamCLI.spawn refused to run outside development or test
|
|
10
|
+
(NODE_ENV must be 'development' or 'test').
|
|
11
|
+
|
|
12
|
+
DreamCLI.spawn is dev-time CLI glue (scaffolding, docs generation,
|
|
13
|
+
type sync). It must never run in a deployed process — production has
|
|
14
|
+
no business shelling out arbitrary commands, and refusing here turns
|
|
15
|
+
the dev-only contract into a runtime invariant. Checking
|
|
16
|
+
\`!isDevelopmentOrTest\` (rather than \`isProduction\`) means
|
|
17
|
+
staging-style envs and any unforeseen NODE_ENV value also fail closed.
|
|
18
|
+
|
|
19
|
+
If you reached this from a deploy step, route the work through direct
|
|
20
|
+
child_process APIs at the call site with explicit argv and a documented
|
|
21
|
+
threat model rather than through this helper.
|
|
22
|
+
|
|
23
|
+
command: ${this.command}
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default class DecryptionWithRotationError extends Error {
|
|
2
|
+
currentKeyError;
|
|
3
|
+
legacyKeyError;
|
|
4
|
+
constructor(currentKeyError, legacyKeyError) {
|
|
5
|
+
super(undefined, { cause: { current: currentKeyError, legacy: legacyKeyError } });
|
|
6
|
+
this.currentKeyError = currentKeyError;
|
|
7
|
+
this.legacyKeyError = legacyKeyError;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
return 'Failed to decrypt with both the current and legacy keys. The ciphertext may have been tampered with, corrupted, or encrypted with a key that is no longer in rotation.';
|
|
11
|
+
}
|
|
12
|
+
}
|