@bostonuniversity/buwp-local 0.4.1 → 0.5.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/lib/config.js CHANGED
@@ -7,6 +7,7 @@ import fs from 'fs';
7
7
  import path from 'path';
8
8
  import chalk from 'chalk';
9
9
  import dotenv from 'dotenv';
10
+ import * as keychain from './keychain.js';
10
11
 
11
12
  const CONFIG_FILE_NAME = '.buwp-local.json';
12
13
  const ENV_FILE_NAME = '.env.local';
@@ -283,6 +284,97 @@ function sanitizeProjectName(name) {
283
284
  .replace(/^-+|-+$/g, ''); // Remove leading/trailing dashes
284
285
  }
285
286
 
287
+ /**
288
+ * Load credentials from macOS keychain
289
+ * Returns all available credentials if present, with platform and keychain support checks
290
+ * @returns {object} Credentials object or empty object if not supported
291
+ */
292
+ export function loadKeychainCredentials() {
293
+ // Only load on macOS where keychain is supported
294
+ if (!keychain.isPlatformSupported()) {
295
+ return {};
296
+ }
297
+
298
+ const credentials = {};
299
+
300
+ // Try to load each known credential from keychain
301
+ for (const key of keychain.CREDENTIAL_KEYS) {
302
+ const value = keychain.getCredential(key);
303
+ if (value !== null) {
304
+ credentials[key] = value;
305
+ }
306
+ }
307
+
308
+ return credentials;
309
+ }
310
+
311
+ /**
312
+ * Create a secure temporary .env file from credentials
313
+ * File is created with restrictive permissions (600) in /tmp
314
+ * @param {object} credentials - Credential key-value pairs
315
+ * @param {string} projectName - Project name for unique filename
316
+ * @returns {string} Path to created temp file
317
+ */
318
+ export function createSecureTempEnvFile(credentials, projectName) {
319
+ const timestamp = Date.now();
320
+ const tempPath = path.join('/tmp', `.buwp-local-${projectName}-${timestamp}.env`);
321
+
322
+ // Build env file content with proper escaping for multiline values
323
+ const lines = Object.entries(credentials).map(([key, value]) => {
324
+ // Escape special characters in values
325
+ // Order matters: backslashes first, then quotes, then newlines
326
+ const escaped = String(value)
327
+ .replace(/\\/g, '\\\\') // Escape backslashes first (\ -> \\)
328
+ .replace(/"/g, '\\"') // Escape double quotes (" -> \")
329
+ .replace(/\n/g, '\\n'); // Escape actual newlines (\n -> \n literal in file)
330
+
331
+ return `${key}="${escaped}"`;
332
+ });
333
+
334
+ const content = lines.join('\n') + '\n';
335
+
336
+ try {
337
+ // Atomic create with strict permissions: user read/write only (600)
338
+ // 'wx' means create exclusive (fail if exists) and writable
339
+ const fd = fs.openSync(tempPath, 'wx', 0o600);
340
+ fs.writeSync(fd, content);
341
+ fs.closeSync(fd);
342
+
343
+ return tempPath;
344
+ } catch (err) {
345
+ throw new Error(`Failed to create secure temp env file: ${err.message}`);
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Securely delete a temporary env file
351
+ * Overwrites file contents with zeros before deletion to prevent recovery
352
+ * @param {string} filePath - Path to temp file to delete
353
+ * @returns {boolean} True if deleted successfully, false if error (non-fatal)
354
+ */
355
+ export function secureDeleteTempEnvFile(filePath) {
356
+ try {
357
+ // Check if file exists
358
+ if (!fs.existsSync(filePath)) {
359
+ return true; // Already gone, consider success
360
+ }
361
+
362
+ // Get file size and overwrite with zeros (secure deletion)
363
+ const stat = fs.statSync(filePath);
364
+ const fd = fs.openSync(filePath, 'r+');
365
+ fs.writeSync(fd, Buffer.alloc(stat.size, 0));
366
+ fs.closeSync(fd);
367
+
368
+ // Now delete the file
369
+ fs.unlinkSync(filePath);
370
+ return true;
371
+ } catch (err) {
372
+ // Log warning but don't fail - temp files are ephemeral anyway
373
+ console.warn(chalk.yellow(`⚠️ Warning: Could not securely delete temp file ${filePath}: ${err.message}`));
374
+ return false;
375
+ }
376
+ }
377
+
286
378
  export {
287
379
  loadConfig,
288
380
  validateConfig,
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import * as config from './config.js';
7
7
  import * as composeGenerator from './compose-generator.js';
8
+ import * as keychain from './keychain.js';
8
9
 
9
10
  export const loadConfig = config.loadConfig;
10
11
  export const validateConfig = config.validateConfig;
@@ -14,3 +15,17 @@ export const generateComposeFile = composeGenerator.generateComposeFile;
14
15
  export const CONFIG_FILE_NAME = config.CONFIG_FILE_NAME;
15
16
  export const ENV_FILE_NAME = config.ENV_FILE_NAME;
16
17
  export const DEFAULT_CONFIG = config.DEFAULT_CONFIG;
18
+
19
+ // Keychain exports
20
+ export const setCredential = keychain.setCredential;
21
+ export const getCredential = keychain.getCredential;
22
+ export const hasCredential = keychain.hasCredential;
23
+ export const listCredentials = keychain.listCredentials;
24
+ export const clearAllCredentials = keychain.clearAllCredentials;
25
+ export const isPlatformSupported = keychain.isPlatformSupported;
26
+ export const isMultilineCredential = keychain.isMultilineCredential;
27
+ export const parseCredentialsFile = keychain.parseCredentialsFile;
28
+ export const CREDENTIAL_KEYS = keychain.CREDENTIAL_KEYS;
29
+ export const CREDENTIAL_GROUPS = keychain.CREDENTIAL_GROUPS;
30
+ export const CREDENTIAL_DESCRIPTIONS = keychain.CREDENTIAL_DESCRIPTIONS;
31
+ export const MULTILINE_CREDENTIALS = keychain.MULTILINE_CREDENTIALS;
@@ -0,0 +1,337 @@
1
+ /**
2
+ * macOS Keychain integration for secure credential storage
3
+ * Uses the `security` command-line tool to interact with macOS Keychain
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import fs from 'fs';
8
+
9
+ // Keychain service name for all buwp-local credentials
10
+ const KEYCHAIN_SERVICE = 'buwp-local';
11
+
12
+ /**
13
+ * All credential keys that can be stored in keychain
14
+ */
15
+ export const CREDENTIAL_KEYS = [
16
+ 'WORDPRESS_DB_PASSWORD',
17
+ 'DB_ROOT_PASSWORD',
18
+ 'SP_ENTITY_ID',
19
+ 'IDP_ENTITY_ID',
20
+ 'SHIB_IDP_LOGOUT',
21
+ 'SHIB_SP_KEY',
22
+ 'SHIB_SP_CERT',
23
+ 'S3_UPLOADS_BUCKET',
24
+ 'S3_UPLOADS_REGION',
25
+ 'S3_UPLOADS_ACCESS_KEY_ID',
26
+ 'S3_UPLOADS_SECRET_ACCESS_KEY',
27
+ 'S3_ACCESS_RULES_TABLE',
28
+ 'OLAP',
29
+ 'OLAP_ACCT_NBR',
30
+ 'OLAP_REGION'
31
+ ];
32
+
33
+ /**
34
+ * Credentials organized by functional group
35
+ */
36
+ export const CREDENTIAL_GROUPS = {
37
+ database: ['WORDPRESS_DB_PASSWORD', 'DB_ROOT_PASSWORD'],
38
+ shibboleth: ['SP_ENTITY_ID', 'IDP_ENTITY_ID', 'SHIB_IDP_LOGOUT', 'SHIB_SP_KEY', 'SHIB_SP_CERT'],
39
+ s3: ['S3_UPLOADS_BUCKET', 'S3_UPLOADS_REGION', 'S3_UPLOADS_ACCESS_KEY_ID', 'S3_UPLOADS_SECRET_ACCESS_KEY', 'S3_ACCESS_RULES_TABLE'],
40
+ olap: ['OLAP', 'OLAP_ACCT_NBR', 'OLAP_REGION']
41
+ };
42
+
43
+ /**
44
+ * Credentials that contain multiline content (cryptographic keys, certificates)
45
+ */
46
+ export const MULTILINE_CREDENTIALS = ['SHIB_SP_KEY', 'SHIB_SP_CERT'];
47
+
48
+ /**
49
+ * Human-readable descriptions for each credential
50
+ */
51
+ export const CREDENTIAL_DESCRIPTIONS = {
52
+ WORDPRESS_DB_PASSWORD: 'WordPress database password',
53
+ DB_ROOT_PASSWORD: 'Database root password',
54
+ SP_ENTITY_ID: 'Shibboleth Service Provider Entity ID',
55
+ IDP_ENTITY_ID: 'Shibboleth Identity Provider Entity ID',
56
+ SHIB_IDP_LOGOUT: 'Shibboleth IDP logout URL',
57
+ SHIB_SP_KEY: 'Shibboleth Service Provider private key (multiline)',
58
+ SHIB_SP_CERT: 'Shibboleth Service Provider certificate (multiline)',
59
+ S3_UPLOADS_BUCKET: 'S3 bucket name',
60
+ S3_UPLOADS_REGION: 'S3 region (e.g., us-east-1)',
61
+ S3_UPLOADS_ACCESS_KEY_ID: 'AWS access key ID',
62
+ S3_UPLOADS_SECRET_ACCESS_KEY: 'AWS secret access key',
63
+ S3_ACCESS_RULES_TABLE: 'S3 access rules table name',
64
+ OLAP: 'OLAP name',
65
+ OLAP_ACCT_NBR: 'OLAP account number',
66
+ OLAP_REGION: 'OLAP region'
67
+ };
68
+
69
+ /**
70
+ * Check if the current platform supports keychain operations
71
+ * @returns {boolean} True if macOS, false otherwise
72
+ */
73
+ export function isPlatformSupported() {
74
+ return process.platform === 'darwin';
75
+ }
76
+
77
+ /**
78
+ * Validate that a credential key is recognized
79
+ * @param {string} key - Credential key to validate
80
+ * @returns {boolean} True if valid
81
+ */
82
+ export function isValidCredentialKey(key) {
83
+ return CREDENTIAL_KEYS.includes(key);
84
+ }
85
+
86
+ /**
87
+ * Get the group name for a credential key
88
+ * @param {string} key - Credential key
89
+ * @returns {string|null} Group name or null if not found
90
+ */
91
+ export function getCredentialGroup(key) {
92
+ for (const [groupName, keys] of Object.entries(CREDENTIAL_GROUPS)) {
93
+ if (keys.includes(key)) {
94
+ return groupName;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Check if a credential contains multiline content
102
+ * @param {string} key - Credential key
103
+ * @returns {boolean} True if credential is multiline
104
+ */
105
+ export function isMultilineCredential(key) {
106
+ return MULTILINE_CREDENTIALS.includes(key);
107
+ }
108
+
109
+ /**
110
+ * Set a credential in the macOS keychain
111
+ * Uses -U flag to update existing entries instead of creating duplicates
112
+ * @param {string} key - Credential key (e.g., 'WORDPRESS_DB_PASSWORD')
113
+ * @param {string} value - Credential value
114
+ * @throws {Error} If platform is not supported or operation fails
115
+ */
116
+ export function setCredential(key, value) {
117
+ if (!isPlatformSupported()) {
118
+ throw new Error('Keychain operations are only supported on macOS');
119
+ }
120
+
121
+ if (!isValidCredentialKey(key)) {
122
+ throw new Error(`Invalid credential key: ${key}`);
123
+ }
124
+
125
+ try {
126
+ // Use -U flag to update if exists, create if doesn't
127
+ // -w flag allows password on command line (needed for automation)
128
+ execSync(
129
+ `security add-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" -w "${value}" -U`,
130
+ { stdio: 'pipe' }
131
+ );
132
+ } catch (err) {
133
+ throw new Error(`Failed to store credential in keychain: ${err.message}`);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get a credential from the macOS keychain
139
+ * @param {string} key - Credential key
140
+ * @returns {string|null} Credential value or null if not found
141
+ * @throws {Error} If platform is not supported
142
+ */
143
+ export function getCredential(key) {
144
+ if (!isPlatformSupported()) {
145
+ throw new Error('Keychain operations are only supported on macOS');
146
+ }
147
+
148
+ if (!isValidCredentialKey(key)) {
149
+ throw new Error(`Invalid credential key: ${key}`);
150
+ }
151
+
152
+ try {
153
+ // -w flag returns only the password (no other metadata)
154
+ let result = execSync(
155
+ `security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" -w`,
156
+ { stdio: 'pipe', encoding: 'utf8' }
157
+ );
158
+ result = result.trim();
159
+
160
+ // Handle hex-encoded values (legacy format for multiline credentials)
161
+ // Check if the value looks like hex (only hex digits, even length)
162
+ if (isMultilineCredential(key) && isHexEncoded(result)) {
163
+ try {
164
+ result = Buffer.from(result, 'hex').toString('utf8');
165
+ } catch (err) {
166
+ // If hex decode fails, return original value
167
+ console.warn(`Warning: Could not decode hex-encoded credential ${key}`);
168
+ }
169
+ }
170
+
171
+ return result;
172
+ } catch (err) {
173
+ // Credential not found
174
+ if (err.message.includes('could not be found')) {
175
+ return null;
176
+ }
177
+ throw new Error(`Failed to retrieve credential from keychain: ${err.message}`);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Check if a string appears to be hex-encoded
183
+ * @param {string} value - Value to check
184
+ * @returns {boolean} True if value looks like hex
185
+ */
186
+ function isHexEncoded(value) {
187
+ // Must be even length and only contain hex digits
188
+ return /^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0;
189
+ }
190
+
191
+ /**
192
+ * Check if a credential exists in the keychain
193
+ * @param {string} key - Credential key
194
+ * @returns {boolean} True if credential exists
195
+ */
196
+ export function hasCredential(key) {
197
+ if (!isPlatformSupported()) {
198
+ return false;
199
+ }
200
+
201
+ if (!isValidCredentialKey(key)) {
202
+ return false;
203
+ }
204
+
205
+ try {
206
+ execSync(
207
+ `security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}"`,
208
+ { stdio: 'pipe' }
209
+ );
210
+ return true;
211
+ } catch (err) {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Delete a credential from the keychain
218
+ * @param {string} key - Credential key
219
+ * @throws {Error} If platform is not supported or operation fails
220
+ */
221
+ export function deleteCredential(key) {
222
+ if (!isPlatformSupported()) {
223
+ throw new Error('Keychain operations are only supported on macOS');
224
+ }
225
+
226
+ if (!isValidCredentialKey(key)) {
227
+ throw new Error(`Invalid credential key: ${key}`);
228
+ }
229
+
230
+ try {
231
+ execSync(
232
+ `security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}"`,
233
+ { stdio: 'pipe' }
234
+ );
235
+ } catch (err) {
236
+ // Ignore if credential doesn't exist
237
+ if (!err.message.includes('could not be found')) {
238
+ throw new Error(`Failed to delete credential from keychain: ${err.message}`);
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * List all stored credentials (returns keys only, not values)
245
+ * @returns {string[]} Array of credential keys that are stored
246
+ */
247
+ export function listCredentials() {
248
+ if (!isPlatformSupported()) {
249
+ return [];
250
+ }
251
+
252
+ const storedKeys = [];
253
+
254
+ for (const key of CREDENTIAL_KEYS) {
255
+ if (hasCredential(key)) {
256
+ storedKeys.push(key);
257
+ }
258
+ }
259
+
260
+ return storedKeys;
261
+ }
262
+
263
+ /**
264
+ * Clear all buwp-local credentials from keychain
265
+ * @returns {number} Number of credentials deleted
266
+ */
267
+ export function clearAllCredentials() {
268
+ if (!isPlatformSupported()) {
269
+ throw new Error('Keychain operations are only supported on macOS');
270
+ }
271
+
272
+ let deletedCount = 0;
273
+
274
+ for (const key of CREDENTIAL_KEYS) {
275
+ try {
276
+ deleteCredential(key);
277
+ deletedCount++;
278
+ } catch (err) {
279
+ // Continue deleting others even if one fails
280
+ }
281
+ }
282
+
283
+ return deletedCount;
284
+ }
285
+
286
+ /**
287
+ * Parse credentials from a JSON file
288
+ * Expected format: { "credentials": { "KEY": "value", ... }, "version": "1.0", ... }
289
+ * @param {string} filePath - Path to credentials JSON file
290
+ * @returns {object} { parsed, unknown, metadata }
291
+ */
292
+ export function parseCredentialsFile(filePath) {
293
+ // Read and parse JSON file
294
+ let data;
295
+ try {
296
+ const content = fs.readFileSync(filePath, 'utf8');
297
+ data = JSON.parse(content);
298
+ } catch (err) {
299
+ if (err.code === 'ENOENT') {
300
+ throw new Error(`File not found: ${filePath}`);
301
+ } else if (err instanceof SyntaxError) {
302
+ throw new Error(`Invalid JSON format: ${err.message}`);
303
+ }
304
+ throw new Error(`Failed to read file: ${err.message}`);
305
+ }
306
+
307
+ // Validate structure
308
+ if (!data.credentials || typeof data.credentials !== 'object') {
309
+ throw new Error('Invalid credentials file format: missing "credentials" object');
310
+ }
311
+
312
+ // Filter and validate keys
313
+ const parsed = {};
314
+ const unknown = [];
315
+
316
+ for (const [key, value] of Object.entries(data.credentials)) {
317
+ if (CREDENTIAL_KEYS.includes(key)) {
318
+ // Validate value is non-empty string
319
+ if (typeof value === 'string' && value.trim().length > 0) {
320
+ parsed[key] = value;
321
+ } else {
322
+ unknown.push({ key, reason: 'empty or invalid value' });
323
+ }
324
+ } else {
325
+ unknown.push({ key, reason: 'unknown credential key' });
326
+ }
327
+ }
328
+
329
+ // Extract metadata
330
+ const metadata = {
331
+ version: data.version || 'unknown',
332
+ source: data.source || 'unknown',
333
+ exported: data.exported || null
334
+ };
335
+
336
+ return { parsed, unknown, metadata };
337
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bostonuniversity/buwp-local",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Local WordPress development environment for Boston University projects",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "echo \"Error: no test specified\" && exit 1",
12
- "buwp-local": "node bin/buwp-local.js start",
12
+ "buwp-local": "node bin/buwp-local.js",
13
13
  "lint": "eslint ."
14
14
  },
15
15
  "keywords": [
package/readme.md CHANGED
@@ -1,3 +1,82 @@
1
1
  # BU WordPress Local Development
2
2
 
3
3
  This repository contains resources and instructions for setting up a local WordPress development environment for Boston University projects. It uses the BU WordPress container image and provides the additional resoures needed to run it locally with Docker.
4
+
5
+
6
+ ## Quickstart for plugin or theme development
7
+
8
+ 1. **Install Docker**: Make sure you have [Docker Desktop](https://www.docker.com/products/docker-desktop) installed and running on your machine.
9
+
10
+ 2. Login to GitHub Packages to access the BU WordPress Docker image (you will need a GitHub access token with `read:packages` scope):
11
+
12
+ ```bash
13
+ echo YOUR_GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
14
+ ```
15
+
16
+ 3. **Install buwp-local CLI**: Install the `buwp-local` CLI tool in your project directory:
17
+
18
+ ```bash
19
+ npm install @bostonuniversity/buwp-local --save-dev
20
+ ```
21
+
22
+ 4. **One time credential keychain setup**: Install credentials in the macOS Keychain for secure storage (optional, macOS only right now):
23
+
24
+ First, download a credentials JSON file through ssh from the dev server:
25
+
26
+ ```bash
27
+ scp user@devserver:/path/to/buwp-local-credentials.json ~/Downloads/
28
+ ```
29
+
30
+ Then run the setup command:
31
+
32
+ ```bash
33
+ npx buwp-local keychain setup --file ~/Downloads/buwp-local-credentials.json
34
+ ```
35
+
36
+ This will store all necessary credentials in your Keychain for future use, for this project and any other buwp-local projects (Keychain is global). (The global Keychain can also be overridden by `.env.local` files in each project.)
37
+
38
+ 5. **Initialize your project**: Run the interactive setup to create your `.buwp-local.json` configuration file:
39
+
40
+ ```bash
41
+ npx buwp-local init
42
+ ```
43
+
44
+ This will guide you through setting up your project name, hostname, port mappings, volume mappings, and service options.
45
+
46
+ 6. **Setup local hostname**: Add your project's local hostname (e.g. `myproject.local`) to your `/etc/hosts` file
47
+
48
+ 7. **Start your local environment**:
49
+
50
+ ```bash
51
+ npx buwp-local start
52
+ ```
53
+
54
+ This will read your configuration, load credentials from Keychain (or `.env.local` if present), and start the Docker containers for your WordPress project.
55
+
56
+ Your local WordPress site should now be accessible at the hostname you configured (e.g. `http://myproject.local`).
57
+
58
+ ## Basic Local setup
59
+
60
+
61
+ 1. **Setup local user**: Create a local WordPress user and add it to the super admin role:
62
+
63
+ If running with Shibboleth enabled, you can set up a local WordPress user with super admin privileges:
64
+
65
+ Create the user:
66
+ ```bash
67
+ npx buwp-local wp user create username username@bu.edu --role=administrator
68
+ ```
69
+
70
+ Promote to super admin:
71
+ ```bash
72
+ npx buwp-local wp super-admin add username@bu.edu
73
+ ```
74
+ 2. Pull snapshot site content:
75
+
76
+ You can pull a snapshot of the production or staging site database and media files into your local environment for testing and development.
77
+
78
+ ```bash
79
+ npx buwp-local wp site-manager snapshot-pull --source=https://www.bu.edu/admissions/ --destination=http://myproject.local/admissions
80
+ ```
81
+
82
+ This will download the latest snapshot from the specified source and import it into your local WordPress environment.
package/.buwp-local.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "projectName": "buwp-local",
3
- "image": "ghcr.io/bu-ist/bu-wp-docker-mod_shib:arm64-latest",
4
- "hostname": "jaydub.local",
5
- "multisite": true,
6
- "services": {
7
- "redis": true,
8
- "s3proxy": true,
9
- "shibboleth": true
10
- },
11
- "ports": {
12
- "http": 80,
13
- "https": 443,
14
- "db": 3306,
15
- "redis": 6379
16
- },
17
- "mappings": [],
18
- "env": {
19
- "WP_DEBUG": true,
20
- "XDEBUG": false
21
- }
22
- }
@@ -1,57 +0,0 @@
1
- ## Plan: Environment-Based Credentials Migration (v0.4.1)
2
-
3
- We've successfully shipped v0.4.0 with interactive init, sandbox support, configurable Docker images, and smart hostname defaults. The codebase is in excellent shape with real-world testing validated in both plugin and sandbox scenarios.
4
-
5
- **Current Problem:** Credentials (database passwords, AWS keys, Shibboleth certs) are currently being read from the config/environment and then **written directly into the generated `docker-compose.yml`**. This means sensitive data sits in a generated file, which isn't ideal for security.
6
-
7
- **Better Approach:** Use Docker Compose's native environment variable interpolation (`${VAR_NAME}`) so credentials stay in `.env.local` and never get written to the compose file.
8
-
9
- ### Steps
10
-
11
- 1. **Update `compose-generator.js` to use environment variable references**
12
- - Database service: Change from `MYSQL_PASSWORD: dbPassword` to `MYSQL_PASSWORD: '${WORDPRESS_DB_PASSWORD:-password}'`
13
- - WordPress service: Change from `WORDPRESS_DB_PASSWORD: config.db?.password` to `WORDPRESS_DB_PASSWORD: '${WORDPRESS_DB_PASSWORD:-password}'`
14
- - S3 proxy service: Change from `AWS_ACCESS_KEY_ID: config.s3?.accessKeyId` to `AWS_ACCESS_KEY_ID: '${S3_UPLOADS_ACCESS_KEY_ID}'`
15
- - WordPress `WORDPRESS_CONFIG_EXTRA`: Change from injecting actual S3 keys to using `${S3_UPLOADS_ACCESS_KEY_ID}` references
16
-
17
- 2. **Update `start.js` to pass `.env.local` to docker compose**
18
- - Add `--env-file` flag to `docker compose up` command
19
- - Ensure `.env.local` path is correctly resolved relative to project
20
-
21
- 3. **Update config loading to remove credential reading**
22
- - Keep `extractEnvVars()` for backward compatibility but mark as deprecated
23
- - Config validation no longer needs to check for credential presence in config object
24
- - Document that credentials should only live in `.env.local`
25
-
26
- 4. **Update documentation**
27
- - `USAGE.md` - Clarify that `.env.local` is the source of truth for credentials
28
- - Add example showing compose file will have `${VAR}` references
29
- - Security best practices section emphasizes this approach
30
-
31
- 5. **Test migration path**
32
- - Verify existing projects with credentials in config still work (backward compat)
33
- - Test new projects using only `.env.local`
34
- - Verify generated compose file contains variable references, not actual values
35
- - Ensure `docker compose` properly reads `.env.local`
36
-
37
- ### Further Considerations
38
-
39
- 1. **Backward compatibility**: Should we warn users if credentials exist in config? Or silently prefer `.env.local`?
40
- 2. **`.env.local` template**: Should we create `.env.local.example` during `init` command?
41
- 3. **Validation**: Should `config --validate` check that `.env.local` exists and has required variables?
42
-
43
- ---
44
-
45
- **Why This Matters:**
46
- - **Security**: Credentials never written to files, only referenced
47
- - **Git safety**: Generated compose files are safe to accidentally commit (no secrets)
48
- - **Best practices**: Aligns with Docker Compose's standard env var pattern
49
- - **Simplicity**: Reduces complexity in config merging logic
50
-
51
- **Effort Estimate:** Low-Medium (4-6 hours)
52
- - Compose generation changes: ~2 hours
53
- - Start command changes: ~1 hour
54
- - Documentation: ~1 hour
55
- - Testing: ~1-2 hours
56
-
57
- **Ready to proceed?** This sets up a cleaner foundation before tackling Phase 2 features like keychain integration.