Setting the file. One moment. Source Secrets · Wix Replatform · wix/skills · Skills Docslib/source-secrets.js
JavaScript·281 lines·8 KB
const
{
buildWixDashboardUrl
}
=
require
(
'./wix-dashboard-url.js'
);
12const { egressFetch } = require('./egress-proxy.js');
13
14const SECRET_NAME_RE = /^[A-Za-z0-9_+=@#$-]+$/;
15const DEFAULT_BASE_URL = 'https://www.wixapis.com';
16const API_PATH = '/_api/cloud-secrets-vault-server/api/v1/secrets';
17
18function authHeaderValue(token) {
19 const trimmed = String(token || '').trim();
20 if (!trimmed) {
21 throw new Error('Wix auth token is required');
22 }
23 if (/^Bearer\s+/i.test(trimmed)) return trimmed;
24 if (/^IST\./.test(trimmed)) return trimmed;
25 return `Bearer ${trimmed}`;
26}
27
28function assertSecretName(name) {
29 if (!name || typeof name !== 'string') {
30 throw new Error('secret name is required');
31 }
32 if (name.length > 50) {
33 throw new Error(`secret name "${name}" exceeds Wix's 50 character limit`);
34 }
35 if (!SECRET_NAME_RE.test(name)) {
36 throw new Error(`secret name "${name}" contains unsupported characters`);
37 }
38}
39
40function normalizeKeySpec(spec) {
41 if (typeof spec === 'string') {
42 assertSecretName(spec);
43 return { key: spec, secretName: spec };
44 }
45 if (!spec || typeof spec !== 'object' || !spec.key) {
46 throw new Error('key specs must be strings or objects with a key');
47 }
48 const key = String(spec.key);
49 const secretName = String(spec.secretName || spec.key);
50 assertSecretName(secretName);
51 return { key, secretName };
52}
53
54function isUsableSecretValue(value, placeholder = PLACEHOLDER_VALUE) {
55 return value != null && String(value).trim() !== '' && String(value) !== placeholder;
56}
57
58function safeJsonError(error) {
59 if (!error) return 'request failed';
60 return error.message || String(error);
61}
62
63function createWixSecretsClient(options = {}) {
64 const fetchImpl = options.fetchImpl || egressFetch;
65 if (!options.fetchImpl && typeof fetch !== 'function') {
66 throw new Error('fetch is unavailable; run on Node 18+ or provide fetchImpl');
67 }
68 const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
69 const authorization = authHeaderValue(options.authToken);
70 const siteId = options.siteId || null;
71
72 function headers(extra = {}) {
73 return {
74 Authorization: authorization,
75 'Content-Type': 'application/json',
76 ...(siteId ? { 'wix-site-id': siteId } : {}),
77 ...extra,
78 };
79 }
80
81 async function request(method, requestPath, body) {
82 const response = await fetchImpl(`${baseUrl}${requestPath}`, {
83 method,
84 headers: headers(),
85 body: body == null ? undefined : JSON.stringify(body),
86 });
87 const text = await response.text();
88 let json = null;
89 if (text) {
90 try {
91 json = JSON.parse(text);
92 } catch {
93 json = null;
94 }
95 }
96 if (!response.ok) {
97 const error = new Error(`Wix Secrets API ${method} ${requestPath} failed with ${response.status}`);
98 error.status = response.status;
99 error.body = json;
100 throw error;
101 }
102 return json || {};
103 }
104
105 return {
106 async listSecretInfo() {
107 const json = await request('GET', API_PATH);
108 return Array.isArray(json.secrets) ? json.secrets : [];
109 },
110 async getSecretValue(name) {
111 assertSecretName(name);
112 try {
113 const json = await request('GET', `${API_PATH}/name/${encodeURIComponent(name)}`);
114 return { exists: true, value: json.value };
115 } catch (error) {
116 if (error && error.status === 404) {
117 return { exists: false };
118 }
119 throw error;
120 }
121 },
122 async createSecret({ name, value, description }) {
123 assertSecretName(name);
124 const json = await request('POST', API_PATH, {
125 secret: {
126 name,
127 value,
128 ...(description ? { description: String(description).slice(0, 200) } : {}),
129 },
130 });
131 return { id: json.id || null };
132 },
133 };
134}
135
136async function resolveSourceSecrets(options) {
137 const envFilePath = options.envFilePath ? path.resolve(options.envFilePath) : null;
138 if (!envFilePath) {
139 throw new Error('envFilePath is required');
140 }
141 const keySpecs = (options.keys || []).map(normalizeKeySpec);
142 if (keySpecs.length === 0) {
143 throw new Error('at least one key is required');
144 }
145 const placeholder = options.placeholder || PLACEHOLDER_VALUE;
146 const createPlaceholders = options.createPlaceholders !== false;
147 const hydrate = options.hydrate !== false;
148 const project = options.project || 'migration';
149 const client = options.client;
150 if (!client) {
151 throw new Error('client is required');
152 }
153
154 let envValues = {};
155 try {
156 envValues = await readEnvFile(envFilePath);
157 } catch (error) {
158 if (!error || error.code !== 'ENOENT') {
159 throw error;
160 }
161 }
162
163 const envStatuses = statEnvValues(envValues, keySpecs.map((spec) => spec.key), { placeholder });
164 const updates = {};
165 const result = {
166 ok: true,
167 placeholder,
168 dashboardUrl: options.metaSiteId
169 ? buildWixDashboardUrl(options.metaSiteId, { path: 'developer-tools/secrets-manager' })
170 : null,
171 keys: {},
172 unresolvedKeys: [],
173 hydratedKeys: [],
174 localPresentKeys: [],
175 };
176
177 for (const spec of keySpecs) {
178 const localStatus = envStatuses[spec.key];
179 const item = {
180 key: spec.key,
181 secretName: spec.secretName,
182 localStatus,
183 status: localStatus === 'present' ? 'local_present' : 'missing',
184 };
185
186 if (localStatus === 'present') {
187 result.localPresentKeys.push(spec.key);
188 result.keys[spec.key] = item;
189 continue;
190 }
191
192 let secretRead;
193 try {
194 secretRead = await client.getSecretValue(spec.secretName);
195 } catch (error) {
196 item.status = 'unreadable';
197 item.error = safeJsonError(error);
198 result.unresolvedKeys.push(spec.key);
199 result.ok = false;
200 result.keys[spec.key] = item;
201 continue;
202 }
203
204 if (secretRead.exists && isUsableSecretValue(secretRead.value, placeholder)) {
205 if (hydrate) {
206 updates[spec.key] = secretRead.value;
207 result.hydratedKeys.push(spec.key);
208 item.status = 'hydrated';
209 } else {
210 item.status = 'secret_present';
211 }
212 result.keys[spec.key] = item;
213 continue;
214 }
215
216 if (secretRead.exists) {
217 item.status = 'placeholder';
218 result.unresolvedKeys.push(spec.key);
219 result.keys[spec.key] = item;
220 continue;
221 }
222
223 if (!createPlaceholders) {
224 item.status = 'missing';
225 result.unresolvedKeys.push(spec.key);
226 result.keys[spec.key] = item;
227 continue;
228 }
229
230 try {
231 await client.createSecret({
232 name: spec.secretName,
233 value: placeholder,
234 description: `RePlatform source credential for ${project}: ${spec.key}`,
235 });
236 item.status = 'created';
237 result.unresolvedKeys.push(spec.key);
238 } catch (error) {
239 item.status = 'failed';
240 item.error = safeJsonError(error);
241 result.unresolvedKeys.push(spec.key);
242 result.ok = false;
243 }
244 result.keys[spec.key] = item;
245 }
246
247 if (Object.keys(updates).length > 0) {
248 await upsertEnvFile(envFilePath, updates);
249 }
250
251 // Presence-only report of the local env file after this call (never values). This is the
252 // sanctioned way to confirm hydration; vault outcomes stay in result.keys[key].status.
253 result.postHydrationReport = await statEnvKeys(
254 envFilePath,
255 keySpecs.map((spec) => spec.key),
256 { placeholder },
257 );
258
259 // `ok` means every requested key is usable locally: an unresolved key is never ok, even
260 // when no vault call failed. needsUser and the per-key statuses tell the two apart.
261 result.needsUser = result.unresolvedKeys.length > 0;
262 if (result.needsUser) {
263 result.ok = false;
264 result.instruction = [
265 result.dashboardUrl ? `Open ${result.dashboardUrl}` : 'Open the site dashboard Secrets Manager.',
266 `Keys: ${result.unresolvedKeys.join(', ')}`,
267 'For each key click the 3 dots --> edit --> Retrieve value --> set the real value --> click Change secret',
268 ].join('\n');
269 }
270
271 return result;
272}
273
274module.exports = {
275 PLACEHOLDER_VALUE,
276 authHeaderValue,
277 createWixSecretsClient,
278 isUsableSecretValue,
279 normalizeKeySpec,
280 resolveSourceSecrets,
281};