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