Setting the file. One moment.
Wix Auth · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page function authExpiredError
— line 130
This file
Number 45.165
Position 165 of 182
Type JavaScript
Size 13 KB
Lines 326 lib/ wix-auth.js
JavaScript · 326 lines · 13 KB
const
{
execFile
}
=
require
(
'node:child_process'
);
12 const { readEnvFile } = require ( './config-env.js' );
13
14 const WIX_AUTH_EXPIRED = 'wix_auth_expired' ;
15 const WIX_AUTH_EXIT_CODE = 75 ;
16 // Site tokens live for about four hours. Renew with roughly thirty minutes left, or once the
17 // token has been held for about three and a half hours, so a long batch does not start with a
18 // credential that is likely to expire before its records finish.
19 const RENEW_WITHIN_MS = 30 * 60 * 1000 ;
20 const MAX_TOKEN_AGE_MS = 3.5 * 60 * 60 * 1000 ;
21 const RENEWAL_WINDOW_MS = 30 * 60 * 1000 ;
22 const MAX_RENEWALS_PER_WINDOW = 5 ;
23 const MINT_TIMEOUT_MS = 120 * 1000 ;
24
25 const REASONS = Object. freeze ({
26 mint_failed: 'scripts/mint-token.sh did not produce a token' ,
27 token_missing: 'config/wix.env has no WIX_AUTH_TOKEN' ,
28 config_unreadable: 'config/wix.env could not be read' ,
29 site_mismatch: 'config/wix.env names a different site than this run' ,
30 still_rejected: 'Wix refused the request again after the token was renewed' ,
31 renewal_limit: 'the token was renewed too often in a short time' ,
32 });
33 const AUTH_EXPIRED_REASON = 'auth_expired' ;
34
35 const OPERATOR_ACTION = 'run `npx --no-install wix whoami` (log in if asked), then `bash scripts/mint-token.sh` in the migration project, answer the wix_auth_expired action, and resume the import; do not re-run from scratch' ;
36
37 class WixAuthInterruption extends Error {
38 constructor ( reason , { httpStatus } = {}) {
39 super ( `${ WIX_AUTH_EXPIRED }: ${ REASONS [ reason ] || reason }. Requires Import Operator: ${ OPERATOR_ACTION }.` );
40 this .name = 'WixAuthInterruption' ;
41 this .code = WIX_AUTH_EXPIRED ;
42 this .interruption = true ;
43 this .reason = reason;
44 // A refused or never-sent request was not applied, but outcome code must not read this as a
45 // target decision about the record: it carries no `status`, so no 4xx branch can claim it.
46 this .outcomeKnown = false ;
47 this .requestApplied = false ;
48 if (Number. isInteger (httpStatus)) this .httpStatus = httpStatus;
49 }
50 }
51
52 // A SIGTERM/SIGINT stop: the run is recording what it knows, so no new request is sent and no
53 // record outcome is invented for one that was refused here. Same latch, same handling.
54 const IMPORT_STOPPING = 'import_stopping' ;
55 class ImportStopRequested extends Error {
56 constructor ( signal ) {
57 super ( `${ IMPORT_STOPPING }: the import received ${ signal } and is recording what it knows; no new request is sent.` );
58 this .name = 'ImportStopRequested' ;
59 this .code = IMPORT_STOPPING ;
60 this .interruption = true ;
61 this .signal = signal;
62 this .outcomeKnown = false ;
63 this .requestApplied = false ;
64 }
65 }
66
67 function isRunInterruption ( error ) {
68 return Boolean (error && error.interruption === true && (error.code === WIX_AUTH_EXPIRED || error.code === IMPORT_STOPPING ));
69 }
70
71 // One latch per project in this process: once renewal fails, every client and the attempt
72 // journal refuse to go on until the Import Operator renews the token and the run resumes.
73 const latched = new Map ();
74 function interruptionFor ( projectDir ) {
75 return projectDir ? latched. get (path. resolve (projectDir)) || null : null ;
76 }
77 // Latches a stop: every token-source client then refuses locally, and the journal refuses a new
78 // `started` row. An earlier wix_auth_expired latch is kept (it is the stop's real cause).
79 function latchStop ( projectDir , signal ) {
80 const root = path. resolve (projectDir);
81 if ( ! latched. has (root)) latched. set (root, new ImportStopRequested (signal));
82 return latched. get (root);
83 }
84 // Only for a resume in the same process after the Import Operator renewed the token.
85 function clearInterruption ( projectDir ) {
86 latched. delete (path. resolve (projectDir));
87 }
88
89 function bareToken ( token ) {
90 return String (token || '' ). trim (). replace ( / ^ Bearer \s + / i , '' );
91 }
92
93 function isApiKey ( token ) {
94 return / ^ IST \. / . test ( bareToken (token));
95 }
96
97 // `OauthNG.JWS.<header>.<payload>.<signature>`: the payload's `exp` (seconds) is the only claim
98 // read. Any segment that decodes to a JSON object with a numeric `exp` counts; anything else is
99 // "unknown", which leaves only the on-refusal renewal.
100 function tokenExpiresAt ( token ) {
101 for ( const segment of bareToken (token). split ( '.' )) {
102 if (segment. length < 8 ) continue ;
103 try {
104 const claims = JSON . parse (Buffer. from (segment, 'base64url' ). toString ( 'utf8' ));
105 if (claims && typeof claims === 'object' && Number. isFinite (claims.exp)) return claims.exp * 1000 ;
106 } catch {
107 // not a JSON segment
108 }
109 }
110 return null ;
111 }
112
113 function tokenIssuedAt ( token ) {
114 for ( const segment of bareToken (token). split ( '.' )) {
115 if (segment. length < 8 ) continue ;
116 try {
117 const claims = JSON . parse (Buffer. from (segment, 'base64url' ). toString ( 'utf8' ));
118 if (claims && typeof claims === 'object' && Number. isFinite (claims.iat)) return claims.iat * 1000 ;
119 } catch {
120 // not a JSON segment
121 }
122 }
123 return null ;
124 }
125
126 function isAuthExpired ( error ) {
127 return Boolean (error && (error.authExpired === true || error.reason === AUTH_EXPIRED_REASON || error.code === AUTH_EXPIRED_REASON ));
128 }
129
130 function authExpiredError ({ httpStatus = 401 } = {}) {
131 const error = new Error ( `${ AUTH_EXPIRED_REASON }: the Wix site token was refused after one refresh; retry this record after the token is renewed` );
132 error.name = 'WixAuthExpiredError' ;
133 error.code = AUTH_EXPIRED_REASON ;
134 error.reason = AUTH_EXPIRED_REASON ;
135 error.authExpired = true ;
136 error.outcomeKnown = true ;
137 error.requestApplied = false ;
138 if (Number. isInteger (httpStatus)) error.status = httpStatus;
139 return error;
140 }
141
142 function runMintScript ( projectDir ) {
143 return new Promise (( resolve , reject ) => {
144 // stdout and stderr are discarded: the script writes the token to config/wix.env only.
145 execFile ( 'bash' , [path. join (projectDir, 'scripts' , 'mint-token.sh' )], { cwd: projectDir, timeout: MINT_TIMEOUT_MS }, ( error ) => {
146 if (error) reject (Object. assign ( new Error ( 'mint-token.sh failed' ), { exitCode: error.code }));
147 else resolve ();
148 });
149 });
150 }
151
152 // `mint(projectDir)` and `now()` are injectable for tests; the default runs the project's own
153 // scripts/mint-token.sh, which mints for config/wix.env's WIX_SITE_ID.
154 function createWixTokenSource ({ projectDir , siteId , mint = runMintScript, now = Date.now, renewWithinMs = RENEW_WITHIN_MS , maxTokenAgeMs = renewWithinMs === RENEW_WITHIN_MS ? MAX_TOKEN_AGE_MS : Infinity } = {}) {
155 if ( ! projectDir || ! siteId) throw new Error ( 'createWixTokenSource: projectDir and siteId are required' );
156 const root = path. resolve (projectDir);
157 const envPath = path. join (root, 'config' , 'wix.env' );
158 let token = null ;
159 let expiresAt = null ;
160 let issuedAt = null ;
161 let inFlight = null ;
162 // `wix token` may hand back the same cached token. Once a renewal returned a token that is
163 // still inside the renewal window, that token is used until it is refused, not renewed again
164 // on every request.
165 let keptAfterRenewal = null ;
166 const renewals = [];
167
168 function fail ( reason , extra ) {
169 const error = Object. assign ( new WixAuthInterruption (reason, extra), { siteId });
170 if ( ! latched. has (root)) latched. set (root, error);
171 return latched. get (root);
172 }
173
174 async function readSiteEnv () {
175 let env;
176 try {
177 env = await readEnvFile (envPath);
178 } catch {
179 throw fail ( 'config_unreadable' );
180 }
181 if ( String (env. WIX_SITE_ID || '' ). trim () !== siteId) throw fail ( 'site_mismatch' );
182 return env;
183 }
184
185 async function load () {
186 const value = bareToken (( await readSiteEnv ()). WIX_AUTH_TOKEN );
187 if ( ! value) throw fail ( 'token_missing' );
188 token = value;
189 expiresAt = isApiKey (value) ? null : tokenExpiresAt (value);
190 issuedAt = isApiKey (value) ? null : tokenIssuedAt (value);
191 return token;
192 }
193
194 async function renew () {
195 const interrupted = latched. get (root);
196 if (interrupted) throw interrupted;
197 if (inFlight) return inFlight;
198 inFlight = ( async () => {
199 const at = now ();
200 while (renewals. length && at - renewals[ 0 ] > RENEWAL_WINDOW_MS ) renewals. shift ();
201 if (renewals. length >= MAX_RENEWALS_PER_WINDOW ) throw fail ( 'renewal_limit' );
202 renewals. push (at);
203 // mint-token.sh mints for the site in config/wix.env: check it is this run's site first.
204 await readSiteEnv ();
205 try {
206 await mint (root);
207 } catch {
208 throw fail ( 'mint_failed' );
209 }
210 const renewed = await load ();
211 // The mint script may return the CLI's cached token. It is still the result of this
212 // bounded refresh attempt, so do not invoke the mint again on every following request;
213 // the request-refusal path will make one later retry if the CLI cache was stale.
214 keptAfterRenewal = renewed;
215 return renewed;
216 })();
217 try {
218 return await inFlight;
219 } finally {
220 inFlight = null ;
221 }
222 }
223
224 const source = {
225 siteId,
226 projectDir: root,
227 get interruption () { return latched. get (root) || null ; },
228 expiresAt : () => expiresAt,
229 // The token to send now, renewed first when it has under `renewWithinMs` left.
230 async current () {
231 const interrupted = latched. get (root);
232 if (interrupted) throw interrupted;
233 if (inFlight) return inFlight;
234 if (token === null ) await load ();
235 const age = issuedAt === null ? null : now () - issuedAt;
236 const shouldRenew = (expiresAt !== null && expiresAt - now () < renewWithinMs)
237 || (age !== null && age >= maxTokenAgeMs);
238 if (shouldRenew && token !== keptAfterRenewal) return renew ();
239 return token;
240 },
241 // True when `sent` is no longer the token in use: another request already renewed it, so a
242 // refusal of `sent` needs only a resend.
243 replaced ( sent ) {
244 return token !== null && sent !== token;
245 },
246 // A refusal of the token that was SENT is expiry when that token can renew: 401 always; 403
247 // only once that token's own `exp` has passed, because a 403 is otherwise a permission answer
248 // that a new token would not change.
249 isExpiryRefusal ( status , sent = token) {
250 if ( ! sent || isApiKey (sent)) return false ;
251 if (status === 401 ) return true ;
252 const sentExpiresAt = tokenExpiresAt (sent);
253 return status === 403 && sentExpiresAt !== null && sentExpiresAt <= now ();
254 },
255 renew,
256 fail,
257 };
258 return Object. freeze (source);
259 }
260
261 // The resumable stop. One pending manual_action per site; a still-pending one is reused.
262 const AUTH_EXPIRED_NS = 'wix-auth-expired' ;
263 function sequencedRequestId ( key , n ) {
264 const hex = crypto. createHash ( 'sha256' ). update ( `${ AUTH_EXPIRED_NS } \0 ${ key } \0 ${ n }` ). digest ( 'hex' );
265 return `uar_${ hex . slice ( 0 , 8 ) }-${ hex . slice ( 8 , 12 ) }-${ hex . slice ( 12 , 16 ) }-${ hex . slice ( 16 , 20 ) }-${ hex . slice ( 20 , 32 ) }` ;
266 }
267
268 async function recordWixAuthExpired ( projectDir , { siteId , reason } = {}) {
269 if ( ! siteId) throw new Error ( 'recordWixAuthExpired: siteId is required' );
270 const { createUserAction , readUserAction } = require ( './user-actions.js' );
271 let n = 1 ;
272 let latest = null ;
273 for (;; n += 1 ) {
274 let action = null ;
275 try {
276 action = await readUserAction (projectDir, sequencedRequestId (siteId, n));
277 } catch (error) {
278 if (error.code !== 'ENOENT' ) throw error;
279 }
280 if ( ! action) break ;
281 latest = action;
282 }
283 if (latest && latest.status === 'pending' ) return latest;
284 return createUserAction (projectDir, {
285 requestId: sequencedRequestId (siteId, n),
286 kind: 'manual_action' ,
287 stage: 'execution' ,
288 title: 'Renew the Wix site token and resume the import' ,
289 prompt: `Requires Import Operator. The import stopped with ${ WIX_AUTH_EXPIRED }${ reason && REASONS [ reason ] ? ` (${ REASONS [ reason ] })` : ''}. No record was marked failed. In the original migration environment, ${ OPERATOR_ACTION }.` ,
290 response: { mode: 'none' },
291 choices: [],
292 subject: { type: WIX_AUTH_EXPIRED , siteId },
293 });
294 }
295
296 // The exit code for a script's top-level catch: an interruption records its resumable action
297 // and exits WIX_AUTH_EXIT_CODE; anything else exits 1 as before.
298 async function stopExitCode ( error , projectDir ) {
299 if ( ! isRunInterruption (error) || error.code !== WIX_AUTH_EXPIRED || ! projectDir || ! error.siteId) return 1 ;
300 try {
301 await recordWixAuthExpired (projectDir, { siteId: error.siteId, reason: error.reason });
302 } catch (recordError) {
303 process.stderr. write ( `could not record the ${ WIX_AUTH_EXPIRED } action: ${ recordError . message } \n ` );
304 }
305 return WIX_AUTH_EXIT_CODE ;
306 }
307
308 module . exports = {
309 WIX_AUTH_EXPIRED,
310 WIX_AUTH_EXIT_CODE,
311 WixAuthInterruption,
312 IMPORT_STOPPING,
313 ImportStopRequested,
314 latchStop,
315 isRunInterruption,
316 interruptionFor,
317 clearInterruption,
318 tokenExpiresAt,
319 tokenIssuedAt,
320 AUTH_EXPIRED_REASON,
321 isAuthExpired,
322 authExpiredError,
323 createWixTokenSource,
324 recordWixAuthExpired,
325 stopExitCode,
326 };