@kirschbaum-development/sst-laravel 0.2.11 → 0.2.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirschbaum-development/sst-laravel",
3
- "version": "0.2.11",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "description": "An unofficial extension of SST to deploy containerized Laravel applications to AWS Fargate.",
6
6
  "main": "laravel-sst.ts",
@@ -1,7 +1,4 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
1
  import { CustomResourceOptions, Input, dynamic } from '@pulumi/pulumi';
4
- import { pullSecrets, toEnvFileContent } from './secrets-manager';
5
2
 
6
3
  export interface RemoteEnvFileLinkedSecret {
7
4
  name: Input<string>;
@@ -43,7 +40,9 @@ const provider: dynamic.ResourceProvider<ResolvedRemoteEnvFileInputs, ResolvedRe
43
40
 
44
41
  async diff(_, olds, news) {
45
42
  return {
46
- changes: stableStringify(olds) !== stableStringify(news),
43
+ changes:
44
+ stableStringify(olds) !== stableStringify(news) ||
45
+ !(await matchesEnvironmentFile(news)),
47
46
  };
48
47
  },
49
48
 
@@ -67,7 +66,9 @@ export class RemoteEnvFile extends dynamic.Resource {
67
66
  }
68
67
 
69
68
  async function writeRemoteEnvironmentFile(inputs: ResolvedRemoteEnvFileInputs) {
70
- const secrets = await pullSecrets(inputs.secretPath);
69
+ const fs = await import('node:fs');
70
+ const path = await import('node:path');
71
+ const secrets = await pullSecretsFromAws(inputs.secretPath);
71
72
 
72
73
  if (!secrets) {
73
74
  throw new Error(`RemoteEnvVault secret not found at ${inputs.secretPath}.`);
@@ -84,6 +85,93 @@ async function writeRemoteEnvironmentFile(inputs: ResolvedRemoteEnvFileInputs) {
84
85
  };
85
86
  }
86
87
 
88
+ async function matchesEnvironmentFile(inputs: ResolvedRemoteEnvFileInputs) {
89
+ const fs = await import('node:fs');
90
+
91
+ if (!fs.existsSync(inputs.envFilePath)) {
92
+ return false;
93
+ }
94
+
95
+ const secrets = await pullSecretsFromAws(inputs.secretPath);
96
+
97
+ if (!secrets) {
98
+ return false;
99
+ }
100
+
101
+ const expected = buildEnvFileContent(secrets, inputs) + '\n';
102
+ const actual = fs.readFileSync(inputs.envFilePath, 'utf8');
103
+
104
+ return actual === expected;
105
+ }
106
+
107
+ async function pullSecretsFromAws(secretPath: string): Promise<Record<string, string> | null> {
108
+ const secretValue = await getSecretValue(secretPath);
109
+
110
+ if (!secretValue) {
111
+ return null;
112
+ }
113
+
114
+ const data = JSON.parse(secretValue);
115
+
116
+ if (isChunkedSecret(data)) {
117
+ return pullChunkedSecrets(secretPath, data.chunks);
118
+ }
119
+
120
+ return data;
121
+ }
122
+
123
+ async function pullChunkedSecrets(basePath: string, chunkCount: number): Promise<Record<string, string>> {
124
+ const allVars: Record<string, string> = {};
125
+ const chunkPromises = Array.from({ length: chunkCount }, (_, i) =>
126
+ getSecretValue(getChunkPath(basePath, i + 1))
127
+ );
128
+
129
+ const chunkValues = await Promise.all(chunkPromises);
130
+
131
+ for (let i = 0; i < chunkValues.length; i++) {
132
+ const chunkValue = chunkValues[i];
133
+
134
+ if (chunkValue) {
135
+ Object.assign(allVars, JSON.parse(chunkValue));
136
+ } else {
137
+ console.warn(`Warning: Chunk ${i + 1} not found at ${getChunkPath(basePath, i + 1)}`);
138
+ }
139
+ }
140
+
141
+ return allVars;
142
+ }
143
+
144
+ async function getSecretValue(secretPath: string): Promise<string | null> {
145
+ const { SecretsManagerClient, GetSecretValueCommand } = await import('@aws-sdk/client-secrets-manager');
146
+ const client = new SecretsManagerClient({});
147
+
148
+ try {
149
+ const response = await client.send(new GetSecretValueCommand({
150
+ SecretId: secretPath,
151
+ }));
152
+
153
+ return response.SecretString || null;
154
+ } catch (error) {
155
+ if (isResourceNotFound(error)) {
156
+ return null;
157
+ }
158
+
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ function isChunkedSecret(data: any): data is { chunked: true; chunks: number } {
164
+ return data && typeof data === 'object' && data.chunked === true && typeof data.chunks === 'number';
165
+ }
166
+
167
+ function isResourceNotFound(error: unknown): boolean {
168
+ return !!error && typeof error === 'object' && 'name' in error && error.name === 'ResourceNotFoundException';
169
+ }
170
+
171
+ function getChunkPath(basePath: string, chunkIndex: number): string {
172
+ return `${basePath}/${chunkIndex}`;
173
+ }
174
+
87
175
  function buildEnvFileContent(
88
176
  secrets: Record<string, string>,
89
177
  inputs: ResolvedRemoteEnvFileInputs,
@@ -125,6 +213,23 @@ function buildEnvFileContent(
125
213
  ].filter(Boolean).join('\n\n');
126
214
  }
127
215
 
216
+ function toEnvFileContent(vars: Record<string, string>): string {
217
+ const sortedKeys = Object.keys(vars).sort();
218
+
219
+ return sortedKeys
220
+ .map((key) => {
221
+ const value = vars[key];
222
+
223
+ if (value.includes(' ') || value.includes('"') || value.includes("'") || value.includes('\n')) {
224
+ const escaped = value.replace(/"/g, '\\"');
225
+ return `${key}="${escaped}"`;
226
+ }
227
+
228
+ return `${key}=${value}`;
229
+ })
230
+ .join('\n');
231
+ }
232
+
128
233
  function hasOwnVariable(vars: Record<string, string>, key: string) {
129
234
  return Object.prototype.hasOwnProperty.call(vars, key);
130
235
  }