deadbyte-mcp 0.15.41 → 0.15.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEADBYTE-MCP.cmd +8 -4
- package/MANIFEST.SHA256 +84 -41
- package/PROVENANCE.json +53 -15
- package/README.txt +27 -18
- package/RELEASE.json +43 -15
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +106 -14
- package/bridge/package.json +1 -1
- package/controller/deadbyte-controller.ps1 +132 -12
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R65-AUTHORITY-CONTINUITY-DESIGN.json +27 -0
- package/docs/R65-REFERENCE-GAP-MATRIX.json +22 -0
- package/package.json +5 -4
- package/scripts/deploy-r48.mjs +123 -7
- package/scripts/gate-windows-r65-prepublish.ps1 +65 -0
- package/scripts/npm-publication-r65.mjs +139 -0
- package/scripts/npm-release-r48.mjs +56 -37
- package/scripts/r65-canonical-full.mjs +45 -0
- package/scripts/r65-focused.mjs +37 -0
- package/scripts/verify-r65-parent-source.mjs +75 -0
- package/src/activation-continuity-engine-r65.mjs +225 -0
- package/src/activation-lock-r65.mjs +155 -0
- package/src/activation-preflight-r65.mjs +197 -0
- package/src/authority-continuity-r65.mjs +541 -0
- package/src/authority-epoch-cli-r65.mjs +10 -0
- package/src/autonomous-policy.mjs +7 -3
- package/src/coding-plane.mjs +12 -3
- package/src/desktop-policy-r40.mjs +6 -3
- package/src/doctor-transactions-r58.mjs +1 -1
- package/src/host-plane.mjs +9 -3
- package/src/process-policy.mjs +7 -4
- package/src/remote-authority.mjs +10 -1
- package/src/runtime-update.mjs +15 -5
- package/src/version.mjs +1 -1
- package/test/audit-context.test.mjs +15 -4
- package/test/autonomous-local-supervisor.test.mjs +7 -6
- package/test/autonomous-supervisor-integration.test.mjs +8 -7
- package/test/capability-resolver.test.mjs +11 -5
- package/test/codex-autonomous-r44.test.mjs +7 -6
- package/test/coding-observation.test.mjs +4 -3
- package/test/commander-real-parity-r47.test.mjs +5 -4
- package/test/commander-torture-r47.test.mjs +5 -4
- package/test/desktop-policy-r40.test.mjs +6 -4
- package/test/desktop-runtime-r40.test.mjs +8 -3
- package/test/host-inline-r45.test.mjs +4 -3
- package/test/machine-fs-runtime.test.mjs +4 -3
- package/test/process-longrun-r36.test.mjs +3 -2
- package/test/process-policy.test.mjs +10 -6
- package/test/process-runtime.test.mjs +9 -6
- package/test/r52-host-revocation.test.mjs +1 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r64-release-contract.test.mjs +2 -1
- package/test/r65-activation-lock-recovery.test.mjs +72 -0
- package/test/r65-activation-lock.test.mjs +59 -0
- package/test/r65-activation-orchestration.test.mjs +179 -0
- package/test/r65-activation-preflight.test.mjs +35 -0
- package/test/r65-authority-continuity-decision.test.mjs +58 -0
- package/test/r65-authority-durable-policy-binding.test.mjs +34 -0
- package/test/r65-authority-epoch-cli.test.mjs +125 -0
- package/test/r65-authority-epoch-races.test.mjs +87 -0
- package/test/r65-authority-epoch-recovery.test.mjs +82 -0
- package/test/r65-authority-epoch-store.test.mjs +94 -0
- package/test/r65-authority-fixture.mjs +41 -0
- package/test/r65-authority-grant-capture.test.mjs +37 -0
- package/test/r65-authority-observation-epoch.test.mjs +25 -0
- package/test/r65-authority-policy-envelope.test.mjs +46 -0
- package/test/r65-authority-restored-safe.test.mjs +75 -0
- package/test/r65-authority-runtime-epoch.test.mjs +115 -0
- package/test/r65-authority-runtime-policy-binding.test.mjs +29 -0
- package/test/r65-authority-test-trust.mjs +20 -0
- package/test/r65-cli-invocation.test.mjs +38 -0
- package/test/r65-controller-authority-epoch-live.test.mjs +100 -0
- package/test/r65-controller-authority-epoch.test.mjs +37 -0
- package/test/r65-deploy-preflight.test.mjs +55 -0
- package/test/r65-epoch-native-activation-integration.test.mjs +141 -0
- package/test/r65-epoch-native-continuity-engine.test.mjs +112 -0
- package/test/r65-epoch-native-cross-plane.test.mjs +95 -0
- package/test/r65-legacy-authority-migration.test.mjs +63 -0
- package/test/r65-legacy-auto-restore-rejection.test.mjs +17 -0
- package/test/r65-npm-publication.test.mjs +114 -0
- package/test/r65-parent-source-verifier.test.mjs +63 -0
- package/test/r65-release-contract.test.mjs +74 -0
- package/test/r65-transaction-precondition.test.mjs +36 -0
- package/test/remote-session-cli.test.mjs +6 -6
- package/test/standing-grants.test.mjs +20 -4
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import {createHash,createPrivateKey,createPublicKey,randomBytes,sign as cryptoSign,verify as cryptoVerify} from 'node:crypto';
|
|
2
|
+
import {mkdir,open,readFile,readdir,rename,rm,writeFile} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
5
|
+
import {decodeUtf8Strict} from './text-policy.mjs';
|
|
6
|
+
|
|
7
|
+
const SHAPAT=/^[0-9a-f]{64}$/;
|
|
8
|
+
const SCHEMAS=Object.freeze({
|
|
9
|
+
process:'deadbyte.process-policy.v1',coding:'deadbyte.coding-policy.v1',
|
|
10
|
+
autonomous:'deadbyte.autonomous-policy.v1',desktop:'deadbyte.desktop-policy.v1',
|
|
11
|
+
host:['deadbyte.host-policy.v1','deadbyte.host-policy.v2']
|
|
12
|
+
});
|
|
13
|
+
const TOP=Object.freeze({
|
|
14
|
+
process:['schema','enabled','grant_file','state_dir','native_host','roots','profiles'],
|
|
15
|
+
coding:['schema','enabled','lease_file','evidence_dir','max_read_bytes','max_write_bytes','roots','commands'],
|
|
16
|
+
autonomous:['schema','enabled','lease_file','evidence_dir','coding_policy_sha256','root_ids','planner','limits','profiles'],
|
|
17
|
+
desktop:['schema','enabled','grant_file','evidence_dir','observer','input','grounder','allowed_input_desktops','allowed_actions','limits'],
|
|
18
|
+
host:['schema','enabled','arm_file','max_read_bytes','max_write_bytes','roots','commands','native_exec']
|
|
19
|
+
});
|
|
20
|
+
const FIELDS=Object.freeze({
|
|
21
|
+
native_host:['path','sha256'],
|
|
22
|
+
process_root:['path','deny_paths'],
|
|
23
|
+
process_profile:['executable','executable_sha256','root_ids','prefix_args','transport','allow_raw_input','max_args','max_arg_bytes','max_sessions','max_input_bytes','max_retained_output_bytes','env_allow','input_actions'],
|
|
24
|
+
coding_root:['path','read','write','exec','deny_paths'],
|
|
25
|
+
coding_command:['kind','executable','executable_sha256','root_ids','prefix_args','extensions','timeout_ms','max_output_bytes'],
|
|
26
|
+
autonomous_planner:['backend','endpoint','model','max_tokens','timeout_ms','max_response_bytes','api_key_env','reasoning_effort','root_id','executable','executable_sha256','args','circuit'],
|
|
27
|
+
autonomous_limits:['max_iterations','max_mutations','max_wall_ms','max_context_bytes','max_diff_bytes','max_files_per_diff','max_stalled_cycles','max_post_replan_stalls','max_replans'],
|
|
28
|
+
autonomous_profile:['kind','root_id','command_id','script_path','script_sha256','args','cwd','timeout_ms','expect_exit_code','required_for_release','run_after_mutation','warmups','iterations','baseline_median_ms','max_regression_pct','description'],
|
|
29
|
+
desktop_provider:['executable','sha256','backend'],
|
|
30
|
+
desktop_limits:['max_frame_age_ms','max_frame_bytes','capture_timeout_ms','input_timeout_ms','ground_timeout_ms','post_observe_timeout_ms','max_preview_bytes','max_text_bytes'],
|
|
31
|
+
host_root:['path','read','write','exec','deny_paths'],
|
|
32
|
+
host_command:['kind','executable','executable_sha256','root_ids','prefix_args','extensions','timeout_ms','max_output_bytes','max_command_chars'],
|
|
33
|
+
native_exec:['enabled','root_ids','timeout_ms','max_output_bytes','max_args','max_arg_chars']
|
|
34
|
+
});
|
|
35
|
+
const STORAGE=new Set(['grant_file','state_dir','lease_file','evidence_dir','arm_file']);
|
|
36
|
+
const sha=value=>createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
37
|
+
const hashBytes=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
38
|
+
const plain=value=>value&&typeof value==='object'&&!Array.isArray(value);
|
|
39
|
+
|
|
40
|
+
function known(value,fields,label){
|
|
41
|
+
if(!plain(value))throw new Error(`${label} invalid`);
|
|
42
|
+
for(const key of Object.keys(value))if(!fields.includes(key))throw new Error(`${label} unknown field ${key}`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function absolute(value,label){
|
|
46
|
+
if(typeof value!=='string'||!path.win32.isAbsolute(value)||value.includes('\0'))throw new Error(`${label} invalid path`);
|
|
47
|
+
return path.win32.normalize(value).toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
function normalize(value,field,label){
|
|
50
|
+
if(value===undefined)return undefined;
|
|
51
|
+
if(field==='description')return undefined; // Non-authoritative presentation text.
|
|
52
|
+
if(field==='path'||field==='executable')return absolute(value,label);
|
|
53
|
+
if(field==='script_path'){
|
|
54
|
+
if(typeof value!=='string'||value.length<1||path.win32.isAbsolute(value)||value.split(/[\\/]/).includes('..')||value.includes('\0'))throw new Error(`${label} invalid relative path`);
|
|
55
|
+
return value.replaceAll('\\','/').toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
if(field.endsWith('sha256')){
|
|
58
|
+
if(typeof value!=='string'||!SHAPAT.test(value))throw new Error(`${label} invalid SHA-256`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if(Array.isArray(value)){
|
|
62
|
+
if(value.length>128||value.some(item=>typeof item!=='string'||item.length>4096||item.includes('\0')))throw new Error(`${label} invalid list`);
|
|
63
|
+
return ['prefix_args','args'].includes(field)?[...value]:[...new Set(value)].sort();
|
|
64
|
+
}
|
|
65
|
+
if(plain(value)){
|
|
66
|
+
if(Object.keys(value).length>256)throw new Error(`${label} oversized map`);
|
|
67
|
+
return Object.fromEntries(Object.entries(value).sort(([a],[b])=>a.localeCompare(b,'en')).map(([key,item])=>{
|
|
68
|
+
if(!/^[A-Za-z0-9._-]{1,64}$/.test(key))throw new Error(`${label} invalid identifier`);
|
|
69
|
+
if(typeof item!=='string'||item.includes('\0')||item.length>65536)throw new Error(`${label} invalid action payload`);
|
|
70
|
+
return [key,item];
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
if(value===null)return null;
|
|
74
|
+
if(typeof value==='boolean'||typeof value==='string')return value;
|
|
75
|
+
if(typeof value==='number'&&Number.isFinite(value)&&value>=0&&value<=Number.MAX_SAFE_INTEGER)return value;
|
|
76
|
+
throw new Error(`${label} invalid value`);
|
|
77
|
+
}
|
|
78
|
+
function fixed(value,fields,label){
|
|
79
|
+
known(value,fields,label);
|
|
80
|
+
return Object.fromEntries(Object.entries(value).filter(([key])=>key!=='description').map(([key,item])=>
|
|
81
|
+
[key,key==='circuit'?fixed(item,['failure_threshold','open_ms','half_open_probe_limit'],`${label}.${key}`):normalize(item,key,`${label}.${key}`)]));
|
|
82
|
+
}
|
|
83
|
+
function map(value,fields,label){
|
|
84
|
+
if(!plain(value)||Object.keys(value).length>256)throw new Error(`${label} invalid map`);
|
|
85
|
+
return Object.fromEntries(Object.entries(value).sort(([a],[b])=>a.localeCompare(b,'en')).map(([id,item])=>{
|
|
86
|
+
if(!/^[A-Za-z0-9._-]{1,64}$/.test(id))throw new Error(`${label} invalid identifier`);
|
|
87
|
+
return [id,fixed(item,fields,`${label}.${id}`)];
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function deriveSecurityEnvelope(plane,raw){
|
|
92
|
+
const schemas=SCHEMAS[plane];
|
|
93
|
+
if(!schemas)throw new Error('unknown authority plane');
|
|
94
|
+
known(raw,TOP[plane],`${plane} policy`);
|
|
95
|
+
if(!(Array.isArray(schemas)?schemas:[schemas]).includes(raw.schema)||typeof raw.enabled!=='boolean')throw new Error(`${plane} policy schema/enabled invalid`);
|
|
96
|
+
const semantics={schema:raw.schema,enabled:raw.enabled};
|
|
97
|
+
for(const [key,value] of Object.entries(raw)){
|
|
98
|
+
if(key==='schema'||key==='enabled'||STORAGE.has(key))continue;
|
|
99
|
+
if(key==='roots')semantics.roots=map(value,FIELDS[`${plane}_root`],`${plane}.roots`);
|
|
100
|
+
else if(key==='profiles'&&plane==='process')semantics.profiles=map(value,FIELDS.process_profile,'process.profiles');
|
|
101
|
+
else if(key==='profiles'&&plane==='autonomous')semantics.profiles=map(value,FIELDS.autonomous_profile,'autonomous.profiles');
|
|
102
|
+
else if(key==='commands')semantics.commands=map(value,FIELDS[`${plane}_command`],`${plane}.commands`);
|
|
103
|
+
else if(key==='native_host')semantics.native_host=fixed(value,FIELDS.native_host,'process.native_host');
|
|
104
|
+
else if(key==='native_exec')semantics.native_exec=fixed(value,FIELDS.native_exec,'host.native_exec');
|
|
105
|
+
else if(key==='planner')semantics.planner=fixed(value,FIELDS.autonomous_planner,'autonomous.planner');
|
|
106
|
+
else if(key==='limits')semantics.limits=fixed(value,FIELDS[`${plane}_limits`],`${plane}.limits`);
|
|
107
|
+
else if(['observer','input','grounder'].includes(key))semantics[key]=fixed(value,FIELDS.desktop_provider,`desktop.${key}`);
|
|
108
|
+
else semantics[key]=normalize(value,key,`${plane}.${key}`);
|
|
109
|
+
}
|
|
110
|
+
const envelope={schema:'deadbyte.authority-envelope.r65.v1',plane,semantics};
|
|
111
|
+
return Object.freeze({...envelope,sha256:sha(envelope)});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function relation(before,after,field){
|
|
115
|
+
if(before===after)return 'EQUAL';
|
|
116
|
+
if(before===undefined)return 'WIDER';
|
|
117
|
+
if(after===undefined)return 'NARROWER';
|
|
118
|
+
if(typeof before==='boolean'&&typeof after==='boolean')return after?'WIDER':'NARROWER';
|
|
119
|
+
if(typeof before==='number'&&typeof after==='number')return after>before?'WIDER':'NARROWER';
|
|
120
|
+
if(Array.isArray(before)&&Array.isArray(after)){
|
|
121
|
+
if(canonicalJson(before)===canonicalJson(after))return 'EQUAL';
|
|
122
|
+
if(['prefix_args','args'].includes(field))return canonicalJson(before)===canonicalJson(after)?'EQUAL':'UNVERIFIED';
|
|
123
|
+
const prior=new Set(before),next=new Set(after);
|
|
124
|
+
if(field==='deny_paths')return [...prior].every(x=>next.has(x))?'NARROWER':'WIDER';
|
|
125
|
+
return [...next].every(x=>prior.has(x))?'NARROWER':'WIDER';
|
|
126
|
+
}
|
|
127
|
+
if(plain(before)&&plain(after)){
|
|
128
|
+
let narrowed=false,unverified=false;
|
|
129
|
+
for(const key of new Set([...Object.keys(before),...Object.keys(after)])){
|
|
130
|
+
const part=relation(before[key],after[key],key);
|
|
131
|
+
if(part==='WIDER')return 'WIDER';
|
|
132
|
+
if(part==='UNVERIFIED')unverified=true;
|
|
133
|
+
if(part==='NARROWER')narrowed=true;
|
|
134
|
+
}
|
|
135
|
+
return unverified?'UNVERIFIED':narrowed?'NARROWER':'EQUAL';
|
|
136
|
+
}
|
|
137
|
+
if(typeof before==='string'&&typeof after==='string'&&field==='path'){
|
|
138
|
+
const prefix=before.endsWith('\\')?before:before+'\\';
|
|
139
|
+
return after.startsWith(prefix)?'NARROWER':'UNVERIFIED';
|
|
140
|
+
}
|
|
141
|
+
return 'UNVERIFIED';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function compareSecurityEnvelopes(plane,prior,successor){
|
|
145
|
+
if(prior?.plane!==plane||successor?.plane!==plane||prior?.schema!=='deadbyte.authority-envelope.r65.v1'||
|
|
146
|
+
successor?.schema!==prior.schema||prior.sha256!==sha({schema:prior.schema,plane,semantics:prior.semantics})||
|
|
147
|
+
successor.sha256!==sha({schema:successor.schema,plane,semantics:successor.semantics}))
|
|
148
|
+
return Object.freeze({relation:'UNVERIFIED',reason:'envelope identity invalid'});
|
|
149
|
+
return Object.freeze({relation:relation(prior.semantics,successor.semantics,''),prior_sha256:prior.sha256,successor_sha256:successor.sha256});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const GRANT_SCHEMAS=Object.freeze({
|
|
153
|
+
process:'deadbyte.process-grant.v1',coding:'deadbyte.coding-grant.v2',
|
|
154
|
+
autonomous:'deadbyte.autonomous-grant.v2',desktop:'deadbyte.desktop-grant.v1',host:'deadbyte.host-grant.v2'
|
|
155
|
+
});
|
|
156
|
+
const GRANT_FIELDS=Object.freeze({
|
|
157
|
+
process:['schema','policy_sha256','instance_nonce','profile_ids','root_ids','granted_at_utc'],
|
|
158
|
+
coding:['schema','policy_sha256','instance_nonce','root_ids','granted_at_utc'],
|
|
159
|
+
autonomous:['schema','policy_sha256','coding_policy_sha256','instance_nonce','root_ids','granted_at_utc'],
|
|
160
|
+
desktop:['schema','policy_sha256','instance_nonce','action_kinds','granted_at_utc'],
|
|
161
|
+
host:['schema','policy_sha256','instance_nonce','granted_at_utc']
|
|
162
|
+
});
|
|
163
|
+
function scopeIds(values,allowed,label){
|
|
164
|
+
if(!Array.isArray(values)||values.length<1||values.length>128||new Set(values).size!==values.length||
|
|
165
|
+
values.some(id=>typeof id!=='string'||!Object.hasOwn(allowed,id)))throw new Error(`${label} scope invalid`);
|
|
166
|
+
return [...values].sort();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function captureStandingGrant({plane,policyBytes,grantBytes,predecessor,candidate,updateId}){
|
|
170
|
+
if(!GRANT_SCHEMAS[plane]||!Buffer.isBuffer(policyBytes)||!Buffer.isBuffer(grantBytes)||
|
|
171
|
+
typeof predecessor?.release_id!=='string'||!SHAPAT.test(String(predecessor?.generation_manifest_sha256))||
|
|
172
|
+
typeof candidate?.version!=='string'||!SHAPAT.test(String(candidate?.manifest_sha256))||
|
|
173
|
+
typeof updateId!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(updateId))throw new Error('authority continuity predecessor/candidate identity invalid');
|
|
174
|
+
let policy,grant;
|
|
175
|
+
try{policy=JSON.parse(decodeUtf8Strict(policyBytes));grant=JSON.parse(decodeUtf8Strict(grantBytes));}
|
|
176
|
+
catch{throw new Error('authority continuity policy/grant JSON invalid');}
|
|
177
|
+
const envelope=deriveSecurityEnvelope(plane,policy);
|
|
178
|
+
known(grant,GRANT_FIELDS[plane],`${plane} grant`);
|
|
179
|
+
if(grant.schema!==GRANT_SCHEMAS[plane])throw new Error(`${plane} grant schema invalid`);
|
|
180
|
+
const policySha256=hashBytes(policyBytes),grantSha256=hashBytes(grantBytes);
|
|
181
|
+
if(grant.policy_sha256!==policySha256)throw new Error(`${plane} grant policy SHA-256 mismatch`);
|
|
182
|
+
if(!SHAPAT.test(String(grant.instance_nonce)))throw new Error(`${plane} grant instance nonce invalid`);
|
|
183
|
+
if(typeof grant.granted_at_utc!=='string'||!Number.isFinite(Date.parse(grant.granted_at_utc)))throw new Error(`${plane} grant timestamp invalid`);
|
|
184
|
+
const grantedScope={};
|
|
185
|
+
if(plane==='process'){
|
|
186
|
+
grantedScope.profile_ids=scopeIds(grant.profile_ids,policy.profiles,'process profiles');
|
|
187
|
+
grantedScope.root_ids=scopeIds(grant.root_ids,policy.roots,'process roots');
|
|
188
|
+
for(const id of grantedScope.profile_ids)if(policy.profiles[id].root_ids.some(rootId=>!grantedScope.root_ids.includes(rootId)))
|
|
189
|
+
throw new Error('process profile root scope exceeds grant');
|
|
190
|
+
}else if(plane==='coding')grantedScope.root_ids=scopeIds(grant.root_ids,policy.roots,'coding roots');
|
|
191
|
+
else if(plane==='autonomous'){
|
|
192
|
+
grantedScope.root_ids=scopeIds(grant.root_ids,Object.fromEntries((policy.root_ids??[]).map(id=>[id,true])),'autonomous roots');
|
|
193
|
+
if(grant.coding_policy_sha256!==policy.coding_policy_sha256)throw new Error('autonomous grant Coding policy SHA-256 mismatch');
|
|
194
|
+
}else if(plane==='desktop'){
|
|
195
|
+
grantedScope.action_kinds=scopeIds(grant.action_kinds,Object.fromEntries((policy.allowed_actions??[]).map(id=>[id,true])),'desktop actions');
|
|
196
|
+
}
|
|
197
|
+
const record={schema:'deadbyte.authority-continuity.r65.v1',update_id:updateId,
|
|
198
|
+
predecessor_release:predecessor.release_id,predecessor_generation_manifest_sha256:predecessor.generation_manifest_sha256,
|
|
199
|
+
candidate_version:candidate.version,candidate_manifest_sha256:candidate.manifest_sha256,plane,
|
|
200
|
+
prior_policy_sha256:policySha256,prior_policy_envelope_sha256:envelope.sha256,
|
|
201
|
+
grant_schema:grant.schema,grant_sha256:grantSha256,grant_instance_nonce:grant.instance_nonce,
|
|
202
|
+
granted_scope:grantedScope,granted_at_utc:grant.granted_at_utc};
|
|
203
|
+
return Object.freeze({...record,subject_sha256:sha(record)});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const DURABLE_EPOCH_ACTIONS=new Set(['grant','transaction_revoke','user_revoke','transaction_restore']);
|
|
207
|
+
const DURABLE_EPOCH_SCHEMA='deadbyte.authority-epoch-event.r65.v1';
|
|
208
|
+
const DURABLE_HEAD_SCHEMA='deadbyte.authority-epoch-head.r65.v1';
|
|
209
|
+
const DURABLE_EPOCH_DOMAIN=Buffer.from('DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1\0','utf8');
|
|
210
|
+
const RESTORE_BINDING_FIELDS=Object.freeze(['prior_grant_sha256','prior_grant_instance_nonce','predecessor_policy_envelope_sha256','successor_policy_envelope_sha256','candidate_manifest_sha256','continuity_subject_sha256']);
|
|
211
|
+
function normalizeRestoreBinding(value,{required=false}={}){
|
|
212
|
+
if(value===undefined||value===null){if(required)throw new Error('authority epoch transaction restore binding required');return null;}
|
|
213
|
+
if(!plain(value)||Object.keys(value).length!==RESTORE_BINDING_FIELDS.length||RESTORE_BINDING_FIELDS.some(key=>!SHAPAT.test(String(value[key]))))
|
|
214
|
+
throw new Error('authority epoch transaction restore binding invalid');
|
|
215
|
+
for(const key of Object.keys(value))if(!RESTORE_BINDING_FIELDS.includes(key))throw new Error('authority epoch transaction restore binding invalid');
|
|
216
|
+
return Object.freeze(Object.fromEntries(RESTORE_BINDING_FIELDS.map(key=>[key,String(value[key])])));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function durableEpochKeys(runtimeDir,{privateRequired=false}={}){
|
|
220
|
+
const trust=path.join(path.dirname(path.resolve(runtimeDir)),'trust');
|
|
221
|
+
const publicKey=createPublicKey(await readFile(path.join(trust,'ed25519-public.pem')));
|
|
222
|
+
if(publicKey.asymmetricKeyType!=='ed25519')throw new Error('authority epoch trust public key invalid');
|
|
223
|
+
const keyId=hashBytes(Buffer.from(publicKey.export({format:'der',type:'spki'})));
|
|
224
|
+
if(!privateRequired)return {publicKey,keyId};
|
|
225
|
+
const privateKey=createPrivateKey(await readFile(path.join(trust,'ed25519-private.pem')));
|
|
226
|
+
if(privateKey.asymmetricKeyType!=='ed25519'||
|
|
227
|
+
hashBytes(Buffer.from(createPublicKey(privateKey).export({format:'der',type:'spki'})))!==keyId)
|
|
228
|
+
throw new Error('authority epoch trust key pair mismatch');
|
|
229
|
+
return {publicKey,privateKey,keyId};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function durableEventBody(event){
|
|
233
|
+
const {attestation,...body}=event;
|
|
234
|
+
return body;
|
|
235
|
+
}
|
|
236
|
+
function verifyDurableEvent(event,publicKey,keyId){
|
|
237
|
+
if(!plain(event)||!plain(event.attestation)||event.attestation.algorithm!=='ed25519'||
|
|
238
|
+
event.attestation.domain!=='DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1'||
|
|
239
|
+
event.attestation.key_id!==keyId||typeof event.attestation.signature_base64!=='string')return false;
|
|
240
|
+
try{
|
|
241
|
+
const signature=Buffer.from(event.attestation.signature_base64,'base64');
|
|
242
|
+
if(signature.length!==64||signature.toString('base64')!==event.attestation.signature_base64)return false;
|
|
243
|
+
return cryptoVerify(null,Buffer.concat([DURABLE_EPOCH_DOMAIN,Buffer.from(canonicalJson(durableEventBody(event)),'utf8')]),publicKey,signature);
|
|
244
|
+
}catch{return false;}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function durableEpochDir(runtimeDir,plane){
|
|
248
|
+
if(!SCHEMAS[plane])throw new Error('authority epoch plane invalid');
|
|
249
|
+
if(typeof runtimeDir!=='string'||runtimeDir.length<1)throw new Error('authority epoch runtime directory invalid');
|
|
250
|
+
return path.join(path.resolve(runtimeDir),'authority-epochs',plane);
|
|
251
|
+
}
|
|
252
|
+
function durableEventName(seq,eventSha256){
|
|
253
|
+
return `${String(seq).padStart(16,'0')}-${eventSha256}.json`;
|
|
254
|
+
}
|
|
255
|
+
function processAlive(pid){
|
|
256
|
+
if(!Number.isInteger(pid)||pid<1)return false;
|
|
257
|
+
if(pid===process.pid)return true;
|
|
258
|
+
try{process.kill(pid,0);return true;}catch(error){return error?.code==='EPERM';}
|
|
259
|
+
}
|
|
260
|
+
async function atomicJsonWrite(file,value){
|
|
261
|
+
const dir=path.dirname(file);
|
|
262
|
+
await mkdir(dir,{recursive:true});
|
|
263
|
+
const tmp=`${file}.tmp-${process.pid}-${randomBytes(8).toString('hex')}`;
|
|
264
|
+
try{
|
|
265
|
+
await writeFile(tmp,canonicalJson(value),{encoding:'utf8',flag:'wx'});
|
|
266
|
+
await rename(tmp,file);
|
|
267
|
+
}finally{
|
|
268
|
+
await rm(tmp,{force:true}).catch(()=>{});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function acquireEpochLock(dir){
|
|
272
|
+
await mkdir(dir,{recursive:true});
|
|
273
|
+
const lockPath=path.join(dir,'.lock');
|
|
274
|
+
for(let attempt=0;attempt<2;attempt++){
|
|
275
|
+
const token=randomBytes(16).toString('hex');
|
|
276
|
+
try{
|
|
277
|
+
const handle=await open(lockPath,'wx');
|
|
278
|
+
try{await handle.writeFile(canonicalJson({schema:'deadbyte.authority-epoch-lock.r65.v1',pid:process.pid,token,created_at_utc:new Date().toISOString()}),'utf8');}
|
|
279
|
+
finally{await handle.close();}
|
|
280
|
+
return async()=>{
|
|
281
|
+
try{
|
|
282
|
+
const current=JSON.parse(await readFile(lockPath,'utf8'));
|
|
283
|
+
if(current?.token===token)await rm(lockPath,{force:true});
|
|
284
|
+
}catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
285
|
+
};
|
|
286
|
+
}catch(error){
|
|
287
|
+
if(error?.code!=='EEXIST')throw error;
|
|
288
|
+
let owner=null;
|
|
289
|
+
try{owner=JSON.parse(await readFile(lockPath,'utf8'));}catch{}
|
|
290
|
+
if(attempt===0&&owner&&Number.isInteger(owner.pid)&&!processAlive(owner.pid)){
|
|
291
|
+
await rm(lockPath,{force:true});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
throw new Error('authority epoch mutation locked');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw new Error('authority epoch mutation locked');
|
|
298
|
+
}
|
|
299
|
+
async function readDurableHead(runtimeDir,plane){
|
|
300
|
+
const dir=durableEpochDir(runtimeDir,plane),headPath=path.join(dir,'head.json');
|
|
301
|
+
let head;
|
|
302
|
+
try{head=JSON.parse(await readFile(headPath,'utf8'));}
|
|
303
|
+
catch(error){
|
|
304
|
+
if(error?.code==='ENOENT')return null;
|
|
305
|
+
throw new Error('authority epoch head invalid');
|
|
306
|
+
}
|
|
307
|
+
if(!plain(head)||head.schema!==DURABLE_HEAD_SCHEMA||head.plane!==plane||
|
|
308
|
+
!Number.isSafeInteger(head.seq)||head.seq<1||!DURABLE_EPOCH_ACTIONS.has(head.action)||
|
|
309
|
+
!SHAPAT.test(String(head.event_sha256))||!SHAPAT.test(String(head.grant_sha256))||
|
|
310
|
+
!SHAPAT.test(String(head.grant_instance_nonce))||
|
|
311
|
+
(head.previous_event_sha256!==null&&!SHAPAT.test(String(head.previous_event_sha256)))||
|
|
312
|
+
typeof head.event_file!=='string'||path.basename(head.event_file)!==head.event_file)
|
|
313
|
+
throw new Error('authority epoch head invalid');
|
|
314
|
+
const headRestoreBinding=normalizeRestoreBinding(head.restore_binding,{required:head.action==='transaction_restore'});
|
|
315
|
+
if(head.action!=='transaction_restore'&&headRestoreBinding!==null)throw new Error('authority epoch unexpected restore binding');
|
|
316
|
+
const expectedName=durableEventName(head.seq,head.event_sha256);
|
|
317
|
+
if(head.event_file!==expectedName)throw new Error('authority epoch head event name mismatch');
|
|
318
|
+
const eventNames=await readdir(path.join(dir,'events'));
|
|
319
|
+
if(eventNames.length>10000)throw new Error('authority epoch event budget exceeded');
|
|
320
|
+
for(const name of eventNames){
|
|
321
|
+
const match=/^(\d{16})-([0-9a-f]{64})\.json$/.exec(name);
|
|
322
|
+
if(!match)throw new Error('authority epoch unexpected event file');
|
|
323
|
+
const seq=Number(match[1]);
|
|
324
|
+
if(!Number.isSafeInteger(seq)||seq>head.seq||(seq===head.seq&&name!==head.event_file))
|
|
325
|
+
throw new Error('authority epoch head replay or incomplete write');
|
|
326
|
+
}
|
|
327
|
+
let eventBytes;
|
|
328
|
+
try{eventBytes=await readFile(path.join(dir,'events',head.event_file));}
|
|
329
|
+
catch{throw new Error('authority epoch event missing');}
|
|
330
|
+
if(hashBytes(eventBytes)!==head.event_sha256)throw new Error('authority epoch event hash mismatch');
|
|
331
|
+
let event;
|
|
332
|
+
try{event=JSON.parse(decodeUtf8Strict(eventBytes));}catch{throw new Error('authority epoch event invalid');}
|
|
333
|
+
const {publicKey,keyId}=await durableEpochKeys(runtimeDir);
|
|
334
|
+
if(!verifyDurableEvent(event,publicKey,keyId))throw new Error('authority epoch event signature invalid');
|
|
335
|
+
for(const key of ['schema','plane','seq','action','update_id','grant_sha256','grant_instance_nonce','previous_event_sha256']){
|
|
336
|
+
const expected=key==='schema'?DURABLE_EPOCH_SCHEMA:head[key];
|
|
337
|
+
if(event?.[key]!==expected)throw new Error('authority epoch head/event mismatch');
|
|
338
|
+
}
|
|
339
|
+
const eventRestoreBinding=normalizeRestoreBinding(event?.restore_binding,{required:head.action==='transaction_restore'});
|
|
340
|
+
if(canonicalJson(eventRestoreBinding)!==canonicalJson(headRestoreBinding))throw new Error('authority epoch restore binding mismatch');
|
|
341
|
+
if((head.policy_sha256!==undefined||head.policy_envelope_sha256!==undefined||
|
|
342
|
+
event.policy_sha256!==undefined||event.policy_envelope_sha256!==undefined)&&
|
|
343
|
+
(!SHAPAT.test(String(head.policy_sha256))||!SHAPAT.test(String(head.policy_envelope_sha256))||
|
|
344
|
+
head.policy_sha256!==event.policy_sha256||head.policy_envelope_sha256!==event.policy_envelope_sha256))
|
|
345
|
+
throw new Error('authority epoch policy binding invalid');
|
|
346
|
+
if(head.seq===1&&head.previous_event_sha256!==null)throw new Error('authority epoch genesis chain invalid');
|
|
347
|
+
if(head.seq>1){
|
|
348
|
+
if(!SHAPAT.test(String(head.previous_event_sha256)))throw new Error('authority epoch predecessor missing');
|
|
349
|
+
const priorName=durableEventName(head.seq-1,head.previous_event_sha256);
|
|
350
|
+
let priorBytes;
|
|
351
|
+
try{priorBytes=await readFile(path.join(dir,'events',priorName));}
|
|
352
|
+
catch{throw new Error('authority epoch predecessor event missing');}
|
|
353
|
+
if(hashBytes(priorBytes)!==head.previous_event_sha256)throw new Error('authority epoch predecessor hash mismatch');
|
|
354
|
+
let prior;
|
|
355
|
+
try{prior=JSON.parse(decodeUtf8Strict(priorBytes));}catch{throw new Error('authority epoch predecessor invalid');}
|
|
356
|
+
if(!verifyDurableEvent(prior,publicKey,keyId)||prior.seq!==head.seq-1||prior.plane!==plane)
|
|
357
|
+
throw new Error('authority epoch predecessor signature invalid');
|
|
358
|
+
}
|
|
359
|
+
return Object.freeze({...head});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
|
|
363
|
+
const root=path.resolve(String(runtimeDir??''));
|
|
364
|
+
if(!runtimeDir)throw new Error('authority epoch runtime directory invalid');
|
|
365
|
+
if(policyBytes!==null&&!Buffer.isBuffer(policyBytes))throw new Error('authority epoch policy bytes invalid');
|
|
366
|
+
return Object.freeze({
|
|
367
|
+
async readHead({plane}){
|
|
368
|
+
return readDurableHead(root,plane);
|
|
369
|
+
},
|
|
370
|
+
async append({plane,action,grantBytes,grantInstanceNonce,updateId=null,expectedHeadSha256=undefined,restoreBinding=null}){
|
|
371
|
+
const transactional=action?.startsWith('transaction_')===true;
|
|
372
|
+
if(!SCHEMAS[plane]||!DURABLE_EPOCH_ACTIONS.has(action)||!Buffer.isBuffer(grantBytes)||
|
|
373
|
+
!SHAPAT.test(String(grantInstanceNonce))||
|
|
374
|
+
(transactional?!/^[A-Za-z0-9._-]{1,96}$/.test(String(updateId??'')):updateId!==null)||
|
|
375
|
+
(expectedHeadSha256!==undefined&&!SHAPAT.test(String(expectedHeadSha256))))
|
|
376
|
+
throw new Error('authority epoch append input invalid');
|
|
377
|
+
if(transactional&&expectedHeadSha256===undefined)
|
|
378
|
+
throw new Error('authority epoch transaction expected head required for compare-and-swap');
|
|
379
|
+
const normalizedRestoreBinding=normalizeRestoreBinding(restoreBinding,{required:action==='transaction_restore'});
|
|
380
|
+
if(action!=='transaction_restore'&&normalizedRestoreBinding!==null)throw new Error('authority epoch restore binding only applies to transaction restore');
|
|
381
|
+
let parsedGrant;
|
|
382
|
+
try{parsedGrant=JSON.parse(decodeUtf8Strict(grantBytes));}catch{throw new Error('authority epoch grant JSON invalid');}
|
|
383
|
+
if(parsedGrant?.instance_nonce!==grantInstanceNonce)throw new Error('authority epoch grant nonce mismatch');
|
|
384
|
+
let policyBinding=null;
|
|
385
|
+
if(policyBytes!==null){
|
|
386
|
+
let parsedPolicy;
|
|
387
|
+
try{parsedPolicy=JSON.parse(decodeUtf8Strict(policyBytes));}catch{throw new Error('authority epoch policy JSON invalid');}
|
|
388
|
+
const policySha256=hashBytes(policyBytes);
|
|
389
|
+
if(parsedGrant?.policy_sha256!==policySha256)throw new Error('authority epoch grant policy digest mismatch');
|
|
390
|
+
policyBinding={policy_sha256:policySha256,policy_envelope_sha256:deriveSecurityEnvelope(plane,parsedPolicy).sha256};
|
|
391
|
+
}
|
|
392
|
+
const dir=durableEpochDir(root,plane),events=path.join(dir,'events');
|
|
393
|
+
const release=await acquireEpochLock(dir);
|
|
394
|
+
try{
|
|
395
|
+
await mkdir(events,{recursive:true});
|
|
396
|
+
const previous=await readDurableHead(root,plane);
|
|
397
|
+
if(expectedHeadSha256!==undefined&&(previous?.event_sha256??null)!==expectedHeadSha256)
|
|
398
|
+
throw new Error('authority epoch compare-and-swap conflict');
|
|
399
|
+
const grantSha256=hashBytes(grantBytes);
|
|
400
|
+
if(action==='transaction_revoke'){
|
|
401
|
+
if(!previous||!['grant','transaction_restore'].includes(previous.action))
|
|
402
|
+
throw new Error('authority epoch transaction revoke requires a current trusted grant head');
|
|
403
|
+
if(previous.grant_sha256!==grantSha256||previous.grant_instance_nonce!==grantInstanceNonce)
|
|
404
|
+
throw new Error('authority epoch transaction revoke grant does not match current head');
|
|
405
|
+
}
|
|
406
|
+
if(action==='transaction_restore'){
|
|
407
|
+
if(!previous||previous.action!=='transaction_revoke')
|
|
408
|
+
throw new Error('authority epoch transaction restore requires transaction revoke head');
|
|
409
|
+
if(previous.update_id!==updateId)
|
|
410
|
+
throw new Error('authority epoch transaction restore owner update mismatch');
|
|
411
|
+
if(normalizedRestoreBinding.prior_grant_sha256!==previous.grant_sha256||
|
|
412
|
+
normalizedRestoreBinding.prior_grant_instance_nonce!==previous.grant_instance_nonce)
|
|
413
|
+
throw new Error('authority epoch transaction restore predecessor grant binding mismatch');
|
|
414
|
+
if(previous.grant_instance_nonce===grantInstanceNonce)
|
|
415
|
+
throw new Error('authority epoch transaction restore requires fresh nonce; revoked nonce cannot be resurrected');
|
|
416
|
+
}
|
|
417
|
+
if(action==='grant'&&previous?.grant_instance_nonce===grantInstanceNonce)
|
|
418
|
+
throw new Error('authority epoch grant requires fresh nonce; prior nonce cannot be resurrected');
|
|
419
|
+
const seq=(previous?.seq??0)+1;
|
|
420
|
+
const body={schema:DURABLE_EPOCH_SCHEMA,plane,seq,action,update_id:updateId,
|
|
421
|
+
grant_sha256:grantSha256,grant_instance_nonce:grantInstanceNonce,restore_binding:normalizedRestoreBinding,
|
|
422
|
+
...(policyBinding??{}),previous_event_sha256:previous?.event_sha256??null,recorded_at_utc:new Date().toISOString()};
|
|
423
|
+
const {privateKey,keyId}=await durableEpochKeys(root,{privateRequired:true});
|
|
424
|
+
const signature=cryptoSign(null,Buffer.concat([DURABLE_EPOCH_DOMAIN,Buffer.from(canonicalJson(body),'utf8')]),privateKey);
|
|
425
|
+
const event={...body,attestation:{algorithm:'ed25519',domain:'DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1',key_id:keyId,signature_base64:signature.toString('base64')}};
|
|
426
|
+
const eventBytes=Buffer.from(canonicalJson(event),'utf8'),eventSha256=hashBytes(eventBytes);
|
|
427
|
+
const eventFile=durableEventName(seq,eventSha256);
|
|
428
|
+
await writeFile(path.join(events,eventFile),eventBytes,{flag:'wx'});
|
|
429
|
+
const head={schema:DURABLE_HEAD_SCHEMA,plane,seq,action,update_id:updateId,
|
|
430
|
+
grant_sha256:grantSha256,grant_instance_nonce:grantInstanceNonce,restore_binding:normalizedRestoreBinding,
|
|
431
|
+
...(policyBinding??{}),
|
|
432
|
+
previous_event_sha256:event.previous_event_sha256,event_sha256:eventSha256,event_file:eventFile};
|
|
433
|
+
await atomicJsonWrite(path.join(dir,'head.json'),head);
|
|
434
|
+
return Object.freeze({...head});
|
|
435
|
+
}finally{await release();}
|
|
436
|
+
},
|
|
437
|
+
async verifyGrant({plane,grantBytes,grantInstanceNonce,policyBytes:verifyPolicyBytes=null}){
|
|
438
|
+
if(!SCHEMAS[plane]||!Buffer.isBuffer(grantBytes)||!SHAPAT.test(String(grantInstanceNonce)))
|
|
439
|
+
return Object.freeze({trusted:false,reason:'epoch-input-invalid'});
|
|
440
|
+
if(verifyPolicyBytes!==null&&!Buffer.isBuffer(verifyPolicyBytes))return Object.freeze({trusted:false,reason:'epoch-policy-input-invalid'});
|
|
441
|
+
let head;
|
|
442
|
+
try{head=await readDurableHead(root,plane);}
|
|
443
|
+
catch(error){return Object.freeze({trusted:false,reason:String(error?.message??error)});}
|
|
444
|
+
if(!head)return Object.freeze({trusted:false,reason:'legacy-no-epoch'});
|
|
445
|
+
if(verifyPolicyBytes!==null){
|
|
446
|
+
try{
|
|
447
|
+
const observedPolicy=JSON.parse(decodeUtf8Strict(verifyPolicyBytes));
|
|
448
|
+
if(head.policy_sha256!==hashBytes(verifyPolicyBytes)||
|
|
449
|
+
head.policy_envelope_sha256!==deriveSecurityEnvelope(plane,observedPolicy).sha256)
|
|
450
|
+
return Object.freeze({trusted:false,reason:'epoch-policy-mismatch'});
|
|
451
|
+
}catch{return Object.freeze({trusted:false,reason:'epoch-policy-invalid'});}
|
|
452
|
+
}
|
|
453
|
+
const grantSha256=hashBytes(grantBytes);
|
|
454
|
+
if(head.grant_sha256!==grantSha256||head.grant_instance_nonce!==grantInstanceNonce)
|
|
455
|
+
return Object.freeze({trusted:false,reason:'epoch-grant-mismatch',seq:head.seq,action:head.action,event_sha256:head.event_sha256});
|
|
456
|
+
const trusted=head.action==='grant'||head.action==='transaction_restore';
|
|
457
|
+
return Object.freeze({trusted,reason:trusted?'epoch-bound-standing-grant':'epoch-revoked',
|
|
458
|
+
seq:head.seq,action:head.action,event_sha256:head.event_sha256,update_id:head.update_id});
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
export async function verifyStandingGrantEpoch({plane,grantFile,grantBytes,grantInstanceNonce,policyPath}){
|
|
465
|
+
if(typeof grantFile!=='string'||grantFile.length<1)return Object.freeze({trusted:false,reason:'epoch-grant-file-invalid'});
|
|
466
|
+
if(typeof policyPath!=='string'||policyPath.length<1)return Object.freeze({trusted:false,reason:'epoch-policy-unavailable'});
|
|
467
|
+
let policyBytes;
|
|
468
|
+
try{policyBytes=await readFile(policyPath);}catch{return Object.freeze({trusted:false,reason:'epoch-policy-unavailable'});}
|
|
469
|
+
const store=createAuthorityEpochStore({runtimeDir:path.dirname(path.resolve(grantFile))});
|
|
470
|
+
return store.verifyGrant({plane,grantBytes,grantInstanceNonce,policyBytes});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
function parseEpochCliArgs(argv){
|
|
475
|
+
if(!Array.isArray(argv)||argv.length<1)throw new Error('authority epoch command missing');
|
|
476
|
+
const command=String(argv[0]);
|
|
477
|
+
if(!['status','grant','user-revoke','transaction-revoke','transaction-restore'].includes(command))
|
|
478
|
+
throw new Error('authority epoch command invalid');
|
|
479
|
+
const out={command,plane:null,grantFile:null,policyFile:null,updateId:null,expectedHeadSha256:undefined,
|
|
480
|
+
priorGrantSha256:undefined,priorGrantNonce:undefined,predecessorPolicyEnvelope:undefined,successorPolicyEnvelope:undefined,
|
|
481
|
+
candidateManifest:undefined,continuitySubject:undefined,restoreBinding:null};
|
|
482
|
+
for(let i=1;i<argv.length;i++){
|
|
483
|
+
const flag=String(argv[i]),value=argv[++i];
|
|
484
|
+
if(value===undefined)throw new Error(`authority epoch ${flag} value missing`);
|
|
485
|
+
if(flag==='--plane')out.plane=String(value);
|
|
486
|
+
else if(flag==='--grant-file')out.grantFile=path.resolve(String(value));
|
|
487
|
+
else if(flag==='--policy-file')out.policyFile=path.resolve(String(value));
|
|
488
|
+
else if(flag==='--update-id')out.updateId=String(value);
|
|
489
|
+
else if(flag==='--expected-head')out.expectedHeadSha256=String(value);
|
|
490
|
+
else if(flag==='--prior-grant-sha256')out.priorGrantSha256=String(value);
|
|
491
|
+
else if(flag==='--prior-grant-nonce')out.priorGrantNonce=String(value);
|
|
492
|
+
else if(flag==='--predecessor-policy-envelope')out.predecessorPolicyEnvelope=String(value);
|
|
493
|
+
else if(flag==='--successor-policy-envelope')out.successorPolicyEnvelope=String(value);
|
|
494
|
+
else if(flag==='--candidate-manifest')out.candidateManifest=String(value);
|
|
495
|
+
else if(flag==='--continuity-subject')out.continuitySubject=String(value);
|
|
496
|
+
else throw new Error(`authority epoch unknown option ${flag}`);
|
|
497
|
+
}
|
|
498
|
+
if(!SCHEMAS[out.plane]||typeof out.grantFile!=='string'||out.grantFile.length<1)
|
|
499
|
+
throw new Error('authority epoch plane/grant file missing');
|
|
500
|
+
if(typeof out.policyFile!=='string'||out.policyFile.length<1)throw new Error('authority epoch policy-file required');
|
|
501
|
+
if(['transaction-revoke','transaction-restore'].includes(command)){
|
|
502
|
+
if(!/^[A-Za-z0-9._-]{1,96}$/.test(String(out.updateId??'')))throw new Error('authority epoch transaction update id invalid');
|
|
503
|
+
if(!SHAPAT.test(String(out.expectedHeadSha256??'')))throw new Error('authority epoch transaction expected head invalid');
|
|
504
|
+
}else if(out.updateId!==null||out.expectedHeadSha256!==undefined){
|
|
505
|
+
throw new Error('authority epoch transaction options not allowed');
|
|
506
|
+
}
|
|
507
|
+
const restoreFields=[out.priorGrantSha256,out.priorGrantNonce,out.predecessorPolicyEnvelope,out.successorPolicyEnvelope,out.candidateManifest,out.continuitySubject];
|
|
508
|
+
if(command==='transaction-restore'){
|
|
509
|
+
out.restoreBinding=normalizeRestoreBinding({
|
|
510
|
+
prior_grant_sha256:out.priorGrantSha256,prior_grant_instance_nonce:out.priorGrantNonce,
|
|
511
|
+
predecessor_policy_envelope_sha256:out.predecessorPolicyEnvelope,successor_policy_envelope_sha256:out.successorPolicyEnvelope,
|
|
512
|
+
candidate_manifest_sha256:out.candidateManifest,continuity_subject_sha256:out.continuitySubject
|
|
513
|
+
},{required:true});
|
|
514
|
+
}else if(restoreFields.some(value=>value!==undefined)){
|
|
515
|
+
throw new Error('authority epoch restore binding options require transaction-restore');
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function authorityEpochCliMain(argv=process.argv.slice(2)){
|
|
521
|
+
const opts=parseEpochCliArgs(argv);
|
|
522
|
+
let grantBytes,grant;
|
|
523
|
+
try{
|
|
524
|
+
grantBytes=await readFile(opts.grantFile);
|
|
525
|
+
grant=JSON.parse(decodeUtf8Strict(grantBytes));
|
|
526
|
+
}catch(error){
|
|
527
|
+
throw new Error(`authority epoch grant unreadable: ${String(error?.message??error)}`);
|
|
528
|
+
}
|
|
529
|
+
if(!plain(grant)||!SHAPAT.test(String(grant.instance_nonce)))throw new Error('authority epoch grant nonce invalid');
|
|
530
|
+
const policyBytes=await readFile(opts.policyFile);
|
|
531
|
+
if(hashBytes(policyBytes)!==grant.policy_sha256)throw new Error('authority epoch grant policy digest mismatch');
|
|
532
|
+
const store=createAuthorityEpochStore({runtimeDir:path.dirname(opts.grantFile),policyBytes});
|
|
533
|
+
if(opts.command==='status'){
|
|
534
|
+
return store.verifyGrant({plane:opts.plane,grantBytes,grantInstanceNonce:grant.instance_nonce,policyBytes});
|
|
535
|
+
}
|
|
536
|
+
const action=opts.command.replaceAll('-','_');
|
|
537
|
+
return store.append({
|
|
538
|
+
plane:opts.plane,action,grantBytes,grantInstanceNonce:grant.instance_nonce,
|
|
539
|
+
updateId:opts.updateId,expectedHeadSha256:opts.expectedHeadSha256,restoreBinding:opts.restoreBinding
|
|
540
|
+
});
|
|
541
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {authorityEpochCliMain} from './authority-continuity-r65.mjs';
|
|
3
|
+
|
|
4
|
+
try{
|
|
5
|
+
const result=await authorityEpochCliMain(process.argv.slice(2));
|
|
6
|
+
process.stdout.write(JSON.stringify(result)+'\n');
|
|
7
|
+
}catch(error){
|
|
8
|
+
process.stderr.write(`DEADBYTE AUTHORITY EPOCH: ${String(error?.message??error)}\n`);
|
|
9
|
+
process.exitCode=1;
|
|
10
|
+
}
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { decodeUtf8Strict } from './text-policy.mjs';
|
|
5
5
|
import { validatePortableRelativePath } from './path-policy.mjs';
|
|
6
|
+
import { verifyStandingGrantEpoch } from './authority-continuity-r65.mjs';
|
|
6
7
|
|
|
7
8
|
export const AUTONOMOUS_POLICY_SCHEMA = 'deadbyte.autonomous-policy.v1';
|
|
8
9
|
export const AUTONOMOUS_LEASE_SCHEMA = 'deadbyte.autonomous-lease.v1';
|
|
@@ -154,8 +155,8 @@ export async function loadAutonomousPolicy(policyPath, codingPolicy) {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export async function autonomousSessionStatus(policy, now = new Date()) {
|
|
157
|
-
let lease;
|
|
158
|
-
try { lease = JSON.parse(decodeUtf8Strict(
|
|
158
|
+
let lease,leaseBytes;
|
|
159
|
+
try { leaseBytes=await readFile(policy.lease_file); lease = JSON.parse(decodeUtf8Strict(leaseBytes)); }
|
|
159
160
|
catch (error) {
|
|
160
161
|
return { configured:true, armed:false, reason:error?.code === 'ENOENT' ? 'disarmed' : 'invalid-lease', expires_at_utc:null };
|
|
161
162
|
}
|
|
@@ -167,8 +168,11 @@ export async function autonomousSessionStatus(policy, now = new Date()) {
|
|
|
167
168
|
const roots = Array.isArray(lease.root_ids) ? [...new Set(lease.root_ids)] : [];
|
|
168
169
|
if (roots.length < 1 || roots.some(id => !policy.root_ids.includes(id))) return { configured:true, armed:false, reason:'roots-invalid', expires_at_utc:null };
|
|
169
170
|
if (lease.schema === AUTONOMOUS_GRANT_SCHEMA) {
|
|
171
|
+
const epoch=await verifyStandingGrantEpoch({plane:'autonomous',grantFile:policy.lease_file,grantBytes:leaseBytes,grantInstanceNonce:lease.instance_nonce,policyPath:policy.policy_path});
|
|
172
|
+
if(!epoch.trusted)return { configured:true,armed:false,reason:epoch.reason,expires_at_utc:null,
|
|
173
|
+
remaining_ms:null,standing:false,instance_nonce:lease.instance_nonce,root_ids:roots };
|
|
170
174
|
return { configured:true, armed:true, reason:'standing-grant', expires_at_utc:null,
|
|
171
|
-
remaining_ms:null, standing:true, instance_nonce:lease.instance_nonce, root_ids:roots };
|
|
175
|
+
remaining_ms:null, standing:true, instance_nonce:lease.instance_nonce, root_ids:roots, authority_epoch:epoch };
|
|
172
176
|
}
|
|
173
177
|
const expires = Date.parse(lease.expires_at_utc);
|
|
174
178
|
if (!Number.isFinite(expires)) return { configured:true, armed:false, reason:'expiry-invalid', expires_at_utc:null };
|
package/src/coding-plane.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
|
|
|
11
11
|
import { createCodingSearch } from './coding-search.mjs';
|
|
12
12
|
import { createSearchCursorCodec } from './search-cursor.mjs';
|
|
13
13
|
import { atomicMutation } from './atomic-mutation.mjs';
|
|
14
|
+
import { createAuthorityEpochStore } from './authority-continuity-r65.mjs';
|
|
14
15
|
import { fileURLToPath } from 'node:url';
|
|
15
16
|
|
|
16
17
|
export const CODING_POLICY_SCHEMA = 'deadbyte.coding-policy.v1';
|
|
@@ -123,8 +124,8 @@ export async function loadCodingPolicy(policyPath) {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export async function codingSessionStatus(policy, now = new Date()) {
|
|
126
|
-
let lease;
|
|
127
|
-
try { lease = JSON.parse(decodeUtf8Strict(
|
|
127
|
+
let lease,leaseBytes;
|
|
128
|
+
try { leaseBytes=await readFile(policy.lease_file); lease = JSON.parse(decodeUtf8Strict(leaseBytes)); }
|
|
128
129
|
catch (error) {
|
|
129
130
|
return { configured: true, armed: false, reason: error?.code === 'ENOENT' ? 'disarmed' : 'invalid-lease', expires_at_utc: null };
|
|
130
131
|
}
|
|
@@ -138,8 +139,16 @@ export async function codingSessionStatus(policy, now = new Date()) {
|
|
|
138
139
|
}
|
|
139
140
|
const roots = [...new Set(lease.root_ids)];
|
|
140
141
|
if (lease.schema === CODING_GRANT_SCHEMA) {
|
|
142
|
+
let policyBytes;
|
|
143
|
+
try { policyBytes=await readFile(policy.policy_path); }
|
|
144
|
+
catch { return { configured:true,armed:false,reason:'epoch-policy-unavailable',expires_at_utc:null }; }
|
|
145
|
+
const epoch=await createAuthorityEpochStore({runtimeDir:path.dirname(policy.lease_file)}).verifyGrant({
|
|
146
|
+
plane:'coding',grantBytes:leaseBytes,grantInstanceNonce:lease.instance_nonce,policyBytes
|
|
147
|
+
});
|
|
148
|
+
if(!epoch.trusted)return { configured:true,armed:false,reason:epoch.reason,expires_at_utc:null,
|
|
149
|
+
remaining_ms:null,standing:false,instance_nonce:lease.instance_nonce,root_ids:roots };
|
|
141
150
|
return { configured: true, armed: true, reason: 'standing-grant', expires_at_utc: null,
|
|
142
|
-
remaining_ms: null, standing: true, instance_nonce: lease.instance_nonce, root_ids: roots };
|
|
151
|
+
remaining_ms: null, standing: true, instance_nonce: lease.instance_nonce, root_ids: roots, authority_epoch:epoch };
|
|
143
152
|
}
|
|
144
153
|
const expires = Date.parse(lease.expires_at_utc);
|
|
145
154
|
if (!Number.isFinite(expires)) return { configured: true, armed: false, reason: 'expiry-invalid', expires_at_utc: null };
|