@@ -86,6 +86,7 @@ export default {
8686 const route = routeWorkerRequest ( request )
8787 const coordinator = env . WORDPRESS_STATE . getByName ( "default" )
8888 if ( route . kind === "operator-reset" ) return resetCanonicalWordPress ( request , env , coordinator )
89+ if ( route . kind === "operator-restore" ) return restoreCanonicalWordPress ( request , env , coordinator )
8990 if ( route . kind === "probe" ) {
9091 return runBootProbe ( route . phase , env . WORDPRESS_STATE_BUCKET )
9192 }
@@ -148,6 +149,46 @@ async function resetCanonicalWordPress(request: Request, env: Env, coordinator:
148149 return response
149150}
150151
152+ async function restoreCanonicalWordPress ( request : Request , env : Env , coordinator : DurableObjectStub ) : Promise < Response > {
153+ if ( request . method !== "POST" ) return new Response ( "Canonical restore requires POST." , { status : 405 } )
154+ const authorization = request . headers . get ( "authorization" )
155+ if ( ! env . WORDPRESS_OPERATOR_TOKEN || ! authorization || ! await secretsMatch ( authorization , `Bearer ${ env . WORDPRESS_OPERATOR_TOKEN } ` ) ) {
156+ return new Response ( "Canonical restore authorization failed." , { status : 401 } )
157+ }
158+ let pointer : MarkdownPointer
159+ try {
160+ pointer = await request . json < MarkdownPointer > ( )
161+ } catch {
162+ return new Response ( "Canonical restore requires a JSON pointer." , { status : 400 } )
163+ }
164+ if ( ! isCanonicalRestorePointer ( pointer ) ) return new Response ( "Canonical restore pointer is invalid." , { status : 400 } )
165+ const manifest = await readMarkdownManifest ( env . WORDPRESS_STATE_BUCKET , pointer )
166+ if ( ! manifest || manifest . revision !== pointer . revision || manifest . manifestKey !== pointer . manifestKey || manifest . persistedAt !== pointer . persistedAt || ! Array . isArray ( manifest . files ) ) {
167+ return new Response ( "Canonical restore manifest is unavailable or inconsistent." , { status : 409 } )
168+ }
169+ const lease = await acquireLease ( coordinator , request . url )
170+ try {
171+ if ( lease . pointer ) {
172+ await abortLease ( coordinator , request . url , lease )
173+ return new Response ( "Canonical restore requires an empty current pointer." , { status : 409 } )
174+ }
175+ const restored = await commitLease ( coordinator , request . url , lease , pointer )
176+ await discardCachedRuntime ( )
177+ return Response . json ( { restored : true , ...restored } )
178+ } catch ( error ) {
179+ await abortLease ( coordinator , request . url , lease )
180+ throw error
181+ }
182+ }
183+
184+ function isCanonicalRestorePointer ( pointer : unknown ) : pointer is MarkdownPointer {
185+ if ( ! pointer || typeof pointer !== "object" ) return false
186+ const candidate = pointer as Partial < MarkdownPointer >
187+ return typeof candidate . revision === "string" && / ^ [ a - f 0 - 9 - ] { 36 } $ / . test ( candidate . revision )
188+ && candidate . manifestKey === `sites/default/markdown/revisions/${ candidate . revision } .json`
189+ && typeof candidate . persistedAt === "string" && Number . isFinite ( Date . parse ( candidate . persistedAt ) )
190+ }
191+
151192async function secretsMatch ( left : string , right : string ) : Promise < boolean > {
152193 const encoder = new TextEncoder ( )
153194 const [ leftHash , rightHash ] = await Promise . all ( [ crypto . subtle . digest ( "SHA-256" , encoder . encode ( left ) ) , crypto . subtle . digest ( "SHA-256" , encoder . encode ( right ) ) ] )
0 commit comments