@everystack/cli 0.2.12 → 0.2.13
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 +6 -1
- package/src/cli/commands/secrets.ts +270 -0
- package/src/cli/commands/update.ts +29 -1
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/secrets.ts +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "CLI and OTA updates for Expo apps on everystack",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"publishConfig": {
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@aws-sdk/client-cloudwatch-logs": "3.1053.0",
|
|
55
55
|
"@aws-sdk/client-lambda": "3.1053.0",
|
|
56
56
|
"@aws-sdk/client-s3": "3.1053.0",
|
|
57
|
+
"@aws-sdk/client-ssm": "3.1053.0",
|
|
57
58
|
"@aws-sdk/s3-request-presigner": "3.1053.0",
|
|
58
59
|
"drizzle-orm": "0.41.0",
|
|
59
60
|
"expo-updates": "55.0.21",
|
|
@@ -73,6 +74,9 @@
|
|
|
73
74
|
"@aws-sdk/client-lambda": {
|
|
74
75
|
"optional": true
|
|
75
76
|
},
|
|
77
|
+
"@aws-sdk/client-ssm": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
76
80
|
"expo-updates": {
|
|
77
81
|
"optional": true
|
|
78
82
|
},
|
|
@@ -90,6 +94,7 @@
|
|
|
90
94
|
"@aws-sdk/client-cloudwatch-logs": "3.1053.0",
|
|
91
95
|
"@aws-sdk/client-lambda": "3.1053.0",
|
|
92
96
|
"@aws-sdk/client-s3": "3.1053.0",
|
|
97
|
+
"@aws-sdk/client-ssm": "3.1053.0",
|
|
93
98
|
"@aws-sdk/s3-request-presigner": "3.1053.0",
|
|
94
99
|
"@types/jest": "29.5.14",
|
|
95
100
|
"@types/node": "22.19.18",
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets management — read/write SST secrets in AWS SSM Parameter Store.
|
|
3
|
+
*
|
|
4
|
+
* SST v3 stores secrets at: /sst/{appName}/{stage}/Secret/{name}/value
|
|
5
|
+
* These are SecureString parameters encrypted with the default KMS key.
|
|
6
|
+
*
|
|
7
|
+
* Full interop with `sst secret set/list` — same SSM paths, same encryption.
|
|
8
|
+
* Secrets set via `everystack secrets set` are readable by SST and vice versa.
|
|
9
|
+
*
|
|
10
|
+
* Requires IAM permissions: ssm:GetParametersByPath, ssm:GetParameter,
|
|
11
|
+
* ssm:PutParameter, ssm:DeleteParameter, kms:Decrypt, kms:Encrypt.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { step, success, fail, info, warn } from '../output.js';
|
|
15
|
+
|
|
16
|
+
interface SecretsContext {
|
|
17
|
+
appName: string;
|
|
18
|
+
stage: string;
|
|
19
|
+
region: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function resolveContext(flags: Record<string, string>): Promise<SecretsContext> {
|
|
23
|
+
const stage = flags.stage;
|
|
24
|
+
if (!stage) {
|
|
25
|
+
fail('--stage is required for secrets management');
|
|
26
|
+
info('Usage: everystack secrets list --stage <name>');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { parseAppName } = await import('../discover.js');
|
|
31
|
+
const appName = await parseAppName();
|
|
32
|
+
const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
|
|
33
|
+
|
|
34
|
+
return { appName, stage, region };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ssmPath(ctx: SecretsContext, name?: string): string {
|
|
38
|
+
const base = `/sst/${ctx.appName}/${ctx.stage}/Secret`;
|
|
39
|
+
return name ? `${base}/${name}/value` : `${base}/`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function getSSM(region: string) {
|
|
43
|
+
const { SSMClient } = await import('@aws-sdk/client-ssm');
|
|
44
|
+
return new SSMClient({ region });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// List
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
async function listSecrets(ctx: SecretsContext): Promise<void> {
|
|
52
|
+
step(`Loading secrets for ${ctx.appName}/${ctx.stage}...`);
|
|
53
|
+
|
|
54
|
+
const client = await getSSM(ctx.region);
|
|
55
|
+
const { GetParametersByPathCommand } = await import('@aws-sdk/client-ssm');
|
|
56
|
+
|
|
57
|
+
const names: string[] = [];
|
|
58
|
+
let nextToken: string | undefined;
|
|
59
|
+
|
|
60
|
+
do {
|
|
61
|
+
const response = await client.send(
|
|
62
|
+
new GetParametersByPathCommand({
|
|
63
|
+
Path: ssmPath(ctx),
|
|
64
|
+
Recursive: true,
|
|
65
|
+
WithDecryption: false, // Don't decrypt — we only need names
|
|
66
|
+
NextToken: nextToken,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
for (const param of response.Parameters || []) {
|
|
71
|
+
if (!param.Name) continue;
|
|
72
|
+
const parts = param.Name.split('/');
|
|
73
|
+
const secretIndex = parts.indexOf('Secret');
|
|
74
|
+
if (secretIndex >= 0 && secretIndex + 1 < parts.length) {
|
|
75
|
+
names.push(parts[secretIndex + 1]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
nextToken = response.NextToken;
|
|
80
|
+
} while (nextToken);
|
|
81
|
+
|
|
82
|
+
if (names.length === 0) {
|
|
83
|
+
info('No secrets found');
|
|
84
|
+
info(`Set one with: everystack secrets set KEY "value" --stage ${ctx.stage}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
success(`${names.length} secret(s):`);
|
|
89
|
+
for (const name of names.sort()) {
|
|
90
|
+
console.log(` ${name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Get
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
async function getSecret(ctx: SecretsContext, name: string): Promise<void> {
|
|
99
|
+
step(`Reading ${name}...`);
|
|
100
|
+
|
|
101
|
+
const client = await getSSM(ctx.region);
|
|
102
|
+
const { GetParameterCommand } = await import('@aws-sdk/client-ssm');
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await client.send(
|
|
106
|
+
new GetParameterCommand({
|
|
107
|
+
Name: ssmPath(ctx, name),
|
|
108
|
+
WithDecryption: true,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (response.Parameter?.Value !== undefined) {
|
|
113
|
+
console.log(response.Parameter.Value);
|
|
114
|
+
} else {
|
|
115
|
+
fail(`Secret "${name}" has no value`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
if (err.name === 'ParameterNotFound') {
|
|
120
|
+
fail(`Secret "${name}" not found in stage "${ctx.stage}"`);
|
|
121
|
+
info(`Set it with: everystack secrets set ${name} "value" --stage ${ctx.stage}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Set
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
async function setSecret(ctx: SecretsContext, name: string, value: string): Promise<void> {
|
|
133
|
+
step(`Setting ${name}...`);
|
|
134
|
+
|
|
135
|
+
const client = await getSSM(ctx.region);
|
|
136
|
+
const { PutParameterCommand } = await import('@aws-sdk/client-ssm');
|
|
137
|
+
|
|
138
|
+
await client.send(
|
|
139
|
+
new PutParameterCommand({
|
|
140
|
+
Name: ssmPath(ctx, name),
|
|
141
|
+
Value: value,
|
|
142
|
+
Type: 'SecureString',
|
|
143
|
+
Overwrite: true,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
success(`${name} set for stage "${ctx.stage}"`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Remove
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
async function removeSecret(ctx: SecretsContext, name: string): Promise<void> {
|
|
155
|
+
step(`Removing ${name}...`);
|
|
156
|
+
|
|
157
|
+
const client = await getSSM(ctx.region);
|
|
158
|
+
const { DeleteParameterCommand } = await import('@aws-sdk/client-ssm');
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await client.send(
|
|
162
|
+
new DeleteParameterCommand({
|
|
163
|
+
Name: ssmPath(ctx, name),
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
success(`${name} removed from stage "${ctx.stage}"`);
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
if (err.name === 'ParameterNotFound') {
|
|
169
|
+
warn(`Secret "${name}" not found in stage "${ctx.stage}" (already removed?)`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Command entry point
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
export async function secretsCommand(
|
|
181
|
+
subcommand: string | undefined,
|
|
182
|
+
positionalArgs: string[],
|
|
183
|
+
flags: Record<string, string>,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
186
|
+
printSecretsHelp();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let ctx: SecretsContext;
|
|
191
|
+
try {
|
|
192
|
+
ctx = await resolveContext(flags);
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
fail(err.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
switch (subcommand) {
|
|
200
|
+
case 'list':
|
|
201
|
+
await listSecrets(ctx);
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'get': {
|
|
205
|
+
const name = positionalArgs[0];
|
|
206
|
+
if (!name) {
|
|
207
|
+
fail('Missing secret name');
|
|
208
|
+
info('Usage: everystack secrets get <name> --stage <stage>');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
await getSecret(ctx, name);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case 'set': {
|
|
216
|
+
const name = positionalArgs[0];
|
|
217
|
+
const value = positionalArgs[1];
|
|
218
|
+
if (!name || value === undefined) {
|
|
219
|
+
fail('Missing secret name or value');
|
|
220
|
+
info('Usage: everystack secrets set <name> <value> --stage <stage>');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
await setSecret(ctx, name, value);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case 'remove': {
|
|
228
|
+
const name = positionalArgs[0];
|
|
229
|
+
if (!name) {
|
|
230
|
+
fail('Missing secret name');
|
|
231
|
+
info('Usage: everystack secrets remove <name> --stage <stage>');
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
await removeSecret(ctx, name);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
default:
|
|
239
|
+
fail(`Unknown secrets subcommand: ${subcommand}`);
|
|
240
|
+
printSecretsHelp();
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
} catch (err: any) {
|
|
244
|
+
fail(err.message);
|
|
245
|
+
info('Ensure your IAM user/role has ssm:* and kms:Decrypt permissions.');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function printSecretsHelp(): void {
|
|
251
|
+
console.log(`
|
|
252
|
+
everystack secrets — Manage SST secrets (SSM Parameter Store)
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
everystack secrets list --stage <name> List all secret names
|
|
256
|
+
everystack secrets get <name> --stage <name> Print a secret value
|
|
257
|
+
everystack secrets set <name> <value> --stage <name> Set or update a secret
|
|
258
|
+
everystack secrets remove <name> --stage <name> Delete a secret
|
|
259
|
+
|
|
260
|
+
Examples:
|
|
261
|
+
everystack secrets list --stage production
|
|
262
|
+
everystack secrets set MAPBOX_API_KEY "pk.abc123" --stage production
|
|
263
|
+
everystack secrets get JWT_SECRET --stage dev
|
|
264
|
+
everystack secrets remove OLD_KEY --stage dev
|
|
265
|
+
|
|
266
|
+
Interop:
|
|
267
|
+
Secrets are stored at the same SSM path as \`sst secret set\`.
|
|
268
|
+
Both tools read and write the same values — full interop.
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
@@ -9,6 +9,7 @@ import { resolveConfig, type CliConfig } from '../config.js';
|
|
|
9
9
|
import { uploadToS3, invokeAction } from '../aws.js';
|
|
10
10
|
import { step, success, warn, fail, info } from '../output.js';
|
|
11
11
|
import { exportApp } from '../utils/export.js';
|
|
12
|
+
import { loadSstSecrets } from '../utils/secrets.js';
|
|
12
13
|
import { walkDirectory } from '../utils/walk.js';
|
|
13
14
|
|
|
14
15
|
export interface UpdateFlags {
|
|
@@ -24,7 +25,7 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
24
25
|
// Signal publish context so app.config.js can detect it's not a dev session.
|
|
25
26
|
// Without this, dotenv guards that skip .env.local for EAS_BUILD/EAS_UPDATE
|
|
26
27
|
// don't know to skip for everystack update, causing local dev overrides
|
|
27
|
-
// (e.g.
|
|
28
|
+
// (e.g. localhost overrides → LAN IP) to leak into the manifest.
|
|
28
29
|
process.env.EVERYSTACK_UPDATE = '1';
|
|
29
30
|
|
|
30
31
|
// Resolve ENVIRONMENT before app.config.js evaluation so the manifest
|
|
@@ -58,6 +59,33 @@ export async function updateCommand(flags: UpdateFlags & Record<string, string>)
|
|
|
58
59
|
} catch {
|
|
59
60
|
// Stage may not be deployed yet — HOST_URL falls back to loadSstOutputs
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
// Load SST secrets for this stage and inject into process.env.
|
|
64
|
+
// This replaces .env.local during `everystack update` — same pattern
|
|
65
|
+
// as EAS injecting stored secrets during `eas update`.
|
|
66
|
+
// app.config.js merges { ...easInfo, ...process.env }, so these win.
|
|
67
|
+
try {
|
|
68
|
+
step('Loading SST secrets...');
|
|
69
|
+
const { parseAppName } = await import('../discover.js');
|
|
70
|
+
const appName = await parseAppName();
|
|
71
|
+
const region = resolvedConfig?.region
|
|
72
|
+
|| process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
|
|
73
|
+
const secrets = await loadSstSecrets({ appName, stage: flags.stage, region });
|
|
74
|
+
const names = Object.keys(secrets);
|
|
75
|
+
if (names.length > 0) {
|
|
76
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
77
|
+
process.env[key] ||= value;
|
|
78
|
+
}
|
|
79
|
+
success(`Loaded ${names.length} secrets: ${names.join(', ')}`);
|
|
80
|
+
} else {
|
|
81
|
+
info('No SST secrets found for this stage');
|
|
82
|
+
}
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
// Secret loading is best-effort — the user may not have SSM permissions
|
|
85
|
+
// or may not use SST secrets (e.g. env vars set manually).
|
|
86
|
+
warn(`Could not load SST secrets: ${err.message}`);
|
|
87
|
+
info('Set secrets with: pnpm sst secret set KEY "value" --stage ' + flags.stage);
|
|
88
|
+
}
|
|
61
89
|
}
|
|
62
90
|
|
|
63
91
|
const branch = flags.branch;
|
package/src/cli/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { cachePurgeCommand } from './commands/cache.js';
|
|
|
12
12
|
import { diagCommand } from './commands/diag.js';
|
|
13
13
|
import { analyzeSSRCommand } from './commands/analyze.js';
|
|
14
14
|
import { logsErrorsCommand, logsTailCommand, logsQueryCommand } from './commands/logs.js';
|
|
15
|
+
import { secretsCommand } from './commands/secrets.js';
|
|
15
16
|
import { fail } from './output.js';
|
|
16
17
|
|
|
17
18
|
const args = process.argv.slice(2);
|
|
@@ -138,6 +139,22 @@ async function main() {
|
|
|
138
139
|
case 'logs:query':
|
|
139
140
|
await logsQueryCommand(flags);
|
|
140
141
|
break;
|
|
142
|
+
case 'secrets': {
|
|
143
|
+
// secrets <subcommand> [positional...] [--flags]
|
|
144
|
+
// Extract positional args: everything after subcommand that isn't a flag or flag value
|
|
145
|
+
const sub = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
|
|
146
|
+
const positional: string[] = [];
|
|
147
|
+
for (let i = 2; i < args.length; i++) {
|
|
148
|
+
if (args[i].startsWith('-')) {
|
|
149
|
+
// Skip flag and its value
|
|
150
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
positional.push(args[i]);
|
|
154
|
+
}
|
|
155
|
+
await secretsCommand(sub, positional, flags);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
141
158
|
default:
|
|
142
159
|
fail(`Unknown command: ${command}`);
|
|
143
160
|
printHelp();
|
|
@@ -175,6 +192,10 @@ Usage:
|
|
|
175
192
|
everystack channels list
|
|
176
193
|
everystack channels create --name <name> [--branch <name>]
|
|
177
194
|
everystack channels edit --name <name> --branch <name>
|
|
195
|
+
everystack secrets list --stage <name> List all secret names
|
|
196
|
+
everystack secrets get <name> --stage <name> Print a secret value
|
|
197
|
+
everystack secrets set <name> <value> --stage <name> Set or update a secret
|
|
198
|
+
everystack secrets remove <name> --stage <name> Delete a secret
|
|
178
199
|
|
|
179
200
|
Stage resolution:
|
|
180
201
|
--stage <name> Discover AWS resources for the given stage by querying AWS APIs.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load SST secrets from AWS SSM Parameter Store.
|
|
3
|
+
*
|
|
4
|
+
* SST v3 stores secrets at: /sst/{appName}/{stage}/Secret/{name}/value
|
|
5
|
+
* These are SecureString parameters encrypted with the default KMS key.
|
|
6
|
+
*
|
|
7
|
+
* The CLI fetches all secrets for a stage and returns them as a flat
|
|
8
|
+
* key-value map. The caller injects them into process.env before
|
|
9
|
+
* expo export, so app.config.js picks them up via `...process.env`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface LoadSecretsOptions {
|
|
13
|
+
appName: string;
|
|
14
|
+
stage: string;
|
|
15
|
+
region: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetch all SST secrets for a given app + stage from SSM Parameter Store.
|
|
20
|
+
*
|
|
21
|
+
* Returns a map of { SECRET_NAME: decrypted_value }.
|
|
22
|
+
* Paginates automatically — SSM returns max 10 parameters per page.
|
|
23
|
+
*
|
|
24
|
+
* Requires IAM permissions: ssm:GetParametersByPath, kms:Decrypt.
|
|
25
|
+
*/
|
|
26
|
+
export async function loadSstSecrets(
|
|
27
|
+
options: LoadSecretsOptions,
|
|
28
|
+
): Promise<Record<string, string>> {
|
|
29
|
+
const { SSMClient, GetParametersByPathCommand } = await import(
|
|
30
|
+
'@aws-sdk/client-ssm'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const client = new SSMClient({ region: options.region });
|
|
34
|
+
const prefix = `/sst/${options.appName}/${options.stage}/Secret/`;
|
|
35
|
+
const secrets: Record<string, string> = {};
|
|
36
|
+
|
|
37
|
+
let nextToken: string | undefined;
|
|
38
|
+
do {
|
|
39
|
+
const response = await client.send(
|
|
40
|
+
new GetParametersByPathCommand({
|
|
41
|
+
Path: prefix,
|
|
42
|
+
WithDecryption: true,
|
|
43
|
+
Recursive: true,
|
|
44
|
+
NextToken: nextToken,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
for (const param of response.Parameters || []) {
|
|
49
|
+
if (!param.Name || param.Value === undefined) continue;
|
|
50
|
+
// Path: /sst/{app}/{stage}/Secret/{NAME}/value → extract NAME
|
|
51
|
+
const parts = param.Name.split('/');
|
|
52
|
+
// parts: ['', 'sst', app, stage, 'Secret', NAME, 'value']
|
|
53
|
+
const secretIndex = parts.indexOf('Secret');
|
|
54
|
+
if (secretIndex >= 0 && secretIndex + 1 < parts.length) {
|
|
55
|
+
const name = parts[secretIndex + 1];
|
|
56
|
+
secrets[name] = param.Value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
nextToken = response.NextToken;
|
|
61
|
+
} while (nextToken);
|
|
62
|
+
|
|
63
|
+
return secrets;
|
|
64
|
+
}
|