@rvoh/dream 2.32.0 → 2.33.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.
Files changed (205) hide show
  1. package/dist/cjs/src/cli/index.js +4 -4
  2. package/dist/cjs/src/db/helpers/syncDbTypesFiles.js +6 -7
  3. package/dist/cjs/src/dream/QueryDriver/Base.js +7 -0
  4. package/dist/cjs/src/dream/QueryDriver/Kysely.js +7 -0
  5. package/dist/cjs/src/dream/QueryDriver/Postgres.js +7 -1
  6. package/dist/cjs/src/dream/QueryDriver/helpers/pg/loadPgClient.js +29 -1
  7. package/dist/esm/src/cli/index.js +4 -4
  8. package/dist/esm/src/db/helpers/syncDbTypesFiles.js +6 -7
  9. package/dist/esm/src/dream/QueryDriver/Base.js +7 -0
  10. package/dist/esm/src/dream/QueryDriver/Kysely.js +7 -0
  11. package/dist/esm/src/dream/QueryDriver/Postgres.js +7 -1
  12. package/dist/esm/src/dream/QueryDriver/helpers/pg/loadPgClient.js +29 -1
  13. package/dist/types/src/cli/index.d.ts +7 -6
  14. package/dist/types/src/dream/QueryDriver/Base.d.ts +3 -0
  15. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
  16. package/dist/types/src/dream/QueryDriver/helpers/pg/loadPgClient.d.ts +5 -0
  17. package/dist/types/src/dream-app/index.d.ts +11 -1
  18. package/docs/assets/hierarchy.js +1 -1
  19. package/docs/assets/search.js +1 -1
  20. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  21. package/docs/classes/db.KyselyQueryDriver.html +47 -45
  22. package/docs/classes/db.PostgresQueryDriver.html +48 -46
  23. package/docs/classes/db.QueryDriverBase.html +46 -44
  24. package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
  25. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  26. package/docs/classes/errors.ColumnOverflow.html +3 -3
  27. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  28. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  29. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  30. package/docs/classes/errors.DecryptionError.html +2 -2
  31. package/docs/classes/errors.DecryptionParseError.html +2 -2
  32. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  33. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  34. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  35. package/docs/classes/errors.InvalidClockTime.html +2 -2
  36. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  37. package/docs/classes/errors.InvalidDateTime.html +2 -2
  38. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  39. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  40. package/docs/classes/errors.NotNullViolation.html +3 -3
  41. package/docs/classes/errors.RecordNotFound.html +3 -3
  42. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  43. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  44. package/docs/classes/errors.ValidationError.html +3 -3
  45. package/docs/classes/index.CalendarDate.html +33 -33
  46. package/docs/classes/index.ClockTime.html +32 -32
  47. package/docs/classes/index.ClockTimeTz.html +35 -35
  48. package/docs/classes/index.DateTime.html +86 -86
  49. package/docs/classes/index.Decorators.html +19 -19
  50. package/docs/classes/index.Dream.html +124 -124
  51. package/docs/classes/index.DreamApp.html +10 -10
  52. package/docs/classes/index.DreamTransaction.html +2 -2
  53. package/docs/classes/index.Env.html +2 -2
  54. package/docs/classes/index.Query.html +65 -65
  55. package/docs/classes/system.CliFileWriter.html +4 -4
  56. package/docs/classes/system.DreamBin.html +2 -2
  57. package/docs/classes/system.DreamCLI.html +11 -11
  58. package/docs/classes/system.DreamImporter.html +2 -2
  59. package/docs/classes/system.DreamLogos.html +2 -2
  60. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  61. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  62. package/docs/classes/system.PathHelpers.html +3 -3
  63. package/docs/classes/utils.Encrypt.html +3 -3
  64. package/docs/classes/utils.Range.html +2 -2
  65. package/docs/functions/db.closeAllDbConnections.html +1 -1
  66. package/docs/functions/db.dreamDbConnections.html +1 -1
  67. package/docs/functions/db.untypedDb.html +1 -1
  68. package/docs/functions/db.validateColumn.html +1 -1
  69. package/docs/functions/db.validateTable.html +1 -1
  70. package/docs/functions/errors.pgErrorType.html +1 -1
  71. package/docs/functions/index.DreamSerializer.html +1 -1
  72. package/docs/functions/index.ObjectSerializer.html +1 -1
  73. package/docs/functions/index.ReplicaSafe.html +1 -1
  74. package/docs/functions/index.STI.html +1 -1
  75. package/docs/functions/index.SoftDelete.html +1 -1
  76. package/docs/functions/utils.camelize.html +1 -1
  77. package/docs/functions/utils.capitalize.html +1 -1
  78. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  79. package/docs/functions/utils.compact.html +1 -1
  80. package/docs/functions/utils.groupBy.html +1 -1
  81. package/docs/functions/utils.hyphenize.html +1 -1
  82. package/docs/functions/utils.intersection.html +1 -1
  83. package/docs/functions/utils.isEmpty.html +1 -1
  84. package/docs/functions/utils.normalizeUnicode.html +1 -1
  85. package/docs/functions/utils.pascalize.html +1 -1
  86. package/docs/functions/utils.percent.html +1 -1
  87. package/docs/functions/utils.range.html +1 -1
  88. package/docs/functions/utils.round.html +1 -1
  89. package/docs/functions/utils.sanitizeString.html +1 -1
  90. package/docs/functions/utils.snakeify.html +1 -1
  91. package/docs/functions/utils.sort.html +1 -1
  92. package/docs/functions/utils.sortBy.html +1 -1
  93. package/docs/functions/utils.sortObjectByKey.html +1 -1
  94. package/docs/functions/utils.sortObjectByValue.html +1 -1
  95. package/docs/functions/utils.uncapitalize.html +1 -1
  96. package/docs/functions/utils.uniq.html +1 -1
  97. package/docs/hierarchy.html +1 -1
  98. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  99. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  100. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  101. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  102. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  103. package/docs/interfaces/types.DecoratorContext.html +2 -2
  104. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  105. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  106. package/docs/interfaces/types.DreamDbConfig.html +13 -5
  107. package/docs/interfaces/types.DurationObject.html +2 -2
  108. package/docs/interfaces/types.EncryptOptions.html +2 -2
  109. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  110. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  111. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  112. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  113. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  114. package/docs/types/openapi.OpenapiFormats.html +1 -1
  115. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  116. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  117. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  118. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  120. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  121. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  122. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  123. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  124. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  125. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  126. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  127. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  128. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  129. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  131. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  139. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  140. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  142. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  144. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  145. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  146. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  147. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  148. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  150. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  151. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  152. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  153. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  154. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  155. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  156. package/docs/types/types.CalendarDateObject.html +1 -1
  157. package/docs/types/types.Camelized.html +1 -1
  158. package/docs/types/types.ClockTimeObject.html +1 -1
  159. package/docs/types/types.DbConnectionType.html +1 -1
  160. package/docs/types/types.DbTypes.html +1 -1
  161. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  162. package/docs/types/types.DreamAttributes.html +1 -1
  163. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  164. package/docs/types/types.DreamClassColumn.html +1 -1
  165. package/docs/types/types.DreamColumn.html +1 -1
  166. package/docs/types/types.DreamColumnNames.html +1 -1
  167. package/docs/types/types.DreamLogLevel.html +1 -1
  168. package/docs/types/types.DreamLogger.html +2 -2
  169. package/docs/types/types.DreamModelSerializerType.html +1 -1
  170. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  171. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  172. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  173. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  174. package/docs/types/types.DreamSerializable.html +1 -1
  175. package/docs/types/types.DreamSerializableArray.html +1 -1
  176. package/docs/types/types.DreamSerializerKey.html +1 -1
  177. package/docs/types/types.DreamSerializers.html +1 -1
  178. package/docs/types/types.DreamVirtualColumns.html +1 -1
  179. package/docs/types/types.DurationUnit.html +1 -1
  180. package/docs/types/types.EncryptAlgorithm.html +1 -1
  181. package/docs/types/types.HasManyStatement.html +1 -1
  182. package/docs/types/types.HasOneStatement.html +1 -1
  183. package/docs/types/types.Hyphenized.html +1 -1
  184. package/docs/types/types.Pascalized.html +1 -1
  185. package/docs/types/types.PrimaryKeyType.html +1 -1
  186. package/docs/types/types.RoundingPrecision.html +1 -1
  187. package/docs/types/types.SerializerCasing.html +1 -1
  188. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  189. package/docs/types/types.Snakeified.html +1 -1
  190. package/docs/types/types.StrictInterface.html +1 -1
  191. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  192. package/docs/types/types.UpdateableProperties.html +1 -1
  193. package/docs/types/types.ValidationType.html +1 -1
  194. package/docs/types/types.ViewModel.html +2 -2
  195. package/docs/types/types.ViewModelClass.html +1 -1
  196. package/docs/types/types.WeekdayName.html +1 -1
  197. package/docs/types/types.WhereStatementForDream.html +1 -1
  198. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  199. package/docs/variables/index.DreamConst.html +1 -1
  200. package/docs/variables/index.ops.html +1 -1
  201. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  202. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  203. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  204. package/docs/variables/system.primaryKeyTypes.html +1 -1
  205. package/package.json +1 -1
@@ -508,10 +508,10 @@ ${INDENT}Examples: User, Place, Room/Bedroom, Settings/CommunicationPreferences`
508
508
  * (package.json scripts, CLI argv, scaffold templates) — never from
509
509
  * runtime request input or any other untrusted external source.
510
510
  *
511
- * Argv-form is the safe choice for any caller that interpolates a
512
- * config value, path, or credential: a database password containing
513
- * `$` or backticks is passed literally to the child rather than
514
- * interpreted by a shell.
511
+ * Argv-form passes config values and paths literally to the child,
512
+ * without interpreting shell metacharacters. Credentials belong in
513
+ * the child's environment, since argv is visible in process listings
514
+ * and in the refused-spawn error.
515
515
  *
516
516
  * ## Layered defense
517
517
  *
@@ -13,16 +13,14 @@ export default async function syncDbTypesFiles(connectionName) {
13
13
  const dbFilename = dbTypesFilenameForConnection(connectionName);
14
14
  const dbSyncFilePath = path.join(dreamPath('types'), dbFilename);
15
15
  const absoluteDbSyncPath = path.join(dreamApp.projectRoot, dbSyncFilePath);
16
+ const password = await driverClass.codegenPassword(dbConf.password);
16
17
  await CliFileWriter.cache(absoluteDbSyncPath);
17
18
  const lowLevelDbOpts = dreamApp.dbCredentialsFor(connectionName);
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}` : ''}`;
19
+ // Pass the URL only in the child's environment. A URL in argv exposes the
20
+ // password in the process command line and DreamCLI's refused-spawn error.
21
+ const userinfo = `${encodeURIComponent(dbConf.user)}${password ? `:${encodeURIComponent(password)}` : ''}`;
24
22
  const url = `${driverClass.syncDialect}://${userinfo}@${dbConf.host}:${dbConf.port}/${dbConf.name}`;
25
- const args = [`--dialect=${driverClass.syncDialect}`, `--url=${url}`];
23
+ const args = [`--dialect=${driverClass.syncDialect}`, '--url=env(DATABASE_URL)'];
26
24
  if (lowLevelDbOpts?.tableIncludePattern) {
27
25
  args.push(`--include-pattern=${lowLevelDbOpts.tableIncludePattern}`);
28
26
  }
@@ -32,6 +30,7 @@ export default async function syncDbTypesFiles(connectionName) {
32
30
  args.push(`--out-file=${absoluteDbSyncPath}`);
33
31
  await DreamCLI.spawn('kysely-codegen', {
34
32
  args,
33
+ env: { ...process.env, DATABASE_URL: url },
35
34
  onStdout: message => {
36
35
  DreamCLI.logger.logContinueProgress(colorize(`[db]`, { color: 'greenBright' }) + ' ' + message, {
37
36
  logPrefixColor: 'greenBright',
@@ -626,6 +626,13 @@ export default class QueryDriverBase {
626
626
  static get syncDialect() {
627
627
  return 'postgres';
628
628
  }
629
+ /** @internal Resolve the credential used by the database type generator. */
630
+ static codegenPassword(password) {
631
+ if (typeof password === 'function') {
632
+ throw new Error('This database adapter does not support a password provider for type generation');
633
+ }
634
+ return Promise.resolve(password);
635
+ }
629
636
  /**
630
637
  * Returns the sql that would be executed by this Query
631
638
  *
@@ -71,6 +71,7 @@ import softDeleteDream from '../internal/softDeleteDream.js';
71
71
  import sqlResultToDreamInstance from '../internal/sqlResultToDreamInstance.js';
72
72
  import Query from '../Query.js';
73
73
  import QueryDriverBase from './Base.js';
74
+ import { PasswordProviderPoolClient } from './helpers/pg/loadPgClient.js';
74
75
  import checkForNeedToBeRunMigrations from './helpers/kysely/checkForNeedToBeRunMigrations.js';
75
76
  import foreignKeyTypeFromPrimaryKey from './helpers/kysely/foreignKeyTypeFromPrimaryKey.js';
76
77
  import runMigration from './helpers/kysely/runMigration.js';
@@ -103,7 +104,10 @@ export default class KyselyQueryDriver extends QueryDriverBase {
103
104
  // Spread pg passthrough first; Dream's resolved fields follow and
104
105
  // always win (per-connection database name, TLS directive).
105
106
  ...(connectionConf.pg ?? {}),
107
+ Client: typeof connectionConf.password === 'function' ? PasswordProviderPoolClient : undefined,
106
108
  user: connectionConf.user || '',
109
+ // pg accepts a callback here and resolves it for each new client when
110
+ // the server requests password authentication.
107
111
  password: connectionConf.password || '',
108
112
  database: DreamApp.getOrFail().dbName(connectionName, dbConnectionType),
109
113
  host: connectionConf.host || 'localhost',
@@ -227,6 +231,9 @@ export default class KyselyQueryDriver extends QueryDriverBase {
227
231
  static get syncDialect() {
228
232
  return 'postgres';
229
233
  }
234
+ static async codegenPassword(password) {
235
+ return typeof password === 'function' ? await password() : password;
236
+ }
230
237
  /**
231
238
  * @internal
232
239
  *
@@ -332,7 +332,13 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
332
332
  keepAlive: true,
333
333
  keepAliveInitialDelayMillis: LOCK_CONNECTION_KEEPALIVE_INITIAL_DELAY_MS,
334
334
  });
335
- await client.connect();
335
+ try {
336
+ await client.connect();
337
+ }
338
+ catch (err) {
339
+ await client.end().catch(() => undefined);
340
+ throw err;
341
+ }
336
342
  return {
337
343
  async tryAcquire(namespace, index) {
338
344
  const result = await client.query('SELECT pg_try_advisory_lock($1::int4, $2::int4) AS locked', [
@@ -8,6 +8,28 @@
8
8
  // @ts-ignore
9
9
  import pg from 'pg';
10
10
  import DreamApp from '../../../../dream-app/index.js';
11
+ export class PasswordProviderPoolClient extends pg.Client {
12
+ connect(callback) {
13
+ if (callback) {
14
+ super.connect(err => {
15
+ if (err)
16
+ this.destroyConnectionStream();
17
+ callback(err);
18
+ });
19
+ }
20
+ else {
21
+ return super.connect().catch(err => {
22
+ this.destroyConnectionStream();
23
+ throw err;
24
+ });
25
+ }
26
+ }
27
+ destroyConnectionStream() {
28
+ // pg reports a rejected password provider without closing its socket.
29
+ ;
30
+ this.connection?.stream.destroy();
31
+ }
32
+ }
11
33
  export default async function loadPgClient({ connectionName, useSystemDb, }) {
12
34
  const dreamconf = DreamApp.getOrFail();
13
35
  const creds = dreamconf.dbCredentialsFor(connectionName)?.primary;
@@ -20,6 +42,12 @@ export default async function loadPgClient({ connectionName, useSystemDb, }) {
20
42
  user: creds.user,
21
43
  password: creds.password,
22
44
  });
23
- await client.connect();
45
+ try {
46
+ await client.connect();
47
+ }
48
+ catch (err) {
49
+ await client.end().catch(() => undefined);
50
+ throw err;
51
+ }
24
52
  return client;
25
53
  }
@@ -508,10 +508,10 @@ ${INDENT}Examples: User, Place, Room/Bedroom, Settings/CommunicationPreferences`
508
508
  * (package.json scripts, CLI argv, scaffold templates) — never from
509
509
  * runtime request input or any other untrusted external source.
510
510
  *
511
- * Argv-form is the safe choice for any caller that interpolates a
512
- * config value, path, or credential: a database password containing
513
- * `$` or backticks is passed literally to the child rather than
514
- * interpreted by a shell.
511
+ * Argv-form passes config values and paths literally to the child,
512
+ * without interpreting shell metacharacters. Credentials belong in
513
+ * the child's environment, since argv is visible in process listings
514
+ * and in the refused-spawn error.
515
515
  *
516
516
  * ## Layered defense
517
517
  *
@@ -13,16 +13,14 @@ export default async function syncDbTypesFiles(connectionName) {
13
13
  const dbFilename = dbTypesFilenameForConnection(connectionName);
14
14
  const dbSyncFilePath = path.join(dreamPath('types'), dbFilename);
15
15
  const absoluteDbSyncPath = path.join(dreamApp.projectRoot, dbSyncFilePath);
16
+ const password = await driverClass.codegenPassword(dbConf.password);
16
17
  await CliFileWriter.cache(absoluteDbSyncPath);
17
18
  const lowLevelDbOpts = dreamApp.dbCredentialsFor(connectionName);
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}` : ''}`;
19
+ // Pass the URL only in the child's environment. A URL in argv exposes the
20
+ // password in the process command line and DreamCLI's refused-spawn error.
21
+ const userinfo = `${encodeURIComponent(dbConf.user)}${password ? `:${encodeURIComponent(password)}` : ''}`;
24
22
  const url = `${driverClass.syncDialect}://${userinfo}@${dbConf.host}:${dbConf.port}/${dbConf.name}`;
25
- const args = [`--dialect=${driverClass.syncDialect}`, `--url=${url}`];
23
+ const args = [`--dialect=${driverClass.syncDialect}`, '--url=env(DATABASE_URL)'];
26
24
  if (lowLevelDbOpts?.tableIncludePattern) {
27
25
  args.push(`--include-pattern=${lowLevelDbOpts.tableIncludePattern}`);
28
26
  }
@@ -32,6 +30,7 @@ export default async function syncDbTypesFiles(connectionName) {
32
30
  args.push(`--out-file=${absoluteDbSyncPath}`);
33
31
  await DreamCLI.spawn('kysely-codegen', {
34
32
  args,
33
+ env: { ...process.env, DATABASE_URL: url },
35
34
  onStdout: message => {
36
35
  DreamCLI.logger.logContinueProgress(colorize(`[db]`, { color: 'greenBright' }) + ' ' + message, {
37
36
  logPrefixColor: 'greenBright',
@@ -626,6 +626,13 @@ export default class QueryDriverBase {
626
626
  static get syncDialect() {
627
627
  return 'postgres';
628
628
  }
629
+ /** @internal Resolve the credential used by the database type generator. */
630
+ static codegenPassword(password) {
631
+ if (typeof password === 'function') {
632
+ throw new Error('This database adapter does not support a password provider for type generation');
633
+ }
634
+ return Promise.resolve(password);
635
+ }
629
636
  /**
630
637
  * Returns the sql that would be executed by this Query
631
638
  *
@@ -71,6 +71,7 @@ import softDeleteDream from '../internal/softDeleteDream.js';
71
71
  import sqlResultToDreamInstance from '../internal/sqlResultToDreamInstance.js';
72
72
  import Query from '../Query.js';
73
73
  import QueryDriverBase from './Base.js';
74
+ import { PasswordProviderPoolClient } from './helpers/pg/loadPgClient.js';
74
75
  import checkForNeedToBeRunMigrations from './helpers/kysely/checkForNeedToBeRunMigrations.js';
75
76
  import foreignKeyTypeFromPrimaryKey from './helpers/kysely/foreignKeyTypeFromPrimaryKey.js';
76
77
  import runMigration from './helpers/kysely/runMigration.js';
@@ -103,7 +104,10 @@ export default class KyselyQueryDriver extends QueryDriverBase {
103
104
  // Spread pg passthrough first; Dream's resolved fields follow and
104
105
  // always win (per-connection database name, TLS directive).
105
106
  ...(connectionConf.pg ?? {}),
107
+ Client: typeof connectionConf.password === 'function' ? PasswordProviderPoolClient : undefined,
106
108
  user: connectionConf.user || '',
109
+ // pg accepts a callback here and resolves it for each new client when
110
+ // the server requests password authentication.
107
111
  password: connectionConf.password || '',
108
112
  database: DreamApp.getOrFail().dbName(connectionName, dbConnectionType),
109
113
  host: connectionConf.host || 'localhost',
@@ -227,6 +231,9 @@ export default class KyselyQueryDriver extends QueryDriverBase {
227
231
  static get syncDialect() {
228
232
  return 'postgres';
229
233
  }
234
+ static async codegenPassword(password) {
235
+ return typeof password === 'function' ? await password() : password;
236
+ }
230
237
  /**
231
238
  * @internal
232
239
  *
@@ -332,7 +332,13 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
332
332
  keepAlive: true,
333
333
  keepAliveInitialDelayMillis: LOCK_CONNECTION_KEEPALIVE_INITIAL_DELAY_MS,
334
334
  });
335
- await client.connect();
335
+ try {
336
+ await client.connect();
337
+ }
338
+ catch (err) {
339
+ await client.end().catch(() => undefined);
340
+ throw err;
341
+ }
336
342
  return {
337
343
  async tryAcquire(namespace, index) {
338
344
  const result = await client.query('SELECT pg_try_advisory_lock($1::int4, $2::int4) AS locked', [
@@ -8,6 +8,28 @@
8
8
  // @ts-ignore
9
9
  import pg from 'pg';
10
10
  import DreamApp from '../../../../dream-app/index.js';
11
+ export class PasswordProviderPoolClient extends pg.Client {
12
+ connect(callback) {
13
+ if (callback) {
14
+ super.connect(err => {
15
+ if (err)
16
+ this.destroyConnectionStream();
17
+ callback(err);
18
+ });
19
+ }
20
+ else {
21
+ return super.connect().catch(err => {
22
+ this.destroyConnectionStream();
23
+ throw err;
24
+ });
25
+ }
26
+ }
27
+ destroyConnectionStream() {
28
+ // pg reports a rejected password provider without closing its socket.
29
+ ;
30
+ this.connection?.stream.destroy();
31
+ }
32
+ }
11
33
  export default async function loadPgClient({ connectionName, useSystemDb, }) {
12
34
  const dreamconf = DreamApp.getOrFail();
13
35
  const creds = dreamconf.dbCredentialsFor(connectionName)?.primary;
@@ -20,6 +42,12 @@ export default async function loadPgClient({ connectionName, useSystemDb, }) {
20
42
  user: creds.user,
21
43
  password: creds.password,
22
44
  });
23
- await client.connect();
45
+ try {
46
+ await client.connect();
47
+ }
48
+ catch (err) {
49
+ await client.end().catch(() => undefined);
50
+ throw err;
51
+ }
24
52
  return client;
25
53
  }
@@ -8,8 +8,9 @@ export type SpawnOptions = Omit<NodeSpawnOptions, 'shell'> & {
8
8
  * Argv elements passed to the child as separate arguments. Defaults to `[]`.
9
9
  * Each entry is passed literally — shell meta-characters (`$`, backticks,
10
10
  * `&&`, spaces, etc.) inside any element are not interpreted by a shell.
11
- * This is the right shape for any caller that interpolates a path or
12
- * credential.
11
+ * This is the right shape for paths and other non-secret values. Pass
12
+ * credentials through the child's environment so they do not appear in
13
+ * the process command line or a refused-spawn error.
13
14
  */
14
15
  args?: string[];
15
16
  };
@@ -103,10 +104,10 @@ export default class DreamCLI {
103
104
  * (package.json scripts, CLI argv, scaffold templates) — never from
104
105
  * runtime request input or any other untrusted external source.
105
106
  *
106
- * Argv-form is the safe choice for any caller that interpolates a
107
- * config value, path, or credential: a database password containing
108
- * `$` or backticks is passed literally to the child rather than
109
- * interpreted by a shell.
107
+ * Argv-form passes config values and paths literally to the child,
108
+ * without interpreting shell metacharacters. Credentials belong in
109
+ * the child's environment, since argv is visible in process listings
110
+ * and in the refused-spawn error.
110
111
  *
111
112
  * ## Layered defense
112
113
  *
@@ -1,4 +1,5 @@
1
1
  import { CompiledQuery, DeleteQueryBuilder, SelectQueryBuilder, UpdateQueryBuilder } from 'kysely';
2
+ import type { DreamDbConfig } from '../../dream-app/index.js';
2
3
  import Dream from '../../Dream.js';
3
4
  import { SchemaBuilderAssociationData, SchemaBuilderColumnData } from '../../helpers/cli/ASTBuilder.js';
4
5
  import { AssociationStatement } from '../../types/associations/shared.js';
@@ -480,6 +481,8 @@ export default class QueryDriverBase<DreamInstance extends Dream> {
480
481
  */
481
482
  static destroyDream(dream: Dream, txn: DreamTransaction<Dream>, reallyDestroy: boolean): Promise<number>;
482
483
  static get syncDialect(): string;
484
+ /** @internal Resolve the credential used by the database type generator. */
485
+ static codegenPassword(password: DreamDbConfig['password']): Promise<string>;
483
486
  /**
484
487
  * Returns the sql that would be executed by this Query
485
488
  *
@@ -44,6 +44,7 @@ export default class KyselyQueryDriver<DreamInstance extends Dream> extends Quer
44
44
  schemaOnly?: boolean;
45
45
  }): Promise<void>;
46
46
  static get syncDialect(): string;
47
+ static codegenPassword(password: DreamDbConfig['password']): Promise<string>;
47
48
  /**
48
49
  * @internal
49
50
  *
@@ -1,4 +1,9 @@
1
1
  import pg from 'pg';
2
+ export declare class PasswordProviderPoolClient extends pg.Client {
3
+ connect(): Promise<void>;
4
+ connect(callback: (err: Error) => void): void;
5
+ private destroyConnectionStream;
6
+ }
2
7
  export default function loadPgClient({ connectionName, useSystemDb, }: {
3
8
  connectionName: string;
4
9
  useSystemDb?: boolean;
@@ -288,7 +288,17 @@ export interface DreamDbCredentialOptions {
288
288
  type UnicodeNormalizationForm = 'NFC' | 'NFD' | 'none';
289
289
  export interface DreamDbConfig {
290
290
  user: string;
291
- password: string;
291
+ /**
292
+ * A fixed password, or a provider for Dream's built-in PostgreSQL driver.
293
+ * PostgreSQL calls the provider when the server requests password
294
+ * authentication for a new physical connection. The provider may return a
295
+ * string or a promise of one. Each such connection gets a fresh result;
296
+ * existing pooled connections keep their credential. A rejected provider
297
+ * fails that connection attempt and reaches its caller; Dream does not cache
298
+ * credentials or retry. Custom query drivers receive the configured value
299
+ * and must handle or reject a provider themselves.
300
+ */
301
+ password: string | (() => string | Promise<string>);
292
302
  host: string;
293
303
  name: string;
294
304
  port: number;
@@ -1 +1 @@
1
- window.hierarchyData = "eJyVk0FPwyAUx7/LO+McuLCVo1tMjHFqYvSw7MDaN0tGYQE60yz77gZ3wTktO0HJ+/P7Aa97cNYGD2JB+YgRSumSgMO1xjIoazyIPVBK42BkgyDgpUXXzZzaobuVHoHARpkKBGUTAq3TIKDU0nv019VqcFI9qEOjgRwrQEDw1VWMXx0XCJS10pVDA2LBimJ5IMCKIqE/dB51l+zaw/9Vf4kBvWGTqBDHxOHZ+vDh0OdbnEn0eEQqH/KEOq2x3Eyt8cFJZcKbslrGJ/oPjc5Z5wd/RXMc2Ch1sLptzNMO3Vrbzxzyj0AOb8QS3kwGeW9K22xlUCuN7yrUcW0lPd4p1FWGQu8eF3UEH3ISL4VQzimhfPzdo/HjRPu12+Lx9HH2qHwjQ1ln+p4P51zfOP1Z5jbMW60vaZXTSB/zcPgCAPBvFw=="
1
+ window.hierarchyData = "eJyVk8FOAjEQht9lzgUpCwvboxATY0RNjB4Ih7I7uA3dlnS6mA3h3U3lsiK65dR2Mv/8X6fTAzhrPYFY8nQ8YZzzFQOHG425V9YQiANwzsNiZIUg4KVG18yd2qO7lYTAYKtMAYIPpwxqp0FAriUR0k2x7p9l90tfaWCnDBDgqegFee8UYJCXShcODYhlMhitjgySwajl/tAQ6qZVtcP/V/41BDxJpgEhrC2GZ0v+wyHFU1xQdHAE13TY7vusxHw7s4a8k8r4N2W1DE/0nzU6Zx31/5LGMCRZm8HqujJPe3QbbT9jnH8IYvzGk5bfXHp5b3Jb7aRXa43vypchtpaEdwp1EYHQWeOqiUiHnIWmMJ5OUsbT7HtGw+EM+7XZ4en2YfeoqJI+LyN5L4tj2pe1P8vC+kWt9TWjci7p8jwevwDboG8X"