Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/deploy-strategy-switch-console.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ jobs:
const list = Array.isArray(bindings) && bindings.length > 0 && bindings.length <= 128 ? bindings : null;
const all = check => !!list && list.every(check);
const named = script?.named_handlers;
// Each valid identifier needs at least three JSON bytes (quotes
// plus one character). The existing 512 KiB response cap therefore
// bounds this list without inventing a provider limit of 32 items.
const handlerEntryLimit = Math.floor(524288 / 3);
const namedList = Array.isArray(named) && named.length <= 128 ? named : null;
const namedCategories = [];
if (namedList) for (const item of namedList) {
Expand All @@ -279,12 +283,12 @@ jobs:
date_midnight_utc: typeof runtime?.compatibility_date === 'string' && /^\d{4}-\d{2}-\d{2}T00:00:00(?:\.0{1,3})?Z$/.test(runtime.compatibility_date),
compatibility_flags: runtime?.compatibility_flags === undefined || (Array.isArray(runtime.compatibility_flags) && runtime.compatibility_flags.every(x => typeof x === 'string')),
usage_model: ['standard','bundled','unbound'].includes(runtime?.usage_model),
named_handlers: named === undefined || (Array.isArray(named) && named.length <= 128 && named.every(item => only(item,['name','handlers']) && identifier(item.name) && (item.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.length <= 32 && item.handlers.every(identifier)))) && new Set(named.map(item => item.name)).size === named.length),
named_handlers: named === undefined || (Array.isArray(named) && named.length <= 128 && named.every(item => only(item,['name','handlers']) && identifier(item.name) && (item.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.length <= handlerEntryLimit && item.handlers.every(identifier)))) && new Set(named.map(item => item.name)).size === named.length),
named_list_bounds: named === undefined || !!namedList,
named_item_keys: named === undefined || (!!namedList && namedList.every(item => only(item,['name','handlers']))),
named_name_format: named === undefined || (!!namedList && namedList.every(item => identifier(item?.name))),
named_names_unique: named === undefined || (!!namedList && new Set(namedList.map(item => item?.name)).size === namedList.length),
named_handler_list_bounds: named === undefined || (!!namedList && namedList.every(item => item?.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.length <= 32))),
named_handler_list_bounds: named === undefined || (!!namedList && namedList.every(item => item?.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.length <= handlerEntryLimit))),
named_handler_identifiers: named === undefined || (!!namedList && namedList.every(item => item?.handlers === undefined || (Array.isArray(item.handlers) && item.handlers.every(identifier)))),
binding_names: all(b => object(b) && bindingName(b.name)),
binding_names_unique: !!list && new Set(list.map(b => b?.name)).size === list.length,
Expand All @@ -303,6 +307,7 @@ jobs:
binding_container: kind(bindings), binding_count: Array.isArray(bindings) ? bindings.length : null,
binding_categories: categories.map(({ signature, ...item }) => item), script: shape(resources?.script), runtime: shape(resources?.script_runtime), contracts,
named_handler_categories: namedCategories.map(({ signature, ...item }) => item),
named_handler_max_entries: namedList ? Math.max(0, ...namedList.map(item => Array.isArray(item?.handlers) ? item.handlers.length : 0)) : null,
writes: 0, retry: false }));
} catch {
console.error(JSON.stringify({ error: 'frontend_schema_readback_failed', http_status: httpStatus, writes: 0, retry: false })); process.exitCode = 1;
Expand Down Expand Up @@ -792,11 +797,15 @@ jobs:
// not discard this field merely to accept a newer response schema.
if (script.named_handlers !== undefined) {
if (!Array.isArray(script.named_handlers) || script.named_handlers.length > 128) fail();
// A handler identifier occupies at least three JSON bytes. The
// 512 KiB transport limit bounds the complete list; 32 is not
// an API contract and rejects the existing unchanged export.
const handlerEntryLimit = Math.floor(524288 / 3);
const exports = new Set();
for (const item of script.named_handlers) {
if (!item || Object.keys(item).some(key => !['name', 'handlers'].includes(key))
|| typeof item.name !== 'string' || !/^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/.test(item.name) || exports.has(item.name)
|| (item.handlers !== undefined && (!Array.isArray(item.handlers) || item.handlers.length > 32
|| (item.handlers !== undefined && (!Array.isArray(item.handlers) || item.handlers.length > handlerEntryLimit
|| item.handlers.some(handler => typeof handler !== 'string' || !/^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/.test(handler))))) fail();
exports.add(item.name);
}
Expand Down
19 changes: 19 additions & 0 deletions tests/console_deploy_safety_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ for(const [item,flag,fieldType]of [
assert.equal(result.named_handler_categories[0].count,1);
assert.ok(!JSON.stringify(result).includes("PRIVATE_SCHEMA_SENTINEL"));
}
const largerSchema=structuredClone(validSchema);
largerSchema.result.resources.script.named_handlers[0].handlers=Array.from({length:33},(_,i)=>`handler${i}`);
const largerProjection=await schemaCase(largerSchema);
assert.equal(largerProjection.contracts.named_handlers,true);
assert.equal(largerProjection.contracts.named_handler_list_bounds,true);
assert.equal(largerProjection.named_handler_max_entries,33);

// Execute the exact selection shell with a fake Git boundary, never checkout the
// test repository or evaluate any selected Worker. Real object identities are
Expand Down Expand Up @@ -346,6 +352,19 @@ async function releaseCase({mutate=()=>{},env={},failAt=-1,responseAt=-1,respons
releaseCases++;return {calls,stdout,stderr};
}
await releaseCase();await releaseCase({env:{STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID:"e".repeat(32)}}); // H's namespace drift no longer affects inherited configuration
const handlerList = Array.from({length:33},(_,i)=>`handler${i}`);
const largerAbi = responses => {for(const i of [1,4])responses[i].result.resources.script.named_handlers[0].handlers=[...handlerList];};
await releaseCase({mutate:largerAbi});
for(const drift of [handlers=>handlers.pop(),handlers=>handlers.push("newHandler"),handlers=>{handlers[32]="changedHandler";}]){
await releaseCase({mutate:r=>{largerAbi(r);drift(r[4].result.resources.script.named_handlers[0].handlers);}},false,5);
}
await releaseCase({mutate:r=>{largerAbi(r);r[1].result.resources.script.named_handlers[0].handlers[32]=null;}},false,2);
await releaseCase({mutate:r=>{largerAbi(r);r[4].result.resources.script.named_handlers[0].PRIVATE_RELEASE_SENTINEL=true;}},false,5);
const oversizedResource=resource("old-code");
oversizedResource.script.named_handlers[0].handlers=Array.from({length:50000},(_,i)=>`handler${i}`);
const oversizedResourceJson=JSON.stringify(wrap({id:priorVersion,resources:oversizedResource}));
assert.ok(Buffer.byteLength(oversizedResourceJson)>524288);
await releaseCase({responseAt:1,response:new Response(oversizedResourceJson,{headers:{"content-type":"application/json"}})},false,2);
for(const env of [{GITHUB_REF:"refs/heads/unapproved"},{CLOUDFLARE_API_TOKEN:""},{CLOUDFLARE_ACCOUNT_ID:"bad"}])await releaseCase({env},false,0);
await releaseCase({readError:true},false,2);
for(let i=0;i<9;i++)await releaseCase({failAt:i},false,i+1);
Expand Down
Loading